Skip to content
UI pluginCSSSettings

TV Guide

Presents your library as live TV channels with a cable-style programme guide. Channels are built from studios, tags, groups or saved filters, and every channel is always broadcasting.

Hippo Chapel
Not reviewed yet

No reviewer has approved this plugin yet. Read the source before installing, or review it for the community.

sha256 3fc8b32c20e49c74f0a5fd044b7454c7ee1eecc3920837af3aa56db2955ab688

Last updated

Oct 3, 2026
First created
Version
1.0-22dbea9
Package id
TVGuide
Commits
38
Repo stars
0

Settings

  • Use 12-Hour Clock booleanShow times with AM/PM throughout TV Guide. Enabled by default. Reload the page after changing this setting.
  • Autoplay Preview booleanPlay the tuned channel in the corner viewer. Disable for a guide-only, no-video view.
  • Show Channel Info Overlay booleanShow channel and scene information in fullscreen. Enabled by default. Disable to hide the overlay while keeping playback controls. Reload the page after changing this setting.
  • Start Muted booleanStart the corner viewer muted. Browsers block autoplay with sound, so disabling this may stop playback from starting on its own.
  • Show Navbar Button booleanShow the TV Guide button in the Stash navigation bar. The guide is always reachable at #tvguide.
  • Demo Mode (SFW) booleanShow fictional text and neutral artwork, hide video and mute audio while preserving TV Guide's layout. Independent of SFW Switch. Masks searches and disables name/logo editing and links to real content. Disabled by default. Reload the page after changing this setting.
  • Minimum Scenes Per Channel numberSources with fewer scenes than this are not turned into channels. Default 5.
  • Guide Window (hours) numberHow many hours of programming the guide grid shows at once. Default 3.
  • Scenes Per Channel numberMaximum scenes in a channel's daily rotation. Larger catalogs rotate through different batches each day. Lower values load faster. Default 100.
  • New Release Window (days) numberInclude scenes whose release date is within this many days. Default 30.
  • Recently Added Window (days) numberInclude scenes added to Stash within this many days. Default 14.
  • Movie Minimum Length (minutes) numberScenes longer than this are included in the Movies channel. Default 90.
  • Short Maximum Length (minutes) numberScenes shorter than this are included in the Shorts channel. Default 5.
  • Short Scene Grouping (minutes) numberConsecutive scenes shorter than this appear as one guide block. Default 15.

README

TV Guide

Presents your Stash library as live TV channels with a cable-style programme guide. Every channel is always broadcasting: open the guide and something is already halfway through, with a corner viewer playing it at its true live position.

How the "live" part works

The schedule is a pure function of (channel, scene pool, wall clock). Nothing is stored, and nothing is randomised per session:

order  = seededShuffle(scenes, hash(channelId + '2026-08-22'))
cursor = (now - local_midnight) % totalRuntime

So reloading the page keeps the same programme playing, at an offset that has advanced by exactly the time that passed. Two browsers looking at the same channel agree without talking to each other.

The broadcast day runs from local midnight. Both the shuffle seed and the playback cursor pivot on it, so they roll over together and the day restarts cleanly instead of jumping mid-programme. A channel holding less than a day of programming simply loops — which is what cable does anyway.

Channels

Press Channels in the toolbar (or c) to open the channel manager: browse every studio, tag, group and saved filter in your library, plus the built-in New releases, Recently added, Movies, and Shorts channels; then choose what becomes a channel.

Two independent things are stored, deliberately apart in TV Guide's Stash plugin configuration, so they follow the same Stash instance across browsers:

  • The lineup (tvguide_lineup) decides which channels exist.
  • Prefs (tvguide_channel_prefs) decide how a channel is presented — pinned, hidden, renamed, re-badged, or given its own scene cap.

They are separate because prefs are keyed by channel id: rename a studio, turn its lineup rule off and back on, and the rename is still there. Storing them together would lose it.

The lineup

TV Guide follows the community SFW Switch plugin when it is installed: media and scene/channel text blur with its toggle, and its “never unblur” option also applies in the guide and fullscreen player. Playback controls stay readable. SFW Switch handles its optional audio muting; TV Guide reflects the actual mute state without overwriting your saved mute preference.

The separate Demo Mode (SFW) plugin setting presents the guide with fictional titles, descriptions and names, geometric artwork and a silent Demo preview panel over the video. Sample content is generated locally and stays consistent across views and reloads. Studio logos become fictional wordmarks with matching studio names in the guide, details and player. Real media stays hidden on hover and in fullscreen; times, progress, channel surfing and layout remain intact. SFW Switch is not required. Search input is masked and still searches real names. Name/logo editing, unmuting and links to real content are disabled in the guide while demo mode is on. Reload after changing the setting. Existing demo-mode preferences are preserved when settings keys are migrated. This presentation filter does not change library data or saved metadata, and only applies inside TV Guide; the surrounding Stash interface is unchanged.

A list of source entries — studios, models (performers), tags, groups and saved filters mix freely in one guide. The Special section contains five individual channels, which are explicit picks just like any other channel:

[
  { "source": "studio", "minScenes": 5 },
  { "source": "performer", "minScenes": 10 },
  { "source": "tag", "ids": ["12", "34"] },
  { "source": "group", "minScenes": 1 },
  { "source": "savedFilter", "names": ["Favourites"] },
  { "source": "special", "ids": ["new-releases", "movies"] }
]

Special channels use Stash scene filters: New releases uses a scene's release date, Recently added uses when it was added to your library, Movies matches longer scenes, and Shorts matches shorter ones. They can be added, removed, pinned, hidden, renamed, and customised exactly like other channels. Fresh installations add all four automatically; existing saved lineups are left unchanged.

All Scenes is an optional fifth channel: add it in Channels → Special. It loads a lightweight index of scene IDs, titles, and durations in pages, ignoring global and per-channel scene caps, and plays every scene with a usable duration once per continuous shuffled cycle. The cycle continues across midnight and repeats only after all scenes have aired; it does not track which scenes you personally watched. Its order and timeline are stable across reloads while the library is unchanged. Changing the library can change the cycle when its index is refreshed. Only the index is needed up front and cached for the browser session. Descriptions, artwork, performers, and stream URLs load on demand for the visible guide time window, the current and next scene, and the selected scene. Moving the time window loads its new scenes, with at most two detail requests in flight.

Scene metadata edits made through Stash update the guide from Stash's existing mutation responses, including edits in other open tabs of the same browser. Titles, descriptions, artwork, performers and tags update without polling, extra API requests, reloading channel pools, or interrupting playback. A small session cache of edits also keeps older cached pools from restoring stale text. This requires Stash's exposed Apollo client; edits made by external tools or background jobs cannot be detected until fresh scene data is otherwise loaded. The current schedule's order and durations stay fixed until its normal refresh.

An entry with minScenes is a rule: while it is on, a newly-added studio becomes a channel on its own. An entry with ids is an explicit pick. Removing a rule-swept channel in the manager freezes the rest into explicit picks — otherwise the next resolve would sweep it straight back in.

Every provider reduces its source to the same thing — a SceneFilterType — so one shared fetcher serves all of them and a new source type is one entry in src/domain/providers/index.js.

Saved filters are the interesting case: any filter you can build in the Stash UI becomes a channel. Their stored object_filter uses the UI's own criterion shape rather than the API's, so src/domain/savedFilterCriteria.js converts it.

Ordering and navigation

The guide is always grouped by type — Pinned first, then a collapsible group per source with a count. Within a group you sort by name or scene count.

Hand-ordering 119 channels is not workable, so finding things is done with: a search box, a grouping dropdown built from the sources you actually have, an A–Z rail beside the channel column for the current source group, and a jump to current button. Selecting a grouping expands it if collapsed. A short current date appears to the left of the guide times. The rail has no letters in the custom-ordered Pinned group.

Pins sit in their own group at the top and are drag-reorderable, or moved with the keyboard. The channel column uses a fixed default width of 200px.

Per channel you can override the display name, point it at your own logo URL, hide it from the guide without removing it from the lineup, and give it its own scene cap when the global one is too small for a large studio.

Controls

Key Action
← → Previous / next programme
↑ ↓ Previous / next channel
Page Up / Page Down Pan by one screen
Home / End Jump to the window edges
N Back to now
Enter Watch in the corner viewer
E / Shift+Enter Open the scene at its live position
M Mute / unmute
C Manage channels

The player carries its own controls over the video: play/pause, mute, theater, fullscreen and Watch. Three sizes — corner, theater (full width, guide scrolling below) and fullscreen. When iPad Safari rejects element fullscreen, TV Guide uses the same viewport-filling fallback as Gallery Mode, so the video and its controls remain available; theater remains the alternative that keeps the guide visible.

Click the video or tab to the player to channel surf with ← / →; Space pauses or resumes. Arrow-key changes scroll the guide to the tuned channel; in native fullscreen, the guide catches up when you exit. Surfing follows the visible guide order, wraps at either end, and respects search, filters, hidden channels and collapsed groups. In fullscreen, scroll down or swipe up for the next channel (reverse for the previous channel). Each wheel gesture or swipe changes one channel with a short vertical slide transition (disabled for reduced motion). The next stream may still need to buffer. If the browser rejects a scene's stream, the guide requests compatible alternate streams from Stash and tries them at the current programme offset. If none plays, the player shows an error instead of silently remaining blank. In fullscreen, controls and channel info appear on video taps, mouse movement or channel changes, and hide together after three seconds of inactivity. They stay visible while paused; entering fullscreen during playback leaves them hidden. The overlay shows the channel number, name, studio logo, current programme, start and end times, minutes remaining, performer names (up to two lines), and a description excerpt (three lines by default, adjusting to the available space when resized). Times follow the clock setting, and minutes remaining freeze while paused. Use the minus button to minimize the overlay to the channel name and the plus button to expand it; this preference is saved across sessions. Hover or tap the overlay to reveal its buttons and resize handle; tap again to hide them. The miniplayer resize handle appears the same way when you hover or tap its picture. Keyboard focus also reveals these controls. Drag the expanded overlay's top-right corner to resize it, or focus the handle and use the arrow keys (Shift for larger steps). Its bottom-left corner stays anchored above the controls. To hide it entirely, disable Show Channel Info Overlay in Stash's TV Guide plugin settings and reload the page. Playback controls remain available. Numbers start at 1 in the full lineup and do not change when filtering, collapsing groups or pinning.

Clicking a programme that is not on now previews it as a still and pauses live playback, with a Back to live control. Clicking anything currently live tunes to it instead. | ? | Shortcut help | | Esc | Close |

While the manager is open it owns the keyboard — it is full of text fields, so guide shortcuts stand aside and Esc closes the panel rather than the guide.

The guide is fully operable by mouse, keyboard and touch. It follows the ARIA grid pattern with a roving tabindex, traps focus while open, and announces channel changes through a live region.

On viewports under 768px, or short touch viewports such as a phone in landscape, the time-grid is replaced by a vertical Now/Next list. The video fills the width and theater mode is hidden. Description, models and tags sit behind a default-collapsed Show more button. Expanded details grow naturally, with one scroll area shared by the video, details and channels. Wider tablets keep the grid.

Settings

Setting Default Purpose
guide_01_12_hour_clock on Show AM/PM times throughout the guide; reload after changing
guide_02_autoplay on Play the tuned channel in the corner
guide_03_channel_info on Show channel information in fullscreen
guide_04_start_muted on Browsers block autoplay with sound
guide_05_navbar_button on The guide is always at #tvguide regardless
guide_06_sfw_text off Demo mode with fictional text and hidden media
guide_07_min_scenes 5 Sources below this become no channel
guide_08_window_hours 3 Hours visible in the grid at once
guide_09_pool_cap 100 Maximum scenes in each daily rotating batch
guide_10_new_release_days 30 Release-date age for New releases
guide_11_recently_added_days 14 Library-added age for Recently added
guide_12_movie_min_minutes 90 Minimum length for Movies
guide_13_short_max_minutes 5 Maximum length for Shorts
guide_14_short_scene_minutes 15 Combine consecutive short scenes into guide blocks

Unset settings use the defaults above inside TV Guide. Stash's native plugin settings form may still display an unset boolean as off or an unset number as zero; TV Guide does not override that form. Explicit saved values are respected.

Numbered keys group the six toggles first, followed by numeric settings, using Stash's native alphabetical ordering. Labels stay unchanged. On startup, existing values are migrated from the old unnumbered keys; already-saved numbered values take priority. The migration preserves guide state and other options, and retries on the next page load if saving fails. Unset settings are not written as defaults.

Capped channels advance through their catalog in daily batches instead of always taking the first 100 scenes. The batch stays stable for the broadcast day, and metadata is fetched only for that batch. For an unchanged catalog, all scenes become eligible within ceil(scene count / cap) days; the daily schedule still shuffles those candidates. A short final batch wraps to the beginning. Midnight refreshes the tuned channel immediately and other rows as they become visible. The continuous All Scenes channel is unchanged and remains the option for playing every scene in sequence without a daily reset.

The TV Guide entry uses a TV icon in Stash's main menu and joins the other items inside the hamburger menu on smaller screens.

Development

npm test           # 770 tests
npm run build      # bundles src/ -> tvguide.js + tvguide.css
npm run watch      # rebuild on change
STASH_PLUGIN_DIR=/path/to/stash/plugins/TVGuide npm run sync

tvguide.js and tvguide.css are build output and are gitignored; build_site.sh runs the build before zipping.

Layout

src/
  api/       GraphQL client, queries, settings, pool cache
  domain/    PURE -- scheduling, layout maths, lineup, prefs, providers
  state/     store, reducer, selectors, effects
  ui/        overlay, grid, list, banner, viewer, manager, keyboard, gestures
  index.js   composition root

Data flows one way: events go to the reducer, the reducer returns new state plus effects, effects do the I/O and dispatch more events. Views are pure functions of selectors, which is why the desktop grid and the mobile list are two renderers over one state tree.

domain/ and state/ hold 100% test coverage (90% branches) — that is where the scheduling, the midnight rollover and the keyboard movement live. api/ and ui/ are held to 80/70.

Subtleties worth knowing before changing this:

  • The visible channel list lives in state.channels, not in a selector. MOVE_FOCUS walks it inside the reducer, so if grouping and sorting only happened at render time, arrow-down would land on the wrong row — or inside a collapsed group. state.allChannels is the raw resolved list; state.channelGroups is for rendering; state.channels is the flattened, collapse-aware order.
  • The now-line and gridlines live in a track overlay that starts where the channel column ends, so their left: % is a percentage of the track. They were previously positioned against the whole scroll container with a margin, which put the line progressively too far right.
  • Condensed rows are not to scale. A channel of two-minute scenes collapses to [N before][prev][current][next][N after], laid out for readability. Those rows do not line up with the clock; the live highlight identifies what is on.
  • The keyboard handler runs in the capture phase and stops propagation on keys it owns, so any control that interprets arrows itself must be listed in handlesOwnKeys — otherwise it never receives them.

More from hippochapel/hippo-stash-plugins

  • Sprite TabAdds a tab to the scene page displaying the full sprite sheet.
  • Gallery ModeAdds a gallery mode button to the video player toolbar for viewing a scene like a photo gallery. Pairs well with the SpriteTab plugin.
  • Theater ModeAdds a theater mode button to the video player that expands the video to the full page width.