From 65e95038c87e229d65b4a1ca4c21aecbaab726b5 Mon Sep 17 00:00:00 2001 From: inorishio Date: Thu, 16 Apr 2026 09:46:22 +0200 Subject: [PATCH] Planning --- Cargo.toml | 17 ++++ ani-cli.md | 228 +++++++++++++++++++++++++++++++++++++++++++++++++ plan.md | 205 ++++++++++++++++++++++++++++++++++++++++++++ src/.gitignore | 1 + src/main.rs | 3 + 5 files changed, 454 insertions(+) create mode 100644 Cargo.toml create mode 100644 ani-cli.md create mode 100644 plan.md create mode 100644 src/.gitignore create mode 100644 src/main.rs diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..dffba6e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "anime" +author = "inorishio" +version = "0.1.0" +edition = "2024" + +[dependencies] +eframe = "0.34.1" +egui = "0.34.1." +reqwest = "0.13.2" +open = "5.3.3" +tokio = "1.52.0" +serde = "1.0.228" +serde_json = "1.0.149" +indicatif = "0.18.4" +directories = "6.0.0" +anyhow = "1.0.102" diff --git a/ani-cli.md b/ani-cli.md new file mode 100644 index 0000000..fb30321 --- /dev/null +++ b/ani-cli.md @@ -0,0 +1,228 @@ +# ani-cli Research + +> Source: +> Version at time of research: 4.11.0 +> Language: POSIX Shell (~500 lines) +> License: GPL-3.0 + +## Overview + +ani-cli is a CLI tool to browse and watch anime. It scrapes [allmanga.to](https://allmanga.to/) via a GraphQL API, decodes obfuscated video source URLs, and launches a media player (mpv, vlc, iina, etc.). + +The **entire codebase is a single file**: `ani-cli` (a POSIX shell script). + +## Architecture / Code Sections + +The script is organized into these logical blocks: + +### 1. UI Functions + +| Function | Purpose | +|---|---| +| `external_menu()` | Wraps rofi/dmenu for external menu support | +| `launcher()` | Dispatches to fzf (default), rofi, or dmenu | +| `nth()` | Core selection function -- pipes items through launcher, supports multi-select | + +### 2. Scraping / API + +All API calls go to `https://api.allanime.day/api` as **GraphQL POST requests** with `Content-Type: application/json`. The referer is set to `https://allmanga.to`. + +#### GraphQL Queries + +**Search anime:** +```graphql +query( + $search: SearchInput + $limit: Int + $page: Int + $translationType: VaildTranslationTypeEnumType + $countryOrigin: VaildCountryOriginEnumType +) { + shows( + search: $search + limit: $limit + page: $page + translationType: $translationType + countryOrigin: $countryOrigin + ) { + edges { + _id + name + availableEpisodes + __typename + } + } +} +``` + +Variables: +```json +{ + "search": { "allowAdult": false, "allowUnknown": false, "query": "" }, + "limit": 40, + "page": 1, + "translationType": "sub", + "countryOrigin": "ALL" +} +``` + +**Get episodes list:** +```graphql +query ($showId: String!) { + show(_id: $showId) { + _id + availableEpisodesDetail + } +} +``` + +**Get episode embed URLs:** +```graphql +query ( + $showId: String! + $translationType: VaildTranslationTypeEnumType! + $episodeString: String! +) { + episode( + showId: $showId + translationType: $translationType + episodeString: $episodeString + ) { + episodeString + sourceUrls + } +} +``` + +### 3. Provider URL Decoding (Hex Cipher) + +The API returns obfuscated source URLs. The `provider_init()` function decodes them using a **hex-pair substitution cipher**. Each two-character hex code maps to an ASCII character: + +``` +79 -> A, 7a -> B, 7b -> C, ... (uppercase letters) +59 -> a, 5a -> b, 5b -> c, ... (lowercase letters) +08 -> 0, 09 -> 1, 0a -> 2, ... (digits) +15 -> -, 16 -> ., 17 -> /, 02 -> :, etc. (symbols) +``` + +Full mapping (hex -> char): + +| Hex | Char | Hex | Char | Hex | Char | Hex | Char | +|-----|------|-----|------|-----|------|-----|------| +| 79 | A | 59 | a | 08 | 0 | 15 | - | +| 7a | B | 5a | b | 09 | 1 | 16 | . | +| 7b | C | 5b | c | 0a | 2 | 67 | _ | +| 7c | D | 5c | d | 0b | 3 | 46 | ~ | +| 7d | E | 5d | e | 0c | 4 | 02 | : | +| 7e | F | 5e | f | 0d | 5 | 17 | / | +| 7f | G | 5f | g | 0e | 6 | 07 | ? | +| 70 | H | 50 | h | 0f | 7 | 1b | # | +| 71 | I | 51 | i | 00 | 8 | 63 | [ | +| 72 | J | 52 | j | 01 | 9 | 65 | ] | +| 73 | K | 53 | k | | | 78 | @ | +| 74 | L | 54 | l | | | 19 | ! | +| 75 | M | 55 | m | | | 1c | $ | +| 76 | N | 56 | n | | | 1e | & | +| 77 | O | 57 | o | | | 10 | ( | +| 68 | P | 48 | p | | | 11 | ) | +| 69 | Q | 49 | q | | | 12 | * | +| 6a | R | 4a | r | | | 13 | + | +| 6b | S | 4b | s | | | 14 | , | +| 6c | T | 4c | t | | | 03 | ; | +| 6d | U | 4d | u | | | 05 | = | +| 6e | V | 4e | v | | | 1d | % | +| 6f | W | 4f | w | | | | | +| 60 | X | 40 | x | | | | | +| 61 | Y | 41 | y | | | | | +| 62 | Z | 42 | z | | | | | + +After decoding, `/clock` is replaced with `/clock.json`. + +### 4. Video Providers + +Four providers are tried in parallel: + +| # | Name | Regex Match | Format | +|---|---|---|---| +| 1 | wixmp (Default) | `/Default :/p` | m3u8 -> mp4 (multi-quality) | +| 2 | youtube | `/Yt-mp4 :/p` | mp4 (single) | +| 3 | sharepoint | `/S-mp4 :/p` | mp4 (single) | +| 4 | hianime | `/Luf-Mp4 :/p` | m3u8 (multi-quality) | + +### 5. Link Extraction (`get_links`) + +After fetching a provider URL, the response is parsed for video links. Three cases: + +1. **wixmp (repackager.wixmp.com)** -- Extracts mp4 URLs with multiple quality options from a comma-separated URL pattern +2. **m3u8 (master.m3u8)** -- Parses the m3u8 playlist for stream variants at different resolutions. Also extracts subtitle URLs and referrer headers +3. **Direct links** -- Used as-is +4. **fast4speed.rsvp** -- YouTube-like links, passed to yt-dlp + +### 6. Quality Selection (`select_quality`) + +- `best` -- first (highest resolution) link +- `worst` -- last numeric resolution link +- Specific (e.g., `1080`) -- grep for matching resolution +- Falls back to `best` if specified quality not found +- Sets `--referrer` and `--sub-file` flags for m3u8 streams + +### 7. Playback (`play_episode`) + +Launches the selected player with the video URL. Supported players: + +| Player | Platform | How | +|---|---|---| +| mpv | Linux/Windows | `mpv` / `mpv.exe` (detached or attached) | +| flatpak mpv | Linux | `flatpak run io.mpv.Mpv` | +| vlc | Linux/Windows | `vlc` / `vlc.exe` | +| iina | macOS | IINA app CLI | +| android_mpv | Android | `am start` intent | +| android_vlc | Android | `am start` intent | +| syncplay | All | Syncplay wrapper | +| download | All | `aria2c` (mp4) or `yt-dlp`/`ffmpeg` (m3u8) | +| catt | All | Chromecast via catt | +| iSH | iOS | VLC URL scheme | + +### 8. History + +- Stored in `~/.local/state/ani-cli/ani-hsts` (or `$ANI_CLI_HIST_DIR`) +- Format: `\t\t` +- Updated after each episode play +- Used by `--continue` to resume watching + +### 9. Main Flow + +``` +1. Parse CLI arguments +2. Check dependencies +3. Search: + a. If --continue: read history, show unwatched, pick one + b. Otherwise: prompt for search query, call search_anime(), pick result +4. Get episode list for selected anime +5. Pick episode (or use --episode flag) +6. play(): + a. get_episode_url() -> fetch embed URLs -> decode providers -> fetch links -> select quality + b. play_episode() -> launch player +7. Loop: next / replay / previous / select / change_quality / quit +``` + +## Dependencies + +- `curl` -- HTTP requests +- `sed`, `grep` -- text processing / parsing +- `fzf` -- interactive selection (or rofi/dmenu) +- `mpv` / `vlc` / `iina` -- video playback +- `aria2c` -- download manager (for mp4 downloads) +- `yt-dlp` -- m3u8 downloads +- `ffmpeg` -- m3u8 downloads (fallback) +- `ani-skip` -- optional, skip anime intros (uses aniskip API + mpv lua scripts) +- `patch` -- self-updating + +## Key Constants + +``` +User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0 +Referer: https://allmanga.to +API Base: https://api.allanime.day +Link Base: allanime.day +``` diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..9406949 --- /dev/null +++ b/plan.md @@ -0,0 +1,205 @@ +# Project Plan: Anime (Rust GUI) + +A cross-platform (Arch Linux + Windows) GUI application for searching, browsing, and watching anime. Inspired by [ani-cli](https://github.com/pystardust/ani-cli). + +## Features + +### Core (MVP) +- Search anime by name via the AllAnime GraphQL API +- Display search results in a list +- Select an anime and view its available episodes +- Select an episode to play in the OS default media player +- Watch history -- track what you've watched, resume from where you left off + +### Extended +- Download episodes (mp4 direct download via reqwest, m3u8 via yt-dlp/ffmpeg subprocess) +- Download progress bar +- Sub/Dub toggle +- Quality selection +- Episode range selection for batch downloads + +--- + +## Tech Stack + +### GUI Framework + +**egui / eframe** +- Immediate-mode GPU-rendered GUI +- Single binary, no runtime dependencies +- Native look on both Linux and Windows +- Easy to iterate on, good for list-based UIs + +### Crates + +| Crate | Version | Purpose | +|---|---|---| +| `eframe` | latest | Window management, OpenGL/wgpu backend for egui | +| `egui` | latest | Immediate-mode UI widgets (comes with eframe) | +| `reqwest` | latest | HTTP client for GraphQL API calls and mp4 downloads | +| `tokio` | latest | Async runtime (required by reqwest, keeps GUI responsive) | +| `serde` | latest | Serialization framework | +| `serde_json` | latest | JSON parsing for GraphQL responses | +| `open` | latest | Open video URL in OS default player (`xdg-open` on Linux, `start` on Windows) | +| `directories` | latest | Cross-platform paths for config/state (history file) | +| `anyhow` | latest | Ergonomic error handling with context | +| `indicatif` | latest | Progress bars for downloads (can integrate into egui) | + +### Standard Library (no extra crate needed) + +| Module | Purpose | +|---|---| +| `std::process::Command` | Shell out to `yt-dlp` / `ffmpeg` for m3u8 downloads | +| `std::fs` | Read/write history file | + +### External Tools (runtime, not compile-time) + +| Tool | Required? | Purpose | +|---|---|---| +| `mpv` / `vlc` / system default | Yes (any player) | Video playback | +| `yt-dlp` | For m3u8 downloads | HLS stream downloading | +| `ffmpeg` | Fallback for m3u8 | HLS stream downloading (if yt-dlp unavailable) | + +--- + +## Architecture + +``` +src/ +├── main.rs -- App entry, eframe::run_native() setup +├── app.rs -- Main App struct implementing eframe::App +│ Holds app state, routes between views +├── views/ +│ ├── mod.rs +│ ├── search.rs -- Search input + results list view +│ ├── episodes.rs -- Episode list view for a selected anime +│ └── player.rs -- "Now playing" view (current episode, next/prev controls) +├── api/ +│ ├── mod.rs +│ ├── client.rs -- Shared reqwest::Client setup (user-agent, referer) +│ ├── search.rs -- search_anime() -- GraphQL search query +│ ├── episodes.rs -- episodes_list() -- GraphQL episodes query +│ ├── sources.rs -- get_episode_url() -- GraphQL embed query + provider decoding +│ ├── providers.rs -- Provider-specific link extraction (wixmp, hianime, etc.) +│ └── cipher.rs -- Hex substitution cipher for decoding provider URLs +├── download.rs -- Download logic: direct mp4 via reqwest, m3u8 via subprocess +├── history.rs -- Read/write watch history to local state file +└── player.rs -- Launch video in OS default player via `open` crate +``` + +--- + +## Data Flow + +``` +┌──────────────────────────────────────────────────────────────┐ +│ GUI (egui) │ +│ │ +│ ┌─────────┐ ┌────────────┐ ┌────────────────────┐ │ +│ │ Search │───>│ Episodes │───>│ Play / Download │ │ +│ │ View │ │ View │ │ View │ │ +│ └─────────┘ └────────────┘ └────────────────────┘ │ +│ │ │ │ │ │ +└───────┼───────────────┼──────────────────┼─────────┼────────┘ + │ │ │ │ + v v v v + search_anime() episodes_list() get_episode_url()│ + │ │ │ │ + └───────┬───────┘ │ ┌────┘ + v v v + AllAnime GraphQL API ┌──────────────┐ + (api.allanime.day) │ Playback │ + ├──────────────┤ + │ open crate │ -> OS default player + │ reqwest │ -> direct mp4 download + │ yt-dlp/ffmpeg│ -> m3u8 download + └──────────────┘ +``` + +--- + +## API Details + +See [ani-cli.md](./ani-cli.md) for full documentation of: +- GraphQL queries and variables +- Hex substitution cipher mapping +- Provider-specific link extraction +- Video source formats (mp4 vs m3u8) + +### Key Constants + +```rust +const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/121.0"; +const ALLANIME_REFERER: &str = "https://allmanga.to"; +const ALLANIME_API: &str = "https://api.allanime.day"; +const ALLANIME_BASE: &str = "allanime.day"; +``` + +--- + +## History File + +- **Linux**: `~/.local/state/anime/history.json` +- **Windows**: `C:\Users\<user>\AppData\Local\anime\state\history.json` +- Resolved via the `directories` crate (`ProjectDirs::from("", "", "anime")`) + +Format (JSON for easy serde): +```json +[ + { + "show_id": "abc123", + "title": "Cyberpunk Edgerunners", + "episode": "5", + "mode": "sub", + "last_watched": "2026-04-14T12:00:00Z" + } +] +``` + +--- + +## Download Strategy + +### Direct MP4 links +- Use `reqwest` to stream the response body to a file +- Show progress via content-length header + bytes received +- No external tools needed + +### m3u8 / HLS streams +- Shell out to `yt-dlp` (preferred) or `ffmpeg` (fallback) via `std::process::Command` +- Pass referrer header as argument +- Capture stdout/stderr for progress reporting +- This avoids reimplementing HLS segment fetching, concatenation, and remuxing + +### Download directory +- Default: current working directory (or configurable) +- File naming: `<anime_title> Episode <number>.mp4` + +--- + +## Build Targets + +| Platform | Target Triple | Notes | +|---|---|---| +| Arch Linux | `x86_64-unknown-linux-gnu` | Primary dev target | +| Windows | `x86_64-pc-windows-msvc` | Cross-platform support | + +Both are tier 1 Rust targets with full support. + +--- + +## Implementation Order + +1. **Project setup** -- Add dependencies to `Cargo.toml`, basic eframe window +2. **API client** -- reqwest client with correct headers, test connectivity +3. **Cipher** -- Implement hex substitution decoder +4. **Search** -- GraphQL search query, parse results +5. **Episodes** -- GraphQL episode list query, parse results +6. **Sources** -- GraphQL embed query, decode providers, extract video links +7. **Playback** -- Open video URL in default player via `open` crate +8. **GUI: Search view** -- Text input, results list, selection +9. **GUI: Episodes view** -- Episode list, selection, play button +10. **GUI: Now playing view** -- Current episode info, next/prev/replay controls +11. **History** -- Read/write history file, continue watching flow +12. **Downloads** -- Direct mp4 download, m3u8 via subprocess, progress reporting +13. **Polish** -- Error handling, loading states, quality selection, sub/dub toggle diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/src/.gitignore @@ -0,0 +1 @@ +/target diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +}