anime-sh 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anime_sh-0.2.0/LICENSE +21 -0
- anime_sh-0.2.0/PKG-INFO +193 -0
- anime_sh-0.2.0/README.md +144 -0
- anime_sh-0.2.0/pyproject.toml +120 -0
- anime_sh-0.2.0/src/anime_sh/__init__.py +18 -0
- anime_sh-0.2.0/src/anime_sh/__main__.py +6 -0
- anime_sh-0.2.0/src/anime_sh/app/__init__.py +8 -0
- anime_sh-0.2.0/src/anime_sh/app/download.py +76 -0
- anime_sh-0.2.0/src/anime_sh/app/library.py +97 -0
- anime_sh-0.2.0/src/anime_sh/app/playback.py +490 -0
- anime_sh-0.2.0/src/anime_sh/app/providers.py +227 -0
- anime_sh-0.2.0/src/anime_sh/app/search.py +151 -0
- anime_sh-0.2.0/src/anime_sh/app/sync.py +72 -0
- anime_sh-0.2.0/src/anime_sh/cli/__init__.py +6 -0
- anime_sh-0.2.0/src/anime_sh/cli/container.py +167 -0
- anime_sh-0.2.0/src/anime_sh/cli/doctor.py +120 -0
- anime_sh-0.2.0/src/anime_sh/cli/main.py +1537 -0
- anime_sh-0.2.0/src/anime_sh/config/__init__.py +6 -0
- anime_sh-0.2.0/src/anime_sh/config/defaults.toml +29 -0
- anime_sh-0.2.0/src/anime_sh/config/loader.py +120 -0
- anime_sh-0.2.0/src/anime_sh/config/paths.py +42 -0
- anime_sh-0.2.0/src/anime_sh/config/schema.py +63 -0
- anime_sh-0.2.0/src/anime_sh/domain/__init__.py +5 -0
- anime_sh-0.2.0/src/anime_sh/domain/errors.py +49 -0
- anime_sh-0.2.0/src/anime_sh/domain/health.py +72 -0
- anime_sh-0.2.0/src/anime_sh/domain/models.py +346 -0
- anime_sh-0.2.0/src/anime_sh/domain/ports.py +265 -0
- anime_sh-0.2.0/src/anime_sh/domain/ranking.py +73 -0
- anime_sh-0.2.0/src/anime_sh/infra/__init__.py +5 -0
- anime_sh-0.2.0/src/anime_sh/infra/cache/__init__.py +1 -0
- anime_sh-0.2.0/src/anime_sh/infra/cache/kv.py +62 -0
- anime_sh-0.2.0/src/anime_sh/infra/db/__init__.py +11 -0
- anime_sh-0.2.0/src/anime_sh/infra/db/database.py +71 -0
- anime_sh-0.2.0/src/anime_sh/infra/db/downloads.py +64 -0
- anime_sh-0.2.0/src/anime_sh/infra/db/health.py +64 -0
- anime_sh-0.2.0/src/anime_sh/infra/db/library.py +337 -0
- anime_sh-0.2.0/src/anime_sh/infra/db/migrations/0001_initial.sql +81 -0
- anime_sh-0.2.0/src/anime_sh/infra/db/migrations_cache/0001_initial.sql +24 -0
- anime_sh-0.2.0/src/anime_sh/infra/downloader/__init__.py +5 -0
- anime_sh-0.2.0/src/anime_sh/infra/downloader/ffmpeg.py +95 -0
- anime_sh-0.2.0/src/anime_sh/infra/http/__init__.py +6 -0
- anime_sh-0.2.0/src/anime_sh/infra/http/client.py +161 -0
- anime_sh-0.2.0/src/anime_sh/infra/http/probe.py +64 -0
- anime_sh-0.2.0/src/anime_sh/infra/metadata/__init__.py +5 -0
- anime_sh-0.2.0/src/anime_sh/infra/metadata/anilist.py +395 -0
- anime_sh-0.2.0/src/anime_sh/infra/players/__init__.py +7 -0
- anime_sh-0.2.0/src/anime_sh/infra/players/mpv.py +281 -0
- anime_sh-0.2.0/src/anime_sh/infra/players/null.py +48 -0
- anime_sh-0.2.0/src/anime_sh/infra/proxy/__init__.py +5 -0
- anime_sh-0.2.0/src/anime_sh/infra/proxy/deobfuscate.py +208 -0
- anime_sh-0.2.0/src/anime_sh/infra/registry.py +66 -0
- anime_sh-0.2.0/src/anime_sh/infra/skiptimes/__init__.py +5 -0
- anime_sh-0.2.0/src/anime_sh/infra/skiptimes/aniskip.py +71 -0
- anime_sh-0.2.0/src/anime_sh/infra/tracker/__init__.py +18 -0
- anime_sh-0.2.0/src/anime_sh/infra/tracker/anilist.py +274 -0
- anime_sh-0.2.0/src/anime_sh/infra/tracker/tokens.py +57 -0
- anime_sh-0.2.0/src/anime_sh/providers/__init__.py +7 -0
- anime_sh-0.2.0/src/anime_sh/providers/allanime/__init__.py +3 -0
- anime_sh-0.2.0/src/anime_sh/providers/allanime/decode.py +104 -0
- anime_sh-0.2.0/src/anime_sh/providers/allanime/keygen.py +158 -0
- anime_sh-0.2.0/src/anime_sh/providers/allanime/provider.py +344 -0
- anime_sh-0.2.0/src/anime_sh/providers/anikoto/__init__.py +3 -0
- anime_sh-0.2.0/src/anime_sh/providers/anikoto/provider.py +320 -0
- anime_sh-0.2.0/src/anime_sh/providers/anizone/__init__.py +5 -0
- anime_sh-0.2.0/src/anime_sh/providers/anizone/provider.py +224 -0
- anime_sh-0.2.0/src/anime_sh/py.typed +0 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/__init__.py +6 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/allanime/__init__.py +3 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/allanime/clock.py +73 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/filemoon/__init__.py +5 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/filemoon/resolver.py +77 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/generic/__init__.py +3 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/generic/hls.py +31 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/mp4upload/__init__.py +3 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/mp4upload/resolver.py +53 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/packed.py +63 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/quality.py +30 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/streamwish/__init__.py +5 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/streamwish/resolver.py +69 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/vidtube/__init__.py +3 -0
- anime_sh-0.2.0/src/anime_sh/resolvers/vidtube/resolver.py +132 -0
- anime_sh-0.2.0/src/anime_sh/tui/__init__.py +10 -0
- anime_sh-0.2.0/src/anime_sh/tui/app.py +112 -0
- anime_sh-0.2.0/src/anime_sh/tui/app.tcss +45 -0
- anime_sh-0.2.0/src/anime_sh/tui/coverart.py +93 -0
- anime_sh-0.2.0/src/anime_sh/tui/format.py +74 -0
- anime_sh-0.2.0/src/anime_sh/tui/screens/__init__.py +0 -0
- anime_sh-0.2.0/src/anime_sh/tui/screens/detail.py +214 -0
- anime_sh-0.2.0/src/anime_sh/tui/screens/help.py +45 -0
- anime_sh-0.2.0/src/anime_sh/tui/screens/home.py +198 -0
- anime_sh-0.2.0/src/anime_sh/tui/screens/mylist.py +78 -0
- anime_sh-0.2.0/src/anime_sh/tui/screens/sources.py +79 -0
- anime_sh-0.2.0/src/anime_sh/tui/widgets.py +77 -0
anime_sh-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Animesh Sharma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
anime_sh-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: anime-sh
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: The terminal-native anime client — providers, mirrors and resolvers, handled for you.
|
|
5
|
+
Keywords: anime,cli,tui,terminal,streaming,anilist,mpv
|
|
6
|
+
Author: Animesh Sharma
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Multimedia :: Video
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Dist: typer>=0.12
|
|
19
|
+
Requires-Dist: rich>=13.7
|
|
20
|
+
Requires-Dist: pydantic>=2.6
|
|
21
|
+
Requires-Dist: pydantic-settings>=2.2
|
|
22
|
+
Requires-Dist: aiosqlite>=0.20
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: curl-cffi>=0.7
|
|
25
|
+
Requires-Dist: cryptography>=42.0
|
|
26
|
+
Requires-Dist: platformdirs>=4.2
|
|
27
|
+
Requires-Dist: textual>=8.0 ; extra == 'all'
|
|
28
|
+
Requires-Dist: pillow>=10.0 ; extra == 'all'
|
|
29
|
+
Requires-Dist: pypresence>=4.3 ; extra == 'all'
|
|
30
|
+
Requires-Dist: pytest>=8.1 ; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest-asyncio>=0.23 ; extra == 'dev'
|
|
32
|
+
Requires-Dist: import-linter>=2.0 ; extra == 'dev'
|
|
33
|
+
Requires-Dist: anyio>=4.3 ; extra == 'dev'
|
|
34
|
+
Requires-Dist: textual>=8.0 ; extra == 'dev'
|
|
35
|
+
Requires-Dist: pillow>=10.0 ; extra == 'dev'
|
|
36
|
+
Requires-Dist: pypresence>=4.3 ; extra == 'discord'
|
|
37
|
+
Requires-Dist: textual>=8.0 ; extra == 'tui'
|
|
38
|
+
Requires-Dist: pillow>=10.0 ; extra == 'tui'
|
|
39
|
+
Requires-Python: >=3.11
|
|
40
|
+
Project-URL: Homepage, https://github.com/animesh/anime-sh
|
|
41
|
+
Project-URL: Repository, https://github.com/animesh/anime-sh
|
|
42
|
+
Project-URL: Issues, https://github.com/animesh/anime-sh/issues
|
|
43
|
+
Project-URL: Documentation, https://github.com/animesh/anime-sh/tree/main/docs
|
|
44
|
+
Provides-Extra: all
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Provides-Extra: discord
|
|
47
|
+
Provides-Extra: tui
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# anime-sh
|
|
51
|
+
|
|
52
|
+
The terminal-native anime client. You type a title; it plays. Providers,
|
|
53
|
+
mirrors, and resolvers are internal details you never have to think about.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
anime "Frieren"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> **Status: M5 — polish.** Bare `anime` launches a keyboard-driven Textual app;
|
|
60
|
+
> playback auto-skips intros and rolls into the next episode on its own. Under
|
|
61
|
+
> it: AniList metadata, three live providers (AllAnime, anikoto, AniZone) fanned
|
|
62
|
+
> out with circuit breakers, resolvers, mpv over JSON IPC, a persistent library
|
|
63
|
+
> (resume/history/favorites), and ffmpeg downloads. See
|
|
64
|
+
> [`docs/architecture.md`](docs/architecture.md).
|
|
65
|
+
|
|
66
|
+
## What works today
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
anime # launch the keyboard-driven TUI (needs [tui] extra)
|
|
70
|
+
anime "Frieren" # search + best match + play episode 1
|
|
71
|
+
anime play "Frieren" -e 18 # a specific episode (add --dub, -q 1080p)
|
|
72
|
+
anime search "frieren" # AniList search (instant; no providers touched)
|
|
73
|
+
anime search --genre action --year 2024 --sort score # browse with filters
|
|
74
|
+
anime trending
|
|
75
|
+
anime recommend "Frieren" # shows for people who liked it (AniList)
|
|
76
|
+
anime related "Attack on Titan" # prequels, sequels, side stories, movies
|
|
77
|
+
anime mark "Frieren" -e 12 # catch up: mark eps 1–12 watched (syncs to AniList)
|
|
78
|
+
anime stats # episodes, hours, top genres & providers
|
|
79
|
+
|
|
80
|
+
anime continue # episodes you started but didn't finish
|
|
81
|
+
anime resume # jump back into the most recent one
|
|
82
|
+
anime history # what you've watched
|
|
83
|
+
anime favorite add "Frieren" # ★ (also: favorite ls / rm)
|
|
84
|
+
anime download "Frieren" -e 1-12 # save a range to disk (ffmpeg); resumes, skips done
|
|
85
|
+
anime download "Frieren" -e 1,3,5 # or a list; also: anime downloads
|
|
86
|
+
|
|
87
|
+
anime auth login # link AniList (one-time); status / logout
|
|
88
|
+
anime sync pull # import your AniList list; sync push sends yours up
|
|
89
|
+
anime list --status watching # your AniList list by status (also planning/completed…)
|
|
90
|
+
anime rate "Frieren" 9 # set a score; anime status "X" completed
|
|
91
|
+
anime next "Mob Psycho 100" # find + play the next season (sequel)
|
|
92
|
+
|
|
93
|
+
anime doctor # player, ffmpeg, config, database, plugins
|
|
94
|
+
anime --version # print the version and exit
|
|
95
|
+
anime config get # dump settings; `config get playback.quality`
|
|
96
|
+
anime config set playback.quality 1080p # also: audio dub, ui.theme nord …
|
|
97
|
+
anime config path | validate
|
|
98
|
+
anime providers ls
|
|
99
|
+
anime cache clear # wipe the disposable metadata cache (or: cache purge)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The TUI home shows Continue Watching, Favorites, Airing This Season, and
|
|
103
|
+
Trending; the detail screen renders cover art, score, studio and a live
|
|
104
|
+
next-episode countdown. Press `?` for keys, `/` to search.
|
|
105
|
+
|
|
106
|
+
**Forgiving search.** You don't have to spell titles exactly the way AniList
|
|
107
|
+
stores them — `dont toy with me`, `dukes son claims he wont love me`, even
|
|
108
|
+
`atack on titan` all find the right show. When AniList's strict search comes up
|
|
109
|
+
empty, anime-sh retries with apostrophes restored and the query's distinctive
|
|
110
|
+
words, then fuzzy-ranks the results against what you typed.
|
|
111
|
+
|
|
112
|
+
**Tab-completion.** Run `anime --install-completion` once for your shell.
|
|
113
|
+
|
|
114
|
+
**AniList sync.** Link your account once with `anime auth login` (create a free
|
|
115
|
+
API client at [anilist.co/settings/developer](https://anilist.co/settings/developer),
|
|
116
|
+
redirect URL `https://anilist.co/api/v2/oauth/pin`, then paste the token — your
|
|
117
|
+
password is never involved). After that, finishing an episode automatically bumps
|
|
118
|
+
your AniList progress. `anime sync pull` imports your existing list into the local
|
|
119
|
+
library; `anime sync push` sends your local history up in one pass.
|
|
120
|
+
|
|
121
|
+
Add `--json` to `search`, `trending`, `play`, `continue`, `history`, and
|
|
122
|
+
`favorite ls` for machine-readable output (`play --json` resolves the stream
|
|
123
|
+
without launching a player). Your library (progress, history, favorites) lives
|
|
124
|
+
in a separate `anime.db` from the disposable cache and renders offline.
|
|
125
|
+
|
|
126
|
+
**Cached catalog.** AniList responses (search, trending, seasonal, schedule,
|
|
127
|
+
details) are cached in a throwaway `cache.db` with short TTLs, so repeat browses
|
|
128
|
+
are instant and recently-seen pages still render offline. It is always safe to
|
|
129
|
+
wipe with `anime cache clear`; nothing user-owned lives there.
|
|
130
|
+
|
|
131
|
+
> Streaming providers break and get Cloudflare-gated constantly — that's the
|
|
132
|
+
> normal operating state, not a bug. When a provider is unreachable, anime-sh
|
|
133
|
+
> degrades cleanly instead of crashing; metadata and your library keep working.
|
|
134
|
+
|
|
135
|
+
**Multiple providers, merged.** anime-sh fans out across providers (currently
|
|
136
|
+
AllAnime + anikoto + AniZone) and falls through to whichever one actually has
|
|
137
|
+
your show — so a title missing from one source still plays from another, with no
|
|
138
|
+
action from you. AniZone serves a clean, un-obfuscated HLS stream with soft
|
|
139
|
+
English subs, so it plays where Cloudflare-gated sites can't.
|
|
140
|
+
|
|
141
|
+
## Install
|
|
142
|
+
|
|
143
|
+
Needs Python 3.11+, plus an external media player (`mpv` recommended) and
|
|
144
|
+
`ffmpeg` for playback/downloads. `anime doctor` reports what's missing.
|
|
145
|
+
|
|
146
|
+
Install straight from GitHub — this puts the `anime` command on your PATH:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
uv tool install "anime-sh[tui] @ git+https://github.com/Anime123450/anime-sh.git"
|
|
150
|
+
# or: pipx install "anime-sh[tui] @ git+https://github.com/Anime123450/anime-sh.git"
|
|
151
|
+
anime doctor
|
|
152
|
+
anime "Frieren"
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
(Not on PyPI yet; once it is, this shortens to `uv tool install "anime-sh[tui]"`.)
|
|
156
|
+
|
|
157
|
+
### From source (dev)
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
git clone https://github.com/Anime123450/anime-sh.git && cd anime-sh
|
|
161
|
+
uv sync --extra dev --extra tui
|
|
162
|
+
uv run anime doctor
|
|
163
|
+
uv run anime # launch the TUI
|
|
164
|
+
uv run pytest -q # tests (no network); add ANIME_SH_LIVE=1 for live ones
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
See [`docs/plugins.md`](docs/plugins.md) to add a provider or resolver.
|
|
168
|
+
|
|
169
|
+
## Develop
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
uv run pytest # fast unit suite — no network
|
|
173
|
+
uv run lint-imports # architecture contracts (must stay green)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## Design
|
|
177
|
+
|
|
178
|
+
anime-sh is layered `cli/tui → app → domain`, with `infra`, `providers`, and
|
|
179
|
+
`resolvers` as swappable adapters behind ports. Dependencies point downward only
|
|
180
|
+
and that is enforced in CI. Identity comes from AniList (every show is keyed by
|
|
181
|
+
its AniList id), so adding a provider is attaching a source to a known identity,
|
|
182
|
+
not fuzzy-matching titles. Full write-up: [`docs/architecture.md`](docs/architecture.md).
|
|
183
|
+
|
|
184
|
+
## Legal
|
|
185
|
+
|
|
186
|
+
anime-sh is a **client**, not a content library. It bundles no media, mirrors
|
|
187
|
+
nothing, and bypasses no DRM. Providers read public pages and are expected to
|
|
188
|
+
break; a broken provider is a degraded experience, not an outage. Provider
|
|
189
|
+
plugins are separable from the core so the project survives any single one.
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT
|
anime_sh-0.2.0/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# anime-sh
|
|
2
|
+
|
|
3
|
+
The terminal-native anime client. You type a title; it plays. Providers,
|
|
4
|
+
mirrors, and resolvers are internal details you never have to think about.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
anime "Frieren"
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
> **Status: M5 — polish.** Bare `anime` launches a keyboard-driven Textual app;
|
|
11
|
+
> playback auto-skips intros and rolls into the next episode on its own. Under
|
|
12
|
+
> it: AniList metadata, three live providers (AllAnime, anikoto, AniZone) fanned
|
|
13
|
+
> out with circuit breakers, resolvers, mpv over JSON IPC, a persistent library
|
|
14
|
+
> (resume/history/favorites), and ffmpeg downloads. See
|
|
15
|
+
> [`docs/architecture.md`](docs/architecture.md).
|
|
16
|
+
|
|
17
|
+
## What works today
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
anime # launch the keyboard-driven TUI (needs [tui] extra)
|
|
21
|
+
anime "Frieren" # search + best match + play episode 1
|
|
22
|
+
anime play "Frieren" -e 18 # a specific episode (add --dub, -q 1080p)
|
|
23
|
+
anime search "frieren" # AniList search (instant; no providers touched)
|
|
24
|
+
anime search --genre action --year 2024 --sort score # browse with filters
|
|
25
|
+
anime trending
|
|
26
|
+
anime recommend "Frieren" # shows for people who liked it (AniList)
|
|
27
|
+
anime related "Attack on Titan" # prequels, sequels, side stories, movies
|
|
28
|
+
anime mark "Frieren" -e 12 # catch up: mark eps 1–12 watched (syncs to AniList)
|
|
29
|
+
anime stats # episodes, hours, top genres & providers
|
|
30
|
+
|
|
31
|
+
anime continue # episodes you started but didn't finish
|
|
32
|
+
anime resume # jump back into the most recent one
|
|
33
|
+
anime history # what you've watched
|
|
34
|
+
anime favorite add "Frieren" # ★ (also: favorite ls / rm)
|
|
35
|
+
anime download "Frieren" -e 1-12 # save a range to disk (ffmpeg); resumes, skips done
|
|
36
|
+
anime download "Frieren" -e 1,3,5 # or a list; also: anime downloads
|
|
37
|
+
|
|
38
|
+
anime auth login # link AniList (one-time); status / logout
|
|
39
|
+
anime sync pull # import your AniList list; sync push sends yours up
|
|
40
|
+
anime list --status watching # your AniList list by status (also planning/completed…)
|
|
41
|
+
anime rate "Frieren" 9 # set a score; anime status "X" completed
|
|
42
|
+
anime next "Mob Psycho 100" # find + play the next season (sequel)
|
|
43
|
+
|
|
44
|
+
anime doctor # player, ffmpeg, config, database, plugins
|
|
45
|
+
anime --version # print the version and exit
|
|
46
|
+
anime config get # dump settings; `config get playback.quality`
|
|
47
|
+
anime config set playback.quality 1080p # also: audio dub, ui.theme nord …
|
|
48
|
+
anime config path | validate
|
|
49
|
+
anime providers ls
|
|
50
|
+
anime cache clear # wipe the disposable metadata cache (or: cache purge)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The TUI home shows Continue Watching, Favorites, Airing This Season, and
|
|
54
|
+
Trending; the detail screen renders cover art, score, studio and a live
|
|
55
|
+
next-episode countdown. Press `?` for keys, `/` to search.
|
|
56
|
+
|
|
57
|
+
**Forgiving search.** You don't have to spell titles exactly the way AniList
|
|
58
|
+
stores them — `dont toy with me`, `dukes son claims he wont love me`, even
|
|
59
|
+
`atack on titan` all find the right show. When AniList's strict search comes up
|
|
60
|
+
empty, anime-sh retries with apostrophes restored and the query's distinctive
|
|
61
|
+
words, then fuzzy-ranks the results against what you typed.
|
|
62
|
+
|
|
63
|
+
**Tab-completion.** Run `anime --install-completion` once for your shell.
|
|
64
|
+
|
|
65
|
+
**AniList sync.** Link your account once with `anime auth login` (create a free
|
|
66
|
+
API client at [anilist.co/settings/developer](https://anilist.co/settings/developer),
|
|
67
|
+
redirect URL `https://anilist.co/api/v2/oauth/pin`, then paste the token — your
|
|
68
|
+
password is never involved). After that, finishing an episode automatically bumps
|
|
69
|
+
your AniList progress. `anime sync pull` imports your existing list into the local
|
|
70
|
+
library; `anime sync push` sends your local history up in one pass.
|
|
71
|
+
|
|
72
|
+
Add `--json` to `search`, `trending`, `play`, `continue`, `history`, and
|
|
73
|
+
`favorite ls` for machine-readable output (`play --json` resolves the stream
|
|
74
|
+
without launching a player). Your library (progress, history, favorites) lives
|
|
75
|
+
in a separate `anime.db` from the disposable cache and renders offline.
|
|
76
|
+
|
|
77
|
+
**Cached catalog.** AniList responses (search, trending, seasonal, schedule,
|
|
78
|
+
details) are cached in a throwaway `cache.db` with short TTLs, so repeat browses
|
|
79
|
+
are instant and recently-seen pages still render offline. It is always safe to
|
|
80
|
+
wipe with `anime cache clear`; nothing user-owned lives there.
|
|
81
|
+
|
|
82
|
+
> Streaming providers break and get Cloudflare-gated constantly — that's the
|
|
83
|
+
> normal operating state, not a bug. When a provider is unreachable, anime-sh
|
|
84
|
+
> degrades cleanly instead of crashing; metadata and your library keep working.
|
|
85
|
+
|
|
86
|
+
**Multiple providers, merged.** anime-sh fans out across providers (currently
|
|
87
|
+
AllAnime + anikoto + AniZone) and falls through to whichever one actually has
|
|
88
|
+
your show — so a title missing from one source still plays from another, with no
|
|
89
|
+
action from you. AniZone serves a clean, un-obfuscated HLS stream with soft
|
|
90
|
+
English subs, so it plays where Cloudflare-gated sites can't.
|
|
91
|
+
|
|
92
|
+
## Install
|
|
93
|
+
|
|
94
|
+
Needs Python 3.11+, plus an external media player (`mpv` recommended) and
|
|
95
|
+
`ffmpeg` for playback/downloads. `anime doctor` reports what's missing.
|
|
96
|
+
|
|
97
|
+
Install straight from GitHub — this puts the `anime` command on your PATH:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
uv tool install "anime-sh[tui] @ git+https://github.com/Anime123450/anime-sh.git"
|
|
101
|
+
# or: pipx install "anime-sh[tui] @ git+https://github.com/Anime123450/anime-sh.git"
|
|
102
|
+
anime doctor
|
|
103
|
+
anime "Frieren"
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
(Not on PyPI yet; once it is, this shortens to `uv tool install "anime-sh[tui]"`.)
|
|
107
|
+
|
|
108
|
+
### From source (dev)
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
git clone https://github.com/Anime123450/anime-sh.git && cd anime-sh
|
|
112
|
+
uv sync --extra dev --extra tui
|
|
113
|
+
uv run anime doctor
|
|
114
|
+
uv run anime # launch the TUI
|
|
115
|
+
uv run pytest -q # tests (no network); add ANIME_SH_LIVE=1 for live ones
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
See [`docs/plugins.md`](docs/plugins.md) to add a provider or resolver.
|
|
119
|
+
|
|
120
|
+
## Develop
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
uv run pytest # fast unit suite — no network
|
|
124
|
+
uv run lint-imports # architecture contracts (must stay green)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## Design
|
|
128
|
+
|
|
129
|
+
anime-sh is layered `cli/tui → app → domain`, with `infra`, `providers`, and
|
|
130
|
+
`resolvers` as swappable adapters behind ports. Dependencies point downward only
|
|
131
|
+
and that is enforced in CI. Identity comes from AniList (every show is keyed by
|
|
132
|
+
its AniList id), so adding a provider is attaching a source to a known identity,
|
|
133
|
+
not fuzzy-matching titles. Full write-up: [`docs/architecture.md`](docs/architecture.md).
|
|
134
|
+
|
|
135
|
+
## Legal
|
|
136
|
+
|
|
137
|
+
anime-sh is a **client**, not a content library. It bundles no media, mirrors
|
|
138
|
+
nothing, and bypasses no DRM. Providers read public pages and are expected to
|
|
139
|
+
break; a broken provider is a degraded experience, not an outage. Provider
|
|
140
|
+
plugins are separable from the core so the project survives any single one.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "anime-sh"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
description = "The terminal-native anime client — providers, mirrors and resolvers, handled for you."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Animesh Sharma" }]
|
|
10
|
+
keywords = ["anime", "cli", "tui", "terminal", "streaming", "anilist", "mpv"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 4 - Beta",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: End Users/Desktop",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Topic :: Multimedia :: Video",
|
|
20
|
+
"Topic :: Utilities",
|
|
21
|
+
]
|
|
22
|
+
dependencies = [
|
|
23
|
+
"typer>=0.12",
|
|
24
|
+
"rich>=13.7",
|
|
25
|
+
"pydantic>=2.6",
|
|
26
|
+
"pydantic-settings>=2.2",
|
|
27
|
+
"aiosqlite>=0.20",
|
|
28
|
+
"httpx>=0.27",
|
|
29
|
+
"curl-cffi>=0.7",
|
|
30
|
+
"cryptography>=42.0",
|
|
31
|
+
"platformdirs>=4.2",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
tui = ["textual>=8.0", "pillow>=10.0"] # pillow renders cover art (optional at runtime)
|
|
36
|
+
discord = ["pypresence>=4.3"]
|
|
37
|
+
all = ["textual>=8.0", "pillow>=10.0", "pypresence>=4.3"]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=8.1",
|
|
40
|
+
"pytest-asyncio>=0.23",
|
|
41
|
+
"import-linter>=2.0",
|
|
42
|
+
"anyio>=4.3",
|
|
43
|
+
"textual>=8.0", # TUI tests run in the default dev env
|
|
44
|
+
"pillow>=10.0",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[project.urls]
|
|
48
|
+
Homepage = "https://github.com/animesh/anime-sh"
|
|
49
|
+
Repository = "https://github.com/animesh/anime-sh"
|
|
50
|
+
Issues = "https://github.com/animesh/anime-sh/issues"
|
|
51
|
+
Documentation = "https://github.com/animesh/anime-sh/tree/main/docs"
|
|
52
|
+
|
|
53
|
+
[project.scripts]
|
|
54
|
+
anime = "anime_sh.cli.main:main"
|
|
55
|
+
anime-sh = "anime_sh.cli.main:main"
|
|
56
|
+
|
|
57
|
+
# Bundled plugins use the same entry-point mechanism as third-party ones.
|
|
58
|
+
[project.entry-points."anime_sh.providers"]
|
|
59
|
+
allanime = "anime_sh.providers.allanime:AllAnimeProvider"
|
|
60
|
+
anikoto = "anime_sh.providers.anikoto:AnikotoProvider"
|
|
61
|
+
anizone = "anime_sh.providers.anizone:AnizoneProvider"
|
|
62
|
+
|
|
63
|
+
[project.entry-points."anime_sh.resolvers"]
|
|
64
|
+
allanime-clock = "anime_sh.resolvers.allanime:AllAnimeClockResolver"
|
|
65
|
+
mp4upload = "anime_sh.resolvers.mp4upload:Mp4UploadResolver"
|
|
66
|
+
megaplay = "anime_sh.resolvers.vidtube:VidtubeResolver"
|
|
67
|
+
filemoon = "anime_sh.resolvers.filemoon:FilemoonResolver"
|
|
68
|
+
streamwish = "anime_sh.resolvers.streamwish:StreamwishResolver"
|
|
69
|
+
generic = "anime_sh.resolvers.generic:GenericStreamResolver"
|
|
70
|
+
|
|
71
|
+
[build-system]
|
|
72
|
+
requires = ["uv_build>=0.11.28,<0.12.0"]
|
|
73
|
+
build-backend = "uv_build"
|
|
74
|
+
|
|
75
|
+
[tool.pytest.ini_options]
|
|
76
|
+
asyncio_mode = "auto"
|
|
77
|
+
testpaths = ["tests"]
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# import-linter: the guardrail that keeps the architecture from rotting.
|
|
81
|
+
# Run with `lint-imports`. Enforced in CI.
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
[tool.importlinter]
|
|
84
|
+
root_package = "anime_sh"
|
|
85
|
+
|
|
86
|
+
[[tool.importlinter.contracts]]
|
|
87
|
+
name = "Layered architecture (cli > tui > app > domain; infra behind ports)"
|
|
88
|
+
type = "layers"
|
|
89
|
+
layers = [
|
|
90
|
+
"anime_sh.cli",
|
|
91
|
+
"anime_sh.tui",
|
|
92
|
+
"anime_sh.app",
|
|
93
|
+
"anime_sh.domain",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
[[tool.importlinter.contracts]]
|
|
97
|
+
name = "Domain is dependency-free (imports nothing else in anime_sh)"
|
|
98
|
+
type = "forbidden"
|
|
99
|
+
source_modules = ["anime_sh.domain"]
|
|
100
|
+
forbidden_modules = [
|
|
101
|
+
"anime_sh.app",
|
|
102
|
+
"anime_sh.infra",
|
|
103
|
+
"anime_sh.providers",
|
|
104
|
+
"anime_sh.resolvers",
|
|
105
|
+
"anime_sh.cli",
|
|
106
|
+
"anime_sh.tui",
|
|
107
|
+
"anime_sh.config",
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
[[tool.importlinter.contracts]]
|
|
111
|
+
name = "App never imports infra/providers/resolvers concretes"
|
|
112
|
+
type = "forbidden"
|
|
113
|
+
source_modules = ["anime_sh.app"]
|
|
114
|
+
forbidden_modules = [
|
|
115
|
+
"anime_sh.infra",
|
|
116
|
+
"anime_sh.providers",
|
|
117
|
+
"anime_sh.resolvers",
|
|
118
|
+
"anime_sh.cli",
|
|
119
|
+
"anime_sh.tui",
|
|
120
|
+
]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""anime-sh — the terminal-native anime client.
|
|
2
|
+
|
|
3
|
+
Layered architecture: cli/tui → app (services) → domain (models + ports) with
|
|
4
|
+
infra/providers/resolvers as swappable adapters behind the ports. See
|
|
5
|
+
``docs/architecture.md``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib.metadata import PackageNotFoundError, version as _pkg_version
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
# Single source of truth: the version declared in pyproject/installed dist.
|
|
12
|
+
__version__ = _pkg_version("anime-sh")
|
|
13
|
+
except PackageNotFoundError: # running from a raw checkout that isn't installed
|
|
14
|
+
__version__ = "0.0.0+unknown"
|
|
15
|
+
|
|
16
|
+
from .cli.main import main
|
|
17
|
+
|
|
18
|
+
__all__ = ["main", "__version__"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Application services — orchestration only.
|
|
2
|
+
|
|
3
|
+
This package may import from `anime_sh.domain` but never from `anime_sh.infra`,
|
|
4
|
+
`providers`, or `resolvers`. It talks to the outside world exclusively through
|
|
5
|
+
the ports in `domain.ports`. `container.py` is the single place allowed to know
|
|
6
|
+
about concrete implementations, and it is wired from the composition root
|
|
7
|
+
(the CLI), not imported by services.
|
|
8
|
+
"""
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""DownloadService — resolve an episode's stream and save it to disk.
|
|
2
|
+
|
|
3
|
+
Reuses the exact same resolve path as playback (so downloads benefit from the
|
|
4
|
+
provider fan-out and resolver fallback), then hands the stream to a Downloader.
|
|
5
|
+
The show's metadata is cached so ``anime downloads`` renders titles offline.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Callable
|
|
13
|
+
|
|
14
|
+
from ..domain.models import Anime, Audio, DownloadStatus
|
|
15
|
+
from ..domain.ports import DownloadStore, Downloader, Library
|
|
16
|
+
from .playback import PlaybackService
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DownloadService:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
*,
|
|
23
|
+
playback: PlaybackService,
|
|
24
|
+
downloader: Downloader,
|
|
25
|
+
store: DownloadStore,
|
|
26
|
+
library: Library,
|
|
27
|
+
download_dir: str = "~/Videos/anime",
|
|
28
|
+
stream_proxy=None,
|
|
29
|
+
) -> None:
|
|
30
|
+
self._playback = playback
|
|
31
|
+
self._downloader = downloader
|
|
32
|
+
self._store = store
|
|
33
|
+
self._library = library
|
|
34
|
+
self._dir = Path(download_dir).expanduser()
|
|
35
|
+
self._stream_proxy = stream_proxy
|
|
36
|
+
|
|
37
|
+
def available(self) -> bool:
|
|
38
|
+
return self._downloader.available()
|
|
39
|
+
|
|
40
|
+
def destination(self, anime: Anime, episode: float) -> Path:
|
|
41
|
+
show = _safe(anime.title.preferred)
|
|
42
|
+
return self._dir / show / f"{show} - E{episode:g}.mp4"
|
|
43
|
+
|
|
44
|
+
async def download(
|
|
45
|
+
self, anime: Anime, episode: float, *, audio: Audio = Audio.SUB,
|
|
46
|
+
on_line: Callable[[str], None] | None = None,
|
|
47
|
+
) -> Path:
|
|
48
|
+
resolved = await self._playback.resolve(anime, episode, audio=audio)
|
|
49
|
+
stream = (
|
|
50
|
+
self._stream_proxy.rewrite(resolved.stream)
|
|
51
|
+
if self._stream_proxy else resolved.stream
|
|
52
|
+
)
|
|
53
|
+
await self._library.save_anime(anime)
|
|
54
|
+
dest = self.destination(anime, episode)
|
|
55
|
+
|
|
56
|
+
download_id = await self._store.add(anime.id, episode, str(dest))
|
|
57
|
+
await self._store.set_status(download_id, DownloadStatus.DOWNLOADING)
|
|
58
|
+
try:
|
|
59
|
+
await self._downloader.download(stream, dest, on_line=on_line)
|
|
60
|
+
except Exception:
|
|
61
|
+
await self._store.set_status(download_id, DownloadStatus.FAILED)
|
|
62
|
+
raise
|
|
63
|
+
await self._store.set_status(download_id, DownloadStatus.DONE, path=str(dest))
|
|
64
|
+
return dest
|
|
65
|
+
|
|
66
|
+
async def history(self, *, limit: int = 50):
|
|
67
|
+
return await self._store.list(limit=limit)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_ILLEGAL = re.compile(r'[<>:"/\\|?*\x00-\x1f]')
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _safe(name: str) -> str:
|
|
74
|
+
"""Filesystem-safe file/dir name."""
|
|
75
|
+
cleaned = _ILLEGAL.sub("", name).strip().rstrip(".")
|
|
76
|
+
return cleaned or "anime"
|