feat: add AniList browsing with on-demand ZokoAnime playback
This commit is contained in:
@@ -1,13 +1,13 @@
|
||||
# tubular
|
||||
|
||||
A modal terminal YouTube and Miruro browser built with Rust and [Ratatui](https://ratatui.rs/). Search for videos or anime episodes, organize YouTube channels and saved Miruro shows with overlapping tags, and open a selection in Terebi.
|
||||
A modal terminal YouTube and AniList browser built with Rust and [Ratatui](https://ratatui.rs/). Search videos or anime catalog titles, organize channels and saved shows with overlapping tags, and open a selection in Terebi. The explicitly separate Miruro legacy catalog and library remain accessible.
|
||||
|
||||
## Requirements
|
||||
|
||||
- A recent stable Rust toolchain, including Cargo, to build and install.
|
||||
- [`yt-dlp`](https://github.com/yt-dlp/yt-dlp) on `PATH` for YouTube searches, feeds, and full descriptions; Miruro uses the native API client.
|
||||
- `curl` on `PATH` for YouTube thumbnails and Miruro cover art.
|
||||
- The **Terebi CLI** installed on `PATH`, together with its app bundle and streaming dependencies, for playback. Single videos use `--new-instance --url … --progress-file PATH`; multiple videos require `--new-instance --queue-file PATH` support. Miruro also requires native episode-URL resolution and version-2 episode progress support.
|
||||
- [`yt-dlp`](https://github.com/yt-dlp/yt-dlp) on `PATH` for YouTube searches, feeds, and full descriptions; AniList and legacy Miruro catalog requests use native HTTPS.
|
||||
- `curl` on `PATH` for thumbnails and anime cover art.
|
||||
- The **Terebi CLI** installed on `PATH`, together with its app bundle and streaming dependencies, for playback. Single videos use `--new-instance --url … --progress-file PATH`; queues require `--new-instance --queue-file PATH`. AniList playback requires the ZokoAnime sub resolver and version-2 `anilist` identity progress support; legacy Miruro retains its own resolver and identities.
|
||||
|
||||
The default playback setup requires **macOS 15+**, since Terebi is a native macOS player. Install Terebi separately and follow its dependency/setup instructions; installing Tubular does not install the player.
|
||||
|
||||
@@ -34,20 +34,21 @@ cargo run --locked -- "rust programming"
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
tubular # Choose YouTube or Miruro
|
||||
tubular # Choose YouTube, AniList, or Miruro legacy
|
||||
tubular "rust programming" # Choose a provider, then search this query
|
||||
tubular --miruro "apothecary" # Skip the chooser and search Miruro directly
|
||||
tubular --anilist "Cowboy Bebop" # Native AniList catalog; resolve sub on selection
|
||||
tubular --miruro "apothecary" # Explicit legacy Miruro browsing
|
||||
tubular --player /path/to/terebi "japanese podcast"
|
||||
tubular --help
|
||||
```
|
||||
|
||||
Startup shows a centered **YouTube / Miruro** selector. Use `j` / `k` or arrows to select and Enter to open that provider's separate interface; Esc, `q`, or Ctrl-C quits without opening a browser. Navigation, confirmation and Esc use the configured `selector.select.next`, `selector.select.previous`, `selector.open` and `selector.close` bindings; quit/interrupt bindings also apply. An initial CLI query searches whichever provider you select. `--miruro` bypasses the chooser; `--help` and `--default-config` remain noninteractive.
|
||||
Startup shows a centered **YouTube / AniList / Miruro legacy** selector. Use `j` / `k` or arrows to select and Enter to open; Esc, `q`, or Ctrl-C quits. Configured selector/quit keys apply. An initial CLI query searches only the chosen catalog. `--anilist` and `--miruro` bypass the chooser and are mutually exclusive; `--help` and `--default-config` remain noninteractive.
|
||||
|
||||
After selection, the browser starts in **normal mode**. Press `i` to type a search; Enter submits a changed query and returns to normal mode. Esc returns without submitting.
|
||||
|
||||
In either provider's normal mode, press `p` (`app.provider`) to reopen the chooser with the current provider selected, then Enter to switch. Esc cancels and returns to browsing (unlike at startup); `q` or Ctrl-C quits. Both browser sessions stay in memory, preserving their queries, results, selection and navigation history when you switch back. Switching leaves running players unaffected and never reapplies the CLI query: it is used only for the initially opened provider. `--miruro` only skips the startup chooser; you can still switch providers with `p`.
|
||||
In normal mode, `p` (`app.provider`) reopens the chooser with the current selection. Enter switches; Esc cancels; `q` or Ctrl-C quits. All three browser sessions retain their own queries, results, selection and history. Shared persisted state and pending progress follow the active browser, without mixing provider-scoped collections or show libraries. Switching never interrupts players or reapplies the initial CLI query.
|
||||
|
||||
Both providers use the same layout: a cyan view heading, bordered search and results, a bordered status line, and a dim one-line key hint bar. Entries use two lines: the title above dimmed metadata. YouTube shows the channel and publication date beneath each video title, without separate channel/date columns; tagged channels remain cyan. The selected row uses black text on cyan with a `> ` marker; saved titles are green, watch-later titles magenta, and `✓` marks watched entries. Metadata and help open in centered cyan-bordered popups; bulk-removal confirmations use yellow borders. Miruro keeps its catalog navigation and title/episode metadata.
|
||||
The browsers share a cyan view heading, bordered search/results/status, and a dim key hint bar. Rows place the title above metadata; YouTube channel/publication/duration behavior is unchanged. Saved titles are green, watch-later titles magenta, and `✓` marks watched entries. AniList and legacy Miruro reuse the anime catalog/series/episode interface, cover-art previews, overlapping show tags, checked queues and offline episode collections.
|
||||
YouTube's dimmed metadata row includes duration as `M:SS` or `H:MM:SS`, or `Unknown duration` when unavailable. Duration is retained in saved collections; older entries without duration remain readable.
|
||||
|
||||
YouTube keeps fast flat listings, then resolves visible videos' original-preferred titles asynchronously, one metadata request at a time. Titles replace the listing text left to right at 20 ms per terminal column, using the smallest groups of whole user-perceived characters with matching column widths. Two narrow characters and one wide character therefore swap together after 40 ms, without shifting the untouched suffix or inserting temporary blanks. Only an exhausted shorter title is padded with trailing spaces; that padding disappears when the animation finishes. Offscreen changes apply immediately, and selection stays on the same video. Results are cached by video ID for the session; failures keep the listing title without automatic retries. This adds background yt-dlp requests, and opening video info can make a separate metadata request. “Original-preferred” uses the full extractor response, not a guarantee that YouTube will never return localized text.
|
||||
@@ -77,9 +78,33 @@ Two or more checked videos launch `terebi --new-instance --queue-file '/private/
|
||||
|
||||
Tubular requests the Kitty keyboard protocol's escape-code disambiguation for its entire terminal session and restores the previous mode on exit. Supporting terminals can then distinguish Ctrl+Return from plain Return. A terminal or multiplexer that does not support/forward this protocol may still send identical bytes for both; Tubular cannot distinguish them. In that case, configure another key, for example `[keys.video]` with `enqueue = "alt+e"`.
|
||||
|
||||
### Miruro browser
|
||||
### AniList catalog and on-demand sub playback
|
||||
|
||||
Choose **Miruro** at startup or use `--miruro` to start directly. Its interface is **catalog → series → episodes**, with a separate saved-show library organized by overlapping category tags, without YouTube's Search/Feed tabs or channel controls.
|
||||
Choose **AniList** or use `--anilist`. Search/detail metadata comes directly from `https://graphql.anilist.co` through the existing native TLS client: a 20-second timeout, no redirects, 2 MiB response limit, HTTP/content-type checks and GraphQL-error rejection. Searches return at most eight titles. Nullable artwork, descriptions, scores, planned totals and airing metadata remain unknown rather than being replaced with fabricated values.
|
||||
|
||||
**Provider availability is UNVERIFIED.** A finished show's known total supplies catalog episode numbers; a releasing show's `nextAiringEpisode.episode - 1` supplies an airing/catalog count, separate from its planned total. Unreleased shows have no aired numbers. Missing totals/airing information remain unknown, not “zero episodes.” In an unknown-count series, press `i` (configured search/edit), enter a positive canonical episode number, then Enter to select it. Enter again launches that explicit episode. The initial direct source is **sub only**, never an automatic dub substitute. Numbers do not establish that a stream exists, including for specials or split seasons.
|
||||
|
||||
Rows, metadata and selection prompts label availability UNVERIFIED. Terebi resolves the current selection fresh; unavailable episodes, malformed/protected pages and network failures appear as distinct player errors, while launch/catalog errors remain visible in Tubular. A successful handoff does not mark an episode watched.
|
||||
|
||||
Identities are `anilist:<positive decimal AniList ID>:<positive episode>`, with both numbers bounded by Int32. The stable handoff is `https://zokoanime.video/stream/ani/ID/EPISODE/sub` (`ani`, not the broken `anilist` route). Sources, tokens and subtitle URLs are never stored in collections or playback snapshots. Queues use the same stable URL and per-item private progress files; version-2 reports identify `provider = "anilist"`, decimal-string `media_id`, and numeric `episode`.
|
||||
|
||||
`anilist-library.toml` stores AniList shows/tags separately from the untouched `miruro-library.toml`. Shared episode collection/state files use exact provider-qualified keys and filter each catalog independently; `miruro:1:1` is not `anilist:1:1`. No opaque Miruro IDs, saved show metadata or old playback sessions are mapped or reinterpreted.
|
||||
|
||||
The controls in the legacy table below also apply to AniList, except unknown-count series use `i` for explicit episode selection. Saved AniList shows reopen current native detail by ID. The display limit is 10,000 known catalog numbers; oversized counts report an error instead of truncating silently.
|
||||
|
||||
Read-only native smoke (does not launch playback):
|
||||
|
||||
```sh
|
||||
cargo run --locked --example anilist_probe
|
||||
cargo test --locked
|
||||
cargo run --locked -- --anilist "Cowboy Bebop"
|
||||
```
|
||||
|
||||
The probe searches the real catalog, selects exact AniList ID 1, fetches detail, and emits the UNVERIFIED episode-1 identity/stable URL. It does not prove playback. See `docs/anilist-direct-provider-feasibility.md` for historical native source/media evidence and outstanding AniList terms/distribution and provider-availability caveats.
|
||||
|
||||
### Miruro legacy browser
|
||||
|
||||
Choose **Miruro legacy** or use `--miruro`. This explicitly retained catalog has its own **catalog → series → returned episodes** session and saved-show library; it is never consulted for AniList browsing or playback.
|
||||
|
||||
Catalog and episode-metadata requests use the official Miruro 2.0 mirrors (`miruro.cx`, `.pw`, `.ws`, `.to`, `.tv`, `.bz`), not their legacy `www.` versions. Each session starts with `.cx`, remembers the last working mirror, and tries each mirror at most once when a host is unreachable, blocked, redirected, unavailable, or returns an unexpected Content-Type/unreadable body. Requests have a shared 25-second budget with at most five seconds per mirror; rate limits and other invalid/unauthorized requests stop immediately. No per-request randomization, directory scraping, cookies, or Cloudflare bypass is used. If every mirror is blocked, browsing still fails with the number of mirrors attempted and the last failure. Playback retains Terebi's existing `barelystarted.miruro.tv` URL contract; catalog failover does not add playback-mirror support.
|
||||
|
||||
@@ -291,7 +316,7 @@ Bulk editing refuses to clear all tags when a target has no `[channels]` registr
|
||||
|
||||
## Watch later
|
||||
|
||||
Watch later is **one stored collection**, deduplicated by YouTube video ID or stable Miruro episode identity, with provider-specific views. In YouTube, category lists are derived from saved videos' channel identities and the **current** subscription categories: adding or removing a channel from a category changes its watch-later list after saving tags or reloading config with `r`. Its combined list contains only YouTube videos. Miruro uses the flat episode lists described above. No category memberships are stored on individual videos.
|
||||
Watch later is **one stored collection**, deduplicated by YouTube video ID or exact provider-qualified AniList/Miruro episode identity, with separate provider views. YouTube category lists still derive from channel identities and current subscription categories; anime uses flat provider-local episode lists. No category memberships are stored on individual videos.
|
||||
|
||||
In YouTube, `W` opens a searchable collection selector from any video view or preview. The All categories (combined) entry comes first, followed by configured categories; it remains available even when there are no categories. Navigate with `j` / `k` or arrows in normal mode, or press `i` to filter by name and use arrows / `Ctrl-j` / `Ctrl-k` while typing. `Enter` browses the selected match; with no matches it does nothing. `Esc` returns to normal mode, then closes the selector without navigating. Reopening clears the search. `Alt-w` still opens the current category's watch later directly.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user