feat: CloudStream meta-repo browser — browse and install sub-repos individually#411
feat: CloudStream meta-repo browser — browse and install sub-repos individually#411test01203 wants to merge 9 commits into
Conversation
Vanilla JS + Supabase single-page app served alongside the existing arvio.tv marketing site. No build step required — just static files. Features: - Google OAuth login (via Supabase GoTrue) - Dashboard with watch stats and recent activity - Profiles view — shows all ARVIO sub-profiles with active/kids badges - Addons manager — lists all Stremio + Telegram addons with enabled status - Watch History — paginated by movie/tv/all, delete individual entries - Watchlist — view and remove items - AI Subtitle Translation — toggle on/off, select model (Groq Llama 70B / Gemini Flash 2.5), configure auto-select and hearing-impaired removal; settings saved directly to cloud sync payload - Settings — card layout, language, OLED mode, skip profile selection Data layer: reads/writes the cloud sync payload stored in profiles.addons.__arvioAccountSyncPayload (same format the TV app uses), and queries watch_history/watchlist tables directly via Supabase REST. Co-Authored-By: koby455 <koby455@gmail.com>
Android TV app (CloudSyncRepository.kt): - Inject PluginDataStore into CloudSyncRepository - buildCloudPayload: export pluginRepositories, pluginScrapers, pluginsEnabled into __arvioAccountSyncPayload so plugins survive device wipes / multi-device - applyCloudPayload: restore repositories + scrapers from cloud on first launch (scraper JS code stays local-only for security; only metadata is synced) Companion web app (app.js): - Add IPTV section: shows all M3U playlists per profile with M3U/EPG URLs, enabled status, favourite groups and favourite channels - Add Plugins section: shows all plugin repositories (name, URL, scraper count, last updated) and individual scrapers (name, version, supported types, content languages, enabled status) with global plugins toggle - Add both to sidebar navigation Co-Authored-By: koby455 <koby455@gmail.com>
- All navigation labels, section titles, badges, toasts, and helper text translated to English in app.js and index.html - Auth screen subtitle and Google button label now in English - timeAgo() helper uses English relative-time strings - Hebrew remains fully supported as a selectable app language (setting stored in cloud sync payload as before) - Add mock-preview.html: standalone demo page with generic sample data for screenshots / PR previews (no Supabase dependency) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cPayload - Add escapeHtml() helper and apply it to every cloud value injected into innerHTML: user display name/email/avatar initial, profile names, addon name/description/logo, IPTV playlist name/URL/EPG/profile label/fav groups+channels, plugin repo name/URL, scraper name/description/version/logo/types/languages, history title/poster, watchlist tmdb_id, settings user ID - Add safeUrl() helper (https/http only) and use it for all image src attributes that come from cloud data (addon logos, scraper logos, user avatar) - saveSyncPayload now reads the existing wrapper first and only updates __arvioAccountSyncPayload and __arvioAccountSyncUpdatedAt, preserving any other fields the TV app may have written Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Normalizes the URL via the browser's URL parser before use and HTML-escapes it, preventing quote/attribute injection from synced logo or avatar URLs in innerHTML img src attributes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds support for CloudStream 'repository-of-repositories' manifests
(files containing a top-level pluginLists array, e.g. repo.json from
github.com/recloudstream/extensions).
## Problem
Previously, adding a meta-repo URL caused ARVIO to merge every plugin
from every sub-repo into a single flat repository entry. Users had no
way to browse or selectively install individual sub-repositories.
## Solution
When the user taps 'Add Repository' and enters a URL that resolves to
a CloudStream meta-repo manifest, ARVIO now:
1. Detects the pluginLists format (rather than silently flattening)
2. Fetches each sub-repo's plugins.json in parallel to get plugin counts
3. Shows a Meta-Repo Browser dialog listing every sub-repository with
its inferred name (e.g. 'user / repo-name') and scraper count
4. Lets the user tap ADD next to whichever sub-repos they want
5. Installs each chosen sub-repo as its own separate repository entry
6. Shows live 'Installing…' / '✓ Added' status per entry
Non-meta-repo URLs are unaffected — the VM falls back to the existing
addRepository() path automatically.
## Files changed (6, +276 lines)
Plugin.kt
Add MetaRepoEntry(name, pluginsUrl, pluginCount, iconUrl?) data class.
ExternalRepoParser.kt
+ tryParseMetaRepo(url) — lightweight check: fetch JSON, test for
pluginLists key, return ExternalRepoManifest or null.
+ resolveMetaRepoEntries(url, manifest) — parallel-fetch each
pluginLists URL to build List<MetaRepoEntry> with plugin counts.
+ inferSubRepoName(url) — derives 'user / repo' label from GitHub
raw URLs; falls back to path segments for other hosts.
PluginManager.kt
+ browseMetaRepo(url): List<MetaRepoEntry>? — returns the entry list
if the URL is a meta-repo, null otherwise (signals normal install).
+ addSubRepository(entry): delegates to addRepository(entry.pluginsUrl).
PluginUiState.kt
+ MetaRepoBrowseResult — holds entries + installing/installed sets.
+ metaRepoBrowseResult field on PluginUiState.
+ BrowseMetaRepo / InstallMetaRepoEntry / DismissMetaRepoBrowser events.
PluginViewModel.kt
+ browseMetaRepo(url) — calls PluginManager; if meta-repo shows the
browser state, otherwise falls back to addRepository().
+ installMetaRepoEntry(entry) — installs one sub-repo and updates the
installing/installed sets for live status feedback.
AddRepository button now dispatches BrowseMetaRepo instead.
PluginScreen.kt
+ MetaRepoBrowserDialog — scrollable list of sub-repos with name,
scraper count, and ADD / Installing… / ✓ Added action per row.
Shows the dialog whenever uiState.metaRepoBrowseResult != null.
…sitory diff
Two compilation issues fixed:
1. browseMetaRepo() and installMetaRepoEntry() were accidentally nested
inside normalizeUrlForComparison() instead of being sibling methods
of the class. This caused Kotlin to reject them ('Modifier private is
not applicable to local function') and left the return statement of
normalizeUrlForComparison stranded outside any function body, leading
to cascading compile errors.
Fixed by restructuring: normalizeUrlForComparison is now a complete
one-liner function, followed by browseMetaRepo and installMetaRepoEntry
as proper top-level private methods of PluginViewModel.
2. CloudSyncRepository.kt changes in this PR duplicated the plugin
DataStore wiring that was already introduced by PR ProdigyV21#402 (now merged
to main). Reverting to the current main version removes the duplicate
diff while keeping all the plugin sync functionality.
|
Found and fixed two structural issues that would prevent the PR from compiling: 1. Broken function nesting in
Fixed: 2. Stale The PR included changes to |
The data class was missing a trailing comma before the new metaRepoBrowseResult field, causing a Kotlin syntax error at line 30: 'Expecting comma or '')'.' This was the only remaining compile failure blocking CI.
|
Fixed the remaining CI failure. The build error was: The data class was missing a trailing comma after before the new field. Added the comma — should be green now. |
Problem
CloudStream uses a repository-of-repositories format: a single
repo.jsonmanifest with apluginListsarray pointing to multiple independentplugins.jsonfiles.Example:
https://raw.githubusercontent.com/recloudstream/extensions/master/repo.jsonPreviously ARVIO would:
Solution
When the user enters a meta-repo URL in the Add Repository dialog, ARVIO now:
pluginListsformat automatically (no new UI required for the URL)plugins.jsonin parallel to count their scrapersuser / repo-namefrom GitHub raw URLs)Non-meta-repo URLs are unaffected — the ViewModel falls back to the existing
addRepository()path automatically.How the format works
Each URL in
pluginListsis a flat array ofExternalPluginEntryobjects (.cs3DEX extensions).Files changed — 6 files, +276 lines
Plugin.ktMetaRepoEntry(name, pluginsUrl, pluginCount, iconUrl?)data classExternalRepoParser.kttryParseMetaRepo()·resolveMetaRepoEntries()·inferSubRepoName()PluginManager.ktbrowseMetaRepo(url)·addSubRepository(entry)PluginUiState.ktMetaRepoBrowseResult· new eventsBrowseMetaRepo/InstallMetaRepoEntry/DismissMetaRepoBrowserPluginViewModel.ktbrowseMetaRepo()·installMetaRepoEntry()·AddRepository→BrowseMetaRepodispatchPluginScreen.ktMetaRepoBrowserDialogcomposable — scrollable sub-repo list with ADD/status buttons🤖 Generated with Claude Code