feat(anime): add anime browser powered by Aniyomi extensions

- Add `subminer anime` / `--anime` and a tray entry to open a browser that searches installed Aniyomi extension sources, shows cover art and episodes, and plays into mpv with overlay/mining attached
- Add an Extensions tab to add repos and install/update/remove sources, and per-source settings for sources needing config
- Support searching all sources at once with streaming, per-source results and status
- Prefer Japanese audio/subtitle tracks from the source and keep the primary subtitle slot reserved for Japanese
- Fix window/tray/Dock handling so the browser and mpv can be switched between without quitting the app or losing the Dock icon
- Add anime.repos, anime.extensionsDir, anime.preferredQuality config keys (no bundled repos or discovery)
This commit is contained in:
2026-07-31 17:16:49 -07:00
parent b204d4dd6e
commit e64ff1a0ee
117 changed files with 7565 additions and 529 deletions
@@ -46,16 +46,16 @@ Add to `stats/src/types/stats.ts` and the backend query module:
```ts
type LibrarySummaryRow = {
title: string; // display title — anime series, YouTube video title, etc.
watchTimeMin: number; // sum(total_active_min) across the window
videos: number; // distinct video_id count
sessions: number; // session count from imm_sessions
cards: number; // sum(total_cards)
words: number; // sum(total_tokens_seen)
lookups: number; // sum(lookup_count) from imm_sessions
title: string; // display title — anime series, YouTube video title, etc.
watchTimeMin: number; // sum(total_active_min) across the window
videos: number; // distinct video_id count
sessions: number; // session count from imm_sessions
cards: number; // sum(total_cards)
words: number; // sum(total_tokens_seen)
lookups: number; // sum(lookup_count) from imm_sessions
lookupsPerHundred: number | null; // lookups / words * 100, null when words == 0
firstWatched: number; // min(rollup_day) as epoch day, within the window
lastWatched: number; // max(rollup_day) as epoch day, within the window
firstWatched: number; // min(rollup_day) as epoch day, within the window
lastWatched: number; // max(rollup_day) as epoch day, within the window
};
```
@@ -21,6 +21,7 @@ Out of scope for this pass: English-token ingestion cleanup and Overview stat-ca
## Files touched (inventory)
Dashboard (`stats/src/`):
- `components/library/LibraryTab.tsx` — collapsible groups (item 1).
- `components/library/MediaDetailView.tsx`, `components/library/MediaHeader.tsx` — delete-episode action (item 4).
- `components/sessions/SessionsTab.tsx`, `components/library/MediaSessionList.tsx` — episode rollup (item 2).
@@ -31,6 +32,7 @@ Dashboard (`stats/src/`):
- New file: `stats/src/lib/session-grouping.ts` + `session-grouping.test.ts`.
Backend (`src/core/services/`):
- `immersion-tracker/query-trends.ts` — extend `TrendRange` and `TREND_DAY_LIMITS` (item 3).
- `immersion-tracker/__tests__/query.test.ts` — 365d coverage (item 3).
- `stats-server.ts` — passthrough if range validation lives here (check before editing).
@@ -93,9 +95,10 @@ Within each day, sessions with the same `videoId` collapse into one parent row s
### Implementation
- New helper in `stats/src/lib/session-grouping.ts`:
```ts
export interface SessionBucket {
key: string; // videoId as string, or `s-${sessionId}` for singletons
key: string; // videoId as string, or `s-${sessionId}` for singletons
videoId: number | null;
sessions: SessionSummary[];
totalActiveMs: number;
@@ -104,6 +107,7 @@ Within each day, sessions with the same `videoId` collapse into one parent row s
}
export function groupSessionsByVideo(sessions: SessionSummary[]): SessionBucket[];
```
Sessions missing a `videoId` become singleton buckets.
- `SessionsTab.tsx`: after day grouping, pipe each `daySessions` through `groupSessionsByVideo`. Render each bucket:
@@ -132,11 +136,13 @@ Within each day, sessions with the same `videoId` collapse into one parent row s
### Backend
`src/core/services/immersion-tracker/query-trends.ts`:
- `type TrendRange = '7d' | '30d' | '90d' | '365d' | 'all';`
- Add `'365d': 365` to `TREND_DAY_LIMITS`.
- `getTrendDayLimit` picks up the new key automatically because of the `Exclude<TrendRange, 'all'>` generic.
`src/core/services/stats-server.ts`:
- Search for any hardcoded range validation (e.g. allow-list in the trends route handler) and extend it.
### Frontend
@@ -202,11 +208,7 @@ Merge Word + Reading into a single column titled "Word". Reading sits immediatel
```tsx
<td className="py-1.5 pr-3">
<span className="text-ctp-text font-medium">{w.headword}</span>
{reading && (
<span className="text-ctp-subtext0 text-xs ml-1.5">
【{reading}】
</span>
)}
{reading && <span className="text-ctp-subtext0 text-xs ml-1.5">【{reading}】</span>}
</td>
```
where `reading = fullReading(w.headword, w.reading)` and differs from `headword`.
@@ -230,6 +232,7 @@ Merge Word + Reading into a single column titled "Word". Reading sits immediatel
### Target behavior
After `ankiNotesInfo` resolves:
- Drop `noteId`s that are not in the resolved map.
- Drop `cardEvents` whose `noteIds` list was non-empty but is now empty after filtering.
- Card events with a positive `cardsDelta` but no `noteIds` (legacy rollup path) still render as `+N cards` — we have no way to cross-reference them, so leave them alone.
@@ -255,6 +258,7 @@ After `ankiNotesInfo` resolves:
### Current behavior
`TrendChart.tsx`, `StackedTrendChart.tsx`, and `WatchTimeChart.tsx` render Recharts components with:
- No `CartesianGrid` → no horizontal reference lines.
- 9px axis ticks → borderline unreadable.
- Height 120 → cramped.
@@ -270,6 +274,7 @@ All three charts share a theme, have horizontal gridlines, readable ticks, and s
### Implementation
Extend `stats/src/lib/chart-theme.ts` with the additional shared defaults (keeping the existing `CHART_THEME` export intact so current consumers don't break):
```ts
export const CHART_THEME = {
tick: '#a5adcb',
@@ -299,6 +304,7 @@ export const TOOLTIP_CONTENT_STYLE = {
```
Apply to each chart:
- Import `CartesianGrid` from recharts.
- Insert `<CartesianGrid stroke={CHART_THEME.grid} {...CHART_DEFAULTS.grid} />` inside each chart container.
- `<XAxis tick={{ fontSize: CHART_DEFAULTS.tickFontSize, fill: CHART_THEME.tick }} />` and equivalent `YAxis`.