From f8334716af6b93b1801e6680d00e0aeeca786072 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sat, 17 Oct 2026 16:43:54 +0200 Subject: [PATCH] feat: save and categorize Miruro shows --- README.md | 17 +- src/keys.rs | 2 +- src/main.rs | 4 +- src/miruro_ui.rs | 761 ++++++++++++++++++++++++++++++++++++++++++++- src/video_state.rs | 401 ++++++++++++++++++++++++ 5 files changed, 1162 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 74ea0d2..a257445 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # tubular -A modal terminal YouTube and Miruro browser built with Rust and [Ratatui](https://ratatui.rs/). Search for videos or anime episodes, organize subscribed YouTube channels with overlapping tags, and open a selection in Terebi. +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. ## Requirements @@ -56,7 +56,7 @@ Each selection opens an independent player instance without replacing existing p ### Miruro browser -Choose **Miruro** at startup or use `--miruro` to start directly. Its interface is **catalog → series → episodes**, without YouTube's Search/Feed tabs, channel controls or category selectors. +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. Catalog autocomplete returns at most eight anime titles, without pagination. Titles show the available format, status, season/year and planned/listed episode counts; press `d` for the selected title's synopsis and other returned facts. The series screen lists only actual returned episodes—not planned or absolute episode numbers—with titles, dates and durations when supplied. Selected episode details appear first in the metadata popup, above the series synopsis. Listed counts do not guarantee that a supported Sun source is available. @@ -67,16 +67,22 @@ Catalog autocomplete returns at most eight anime titles, without pagination. Tit | Enter | Open a series, or resolve a fresh direct **ssub / sun** source and play the episode | | `-` / `+` | Back / forward through cached screens, preserving query and selection | | `w` / `v` | Save the selected episode to watch later / saved episodes | +| `s` | Edit the selected show's category tags; Enter saves the show, including with no tags | +| `c` | Choose All saved shows or a category, then Enter opens a saved show’s episodes | | `m` / `M` | Mark the selected episode as watching / watched (also in metadata) | | `W` / `V` / `C` | Open Miruro's offline watch-later / saved / currently-watching episodes | | `f` | Cycle All / Unwatched / Watched in watch later | -| `x` / `X` | Remove the selected / confirm removal of visible episodes from the active collection | +| `x` / `X` | Remove the selected / confirm removal of visible episodes from the active collection; `x` also removes a saved show from the library | | `d` | Show/hide the synopsis/metadata popup; ↓ / ↑, Ctrl-j / Ctrl-k and page keys scroll; `j` / `k` change selection | | Esc | Cancel input or close help/metadata and return to the episode/title list | | `r` | Refresh the active screen and reload configured keys | | `?` / `q` | Keyboard help / quit | -These actions reuse the existing `[keys]` configuration; live hints and help omit unsupported YouTube actions. Only episodes—not catalog titles—can be saved or have their watch status edited. Reopening a saved episode resolves new URLs; identity is the opaque Miruro catalog ID plus its returned episode number. Each interface shows only its provider's collections, while playback, watched state and storage remain shared. Episode markers show saved/watch-later/currently-watching membership and watched status; Terebi owns resume positions. +These actions reuse the existing `[keys]` configuration; live hints and help omit unsupported YouTube actions. Show saving/tagging uses `channel.tags`, and the library picker uses `category.browse`. From catalog results, a series screen, or the saved-show library, press `s` to edit the **show's** tags: `j` / `k` selects, Space checks/unchecks, `i` filters, and `n` enters a new tag name. Enter stages a new tag; Enter again from the tag list saves the show and all checked tags. Esc cancels without saving. A show can belong to multiple categories or remain saved without tags. The `c` picker starts in normal mode; `i` filters, Enter browses, and Esc leaves filtering before closing. Saved-show lists are alphabetical and stored offline; Enter fetches current episodes using the saved catalog ID and title. `x` removes only the show and its tags, leaving episode collections and watch status unchanged. Categories disappear when no saved shows use them. Miruro's show categories are independent of YouTube's channel tags. + +Space checks categories for combined browsing; Enter opens their deduplicated union even when the filter hides them. With no checks, Enter opens the hovered category or All saved shows. + +Only episodes can have their watch status edited. Reopening a saved episode resolves new URLs; identity is the opaque Miruro catalog ID plus its returned episode number. Each interface shows only its provider's collections, while playback, watched state and storage remain shared. Episode markers show saved/watch-later/currently-watching membership and watched status; Terebi owns resume positions. Stream and external VTT subtitle headers are supplied explicitly; Terebi selects the source's declared default subtitle. Sources requiring media redirects, cross-origin HLS, proxies, iframe playback, or provider-specific playlist rewriting fail explicitly. No provider/language fallback or automatic expired-source refresh is performed. @@ -271,6 +277,7 @@ Files are siblings of `config.toml`: - `saved-videos.toml`: independent versioned saved-video metadata. - `currently-watching.toml`: versioned metadata for unfinished videos, added by playback or marked manually. - `watch-state.toml`: versioned global watched video IDs. +- `miruro-library.toml`: versioned saved-show catalog metadata and overlapping category tags, independent of episode collections. - `playback-sessions/`: private per-launch progress streams, expected identities, metadata snapshots, durable admission/manual-edit markers, and Miruro source documents. State writes are atomic and owner-only. Invalid state files or detected external edits are reported rather than overwritten; repair the file or restart before retrying. Each state file is limited to 8 MiB. @@ -301,7 +308,7 @@ Use `x` for individual removal or `X` for bulk confirmation. Removal neither mar ## Collection removal -Removal controls apply **only in watch-later, saved-video, and currently-watching views**, including previews opened from those lists. Search, Feed, Miruro catalog/episode browsing, ordinary channel/category views, and their previews do not expose removal actions or hints. Text fields and selectors retain their normal controls. +Episode removal controls apply **only in watch-later, saved-video, and currently-watching views**, including previews opened from those lists. The Miruro saved-show library additionally supports `x` to remove a show and its tags, without removing episodes or watch status. Search, Feed, Miruro catalog/episode browsing, ordinary YouTube channel/category views, and their previews do not expose removal actions or hints. Text fields and selectors retain their normal controls. `x` immediately removes only the selected video/episode from the active collection. `X` snapshots the current provider's visible list and opens bulk confirmation: YouTube watch later uses the current category and All/Unwatched/Watched filter, and saved videos uses the current category's entire saved list; Miruro uses its flat list and any active watch-later filter. Currently watching uses the current provider's entire list. An empty list does not open confirmation. Neither interface's removal controls touch the other provider's entries. diff --git a/src/keys.rs b/src/keys.rs index a23b95b..df44ecb 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -129,7 +129,7 @@ actions! { CategoryBrowse, "category.browse", "Browse categories", VIEW, ["c"]; ChannelBrowse, "channel.browse", "Browse subscribed channels", VIEW, ["u"]; VideoChannel, "video.channel", "Open the selected video's channel", VIEW, ["U"]; - ChannelTags, "channel.tags", "Edit the selected channel's category tags", VIEW, ["s"]; + ChannelTags, "channel.tags", "Edit the selected YouTube channel's or Miruro show's category tags", VIEW, ["s"]; CategoryTags, "category.tags", "Edit category tags for all channels in this category", VIEW, ["S"]; CategoryRename, "category.rename", "Rename the current or selected category", &[Context::Normal, Context::CollectionNormal, Context::SelectorNormal, Context::TagList], ["R"]; CategorySave, "category.save", "Save the combined category view as a new category", VIEW, ["P"]; diff --git a/src/main.rs b/src/main.rs index 4dd1485..84387ba 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,9 +6,9 @@ //! Startup offers a centered YouTube/Miruro chooser; --miruro starts directly. Browsers start in normal mode. //! A CLI query searches the selected provider after confirmation. Press i to edit. //! `--miruro apothecary` opens the dedicated Catalog → series → episodes interface. -//! Miruro autocomplete returns at most eight catalog titles, without pagination; catalog titles cannot be saved. +//! Miruro autocomplete returns at most eight catalog titles, without pagination; s saves shows with overlapping tags. //! Playback resolves fresh direct ssub/sun sources off-thread and passes a private source file to Terebi. -//! Miruro episode collections retain stable catalog/episode keys, never stream URLs; channel tags remain YouTube-only. +//! Miruro episode collections retain stable catalog/episode keys, never stream URLs; c browses saved shows by category. //! Route titles follow Miruro's Unicode NFD slug helper; unrepresentable titles report an explicit error. //! Category and combined-category queries search their channels' full histories; other views search YouTube. //! Insert: Enter submits a trimmed changed query; clearing a category query restores uploads. diff --git a/src/miruro_ui.rs b/src/miruro_ui.rs index 17d3785..44d3245 100644 --- a/src/miruro_ui.rs +++ b/src/miruro_ui.rs @@ -1,4 +1,5 @@ use std::{ + collections::BTreeSet, io, path::PathBuf, time::{Duration, Instant}, @@ -77,6 +78,10 @@ enum Content { filter: WatchFilter, videos: Vec