Commit graph

2024 commits

Author SHA1 Message Date
BoulderBadgeDad
8eadadc1ae youtube retention: auto-clean old channel episodes (per-channel keep window)
Some checks failed
Compile the app and run tests / sanity-check (push) Has been cancelled
new opt-in feature — default keeps everything, so nothing changes unless a channel sets it.

- history got smarter: youtube rows now carry channel_id + published_at (mined from
  search_ctx) + a pruned_at marker. new queries: youtube_channels_with_downloads,
  youtube_channel_episodes, mark_download_pruned.
- core/video/retention.py (pure): parse_retention + episodes_to_prune — age by UPLOAD
  date (published_at, filename fallback); 'count_N' keeps newest N, 'days_N' keeps last N
  days; undated episodes never pruned.
- handler auto_video_clean_youtube_episodes: for each channel with a policy, delete the
  out-of-window video + its -thumb/.nfo sidecars (only the exact recorded dest_path, never
  walks folders), then mark the history row pruned — KEPT so the scan never re-downloads it.
- 'Keep' dropdown in each channel's cog modal (Everything / Last 30 episodes / Last 3 / 6
  months); playlists excluded. seeded daily automation + register/block/label/icon/sort.
pure math + handler (i/o injected) + the prune-keeps-dedup DB contract all tested.
2026-06-26 17:24:59 -07:00
BoulderBadgeDad
151bb7b542 video: hourly 'Update Video Database' safety net (the second-trigger pattern)
dual triggers on one automation aren't a thing (one trigger per row, same as music), so
this adds a second automation sharing the handler — like Auto-Deep Scan TV/Movie already
do. 'Auto-Update Video Database (Hourly)' runs the same cheap incremental server-read on
an hourly schedule, so MANUAL library additions (which Plex auto-scans) appear within the
hour instead of waiting for the weekly deep scan; the existing after-scan one still gives
instant updates for SoulSync-initiated scans.

new action_type video_update_database_hourly (seeder keys on action_type) reusing
auto_video_update_database in incremental mode; registered, seeded (schedule 1h, 15m
initial delay), UI block, label/icon, sort order. tested.
2026-06-26 16:39:28 -07:00
BoulderBadgeDad
ca1348b2c2 downloads: youtube 'open' goes to the in-app channel page, not youtube.com
the open button now opens the channel inside soulsync (channels-as-shows) via
video-open-detail {kind:'channel', source:'youtube', id}, matching how movie/show
cards open their in-app detail. enqueue_ctx now carries channel_id (new downloads)
and youtube_video_detail returns it too (so older downloads resolve the channel in
the drawer). drawer gets an 'Open channel' button alongside 'Open on YouTube';
falls back to the youtube video link only when the channel id is unknown.
2026-06-26 13:24:41 -07:00
BoulderBadgeDad
3f924812da downloads: youtube card open button → YouTube link (was opening a random show)
the open-show button rendered for youtube cards too; the click ran parseInt on the
youtube video id → ids starting with a digit became a small library id (e.g. 3 →
'3rd Rock from the Sun'), others became NaN → broken link. youtube cards now render a
real YouTube link for that button; movie/show/episode keep the open-detail button.
regression test pins the youtube branch.
2026-06-26 13:19:47 -07:00
BoulderBadgeDad
b916e6a2f3 downloads drawer: episode detail + youtube treatment + availability line
- EPISODE downloads now show the specific episode: still + 'S02E05 · air date' + that
  episode's own title + synopsis (was just the show synopsis). meta endpoint takes
  ?season=&episode= → engine.tmdb_season; show cast/logo stay as context.
- YOUTUBE drawer: big 16:9 thumbnail + channel · duration · views · upload date +
  description. new yt-meta route → db.youtube_video_detail (cached duration/views).
- AVAILABILITY line: the chosen source's free-slot/queue/speed snapshot, stashed in
  search_ctx at grab time (build_download_record) → 'Availability: ✓ free slot · queue 0
  · 2.1 MB/s'.
drawer also reworked into clean youtube / episode / movie-show branches; first paint
shows 'Loading…' instead of flashing 'no synopsis'. tested.
2026-06-26 13:06:44 -07:00
BoulderBadgeDad
c35ec43466 downloads drawer: rich TMDB detail + fix cast photos + stop OMDb quota burn
drawer upgrades (the TMDB call we already make returns all this — just render it):
- FIX cast photos — used c.profile_url; the field is c.photo. now shows headshots.
- title LOGO header (falls back to text) + meta line (year · rating · runtime · network).
- tagline, director/creator, ▶ trailer link, and a 'Watch on' provider-logo row.
- meta endpoint widened to return logo/cast-photo/rating/runtime/crew/trailer/providers.

OMDb spam fix (the 'Request limit reached!' tracebacks):
- the new bulk schedule-refresh called refresh_show_art per show, which did an OMDb
  ratings backfill each → blew the daily quota. refresh_show_art gains with_ratings;
  the automation passes False (it only needs episode schedules).
- _backfill_ratings now latches _omdb_blocked on OMDbAuthError → one quiet warning +
  stops calling OMDb for the rest of the process, instead of a traceback per show.
tested (cast/logo/trailer contract, no-ratings plumbing, the latch-off behavior).
2026-06-26 12:49:11 -07:00
BoulderBadgeDad
a30a70fafd automations page: friendly labels + icons for the video maintenance actions
the video-prefixed maintenance actions (clean_search_history, clean_completed_downloads,
full_cleanup, backup_database) + the new refresh_airing_schedules weren't in the action
label/icon maps, so they fell back to the gear + raw action_type ('⚙️ video_full_cleanup').
added all five to both maps (🗓️/🗑️//🧹/💾, mirroring the music side). regression test
asserts every seeded video automation resolves a label AND an icon, so this can't recur.
2026-06-26 11:51:29 -07:00
BoulderBadgeDad
9aee723d1a video: 'Refresh Airing TV Schedules' automation — keep the calendar current
the airing automation reads the LOCAL episodes table, which only refreshes on initial
enrichment / manual re-match / lazy on-view — so a newly-announced or rescheduled
episode could be missed indefinitely. (the deep TV scan is a SERVER scan, unrelated.)

new daily automation re-pulls TMDB episode schedules (air dates/stills) for still-airing
watchlist shows so the calendar is fresh when the airing run reads it:
- db.watchlist_continuing_shows(): effective-watchlist library shows that are still airing
  (skips tmdb-only follows — no episodes — and ended/canceled shows; keeps unknown status).
- handler mirrors the standard: injected fetch_shows + refresh_show seams, live per-show
  progress, _manages_own_progress; reuses engine.refresh_show_art (match + episode cascade).
- seeded daily at 23:00 (2h before the 01:00 airing run), registered, UI block, sorted in
  the page right before the airing automation.
seam-level tested + a regression that it's seeded before the airing run.
2026-06-26 11:47:15 -07:00
BoulderBadgeDad
9a8550661b downloads page: click-to-expand detail drawer (synopsis + cast + facts)
click a card → it expands inline into a detail drawer (open state survives the
in-place re-patches via _expanded):
- big backdrop + synopsis + genres + a cast strip (photos/names/characters),
  lazily fetched from TMDB by the grab's tmdb id (new /downloads/meta/<kind>/<id>
  endpoint → engine.tmdb_full_detail). youtube shows channel + description instead.
- a facts grid: status, quality target, release, format, source+queue, size,
  attempts, copyable dest path, full error.
- big actions: open in library / open on youtube / copy path / cancel / retry.
all type-themed (Cinema palette) and scoped to .vdpg-card. contract-tested + the
meta route is registered.
2026-06-26 11:30:36 -07:00
BoulderBadgeDad
458bf01599 downloads page: Cinema per-type theming + sidebar live count + status polish
- per-type colour (movie=azure / TV=violet / youtube=red) on a left accent bar, the
  status pill, the progress fill + quality chip — tell the three apart at a glance.
- bigger poster art + a type corner badge (reads even with a poster).
- status is now a coloured pill; queued/searching/importing get an indeterminate
  shimmer instead of a frozen bar; 'Importing' + 'Import failed' surfaced with their
  own context lines ('Moving into your library…').
- sidebar Downloads nav shows a live active-count badge, kept fresh off-page by a
  light poll (skips the fetch while the page's own poll is running).
all scoped to .vdpg-card so the music downloads page is untouched. contract-tested.
2026-06-26 11:22:56 -07:00
BoulderBadgeDad
cc8cc78b97 video downloads: consistent stage→import→library pipeline + 'Importing' status
both lanes now expose the post-processing phase instead of jumping downloading→completed:
- YouTube now STAGES in the shared download folder (a 'youtube' subdir) then transfers to
  the library — same as movies/TV — so no partial .part files land in the Plex folder.
  process_youtube_download gains stage_dir + move seams: download → 'importing' → move →
  completed (import_failed if the move dies; wish kept for retry). falls back to
  straight-to-library when no download folder is set.
- movies/TV: the organizer flips the row to 'importing' before the (slow) move + sidecars +
  subtitles, so that phase is visible.
- 'importing' is now an active status (get_active_video_downloads + youtube concurrency count
  + the downloads page status map), so the row stays on the page + holds its worker slot.
tested (staging + import-failure paths).
2026-06-26 11:11:29 -07:00
BoulderBadgeDad
8fe73ac924 video: longer adaptive slskd wait + stop the automations page blink
two issues:
1. slskd search gave up at 22s (then stopped the search), but slskd gathers peers
   over its full ~60s window — so most movie searches were killed before results
   arrived. now waits up to 55s but returns early once results SETTLE (12+ hits, or
   no new hits for ~12s) so fast searches don't burn the whole window. matches the
   music side's longer wait.
2. the automations page re-rendered the WHOLE system section every 8s poll
   (replaceChild) → blink + wiped the socket's live progress. now it skips the
   rebuild unless something structural changed (added/removed/toggled/ran); live
   progress comes via socket, the countdown ticks locally.
2026-06-26 09:54:18 -07:00
BoulderBadgeDad
b55c245fe6 video downloads: re-download + clear history actions
after you delete a file, the scans dedup against download history so it won't
re-grab — this gives you the escape hatch:
- per-row 'Re-download' button in the History modal: forgets that grab (DELETE
  /downloads/history/<id>) so the next scan re-adds + re-downloads it.
- 'Clear' button: wipes the whole history (guarded confirm).
db.delete_download_history / clear_download_history(kind?) + the two endpoints.
tested (db methods + endpoints).
2026-06-26 09:44:56 -07:00
BoulderBadgeDad
f0e1fda0fc video wishlist badge: poll so it reflects server-side removals (downloads)
the remaining badge bug: when a download finishes it removes its item from the
wishlist SERVER-SIDE, which fires no frontend 'changed' event — so the badge went
stale and only 'jumped' (e.g. down to the tv-only count) on the next navigation.

add a lightweight badge poll: refreshes the authoritative /wishlist/counts every
8s while downloads are active (uses the downloads page's _vdpgAnyActive), every
30s idle, paused when the tab is hidden. now the count tracks down live as wishlist
items get downloaded. frontend add/remove events still fire instantly as before.
2026-06-26 00:19:54 -07:00
BoulderBadgeDad
1722618c6d video wishlist badge: stop the second writer clobbering it with the TV-only count
the other half of the bug: the endpoint total was fixed, but setCounts() (movie/
episode load) AND setYtCounts() (youtube load) ALSO wrote the nav badge from their
OWN partial state. these load separately, and on the dashboard only one runs — so
whichever fired last overwrote the badge with a partial count (correct number,
then 'switches' to TV-only).

fix: the /wishlist/counts endpoint is the single source of truth for the grand
total; both setters now just call refreshBadge() to (re)sync from it instead of
computing a partial sum. regression test pins it.
2026-06-25 23:21:59 -07:00
BoulderBadgeDad
23c64e000a youtube playlists: same settings cog/modal as channels
playlist cards on the watchlist tab now get the same hover cog → the per-channel
settings modal (custom show-name + quality override). the modal is kind-aware so
it reads 'Playlist settings' + 'the playlist's real name'.

no backend change needed: playlist videos are wishlisted with channel_id =
playlist_id, so the existing get/set_channel_settings + enqueue override lookup
already key off the playlist id. UI-only: cog on playlistCard + data-kind passed
to openChannelSettings(id, title, kind). test covers both cogs.
2026-06-25 23:07:15 -07:00
BoulderBadgeDad
f734905ba1 youtube channels: per-channel settings modal UI (cog on the Channels tab)
hover a channel on the watchlist Channels tab -> a settings cog appears (mirror
of the unfollow x) -> opens a self-contained modal:
- Show name (folder): overrides the $channel folder/show-name token; blank uses
  the real channel name.
- 'Force a specific quality' toggle -> resolution/codec/container/60fps/HDR
  override; off = use the global youtube quality from Settings.

VideoYoutube.openChannelSettings(id, title) builds the modal, loads via GET, saves
via POST /youtube/channel/<id>/settings. .vyt-cset-* CSS. string-contract test.

(note: 10 pre-existing soundcloud music-orchestrator test failures are unrelated
to this work — they fail on a clean HEAD too.)
2026-06-25 23:01:35 -07:00
BoulderBadgeDad
846d217dca video automations: order the System list as a logical pipeline
the API returns system automations newest-created-first, so a re-seeded row
(e.g. the just-migrated movie/episode processors) jumps to the top and jumbles
the grouping. the video page now re-sorts its System list by an explicit order:
scans (fill) → processors (drain) → library scan/sync → maintenance. scoped to
the video page only (music ordering untouched); unknown/future actions fall to
the end keeping API order. string-contract test pins the scan<proc<maint order.
2026-06-25 22:38:26 -07:00
BoulderBadgeDad
7d7d242e5d video automations: rename Download->Process + group into a clear pipeline
- 'Download X Wishlist' -> 'Process X Wishlist' everywhere (label + action_type):
  matches the music side's 'process_wishlist', the already-process youtube action,
  and reads right (movie/episode do search+pick+download, not just download).
  action_types video_download_movie/episode_wishlist -> video_process_*.
- group the new automations as a two-stage pipeline in the seed list + builder
  palette: Stage 1 SCANS (people/channels/playlists) fill the wishlist, Stage 2
  PROCESSORS (movie/episode/youtube) drain it. icons/labels/drift test updated.

brand-new this session so no migration needed. 337 automation tests green.
2026-06-25 22:26:31 -07:00
BoulderBadgeDad
b404420c76 video automations: auto-download movie + episode wishlist (soulseek)
the soulseek counterpart of the youtube drain — finally makes the people/airing
scans pay off. two automations (Auto-Download Movie / Episode Wishlist, hourly):
for each wished+released item, do a bounded blocking slskd search, pick the top
ACCEPTED release per the quality profile, and enqueue it exactly like a manual
grab (same add_video_download shape → the monitor finishes + organises it).

same standard as youtube: processes the WHOLE eligible wishlist (no total cap)
but searches a few at a time (max_concurrent, default 3, via a thread pool). a
busy guard skips the next hourly tick while a drain is still working. movies gate
on status='wanted' (skips monitored); episodes are all-wished. items already
downloading are skipped. quiet skip if the library folder isn't set.

reuses the real infra (build_query/slskd_search/_evaluate_hits/start_download/
download_monitor) — pure pick/select/record seam-tested; db queries added. 377
automation+video tests green.
2026-06-25 22:08:02 -07:00
BoulderBadgeDad
fb9459fb5c video automations: scan watchlist YouTube playlists (mirror-all)
playlists are followable but had no scan. new 'Scan Watchlist Playlists'
automation, sibling of the channel scan but a different rule: a playlist is a
curated finite set, so MIRROR it — wishlist every long-form video you don't have
plus any later additions (no forward-looking baseline, no last-N net).

playlist-as-show: videos wishlisted under the playlist's title, so the worker
files them as 'Playlist Name / Season YEAR / ... - date - title' (the ytdl-sub
tv_show_name-on-a-playlist convention). reuses all the channel plumbing — same
wishlist rows, same Download YouTube Wishlist drain, quality + org template.

seeded every 6h (Auto-Scan Watchlist Playlists); block + registration + icon +
drift test. dedup reuses wishlisted_video_ids_for_channel (parent_source_id=PL).
seam-tested; 944-test sweep green.
2026-06-25 21:18:53 -07:00
BoulderBadgeDad
3254e8a6c4 settings: youtube path template field on the Library tab
surface the youtube_template (built into organization.py) on Settings -> Library
Organization, next to the movie/episode templates: input + variable hints
($channel/$year/$date/$month/$day/$title/$videoid) + live preview, wired into
load/collect/save/reset. backend already round-trips it (load/save normalize).
2026-06-25 19:40:56 -07:00
BoulderBadgeDad
c47dccc7d7 youtube fulfillment: drain wishlist -> download queue
new 'Download YouTube Wishlist' automation: pushes a polite batch of wished
youtube videos into the shared video_downloads queue + spawns the yt-dlp worker
per video. skips in-flight ones (no double-grab); big backlogs drain over
several scheduled runs (batch_size, default 3). needs the youtube library folder
set.

- download_monitor: SKIP source='youtube' rows (owned by their worker thread, no
  slskd transfer to match) — surgical, slskd path untouched.
- db.youtube_wishlist_to_download(): flat newest-first list of wished videos with
  channel/title/date/thumb for organising.

block + registration + icon/label + drift test. all seam-tested.
2026-06-25 19:30:02 -07:00
BoulderBadgeDad
31d284ab0d video automations: watchlist scans for people + channels
two new video-side automation blocks that keep the wishlist fed:

- scan watchlist people: for each followed person, wishlist every un-owned
  movie they acted in or directed (back catalog + upcoming). released ->
  wanted, upcoming -> monitored (engine skips it til it's out, promotes on
  release). grabs rich detail at add time (backdrop/cast/overview/etc +
  provenance) into a new video_wishlist.detail_json col. fast re-runs skip
  already-wishlisted + only promote.

- scan watchlist channels: for each followed youtube channel, wishlist new
  long-form uploads (shorts excluded). forward-looking from follow time +
  a last-N safety net (default 10). diffs against wishlisted/downloaded/
  dismissed so it never dupes. pair with a 6h schedule trigger. scan-only;
  fulfillment engine comes later.

both are pure handlers with injected seams + full seam tests. add_movie_to_
wishlist gains status + detail_json (promote-only upsert). no schema break,
music side untouched.
2026-06-25 19:07:06 -07:00
BoulderBadgeDad
877a86c9b0 Video Discover: split Top 10 today into Movies + TV Shows (Netflix-style)
The mixed /trending/all chart is movie-heavy — TV was nearly absent (only
1 of the top 10 was a show). Split it like Netflix: 'Top 10 today' now holds
two ranked rails, Movies and TV Shows, from the dedicated /trending/movie/day
and /trending/tv/day charts (full 10 of each).

- client.trending(window, kind): single-type charts force the kind into
  _disc_map (those endpoints omit media_type).
- engine.trending(window, kind): kind in the cache key.
- /discover/list: key=trending_movies_today / trending_tv_today, both treated
  as single un-paged charts; inherit the same hide-owned + ranked rendering.
- frontend: one 'Top 10 today' group, two ranked shelves titled Movies / TV
  Shows (group header carries the 'Top 10' framing).
2026-06-24 08:00:13 -07:00
BoulderBadgeDad
e7552d3c62 Video Discover: fix orphaned Top 10 numerals when hiding owned + lift 'New' up the page
Bug: with 'Hide owned' on, an owned title in the Top 10 (e.g. House of the
Dragon) had its card hidden by the global .vdsc-hide-owned rule while its big
rank numeral stayed — a blank gap with just a number. The ranked rail fetched
the true chart (ignoring hide_owned) precisely to keep the chart intact, which
collided with that CSS. Fix: ranked rail now honours hide_owned at fetch time
(owned dropped server-side, ranks stay contiguous 1-N), plus a CSS safety net
exempting ranked cards from the hide rule so a numeral can never be stranded.
When hide-owned is OFF the true chart still shows owned with the 'In Library'
ribbon.

Ordering: moved 'New & noteworthy' above the async collection/taste groups so
the two always-visible discovery rows (Top 10 + New) anchor the top of the
page, matching how streaming services surface new releases.
2026-06-24 07:33:58 -07:00
BoulderBadgeDad
24b79dc29e Video Discover: sticky 'jump to section' nav for the deep rail stack
The page is now a long stack of rail groups (For you / Top 10 / New /
Trending / Mood / Studios / Genres / More). A sticky chip bar lets you
jump straight to any section — Netflix/Disney-style category nav. Chips
stay in lockstep with the groups: async-only groups (foryou/collection/
taste) reveal their chip when filled; pruned groups hide theirs. Lives
inside the shelves host so it auto-hides in the Browse-all grid view.
2026-06-23 23:58:53 -07:00
BoulderBadgeDad
a275cbfb7a Video Discover: Top 10 ranked rail + New/Mood/Studios sections + NEW badge
Best-in-class discovery pass (Netflix/Disney-class):
- iconic ranked 'Top 10 today' rail (daily trending, big outlined rank
  numerals, true global chart via lang=any, single un-paged fetch)
- 'New & noteworthy' (date-windowed new releases + theatrical pipeline)
- 'By mood' (genre AND-combos w/ vote floors: feel-good, mind-bending…)
- 'From the studios' (Pixar/Ghibli/A24/Marvel/DreamWorks via company ids)
- 'Something different' (quick watches / epics / family) merged with gems
- 'NEW' flag on un-owned current-year cards
Preserves async-loader group ids (foryou/collection/taste). Empty rails
self-prune; wrong studio ids just drop their rail.
2026-06-23 23:55:30 -07:00
BoulderBadgeDad
82cbc22577 video discover: organise rails into fixed, labelled sections (stable order)
The three async personalized loaders each did insertAdjacentHTML('afterbegin'), so 'Recommended
for you' / 'More like X' / gap rails landed in whatever order their fetch finished — the top of
the page reshuffled every load, and taste-based rails were scattered through generic ones.

Now Discover renders 6 authored groups in a fixed order, each with a header:
  For you · Finish your collection · More of what you like · Trending & popular ·
  Browse by genre · Hidden gems & more
Each async loader fills its own group's body (deterministic position) instead of racing to the
top; async-only groups (gaps) stay hidden until filled, and a group whose rails all drop out is
pruned. Extracted lazyShelfHtml/filledShelfHtml/shelfNav helpers; lazy-load, stagger, gen-guard,
see-all and scroll arrows all unchanged.
2026-06-23 10:39:41 -07:00
BoulderBadgeDad
4f24ac2733 video discover: stop duplicate 'Recommended for you' rows stacking on rail rebuild
Toggling service/language chips calls reloadRails(), which clears the shelves synchronously
but the personalized loaders (foryou/gaps/morelike) fetch async and insertAdjacentHTML afterbegin.
Rapid re-toggles let superseded in-flight fetches prepend anyway -> N 'Recommended for you' rows
piled up. Each rebuild now bumps state.railGen and every loader bails if its captured gen is
stale before prepending; chip-driven rebuilds are also debounced (350ms) so multi-select
coalesces into one rebuild. The 'On your streaming services' rail itself was building fine -
it was just buried under the duplicates.
2026-06-23 10:00:52 -07:00
BoulderBadgeDad
cd29239b2f video discover: move 'My services' out of the page-wide bar (it's a rail builder, not a filter)
'My services' only builds the optional 'On your streaming services' rail, but sitting under
the 'Across Discover' header it read as a page-wide filter (user confusion: 'i thought it
affected the whole page because that's what it says'). Moved it to its own self-describing row
('pick what you subscribe to — adds a rail to your feed') below the bar. 'Across Discover' now
holds only the genuinely page-wide controls: Hide owned + Languages. Markup/CSS only — the
data-vdsc-myprov hook is unchanged.
2026-06-23 09:53:36 -07:00
BoulderBadgeDad
1feb34621e video discover: cascade rail cards in instead of one block flash
Each rail showed a shimmer skeleton then revealed the whole row in a single fade — on a
big library with hide-owned on (which pages deep server-side) that reads as a long blank then
a pop. Cards now stagger in left-to-right via a per-card --i index (capped) + a 'backwards'
fill-mode keyframe (so the entrance animation releases and :hover transforms still work).
Applies to lazy rails (fillShelf) and the prepended personalized rows (staggerWithin).
2026-06-23 09:32:18 -07:00
BoulderBadgeDad
73395b9668 video discover: give the Browse panel its own language filter (self-contained search)
The Browse-all grid silently inherited the global rail language preference, so users
couldn't ad-hoc browse foreign cinema without changing their homepage prefs. Added a
language chipset to the Browse panel (auto-wired via the generic chip handler -> state.sel.lang)
and the grid now always sends lang= : a real code filters, 'any' opts OUT of the rail
preference entirely. Route treats lang=any as 'no language filter'. Added a 'Browse the full
catalog' eyebrow so the panel reads as a self-contained search, parallel to 'Across Discover'.

Hide-owned and the saved 'My services' pref stay single/global by design (they apply page-wide);
the Browse panel's provider chips remain its own grid filter.
2026-06-23 09:26:22 -07:00
BoulderBadgeDad
dffc5c2e5d video discover: lift page-wide controls into a distinct 'Across Discover' bar
Hide-owned + language + my-services were inside the Browse-all filter panel, making them read
as grid-only when they actually affect every rail. Moved them into a labelled 'Across Discover'
strip above the Browse panel (JS finds them by data-attribute, so no logic change). Browse panel
now holds only its grid filters (kind/sort/genre/source/decade).
2026-06-23 09:19:19 -07:00
BoulderBadgeDad
5906576a13 video discover phase 3: 'On your streaming services' rail
A saved streaming-services preference drives a personalized 'On your streaming services' rail:
- GET/POST /discover/providers-pref (TMDB provider ids); /discover/list OR-joins multiple
  providers (comma->pipe) into with_watch_providers.
- A 📺 multi-select in the toolbar (Netflix/Prime/Disney+/Max/Apple TV+/Hulu/Paramount+/Peacock);
  selecting services saves the preference and rebuilds the rails.
- The rail (high priority, after taste) appears once you've picked services, showing what's
  streaming on yours.
2026-06-23 00:33:35 -07:00
BoulderBadgeDad
6734b6ab22 video discover phase 4 (UI): 'Not interested' card button + ignore-list modal
- A ✕ 'Not interested' button on every un-owned Discover card (hover) — adds to the ignore list
  and fades the card out instantly.
- A '🚫 Ignore List' button top-right of the hero opens a vibey glassmorphic modal: a header
  explaining what it is, a search box to hide any movie/show directly (TMDB search), and a poster
  grid of everything hidden with one-click 'Un-hide'. Empty state guides the user.
- Card button + modal both POST /discover/ignore; ignored titles vanish from every rail (via
  _stamp_owned). Video-only, additive.
2026-06-23 00:29:46 -07:00
BoulderBadgeDad
d019e3aca1 video discover: hidden-gems rails (highly-rated, non-blockbuster)
Add 'Hidden Gems' (movies) + 'Critically Acclaimed Shows' rails — vote_average.desc with the
backend's vote_count floor filtering out single-vote noise, and the language preference applied.
Slot them above the decade/foreign rails.
2026-06-23 00:13:19 -07:00
BoulderBadgeDad
f1b6fd5e31 video discover: language preference UI (multi-select chips)
A 🌐 multi-select chip row in the Discover toolbar (EN/KO/JA/ES/FR/HI/DE/IT) to pick which
original languages appear in the general/curated rails. Loads the current preference from
/discover/languages, toggling a chip POSTs the new set and rebuilds the rails (never empty —
at least one stays on). Extracted reloadRails() (now shared by the hide-owned toggle + language
chips). Default EN, so the rails are English unless you opt more in.
2026-06-23 00:11:56 -07:00
BoulderBadgeDad
28fe3d2b0b video discover: preferred-languages filter (keep Bollywood etc. out of general feeds)
The general/curated rails (Popular/Trending/Top Rated + genre/decade) pull TMDB's GLOBAL lists,
flooding feeds with foreign-language titles (Bollywood). Add a multi-language preference:
- _disc_map now carries original_language (+ popularity) on each item.
- discover_languages setting (default 'en'); /discover/list post-filters general/curated rails
  to it (dropping known non-preferred-language titles) and pages deeper to keep rails full.
  Rails with an explicit lang (the dedicated foreign rails) bypass the filter.
- GET/POST /discover/languages to read/set the preference.
- Removed the hardcoded lang=en on general rails (the setting drives it now).
Default 'en' immediately fixes the Bollywood flood; UI to pick languages next.
2026-06-23 00:09:51 -07:00
BoulderBadgeDad
e7b1a239b4 video discover phase 2: blended 'Recommended for you' wall
A single personalized wall aggregating TMDB recommendations across many of your owned titles
(random_owned_titles seeds), ranked by consensus — a title recommended by more of your library
ranks higher (ties by rating then popularity), owned + seed titles excluded.
- core/video/discovery_recs.py: pure blend_recommendations (dedup/consensus/exclude), 7 tests.
- /api/video/discover/foryou aggregates ~12 seeds' recommendations.
- loadForYou() prepends the 'Recommended for you' rail on top of the stack; re-runs on the
  hide-owned toggle.
2026-06-23 00:05:26 -07:00
BoulderBadgeDad
d984895cba video discover: keep foreign titles out of general rails + fix hide-owned sparse rows
Two Discover UX issues:
- Foreign-language titles leaked into the general genre/decade rails. Added an
  original-language filter (with_original_language) through client.discover -> discover_filter
  -> /discover/list (?lang=); the genre/decade/'because you like' rails now pin lang=en, and a
  handful of dedicated foreign rails (Korean/Japanese/Spanish/French/Hindi) house non-English.
- 'Hide owned' + a huge library = nearly-empty rails (a 2-page batch was mostly owned, then
  CSS-hidden to almost nothing). /discover/list now takes hide_owned=1: it drops owned
  server-side and pages DEEPER (up to 8) until a rail has ~24 un-owned. fillShelf passes
  hide_owned when the toggle's on; toggling re-renders the rails (+ personalized rows) instead
  of just CSS-hiding cards.
2026-06-22 23:59:21 -07:00
BoulderBadgeDad
a8ce359cb7 video discover phase 1e: 'what am I missing' gap rails on the discover page
loadGaps() fetches /discover/gaps and prepends the gap rails ('Complete the <franchise>',
'More from <director/creator>') above the rail stack, mirroring loadMoreLike. Cards are the
standard un-owned TMDB cards — already actionable (VideoGet add-to-watchlist/get), so a
missing franchise entry or director film is one click from your queue. Best-effort/additive.
2026-06-22 23:30:05 -07:00
BoulderBadgeDad
d162966cee Video tools: add a Server Scan tool (trigger the media server to index new downloads)
This is the tool originally asked for — DISTINCT from the Library Scan (where
SoulSync reads the server into video.db). Server Scan tells Plex/Jellyfin to
rescan its OWN folders so newly-downloaded files get indexed, then a Library Scan
pulls them in. It's the manual twin of the post-download 'Scan Video Server'
automation, and targets Movies / TV / both like the Library Scan.

- POST /api/video/scan/server {media_type} -> refresh_video_server_sections (trigger)
- GET /api/video/scan/server/status?media_type -> {scanning:true|false|null} (live poll)
- new Server Scan card on the video Tools page + video-server-scan.js controller,
  mirroring the music live-status UX (phase + working bar); resumes if the page
  opens mid-scan. Server scans have no % (Plex doesn't report one) so the bar is a
  working indicator. Both backend functions already existed + are media-type aware.

Seam tests: trigger threads media_type (movie / default all), status reports the
scanning flag (True / null passthrough), and the blueprint exposes both routes.
2026-06-22 08:26:24 -07:00
BoulderBadgeDad
70ad5af378 Video tools: fix Library Scan card layout — wrap the 3 controls
Adding the Movies/TV target gave the scan card three controls (target + mode +
button), one more than music's two, overflowing the shared no-wrap flex row and
clipping the Scan Library button past the card edge. Scoped CSS on the video Tools
page lets the row wrap: two selects share the top row, the button takes its own
full-width row. Music's .tool-card-controls is untouched.
2026-06-22 08:20:47 -07:00
BoulderBadgeDad
b26f664326 Video tools: scan a Movies OR TV library (not just both)
The video Library Scan tool only scanned 'all' — but movies and TV are
independent libraries (unlike music's single library). The scanner backend
already supported media_type='movie'|'show'|'all'; this just wires it up:
- /api/video/scan/request now reads media_type and threads it to request_scan
- the Tools card gains a target selector (All / Movies Only / TV Shows Only)
  alongside the existing mode dropdown, matching the music scan's UX
- the live status detail reflects the target (no confusing '0 shows' on a
  movies-only scan)

Seam test: the endpoint passes both mode and media_type through (default all/full,
explicit movie/deep, TV-only). Existing scanner media-type/scope tests unchanged.
2026-06-21 23:27:35 -07:00
BoulderBadgeDad
7fac921afc Video show detail (TMDB source): show the Watchlist + Trailer actions
The TMDB-source show detail page rendered an empty action bar — renderActions
early-returned for source='tmdb' (a stale "previews have no actions" assumption that
predates the curated, tmdb_id-keyed watchlist), so the Watchlist button never showed
and the rest was skipped. Now an AIRING show gets the Watchlist button whether it's
owned or a TMDB preview (ended/cancelled stay terminal → no button); Trailer renders
from the payload; Get Missing stays library-only. Also fixed toggleWatchlist sending
a bogus library_id + 404 poster proxy for tmdb previews (data.id is the tmdb id there)
— it now omits library_id and uses the proxied TMDB poster, mirroring the card-hover add.
2026-06-21 14:57:32 -07:00
BoulderBadgeDad
ed2fbf2ae4 Video download history: API + beautiful timeline modal (phase 2)
GET /api/video/downloads/history (paged, ?kind/search/outcome) + /history/<id>, both
returning the live tab counts. New self-contained modal (video-download-history.js,
.vdh-* styles) opened from a History button on the Downloads page: day-grouped
timeline of every grab with poster, title, S/E, quality/resolution/codec/size and an
outcome badge; rows expand in place to reveal the full detail (release, source/uploader,
codecs, dest path, grabbed/finished times, error). Tabs (All/Movies/TV), search,
load-more, and a live count badge on the button.
2026-06-21 14:15:37 -07:00
BoulderBadgeDad
b6320d1a30 Video: two system deep-scan automations (Movie + TV), independently scoped
Video twin of music's 'Auto-Deep Scan Library', split in two because Movies and TV
are separate libraries — scanning the TV library must not pull in new movies and
vice-versa.

- scanner: add a media_type param ('all'|'movie'|'show', friendly aliases) that
  gates the movies vs shows passes (and their pruning), plus an in_progress busy
  guard so the singleton scanner can't be stomped by an overlapping run.
- video_scan_library handler: thread media_type through, skip cleanly when the
  scanner is busy, and name only the scanned library in the summary.
- two system automations (owned_by=video, weekly deep scan, staggered start delays):
  'Auto-Deep Scan Movie Library' + 'Auto-Deep Scan TV Library'. Distinct action
  types (video_deep_scan_movies / _tv) because the seeder keys on action_type; both
  reuse the one handler, scoped via action_config.
- builder block gains a Library selector (Movies+TV / Movies / TV) so custom scans
  can scope too; card label/icon maps cover the video action types.

Seam tests for scanner scope + busy guard, handler scope + skip, registration set.
2026-06-21 12:14:46 -07:00
BoulderBadgeDad
36f944be25 Video automations: make the builder sidebar scrollable like the music side
The Automations page reuses the music builder, whose .automations-builder-view is
height:100% so its trigger/action sidebar + canvas scroll independently. On music it
fills #automations-page (a .page at height:100%); on video it sat in a .video-subpage
with no height, so height:100% collapsed to content height and the sidebar grew
instead of scrolling (looked unformatted). #video-page-host is itself a .page, so
give just the automations subpage a definite height to resolve the chain. Scoped to
automations so every other video page keeps its natural document-flow scroll.
2026-06-21 11:55:42 -07:00
BoulderBadgeDad
8053bf339c Video wishlist + watchlist: restyle toolbar controls to the video-side standard
The Movies/TV/YouTube (and Shows/People/Channels) tabs, search bar, sort select and
clear-all read as generic dark glass. Align them to the video side's polished
language: selected tab now lights up with an accent outline + ring glow (the same
focus treatment as the search field) instead of a filled accent block; search is a
focus-ring shell with an accent icon; sort drops the native OS arrow for a custom
chevron; every control shares one 42px height + 12px radius + accent-ring focus.
Same treatment applied to the watchlist page so the two match.
2026-06-21 11:05:28 -07:00