Skip to content

feat: add the Airing Schedule utility, read the dashboard's upcoming anime from it, and add its settings - #1497

Closed
revam wants to merge 12 commits into
masterfrom
feat/airing-schedule
Closed

revam wants to merge 12 commits into
masterfrom
feat/airing-schedule

Conversation

@revam

@revam revam commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Why

The server now has an airing schedule service (/api/v3/AiringSchedule/*). Provider plugins (AnimeSchedule.net, Syoboi Calendar, TVmaze and others) supply timed broadcast and streaming airings, with channels, delays and sub/dub tracks. The WebUI had no way to see those airings or to configure the service, and the dashboard's Upcoming Anime widget still read the old Dashboard/AniDBCalendar which was based on AniDB's date-only data.

Where this sits

  • Branch: feat/airing-schedule, against master. It does not depend on the open metadata PRs; where it needed something that only lives in refactor(settings): replace the TMDB tab with settings for every metadata source #1489, it carries a copy (see Overlap to dedupe).
  • Server: needs the server's airing schedule routes from the current core master (6.0 alpha, unreleased). Against an older core the page fails to load.
  • Commits: ten, one per area, so it reads commit by commit:
    1. feat(languages): let callers pick from the languages modal
    2. feat(components): add a compact MultiStateButton and an image position to BackgroundImagePlaceholderDiv
    3. feat(airing-schedule): add an airing schedule page (Part 1, without the season view)
    4. feat(airing-schedule): add a season view (Part 1, Season)
    5. feat(airing-schedule): add an airing schedule settings page (Part 3)
    6. feat(dashboard): read the upcoming anime from the airing schedule (Part 2)
    7. feat(airing-schedule): show unresolved airings (Parts 1 and 2)
    8. feat(airing-schedule): show recently aired episodes in the season view (Part 1, Season)
    9. feat(airing-schedule): add a season section editor (Part 1, Season)
    10. feat(airing-schedule): filter the views by episode type (Part 1)

Part 1: Airing Schedule page

Utilities > Airing Schedule (/webui/utilities/airing-schedule), with Season (the default), Month, Week and Agenda views.

Toolbar and header

  • Panel title: Airing Schedule | <period>; the period slides out and in the direction of travel (forward for later, back for earlier), and swaps at once on a view switch or with reduced motion. The header's right side counts the anime, seasons or airings shown.
  • Toolbar: Refresh (R), My Collection / All Series, Every Channel, Hide Estimates, Show Reruns. Beside it, in boxes of their own that wrap together: the view switcher (Season | Month | Week | Agenda), the navigation (previous, today or current season, next) and a settings gear.
  • The gear opens Airing Schedule Settings, a modal in two tabs. General holds the airing kind (All Kinds by default, left out of the URL), the provider and the channel list (toggles, country badges, counts, search past eight channels, Show All/Hide All). Channel changes last for the session and are never saved; the channels hidden on the settings page start off.
  • Every filter but the channels and Every Channel lives in the URL. Without view in the URL the page opens on the last view used (Redux, for the session). Series without files are always shown (the "Include Series Without Files" option and includeMissing are gone).

Month, Week, Agenda (read from GET AiringSchedule/Calendar, grouped and sorted on the server)

  • Month always draws six whole weeks from the week of the 1st, fetched as one request, so the grid keeps its height; days of other months dim their date and background, not their airings. A day's +N more opens that day's week.
  • Month and week keep their grid while a period loads: each day shows three skeleton entries, then fades its entries in.
  • The agenda is a virtual list on the page's scroll container (headings and entries as rows).
  • One entry per episode and local day, led by the server's preferred airing; with Every Channel on, +N more lists the others in place (a popover in the month's rows). Reruns only with Show Reruns, with a badge. Date-only episodes (AniDB air date) are all-day entries. Delayed airings show at their new slot. Hover shortcut on a channel to hide it, with Undo.
  • Now: a Now badge on an airing while it is on air (AiredAt until EndsAt), tooltip "On air until HH:mm".
  • Local files: the collection's files icon, muted, when the airing's episode has local files (VideoCount), with the count in its tooltip.
  • Unresolved airings: an airing for an episode AniDB does not list yet (IsResolved: false) reads Ep N from its Number, with a muted "Not on AniDB yet" hint, links to its series and never shows the files icon. The same goes for the season cards and the dashboard widget.

Season

  • Slider: a strip of seasons that keeps the shown one centred, two on each side and the outer ones half cut and faded. Moved by a click, the arrow keys or a swipe; next stops at the last announced season. Read from GET AiringSchedule/Season/ByYear without images.
  • Sticky: the slider sticks to the top of the page's scroll container on a frosted bar (data-stuck, judged from the layout on scroll and resize).
  • Season browser: clicking the shown season's pill swaps the anime for every year's seasons (image and count per season, newest year first), with a "Click/Tap to open/close" hint under the pill that flips over and hides while stuck. Opening pushes a history entry (browse=true), so Back closes it; closing steps back over that entry; a pick replaces it, so Back then returns to the page before the browser opened. Esc closes it.
  • Scroll: closing returns to where the page was while the same season is shown; otherwise the page lands just under the slider. Opening while scrolled past the slider keeps it pinned and centres the shown season's year only when it is out of view. Nothing scrolls on load: lists mount where the page already is.
  • Cards (from POST AiringSchedule/Season/{year}/{season}/Sections): the server's sections (TV & Web, Half Length, Continuing, Movies, OVAs & Specials) and order, or the user's layout; empty sections are hidden; countdown to the next new episode, its regular slot and provider icon, the episode length, "Airing now" with "ends in ..." while on air and "Aired 30m ago" after; the files icon; source, overview, top tags, Missing Files / In Collection.
  • Layout: cards in 1 to 3 columns and the browser in 1 to 4, at fixed width steps (getGridColumnCount), with minmax(0, 1fr) so phones never scroll sideways. Both are virtual lists on the page's scroll container.
  • Recently aired: a "Show Recently Aired" switch and its hours (1 to 24, 12 by default) in the gear's modal, kept in WebUI_Settings.airingSchedule.recentlyAired. While on, the current and previous seasons' sections are read with at that many hours back, rounded down to 15 minutes, so a card keeps an episode that just aired ("Aired 2h ago", by the clock) and moves on as the next 15 minute step is read. The season list and the calendar views never send it.
  • Oldest year: in the page's settings modal (the gear), kept in WebUI_Settings.airingSchedule.oldestSeasonYear and sent as fromYear.
  • Section editor: the modal's Sections tab lists the layout's sections to drag into order, each with a summary and a gear that opens an inline editor: title, anime types (none = every type, a rest group), and Continuing and Half Length as Any / Yes / No. Add, remove and Reset to Default. Edits save on their own after 500 ms to WebUI_Settings.airingSchedule.sections (null follows the server default from GET AiringSchedule/Season/Sections/Default, which the first edit copies). Muted hints flag a section after a rest group and a layout with no rest group. The sections read always POSTs { Sections }, null for the default.
  • Episode types: an "Episode Types" multi-select in the General tab, kept in WebUI_Settings.airingSchedule.episodeTypes (null for every type). It is sent as episodeType on the season sections read and as type on the calendar reads; the season list and the dashboard widget ignore it.

Loading and data

  • Skeletons of each view's cards in place of spinners, fading out downwards in three steps (full, faint, gone) under a mask, pulsing unless motion is reduced; they fade in after 150 ms so quick loads never flash them, and the content fades in where they were. The season skeleton fills the view plus a row (at least five).
  • Stale times: calendar and season sections 5 minutes, seasons by year an hour, providers once (shared through AiringProvidersContext). airing:provider.swept and airing:episode.aired invalidate the calendar and the sections.
  • One shared 30-second clock (useNow) for every countdown and on-air mark.

Debug builds only: ?clockOffset=
A signed offset (-90m, 7h40m, -1d6h) shifts the page's clock (still ticking): today, the current season, the default period and the slider. While shifted, the season, years and sections reads send at with the shifted time so the server agrees, and the header shows the shifted clock. Ignored in production builds.

Look first at: src/pages/utilities/AiringSchedule.tsx, src/components/Utilities/AiringSchedule/ (SeasonView, SeasonSlider, SeasonBrowser, SeasonCard, CalendarViews, AiringEntry, Skeleton), src/core/utilities/seasonSlider.ts, seasonGrid.ts, airingSchedule.ts, clock.ts, src/hooks/useNow.ts.

Part 2: Dashboard widget

UpcomingAnime reads GET AiringSchedule/Calendar (date-only episodes included), same size and tabs, as a timeline from 12 hours ago: past date-only episodes first, timed airings in order, future date-only ones last, a "You Are Here" mark at now, aired ones muted. Each card shows the channel and provider icon; reruns are left out; Every Channel adds a +N per card. Its header links to the Airing Schedule. Display Options gained the series shown, days ahead and air times. useDashboardCalendarQuery is removed.

Look first at: src/pages/dashboard/panels/UpcomingAnime.tsx, src/pages/dashboard/components/AiringDetails.tsx, src/components/Dashboard/DashboardSettingsModal.tsx.

Part 3: Airing schedule settings

Settings > Airing Schedule, built like the metadata settings in #1489. Everything but a provider's plugin configuration goes into a draft on SettingsPage, sent by the shared Save and dropped by Cancel; a failed part shows its error and stays in the draft.

  • General Options from the service's schema (DynamicField, via GET AiringSchedule/Configuration).
  • Refresh Anime Airing Soon: AiringSoonWindowHours (1 to 168) and AiringSoonIncludeDateOnly.
  • Airing Preferences: preferred channels (searchable add modal) and tracks (language from the languages modal).
  • Provider Priority: drag-and-drop order; a section per provider for its kinds, sweep interval and plugin configuration.
  • Channel Visibility: hide channels for everyone (PUT AiringSchedule/Channel/Hidden).
  • Studio & Genre Sources: SeasonDetailSourceOrder, with AniDB always listed and never removable.
  • Channels (admin only, saved at once): country badges, aliases (PUT .../Aliases), Merge into... (POST .../Merge) and a Possible Duplicates panel.
  • Languages modal: opt-in selection (single pick, noneLabel) and exclude; existing call sites unchanged.

Look first at: src/pages/settings/tabs/AiringScheduleSettings.tsx, src/components/Settings/AiringScheduleSettings/, src/core/react-query/airing-schedule/draft.ts, src/core/utilities/airingChannels.ts, src/components/Dialogs/AiringChannel*Modal.tsx, LanguagesModal.tsx.

Overlap to dedupe

Shared with #1489 (metadata settings), whichever lands second drops its copy:

Feedback wanted

  • Season as the default view, and All Kinds as the default kind: the right defaults?
  • Settings placement: its own tab next to TMDB (next to "Metadata" once refactor(settings): replace the TMDB tab with settings for every metadata source #1489 lands), or a section of the metadata page?
  • Month and week density: day columns of cards rather than a time grid.
  • Possible Duplicates merges with one click and no confirmation; should it confirm? Should the kept channel be the oldest, or the one with a country or the most aliases?
  • VITE_MIN_SERVER_VERSION is unchanged; no released server has these routes yet, so which build should it require?
  • Keep the dev-only ?clockOffset= in the shipped code, or drop it before merging?

Testing

  • On the tip: pnpm lint, pnpm tscheck and pnpm build clean; pnpm test 251 passed (new: season boundaries, the slider strip and browser history/scroll rules, grid columns, the clock offset, channel search and duplicate groups). No dependency changes.
  • Against a local core with real airings (earlier in the branch): every view at 1920px and 1000px, the filters, date-only entries, provider icons, grouped airings and Every Channel, reruns, season sections and counts, the channel filter (channel= on every read) and hide shortcut, the dashboard widget, and the settings page's draft, Cancel and Channel Visibility save.
  • Settings sections (Studio & Genre Sources, Preferred Channels, Provider Priority, aliases, merging, duplicates, non-admin view) against a stub server; saving them against a real server was not tried, apart from Channel Visibility.
  • To try: open Utilities > Airing Schedule, step through seasons with the slider, open the browser from the pill and use Back; switch to Week and go back a week or two to see the files icon on episodes you have.

Server gaps seen on test data

  • AiringSeries.ShokoID is only set when an airing resolves through a Shoko episode.
  • Some provider schedules carry no AniDB anime, so their airings never reach that anime's card; those anime fall back to their AniDB air dates.
  • Some current anime have neither a linked schedule nor a future AniDB air date, so they show "Airing since".

revam added 12 commits October 6, 2026 16:19
- Added a `selection` option to `LanguagesModal` that hands the picked languages to the caller instead of saving a settings preference, with `multiple: false` for a single pick and `noneLabel` for an explicit "no language" choice
- Added an `exclude` option to leave pseudo-languages out of the list
- Kept every existing call site unchanged
…ion to `BackgroundImagePlaceholderDiv`

- Added a `compact` option to `MultiStateButton` for narrower, lower buttons sized to their labels, for a toolbar
- Added a `position` option to `BackgroundImagePlaceholderDiv` that sets where the image sits in its frame
- Added the Airing Schedule utility, with month, week and agenda views of `GET AiringSchedule/Calendar`, under the utilities menu
- Put the refresh, scope and channel toggles in a toolbar, the view switcher and the period navigation beside it, and the period in the title, sliding in the direction of travel
- Moved the channel filter, airing kind and provider behind a settings gear, kept with the last view for the session
- Kept the month and week grids while they load, with skeletons in each day, and drew only the agenda rows in view
- Refetched the airings on the `airing` feed's sweeps, marked what is on air now and which episodes have local files, and let debug builds shift the schedule's clock
- Added a season view, now the default, listing a season's anime in the server's sections with their next new episode and a countdown, as cards in one to three columns drawn only while in view
- Picked the season from a slider kept in view while scrolling, and browsed every season by year from it, in place of a separate archive view
- Returned to the slider, or to where you were, after picking a season or closing the browser, and kept the slider pinned while the browser opens
- Showed skeleton cards while a season or the browser loads, and refetched the season's anime every few minutes and on the `airing` feed's sweeps
- Added the season view's oldest year to the airing schedule's settings modal, saved to the user's WebUI settings once typing pauses
- Added an `Airing Schedule` settings page with general options, the "Refresh Anime Airing Soon" window, airing preferences, channel visibility, "Studio & Genre Sources", the provider priority and a section per provider
- Kept the page's changes in a draft sent by the settings page's Save with the core settings, the service's own options as a JSON patch on its configuration
- Set each provider's kinds, sweep interval and plugin configuration, and showed its icon and, when it differs, its plugin's name
- Added channel search, aliases, merges and a duplicate check, the preferred channels and tracks, and the season detail sources with `OrderList` and `TextSourcesModal`
- Switched the upcoming anime widget from `Dashboard/AniDBCalendar` to `GET /api/v3/AiringSchedule/Calendar`, without reruns, and removed the unused `useDashboardCalendarQuery`
- Laid the widget out as a timeline from 12 hours ago with a "You Are Here" mark, one card per episode and day with its channel and provider icon
- Added a link from the widget's header to the airing schedule
- Added dashboard options for the widget's days ahead and air times, kept in the WebUI settings
- Added `SequenceNumber` and `IsResolved` to the airing, and `includeUnresolved` to the airing reads
- Labelled an unresolved airing `Ep N` from its `Number`, with a muted "Not on AniDB yet" hint in the entries, cards and the dashboard widget
- Linked an unresolved airing to its series rather than an episode, and showed no local files for it
- Added a "Show Recently Aired" switch and its hours, 1 to 24, to the airing schedule settings modal, saved in `WebUI_Settings.airingSchedule.recentlyAired`
- Read the current and previous seasons' sections with `at` that many hours back, rounded down to 15 minutes, so a card keeps an episode that aired since
- Kept a season's cards while the next 15 minute step is read, which moves them on
- Showed how long ago a card's episode aired, by the clock, in place of "Just aired"
- Split the settings modal into General and Sections tabs
- Added a draggable section list with an inline editor for the title, anime types, continuing and half length, saved as you edit to `airingSchedule.sections`
- The season view always reads its sections by `POST`, with the saved layout or `null` for the server default, and hides the empty ones
- Dropped the section `ID`; sections are keyed by their place in the layout
- Added an "Episode Types" multi-select to the General tab, saved to `airingSchedule.episodeTypes`, `null` for every type
- Sent the types as `episodeType` on the season sections read and as `type` on the calendar read
- Read `Season` and `Season/ByYear` by POST with the stored layout, which joined their query keys
- Showed the left-out hint only for a custom layout without a rest group
@revam

revam commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

Split into three stacked PRs (union reproduces this branch's tree exactly; verified with an empty git diff against 7e5386b3):

Each PR's body states what it shares with the others. The show unresolved airings change is split 1/2 by necessity (AiringDetails.tsx is created in part 2).

@revam

revam commented Oct 7, 2026

Copy link
Copy Markdown
Member Author

Closing in favour of the three stacked parts split from this branch (their union reproduces 7e5386b3 exactly):

The discussion here applies to the parts; please continue it on the relevant PR. The show unresolved airings change is split across #1500/#1501 because AiringDetails.tsx is created in part 2. Reviewers: each part's body lists what it shares with the others, and part 3 repeats the #1489 dedupe note from this description.

@revam revam closed this Oct 7, 2026
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