6.5 KiB
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 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:
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:
{
"search": { "allowAdult": false, "allowUnknown": false, "query": "<search term>" },
"limit": 40,
"page": 1,
"translationType": "sub",
"countryOrigin": "ALL"
}
Get episodes list:
query ($showId: String!) {
show(_id: $showId) {
_id
availableEpisodesDetail
}
}
Get episode embed URLs:
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:
- wixmp (repackager.wixmp.com) -- Extracts mp4 URLs with multiple quality options from a comma-separated URL pattern
- m3u8 (master.m3u8) -- Parses the m3u8 playlist for stream variants at different resolutions. Also extracts subtitle URLs and referrer headers
- Direct links -- Used as-is
- fast4speed.rsvp -- YouTube-like links, passed to yt-dlp
6. Quality Selection (select_quality)
best-- first (highest resolution) linkworst-- last numeric resolution link- Specific (e.g.,
1080) -- grep for matching resolution - Falls back to
bestif specified quality not found - Sets
--referrerand--sub-fileflags 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
--continueto 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 requestssed,grep-- text processing / parsingfzf-- interactive selection (or rofi/dmenu)mpv/vlc/iina-- video playbackaria2c-- download manager (for mp4 downloads)yt-dlp-- m3u8 downloadsffmpeg-- 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