Skip to content

feat: CloudStream meta-repo browser — browse and install sub-repos individually#411

Open
test01203 wants to merge 9 commits into
ProdigyV21:mainfrom
test01203:feat/cloudstream-meta-repo-browser
Open

feat: CloudStream meta-repo browser — browse and install sub-repos individually#411
test01203 wants to merge 9 commits into
ProdigyV21:mainfrom
test01203:feat/cloudstream-meta-repo-browser

Conversation

@test01203

Copy link
Copy Markdown
Contributor

Problem

CloudStream uses a repository-of-repositories format: a single repo.json manifest with a pluginLists array pointing to multiple independent plugins.json files.

Example: https://raw.githubusercontent.com/recloudstream/extensions/master/repo.json

Previously ARVIO would:

  • Flatten all plugins from all sub-repos into one merged repository entry
  • Give the user no visibility into how many sub-repos existed or what they contained
  • Make selective installation impossible

Solution

When the user enters a meta-repo URL in the Add Repository dialog, ARVIO now:

  1. Detects the pluginLists format automatically (no new UI required for the URL)
  2. Fetches each sub-repo's plugins.json in parallel to count their scrapers
  3. Shows a browser dialog listing every sub-repository with:
    • Inferred human-readable name (user / repo-name from GitHub raw URLs)
    • Scraper count
    • Live status: ADDInstalling…✓ Added
  4. Installs each chosen sub-repo as its own separate repository entry (full parity with manually-added repos — cloud-synced, individually toggleable, removable)

Non-meta-repo URLs are unaffected — the ViewModel falls back to the existing addRepository() path automatically.


How the format works

repo.json                     ← meta-repo manifest
  name: "My Collection"
  pluginLists: [
    "https://.../extensions/builds/plugins.json",   ← sub-repo A
    "https://.../more-plugins/builds/plugins.json", ← sub-repo B
    ...
  ]

Each URL in pluginLists is a flat array of ExternalPluginEntry objects (.cs3 DEX extensions).


Files changed — 6 files, +276 lines

File Change
Plugin.kt MetaRepoEntry(name, pluginsUrl, pluginCount, iconUrl?) data class
ExternalRepoParser.kt tryParseMetaRepo() · resolveMetaRepoEntries() · inferSubRepoName()
PluginManager.kt browseMetaRepo(url) · addSubRepository(entry)
PluginUiState.kt MetaRepoBrowseResult · new events BrowseMetaRepo / InstallMetaRepoEntry / DismissMetaRepoBrowser
PluginViewModel.kt browseMetaRepo() · installMetaRepoEntry() · AddRepositoryBrowseMetaRepo dispatch
PluginScreen.kt MetaRepoBrowserDialog composable — scrollable sub-repo list with ADD/status buttons

🤖 Generated with Claude Code

The-cpu-max and others added 8 commits June 16, 2026 20:21
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.
@test01203

Copy link
Copy Markdown
Contributor Author

Found and fixed two structural issues that would prevent the PR from compiling:

1. Broken function nesting in PluginViewModel.kt (compile error)

browseMetaRepo() and installMetaRepoEntry() were accidentally inserted inside normalizeUrlForComparison() instead of after it. Kotlin doesn't allow private local functions, so the compiler rejected both with "Modifier 'private' is not applicable to local function", and the return url.trim()... body of normalizeUrlForComparison was left stranded outside any function, causing cascading errors.

Fixed: normalizeUrlForComparison is now a proper one-liner function, and browseMetaRepo / installMetaRepoEntry are sibling private fun methods of the class.

2. Stale CloudSyncRepository.kt diff

The PR included changes to CloudSyncRepository.kt that duplicate the plugin DataStore wiring already introduced by PR #402 (now merged to main). This wasn't a compile error but would create a misleading diff and a messy merge. Reverted to the current main version — all plugin sync functionality remains intact via the already-merged PR.

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.
@test01203

Copy link
Copy Markdown
Contributor Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants