Skip to content

feat(playlists): switch movies, series and live TV off per playlist - #381

Open
TheDutchSmoke wants to merge 1 commit into
bilipp:mainfrom
TheDutchSmoke:feat/per-playlist-content-sync
Open

TheDutchSmoke wants to merge 1 commit into
bilipp:mainfrom
TheDutchSmoke:feat/per-playlist-content-sync

Conversation

@TheDutchSmoke

@TheDutchSmoke TheDutchSmoke commented Nov 5, 2026 •

Copy link
Copy Markdown

What

Each playlist gets three Content switches (Settings › Playlists › playlist, iOS/macOS and tvOS): Movies, Series and Live TV. A kind that is off is skipped by the sync, what earlier syncs brought in is removed from the library, and its tab leaves the tab bar. Sources without live channels (WebDAV, Jellyfin/Emby/Silo, Plex) don't offer the live switch.

Closes the request to fetch movies and series (and, for parity, live TV) selectively per provider.

How

  • Storage follows hiddenTabsRaw: one comma-separated PlaylistTab set, Playlist.disabledContentRaw, default "" so existing stores migrate without a plan. Typed accessors disabledContent / syncs(_:); Playlist?.showsTab hides the tab of a disabled kind too.
  • Mirrored through iCloud (SyncedPlaylist, PlaylistConfigValues with a decodeIfPresent fallback for shadow baselines written before the field) and Lume Cloud (optional on the wire: a record an older client wrote still opens as "fetch everything"). Paired spec change: spec: playlist payload carries disabledContentRaw LumeCloud#2.
  • Pipelines guard their phases on playlist.syncs(_:): Xtream and Stalker skip the kind's categories and content, Jellyfin/Emby/Silo and Plex walk no library of that kind, the m3u/WebDAV import drops the kind's entries from each classified batch (M3UClassifiedBatch.dropping) before the upsert.
  • Removal is one place, ContentSyncManager.removeDisabledContent, run by performSync after any source, so a switch flipped on another device applies here as well. It uses PlaylistDeletion's per-kind deletes (factored out of removeOrphanedContent: paged movies and series so cast and episodes cascade, bulk live streams plus the orphaned guide listings), prunes the kind's categories and drops its Xtream digest. The m3u prune skips the guarded sweeps of a disabled kind: its empty seen set would read as a truncated payload and hold the file fingerprint back forever.
  • UI: the switch saves, clears the m3u/WebDAV fingerprints (an unchanged file would otherwise skip the very import that applies it), reconciles cloud sync and starts a sync. The progress sheet lists only the steps that will run (SyncStep.steps(for:full:disabled:)).
  • CloudSyncEngine's playlist apply helpers move to CloudSyncEngine+Playlists.swift and the codec compatibility test to its own suite, both for the lint length limits. Footer string added to the catalog in all 10 languages.

Tested

Xcode 27.2 (27B52), iPhone 17 simulator, iOS 27.2; LumeEngine v0.4.0, LumeRecorder v0.1.1, LumeCloud main + #2.

  • 15 new tests: PlaylistTabTests (encode/decode, tab hiding, selectable kinds, translations), ContentSyncDisabledContentTests (m3u end to end: movies off, series+live off, off→on round trip, everything-on removes nothing; removeDisabledContent leaves other kinds and other playlists alone, cascades episodes, drops the kind's digest; progress steps; batch filter), CloudSyncPlaylistTabsTests (legacy baseline, both directions), LumeCloudPayloadCodecTests + LumeCloudPayloadCodecCompatTests (round trip, older record without the field).
  • M3USyncTests, XtreamDigestSkipTests, ContentSyncPruneTests, LumeCloudIntegrationTests, LumeCloudVectorExportTests (against the regenerated vectors) all pass.
  • Full LumeTests run: everything passes except failures that reproduce identically on main in this environment (ungenerated ExampleData/ fixtures, a Dutch system locale for SportsLabelsTests, and SportsResolveCacheTests), plus two wall-clock LumeEngineShutdownWaitTests that pass when run without the rest of the suite loading the machine.
  • macOS compile succeeds (CODE_SIGNING_ALLOWED=NO). SwiftFormat and SwiftLint --strict clean on every changed file; xcstrings normalised.
  • Not run: tvOS build (no tvOS simulator runtime here); the tvOS section mirrors the existing tvSyncSection and uses TVOptionToggleRow.

Notes for review

  • The published FFmpeg xcframework LumeEngine v0.4.0 downloads still has its module map at the Headers root and collides with LibDovi ("Multiple commands produce …/include/module.modulemap"); LumeEngine#61 fixes the build script but no release carries it yet. Built here with a local BinaryDependencies/FFmpeg.xcframework copy with the map nested, as that script now does.
  • Design choice: a disabled kind implies a hidden tab (no empty tab, no extra logic in MainTabView). Shout if you'd rather keep the two independent.

A playlist always fetched all three kinds. Each playlist now has three
Content switches (Settings › Playlists › playlist): a kind that is off is
skipped by the sync, what earlier syncs brought in is removed, and its tab
leaves the tab bar.

Storage follows hiddenTabsRaw: one comma-separated PlaylistTab set,
Playlist.disabledContentRaw, defaulting to "" so existing stores migrate
without a plan. It rides the playlist config through iCloud
(SyncedPlaylist, PlaylistConfigValues with a decodeIfPresent fallback) and
Lume Cloud (optional on the wire; a record an older client wrote still
opens as "fetch everything").

Every pipeline guards its phases on Playlist.syncs(_:): Xtream and Stalker
skip the kind's categories and content, Jellyfin/Emby/Silo and Plex walk no
library of that kind, and the m3u/WebDAV import drops the kind's entries
from each classified batch before the upsert. The removal is one place,
ContentSyncManager.removeDisabledContent, run by performSync after any
source so a switch flipped on another device applies here too. It uses
PlaylistDeletion's per-kind deletes (factored out of removeOrphanedContent:
paged movies and series so cast and episodes cascade, bulk live streams
with the orphaned guide listings), prunes the kind's categories and drops
its Xtream digest. The m3u prune skips the guarded sweeps of a disabled
kind: its empty seen set would read as a truncated payload and hold the
file fingerprint back forever.

The switch in Settings saves, clears the m3u/WebDAV fingerprints (an
unchanged file would otherwise skip the very import that applies it),
reconciles cloud sync and starts a sync. The progress sheet lists only the
steps that will run (SyncStep.steps(for:full:disabled:)). Sources without
live channels don't offer the live switch.

CloudSyncEngine's playlist apply helpers move to CloudSyncEngine+Playlists
and the codec compatibility test to its own suite, both for the lint
length limits.

Paired with bilipp/LumeCloud: spec vectors and PROTOCOL.md gain the field.

Tested on Xcode 27.2, iPhone 17 (iOS 27.2) simulator: LumeTests passes
(the 15 new tests, every sync/cloud suite, and the full suite apart from
failures that reproduce on main in this environment: missing ExampleData
fixtures and a Dutch locale).
@TheDutchSmoke
TheDutchSmoke force-pushed the feat/per-playlist-content-sync branch from 4392807 to 6b4d735 Compare November 7, 2026 11:05
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.

1 participant