Planning
This commit is contained in:
+17
@@ -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"
|
||||||
+228
@@ -0,0 +1,228 @@
|
|||||||
|
# ani-cli Research
|
||||||
|
|
||||||
|
> Source: <https://github.com/pystardust/ani-cli>
|
||||||
|
> 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": "<search term>" },
|
||||||
|
"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: `<episode_no>\t<show_id>\t<title>`
|
||||||
|
- 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
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
fn main() {
|
||||||
|
println!("Hello, world!");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user