Terebi
A native macOS player for local media, yt-dlp-supported videos (including YouTube), Miruro episodes and AniList-numbered ZokoAnime sub episodes, with ordered playback queues, saved playback positions, compact mode and Chromecast support.
Build and install
Requires macOS 15+, Xcode Command Line Tools with Swift 6, Python 3.9+ and Homebrew build tools. Build natively on Apple Silicon or Intel; Rosetta and cross-architecture builds are not supported.
brew install pkg-config meson ninja
# Intel Macs also need: brew install nasm
make install
export PATH="$HOME/.local/bin:$PATH"
Open Terebi from ~/Applications or Spotlight. The CLI is installed at ~/.local/bin/terebi; add that directory to your shell's PATH to keep it available.
The first build downloads dependencies. Quit Terebi before updating, then run make install again. To install elsewhere, use make install APPDIR="/Applications" (requires a writable directory) or set PREFIX to change the CLI prefix.
To run without installing, use make run. The built .build/terebi.app is self-contained; no separate FFmpeg, Python or yt-dlp installation is needed at runtime. Builds are ad-hoc signed, not notarized. Bundled licenses and source archives are in the app's Resources; public binary redistribution needs a separate license audit.
Usage
- Local files: use ⌘O, drag files onto the player, or choose Finder's Open With. Multiple files form a queue in natural filename order, with automatic advance. Use View → Show Queue to play, reorder or remove items; + → file icon or dropping files onto the sidebar appends them. The queue lasts only for the session; opening other media replaces it.
- Streams: use Open Stream… (⇧⌘O) or drop an HTTP(S) video link. To append without replacing playback, press the sidebar's +, then the link icon: the URL field opens upwards; Return fetches and adds the video. The link icon toggles the field; Esc dismisses it and cancels a pending fetch. Fetch errors leave the queue unchanged. Links are passed to the bundled yt-dlp without a site allowlist; extraction must return a single streamable video, not a playlist or channel. Miruro keeps its dedicated resolver: links must use
https://barelystarted.miruro.tv/watch/<opaque-id>/<slug>?ep=<episode>with an explicit positive episode number. - Stream queues: integrations can supply an ordered yt-dlp/Miruro queue with
--new-instance --queue-file /absolute/queue.json, or append without interrupting the running player using--enqueue --queue-file /absolute/queue.json(also for a single item). Enqueue starts playback if the player is empty or not running. The same sidebar supports play, reorder, remove, previous/next and automatic advance. Unresolved titles, including the selected row, load serially at lower priority without waiting for playback loading, buffering or quality extraction. YouTube uses a title-only oEmbed request; other sites use yt-dlp metadata and Miruro uses optional episode metadata. Arrival still depends on the site/network; failed lookups retain the supplied label, and full playback metadata takes precedence. Playback sources still resolve fresh when selected or revisited; local-file batches still use natural filename order. yt-dlp items use the original page URL for resume and version-1 progress reports; recognized YouTube video URLs still canonicalize. - Seeking: click or drag the progress track or thumb to seek. The thumb centers on the cursor, clamped to the track endpoints. While mouse-seeking, the segment under the thumb smoothly thickens and returns to normal on release. Hovering a named segment fades its title in above the segment's center. Reduce Motion disables these transitions.
- YouTube chapters: when yt-dlp provides usable chapters, small rounded gaps mark their boundaries in the playback track. Boundaries use the current player's duration; missing or malformed chapter data leaves ordinary playback unchanged. Chapters do not add opening/ending skip buttons. Availability varies, and description-derived timestamps may differ from YouTube's displayed chapters.
- Sponsored ranges: optional SponsorBlock sponsor/skip ranges paint the existing track crimson without adding chapter boundaries or changing chapter gaps. Hover shows Sponsor alongside any chapter title; accessibility also identifies sponsored ranges. Inside a range, Skip Sponsor uses the opening/ending button without a timeout or countdown fill. Clicking seeks to the range's end; automatic skipping is a separate opt-in.
- Miruro metadata: titles and opening/ending intervals load independently of playback when available. Small rounded gaps mark interval boundaries in the slider. Inside a segment, Skip Opening or Skip Ending appears at the top-right below the titlebar for up to ten seconds or until the segment ends; clicking seeks to its end. A light background fill grows as the offer expires, with a short entrance/exit transition; Reduce Motion uses stepped countdown updates without the transition. Ignored offers stay dismissed until the media is closed or replaced. Missing/invalid metadata leaves playback usable, and timings may differ between cuts. Nothing skips automatically.
- Resume: videos resume their saved position; finished videos restart from the beginning. Live and audio-only media do not save progress.
- Compact mode: press P for a floating player; double-click it to restore. Drag noninteractive areas to move the window. The queue starts hidden each time you enter compact mode; ⇧⌘L or gear → Show Queue expands it below the video. Its top fade has a 12-point solid section and a 20-point transition, with the first row starting below both. The same shortcut toggles the normal sidebar, whose visibility is preserved when leaving compact mode. Use the titlebar pin to keep the normal window on top.
- Quality: yt-dlp defaults to SDR up to 1080p when resolution is reported. Missing resolution/codec hints are left to native probing. Use gear → Quality for other reported resolutions or automatic quality.
Keyboard shortcuts
| Action | Default shortcut |
|---|---|
| Play / pause | Space or K |
| Seek backward / forward 5 seconds | ← / → |
| Seek backward / forward 10 seconds | J / L |
| Step one frame backward / forward | , / . |
| Volume down / up | ↓ / ↑ |
| Mute / unmute | M |
| Fullscreen | F or ⌃⌘F; Esc to leave |
| Compact mode | P or ⌃⌘P |
| Show/hide queue (normal or compact) | ⇧⌘L |
| Always on top | ⇧P |
| Close media | ⇧⌘W |
| Quit | Q or ⌘Q |
Chromecast
Open media, select the Cast icon or Playback → Cast to Device, and choose a receiver on the same LAN. Allow macOS Local Network access and incoming firewall connections. Use Find Cast Devices… to retry discovery.
Playback controls and queue navigation work on the receiver. Play on This Mac returns to local playback at the receiver's position. Keep the Mac connected: it relays the media and prevents idle system sleep during playback, while allowing display sleep when casting. Closing the lid or explicitly sleeping the Mac can interrupt playback.
Cast output is limited to 1080p/30 fps; frame stepping and stream quality changes are local-only. Use Cast only on a trusted network. AirPlay is not supported.
Command line
terebi "/path/to/video.mp4"
terebi 'https://youtu.be/jNQXAC9IVRw'
terebi --new-instance --no-config "/path/to/video.mp4"
terebi --enqueue --queue-file "/absolute/private/session/queue.json"
terebi --help
The CLI opens one file or supported stream, reusing the running player by default. With no arguments, it opens or activates the player. --new-instance starts a separate player; --no-config uses built-in settings on a fresh launch. Put these options before the media argument. Before installation, use .build/terebi instead of terebi.
For native episode-URL playback, ordered stream queues, external resolved sources and progress reporting, see the integration contract.
AniList → ZokoAnime direct playback
terebi --url 'https://zokoanime.video/stream/ani/1/1/sub'
The canonical handoff still uses /stream/ani/<AniList ID>/<episode>/sub, not the site's broken documented anilist route. IDs and episode numbers must be canonical positive decimal integers within Int32.max. Before fetching a provider source, Terebi queries AniList's exact idMal and requests ZokoAnime's /stream/mal/<MAL ID>/<episode>/sub route. It never substitutes an AniList number into the MAL namespace or guesses a match by title. A missing mapping is a lookup failure, not evidence of a missing episode. Only sub is admitted; there is no silent dub fallback.
Resolution uses bounded ordinary HTTPS with certificate verification, no cookies/credentials, no redirect following, and no browser or advertising/challenge script execution. AniList's returned catalog ID must match the requested identity; invalid mappings and GraphQL errors are rejected. The same metadata response can supply the playback title, with AniList <ID> · Episode <number> (sub) used when the title is absent. Only the provider's static window.__P Base64/XOR configuration is decoded with the first-party otaku-embed-v1 codec. Poster, download, chapter, view/telemetry and player-script fields are ignored and never fetched.
The existing process-local loopback HLS relay normalizes playlists for the native decoder. This provider alone permits HTTPS playlists/VTT on hls.dramahot.top and HTTPS media resources on that host or the demonstrated hls1.drama1.cfd segment origin (default port only). Every manifest URI is checked before registration and fetching, upstream redirects are refused, and only the fixed public Referer: https://zokoanime.video/ is supplied to those admitted origins. Upstream URLs/tokens remain in memory and are not progress identities. Native global same-origin policy and TLS verification are unchanged; new CDN hosts fail closed until explicitly reviewed. Subtitle redirects retain the existing same-origin-only restriction.
Resume, queue deduplication and per-item reports use anilist:<ID>:<episode>, separately from existing miruro: and YouTube identities. Queues use the existing version-1 queue document, with the watch URL above as url; progress JSON uses the existing version-2 fields with media_identity: {"provider":"anilist","media_id":"1","episode":1}. Old Miruro history is neither migrated nor reinterpreted. Resolved-source files also accept identity.provider = "anilist" with canonical decimal IDs; as with Miruro files, the caller owns the explicitly supplied source and headers.
For browsing clients, the CLI also provides a headless, read-only source check:
terebi --check-source 'https://zokoanime.video/stream/ani/1/1/sub'
# {"version":1,"provider":"zokoanime","track":"sub","status":"available"}
It fetches the exact AniList-to-MAL mapping, a fresh MAL-route embed configuration and the bounded root HLS manifest, using the same resolver, origin policy, HTTP transport and manifest validation as playback. It omits title fields and does not start the app, native decoder, loopback relay or audio; read configuration; download subtitles, nested playlists or segments; or write progress/resume/configuration. The entire check has a 15-second deadline and supports cancellation. No retries, source URLs, tokens or raw resource errors are emitted or persisted.
Exactly one bounded version-1 JSON line is written to stdout. available means fresh source configuration plus an accessible valid root manifest, not guaranteed complete playback. unavailable means only the provider's explicit missing-episode page. Network, access/protection, malformed configuration, manifest, cancellation and deadline failures return failed, never unavailable. All three completed statuses exit zero; bad syntax, unsupported URLs and combinations with other flags exit nonzero without JSON. Only the canonical AniList sub watch URL is supported, not arbitrary sources or legacy Miruro URLs.
Rebuild and reinstall Terebi (make install) before a browsing client uses this command; older installed CLIs do not support it. Clients should treat missing/invalid output as a failed check while keeping ordinary playback available, cache results only for the session, and check selected/visible episodes rather than scanning a whole catalog.
Configuration
The optional config file is ~/.config/terebi/config.toml, or $XDG_CONFIG_HOME/terebi/config.toml when XDG_CONFIG_HOME is absolute. To print all supported settings and key bindings:
terebi --default-config
Copy the settings you want to change into the config file; omitted settings keep their defaults. Settings load at startup, so restart the player after editing. Menu changes are session-only except the SponsorBlock toggles, whose saved preferences override their startup config settings.
The queue shortcut can be rebound with show_queue and hide_queue under [keys]; give both actions the same chord to keep a single toggle.
SponsorBlock
SponsorBlock and automatic sponsor skipping are off by default. Playback → SponsorBlock → Show Sponsored Ranges enables lookups, markers and manual skipping; Automatically Skip Sponsors separately opts into automatic skipping. Both choices are saved across launches. Saved choices override their respective config settings; without them, the startup settings apply:
[playback]
sponsorblock = true
sponsorblock_autoskip = false
Use false (the generated defaults) or omit these settings to default to disabled when no preference is saved; non-boolean values are rejected. Autoskip alone does not enable SponsorBlock lookups. The optional lookup runs independently of playback, only for canonical YouTube videos. Failure leaves playback unchanged with no sponsored markers.
Enabling it adds a connection to sponsor.ajay.app. Requests use a bucket identified by the first four hexadecimal characters of the video ID's SHA-256 hash, reducing disclosure compared with sending the full ID; this is not anonymous. Terebi sends no cookies, account credentials or API key and makes no submissions, votes or view reports.
Ranges require finite, strictly valid bounds inside the actual player duration. A known reference duration that differs from playback by one second or more is rejected. Ranges with an unknown reference duration are retained until the player reports an actual finite duration, then validated before display.
Autoskip seeks to the end of a sponsored range during playing, nonbuffering video playback, not while paused or a seek is pending. Seeking into a range or up to five seconds before its start leaves that range manual-only for that visit, including when playback resumes or metadata arrives later. A later seek replaces that intent; quality changes preserve it. Saved-position restoration gets the same protection. The manual button remains available throughout the range.
Miruro providers
Choose Playback → Miruro → Video Host or Caption Provider in Terebi's menu bar, or use gear → Miruro, to change providers for the session. Choices retain both provider and track: ssub (soft subtitles), sub (hard subtitles), or dub (dubbed audio). Video and caption providers are independent; available choices come from the episode response. These menus do not write the config file.
Optional startup preferences:
[miruro]
video_provider = ""
caption_provider = ""
An empty or omitted preference means Auto. Automatic video selection prefers Japanese Sun, then the returned provider order; each provider's variant order is respected. A bare name such as "bee" prefers that provider's Japanese variant; "bee:ssub" or "sun:sub" selects a specific variant. Dub requires an explicit choice such as "bun:dub"—it is never an automatic language fallback. Dub-only episodes retain their choices in the menu after Automatic reports that no Japanese source is available.
Automatic captions prefer the selected provider/track, then a compatible track from that provider, Sun, and returned order. Caption discovery is independent of video compatibility, including iframe-only providers. Missing preferences fall back to automatic selection. Provider names accept up to 128 ASCII letters, digits, underscores or hyphens, optionally followed by :ssub, :sub or :dub. Unknown config keys and non-string values are rejected.
Native playback supports the returned proxy configuration and HLS query/segment-origin rules used by Bee/Hop. MegaPlay iframe sources used by Bun are extracted natively; no embedded browser, browser cookies or advertising scripts are run. Unsupported embed hosts fail rather than being passed to the native decoder. Source expiry and upstream deployment changes can still interrupt playback.
Limitations
- SDR output only; HDR is not supported.
- No embedded or YouTube captions, manual audio-track selection, rotation or deinterlacing. External WebVTT subtitles are supported for resolved streams.
- yt-dlp support does not guarantee playback: extraction must yield HTTP(S)/HLS media without download-only fragment assembly. Audio-only results with explicit codec metadata are unsupported. Private, age-restricted, region-blocked or sign-in-required videos may fail; browser cookies are not used. Rebuild and reinstall for bundled extractor updates.
- YouTube extraction uses IPv4 to avoid anti-bot failures on affected IPv6 routes. This does not bypass account or regional restrictions; other video sites retain their default network routing.
- Seeking can be slow on long-GOP or unindexed media. Live streams cannot be sought, and expired stream URLs are not automatically refreshed.
- Audio-device changes during playback are not handled.
Development
Install FFmpeg for test fixtures (brew install ffmpeg), then run make check. The suite requires a graphical macOS session and a working audio device; it launches players and plays short test audio. See the Makefile for individual check targets.
For the focused native compact-queue smoke (transparent, noninteractive app window), run TEREBI_CHECK_COMPACT_QUEUE_ONLY=1 make check-startup. make check-window check-config covers dropdown geometry, resizing, metadata aspect changes, restoration and configurable queue shortcuts without displaying windows.
make check-sponsorblock exercises the bounded native lookup and duration validation with local fixtures; .build/sponsorblock-check --live performs read-only public API probes. make check-resolved includes persistent manual sponsor skips, native autoskip, deliberate relative/slider seek protection, saved preferences, opt-in/failure isolation, same-video cancellation, direct/saved/queue/enqueue launches, quality changes and Miruro separation. The native slider regression (.build/cast-playback-check --slider-regression, after building that target) checks crimson painting across chapter gaps, buffering, active emphasis, hover and seeking.
make check-zoko check-reporting check-cli check-stream check-progress covers provider admission/codec/error categories, bounded resources, scoped CDN policy, isolated resume and report identity without launching a GUI. Optional make check-zoko-live resolves current AniList 1 episode 1, loads VTT, and uses the actual native player while paused to decode/step video, seek/decode at 600 seconds, restore position and assert version-2 AniList progress. It creates no windows and does not start audio playback, but production Core Audio still initializes an output device. No tokenized addresses are printed or retained after the check. This is not a full-episode, audio-output, pixel/subtitle-rendering or UI smoke test, nor proof of availability for other catalog entries. Full make check still launches GUI/audio checks.
make check-source exercises the public parser/result contract, error redaction, shared manifest validation, total deadline/cancellation and actual CLI rejection paths without GUI/audio or native-player linking. Optional make check-source-live runs the actual CLI, copied outside its app bundle with an intentionally invalid configuration and isolated state directories: Cowboy Bebop, Black Clover episodes 1/170 and Black Clover Season 2 episode 1 must resolve through their exact MAL mappings, while Cowboy Bebop episode 27 remains unavailable, with no state writes. These source-only probes fetch no segments and are distinct from check-zoko-live's native playback checks. .build/zoko-check --live-black-clover exercises native decoding, ten-minute seek, subtitles, resume and original AniList progress identity for Black Clover.
Acknowledgements
Uses SponsorBlock data under the CC BY-NC-SA 4.0 license. Distribution involving this data must account for its NonCommercial and ShareAlike conditions; obtain separate permission from the relevant rights holders if your intended use needs it. This data credit does not relicense Terebi's application source.