Skip to content

Auto-download newly aired episodes for subscribed shows #76

Description

@akvasha

Priority: Medium | Effort: Medium

Motivation

Today, watching a currently-airing show is a manual loop: episode airs → check CalendarView → open AnimeDetailView → wait for the new episode row to populate → click Download. For users tracking 5–10 simultaneous airing shows, this means returning to the app every few days and repeating the same chore for each one.

We already have all the building blocks: shikimori:get-calendar knows which tracked shows have new episodes (and when), DownloadManager knows how to queue, and animePrefs remembers each show's preferred translation. Glue them together so that opting a show into "auto-download" makes new episodes show up on disk without any further interaction.

Approach & Architecture

A new main-process worker (src/main/auto-downloader.ts) that:

  1. Reads a per-show subscription map from a new autoDownloadSubscriptions electron-store key.
  2. On each tick, walks every subscribed show, fetches the latest aired episode count (via existing shikimori:get-calendar cache + shikimoriUserRates cache — Shikimori episodes_aired is the source of truth), and identifies forward-only newly-aired episodes (those with episodeInt > subscription.lastSeenEpisodesAired).
  3. For each newly-aired episode, resolves a DownloadRequest exactly like the manual path does: pick translation via priority chain (subscription.translationType/authoranimePrefs[animeId] → global defaults), call getEpisode(animeId) to find the matching translation row, probe its real quality with probe-embed-quality, build a DownloadRequest and feed it to the existing download:enqueue handler.
  4. Dedupes against downloadedEpisodes[animeId:episodeInt] (already-downloaded), the live download-manager queue (already-queued), and an in-memory "this tick already enqueued" set to prevent overlapping ticks from double-queueing.
  5. Updates subscription.lastSeenEpisodesAired after a successful enqueue so subsequent ticks don't re-queue the same episode even if the download later fails.

Tick triggers (in OR fashion, all gated by a single 60s reentrancy lock):

  • App start (~30s after app.whenReady fires, after Shikimori rates load)
  • Periodic timer every 15 minutes
  • Reactive: on shikimori:rates-refreshed (the user just refreshed their watchlist) and on shikimori:get-calendar cache invalidation
  • Manual: a "Run now" button in Settings > General > Auto-download

Rate-limit / safety guards:

  • MAX_ENQUEUES_PER_TICK = 10 — a hard cap on episodes queued in a single worker run, so a misconfigured lastSeenEpisodesAired (or a show that suddenly returns 200 episodes_aired) can't dump 100s of items into the queue.
  • The forward-only design (subscription stamps lastSeenEpisodesAired = current episodes_aired at subscribe time) means newly-subscribed shows never backfill — only episodes that air after the subscription click are queued.
  • Per-show subscription disable also pauses any in-flight queued items for that show? No — disabling stops future enqueues; existing queue items keep flowing. (Symmetric with manual downloads.)

Subscription record shape (autoDownloadSubscriptions[animeId]):

interface AutoDownloadSubscription {
  animeId: number
  malId: number
  subscribedAt: number          // epoch ms
  lastSeenEpisodesAired: number // bumped after each successful enqueue
  translationType?: string      // optional override; falls back to animePrefs → global
  author?: string               // optional override; falls back to animePrefs → global
}

UI / UX Considerations

  • AnimeDetailView: a new subscribe toggle button next to the existing star/library button — a small pill labeled "Auto-download" with a clock/repeat icon. Tooltip explains the forward-only behavior ("New episodes will be downloaded automatically as they air."). Visible only for ongoing shows (i.e. when the show has episodes_aired < total_episodes or is ongoing/anons on Shikimori). Greyed out (with explanation) when the user is not connected to Shikimori — the calendar pipeline depends on it.
  • CalendarView: subscribed shows get a small ↻ chip on their card so the user can see at a glance which entries will auto-download.
  • Settings > General > Auto-download (new section):
    • Master toggle: "Auto-download new episodes for subscribed shows" (default off the first time the user opens the app post-feature; on once they subscribe to anything — see migration note in Risks).
    • "Run now" button (calls auto-dl:trigger).
    • Read-only counter: "X shows subscribed", clicking opens a small modal with the list (anime name + last-checked timestamp + per-show unsubscribe button).
  • Notifications: piggyback on the existing notificationMode setting — the system notification that fires when a download completes already labels the show, so no new notification UX is needed. We add an in-flight broadcast auto-dl:enqueued so the user sees a brief toast when a subscription kicks in (only when the app is foregrounded).

Implementation Plan

  1. Main Process (src/main/):
    • New file src/main/auto-downloader.ts exporting runAutoDownloadTick(opts: { reason: 'startup'|'timer'|'rates-refreshed'|'calendar-refresh'|'manual' }). Implements the tick logic, single 60s reentrancy lock, MAX_ENQUEUES_PER_TICK cap, dedupe set.
    • Translation/quality resolver helper: given (animeId, malId, episodeInt, subscription), returns a DownloadRequest or null (and a reason: no-translation / not-on-smotret / already-downloaded / already-queued / embed-failed).
    • Wire up triggers in src/main/index.ts: setInterval(15 * 60 * 1000), post-whenReady startup tick, listener bumps on the IPC paths that already touch shikimoriUserRates/calendar cache.
    • New IPC handlers: auto-dl:get-subscription (animeId), auto-dl:set-subscription (animeId, enabled, optional overrides), auto-dl:list-subscriptions, auto-dl:trigger, auto-dl:get-status (last-tick timestamp + result counters).
    • New broadcast: auto-dl:tick-result ({ enqueued, skipped, errors, ranAt }) so the renderer can refresh the status panel without polling.
    • New broadcast: auto-dl:enqueued ({ animeId, episodeInt, animeName }) for the foreground toast.
    • New store key + default in electron-store schema.
  2. Preload & IPC (src/preload/):
    • Note: update both src/preload/index.ts and src/preload/types.d.ts for every new IPC channel above.
    • Expose autoDlGetSubscription, autoDlSetSubscription, autoDlListSubscriptions, autoDlTrigger, autoDlGetStatus.
    • Expose listener helpers onAutoDlTickResult, onAutoDlEnqueued.
  3. Renderer (src/renderer/):
    • AnimeDetailView.vue: add subscribe toggle button, hydrate state from autoDlGetSubscription, save with autoDlSetSubscription. Hide for shows without a malId (we can't resolve airing data) and for shows already finished. Show a small "Will catch new episodes from EpN onward" hint under the toggle, where N = current episodes_aired + 1.
    • CalendarView.vue: query autoDlListSubscriptions once per load; render the ↻ chip on cards whose animeId is subscribed.
    • SettingsView.vue: new "Auto-download" section under General — global toggle, "Run now", counter + modal listing subscriptions.
    • Top-level toast handler in App.vue (or wherever existing toasts live) for auto-dl:enqueued.

Files to Touch

  • New: src/main/auto-downloader.ts
  • Modify: src/main/index.ts (IPC handlers + tick wiring + store schema entry)
  • Modify: src/preload/index.ts
  • Modify: src/preload/types.d.ts
  • Modify: src/renderer/src/components/AnimeDetailView.vue
  • Modify: src/renderer/src/components/CalendarView.vue
  • Modify: src/renderer/src/components/SettingsView.vue
  • Modify: src/renderer/src/App.vue (toast hookup, if needed)
  • Documentation: DESIGN.md (new IPC channels, new store keys, new auto-downloader section)

Testing Strategy

  • Forward-only stamp: subscribe to an ongoing show with episodes_aired = 5. Wait for next episode. Verify only ep 6 is queued, never eps 1–5.
  • Translation resolution: subscribe to a show after manually downloading ep 5 with voiceRu/AniDub. Verify ep 6 picks up the same translation, not the global default.
  • Override path: set per-subscription translationType = subRu. Verify ep 6 gets subRu even if animePrefs says otherwise.
  • Dedupe: manually queue ep 6, then run auto-dl:trigger. Verify the worker skips ep 6.
  • Already downloaded: have ep 6 on disk, episodes_aired jumps to 7. Verify only ep 7 is queued.
  • Rate cap: simulate a subscription where lastSeenEpisodesAired = 0 and episodes_aired = 50. Verify only MAX_ENQUEUES_PER_TICK items are queued in one tick, and the next tick picks up where it left off.
  • No Shikimori: log out; verify the subscribe button is disabled with a clear "Connect to Shikimori" message and ticks become no-ops without errors.
  • Show not on smotret-anime: subscribe to an MAL-only entry (no malIdMap resolution); verify the worker logs "not-on-smotret" and doesn't retry every tick (de-prioritize via a backoff field on the subscription).
  • Reentrancy: kick auto-dl:trigger 5 times rapidly. Verify only one tick runs and the rest are coalesced.
  • Cross-platform: Windows/Mac/Linux paths shouldn't matter (worker is renderer-process-agnostic), but verify notifications fire on each.

Risks & Edge Cases

  • Embed URL freshness: smotret-anime stream URLs expire. The auto-downloader runs the embed probe inline at enqueue time (same as manual path), so this is fine — but if processQueue defers a queued item by hours, the existing 416-retry path must handle the URL having gone stale. Verify on a delayed queue.
  • Calendar API drift: if Shikimori reports episodes_aired jumping non-monotonically (rare, but possible on schedule corrections), the worker might re-queue an episode the user already grabbed. Dedupe against downloadedEpisodes covers this.
  • Translation availability lag: a newly-aired episode may show up in the Shikimori calendar before any translation is available on smotret-anime. The resolver returns no-translation and the tick stamps a separate lastTranslationCheckAt field on the subscription so it backs off (e.g. retry once per tick for 24h, then daily) instead of hammering getEpisode every 15 min. Note: this means the lastSeenEpisodesAired stamp is only bumped after a successful enqueue or a successful "already downloaded" detection — never on no-translation.
  • User unsubscribes mid-tick: handled because we read the subscription set fresh inside the lock; an unsubscribe between ticks just removes the show from the next pass.
  • Quota / disk-full: existing download-manager error path is reused; failed downloads surface in DownloadsView as today.
  • Double-firing on rates-refreshed: the 60s reentrancy lock + reason aggregation prevents two ticks from running back-to-back even if multiple triggers fire within the lock window.
  • Concurrent app instances: electron-store is process-local; if the user runs two app instances, both could enqueue the same episode. Out of scope — single-instance lock already exists for the main app, but call it out.
  • Migration: the master Settings toggle should default to on for users who explicitly subscribe to a show — i.e. the first auto-dl:set-subscription call also flips the master toggle on (with a one-shot toast explaining it). This avoids the confusing case where the user subscribes but the global gate silently blocks anything from happening.

Out of Scope

  • Backfill of past missed episodes — explicitly forward-only per the answered scoping question. Could be a follow-up issue ("Catch up since last watched") with its own UX warning.
  • Auto-download for Planned shows — only Watching/Rewatching shows trigger auto-downloads; planned shows still appear in the calendar but don't subscribe-by-default. Subscribing manually to a planned show works but the user has to opt in show-by-show.
  • Per-show schedule overrides ("only download Tuesday episodes") — niche; handle if requested.
  • Auto-merge / auto-move-to-cold gating — these continue to obey their existing global settings; no new wiring.
  • Push notification when subscription falls behind (e.g. 3 days of no-translation) — could be a follow-up if users hit this regularly.
  • Smart quality picker that depends on free disk space — out of scope; uses the existing global default quality.
  • Background ticks while app is closed — Electron only runs while open; users who quit the app won't see auto-downloads until next launch (the startup tick will catch up at that point, bounded by MAX_ENQUEUES_PER_TICK).

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions