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.
Files changed (93) hide show
  1. anime_sh-0.2.0/LICENSE +21 -0
  2. anime_sh-0.2.0/PKG-INFO +193 -0
  3. anime_sh-0.2.0/README.md +144 -0
  4. anime_sh-0.2.0/pyproject.toml +120 -0
  5. anime_sh-0.2.0/src/anime_sh/__init__.py +18 -0
  6. anime_sh-0.2.0/src/anime_sh/__main__.py +6 -0
  7. anime_sh-0.2.0/src/anime_sh/app/__init__.py +8 -0
  8. anime_sh-0.2.0/src/anime_sh/app/download.py +76 -0
  9. anime_sh-0.2.0/src/anime_sh/app/library.py +97 -0
  10. anime_sh-0.2.0/src/anime_sh/app/playback.py +490 -0
  11. anime_sh-0.2.0/src/anime_sh/app/providers.py +227 -0
  12. anime_sh-0.2.0/src/anime_sh/app/search.py +151 -0
  13. anime_sh-0.2.0/src/anime_sh/app/sync.py +72 -0
  14. anime_sh-0.2.0/src/anime_sh/cli/__init__.py +6 -0
  15. anime_sh-0.2.0/src/anime_sh/cli/container.py +167 -0
  16. anime_sh-0.2.0/src/anime_sh/cli/doctor.py +120 -0
  17. anime_sh-0.2.0/src/anime_sh/cli/main.py +1537 -0
  18. anime_sh-0.2.0/src/anime_sh/config/__init__.py +6 -0
  19. anime_sh-0.2.0/src/anime_sh/config/defaults.toml +29 -0
  20. anime_sh-0.2.0/src/anime_sh/config/loader.py +120 -0
  21. anime_sh-0.2.0/src/anime_sh/config/paths.py +42 -0
  22. anime_sh-0.2.0/src/anime_sh/config/schema.py +63 -0
  23. anime_sh-0.2.0/src/anime_sh/domain/__init__.py +5 -0
  24. anime_sh-0.2.0/src/anime_sh/domain/errors.py +49 -0
  25. anime_sh-0.2.0/src/anime_sh/domain/health.py +72 -0
  26. anime_sh-0.2.0/src/anime_sh/domain/models.py +346 -0
  27. anime_sh-0.2.0/src/anime_sh/domain/ports.py +265 -0
  28. anime_sh-0.2.0/src/anime_sh/domain/ranking.py +73 -0
  29. anime_sh-0.2.0/src/anime_sh/infra/__init__.py +5 -0
  30. anime_sh-0.2.0/src/anime_sh/infra/cache/__init__.py +1 -0
  31. anime_sh-0.2.0/src/anime_sh/infra/cache/kv.py +62 -0
  32. anime_sh-0.2.0/src/anime_sh/infra/db/__init__.py +11 -0
  33. anime_sh-0.2.0/src/anime_sh/infra/db/database.py +71 -0
  34. anime_sh-0.2.0/src/anime_sh/infra/db/downloads.py +64 -0
  35. anime_sh-0.2.0/src/anime_sh/infra/db/health.py +64 -0
  36. anime_sh-0.2.0/src/anime_sh/infra/db/library.py +337 -0
  37. anime_sh-0.2.0/src/anime_sh/infra/db/migrations/0001_initial.sql +81 -0
  38. anime_sh-0.2.0/src/anime_sh/infra/db/migrations_cache/0001_initial.sql +24 -0
  39. anime_sh-0.2.0/src/anime_sh/infra/downloader/__init__.py +5 -0
  40. anime_sh-0.2.0/src/anime_sh/infra/downloader/ffmpeg.py +95 -0
  41. anime_sh-0.2.0/src/anime_sh/infra/http/__init__.py +6 -0
  42. anime_sh-0.2.0/src/anime_sh/infra/http/client.py +161 -0
  43. anime_sh-0.2.0/src/anime_sh/infra/http/probe.py +64 -0
  44. anime_sh-0.2.0/src/anime_sh/infra/metadata/__init__.py +5 -0
  45. anime_sh-0.2.0/src/anime_sh/infra/metadata/anilist.py +395 -0
  46. anime_sh-0.2.0/src/anime_sh/infra/players/__init__.py +7 -0
  47. anime_sh-0.2.0/src/anime_sh/infra/players/mpv.py +281 -0
  48. anime_sh-0.2.0/src/anime_sh/infra/players/null.py +48 -0
  49. anime_sh-0.2.0/src/anime_sh/infra/proxy/__init__.py +5 -0
  50. anime_sh-0.2.0/src/anime_sh/infra/proxy/deobfuscate.py +208 -0
  51. anime_sh-0.2.0/src/anime_sh/infra/registry.py +66 -0
  52. anime_sh-0.2.0/src/anime_sh/infra/skiptimes/__init__.py +5 -0
  53. anime_sh-0.2.0/src/anime_sh/infra/skiptimes/aniskip.py +71 -0
  54. anime_sh-0.2.0/src/anime_sh/infra/tracker/__init__.py +18 -0
  55. anime_sh-0.2.0/src/anime_sh/infra/tracker/anilist.py +274 -0
  56. anime_sh-0.2.0/src/anime_sh/infra/tracker/tokens.py +57 -0
  57. anime_sh-0.2.0/src/anime_sh/providers/__init__.py +7 -0
  58. anime_sh-0.2.0/src/anime_sh/providers/allanime/__init__.py +3 -0
  59. anime_sh-0.2.0/src/anime_sh/providers/allanime/decode.py +104 -0
  60. anime_sh-0.2.0/src/anime_sh/providers/allanime/keygen.py +158 -0
  61. anime_sh-0.2.0/src/anime_sh/providers/allanime/provider.py +344 -0
  62. anime_sh-0.2.0/src/anime_sh/providers/anikoto/__init__.py +3 -0
  63. anime_sh-0.2.0/src/anime_sh/providers/anikoto/provider.py +320 -0
  64. anime_sh-0.2.0/src/anime_sh/providers/anizone/__init__.py +5 -0
  65. anime_sh-0.2.0/src/anime_sh/providers/anizone/provider.py +224 -0
  66. anime_sh-0.2.0/src/anime_sh/py.typed +0 -0
  67. anime_sh-0.2.0/src/anime_sh/resolvers/__init__.py +6 -0
  68. anime_sh-0.2.0/src/anime_sh/resolvers/allanime/__init__.py +3 -0
  69. anime_sh-0.2.0/src/anime_sh/resolvers/allanime/clock.py +73 -0
  70. anime_sh-0.2.0/src/anime_sh/resolvers/filemoon/__init__.py +5 -0
  71. anime_sh-0.2.0/src/anime_sh/resolvers/filemoon/resolver.py +77 -0
  72. anime_sh-0.2.0/src/anime_sh/resolvers/generic/__init__.py +3 -0
  73. anime_sh-0.2.0/src/anime_sh/resolvers/generic/hls.py +31 -0
  74. anime_sh-0.2.0/src/anime_sh/resolvers/mp4upload/__init__.py +3 -0
  75. anime_sh-0.2.0/src/anime_sh/resolvers/mp4upload/resolver.py +53 -0
  76. anime_sh-0.2.0/src/anime_sh/resolvers/packed.py +63 -0
  77. anime_sh-0.2.0/src/anime_sh/resolvers/quality.py +30 -0
  78. anime_sh-0.2.0/src/anime_sh/resolvers/streamwish/__init__.py +5 -0
  79. anime_sh-0.2.0/src/anime_sh/resolvers/streamwish/resolver.py +69 -0
  80. anime_sh-0.2.0/src/anime_sh/resolvers/vidtube/__init__.py +3 -0
  81. anime_sh-0.2.0/src/anime_sh/resolvers/vidtube/resolver.py +132 -0
  82. anime_sh-0.2.0/src/anime_sh/tui/__init__.py +10 -0
  83. anime_sh-0.2.0/src/anime_sh/tui/app.py +112 -0
  84. anime_sh-0.2.0/src/anime_sh/tui/app.tcss +45 -0
  85. anime_sh-0.2.0/src/anime_sh/tui/coverart.py +93 -0
  86. anime_sh-0.2.0/src/anime_sh/tui/format.py +74 -0
  87. anime_sh-0.2.0/src/anime_sh/tui/screens/__init__.py +0 -0
  88. anime_sh-0.2.0/src/anime_sh/tui/screens/detail.py +214 -0
  89. anime_sh-0.2.0/src/anime_sh/tui/screens/help.py +45 -0
  90. anime_sh-0.2.0/src/anime_sh/tui/screens/home.py +198 -0
  91. anime_sh-0.2.0/src/anime_sh/tui/screens/mylist.py +78 -0
  92. anime_sh-0.2.0/src/anime_sh/tui/screens/sources.py +79 -0
  93. 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.
@@ -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
@@ -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,6 @@
1
+ """Enables ``python -m anime_sh``."""
2
+
3
+ from .cli.main import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -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"