anipy-cli 2.7.17__py3-none-any.whl → 3.8.2__py3-none-any.whl

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 (67) hide show
  1. anipy_cli/__init__.py +2 -20
  2. anipy_cli/anilist_proxy.py +229 -0
  3. anipy_cli/arg_parser.py +109 -21
  4. anipy_cli/cli.py +98 -0
  5. anipy_cli/clis/__init__.py +17 -0
  6. anipy_cli/clis/anilist_cli.py +62 -0
  7. anipy_cli/clis/base_cli.py +34 -0
  8. anipy_cli/clis/binge_cli.py +96 -0
  9. anipy_cli/clis/default_cli.py +115 -0
  10. anipy_cli/clis/download_cli.py +85 -0
  11. anipy_cli/clis/history_cli.py +96 -0
  12. anipy_cli/clis/mal_cli.py +71 -0
  13. anipy_cli/{cli/clis → clis}/seasonal_cli.py +9 -6
  14. anipy_cli/colors.py +14 -8
  15. anipy_cli/config.py +387 -90
  16. anipy_cli/discord.py +34 -0
  17. anipy_cli/download_component.py +194 -0
  18. anipy_cli/logger.py +200 -0
  19. anipy_cli/mal_proxy.py +228 -0
  20. anipy_cli/menus/__init__.py +6 -0
  21. anipy_cli/menus/anilist_menu.py +671 -0
  22. anipy_cli/{cli/menus → menus}/base_menu.py +9 -14
  23. anipy_cli/menus/mal_menu.py +657 -0
  24. anipy_cli/menus/menu.py +265 -0
  25. anipy_cli/menus/seasonal_menu.py +270 -0
  26. anipy_cli/prompts.py +387 -0
  27. anipy_cli/util.py +268 -0
  28. anipy_cli-3.8.2.dist-info/METADATA +71 -0
  29. anipy_cli-3.8.2.dist-info/RECORD +31 -0
  30. {anipy_cli-2.7.17.dist-info → anipy_cli-3.8.2.dist-info}/WHEEL +1 -2
  31. anipy_cli-3.8.2.dist-info/entry_points.txt +3 -0
  32. anipy_cli/cli/__init__.py +0 -1
  33. anipy_cli/cli/cli.py +0 -37
  34. anipy_cli/cli/clis/__init__.py +0 -6
  35. anipy_cli/cli/clis/base_cli.py +0 -43
  36. anipy_cli/cli/clis/binge_cli.py +0 -54
  37. anipy_cli/cli/clis/default_cli.py +0 -46
  38. anipy_cli/cli/clis/download_cli.py +0 -92
  39. anipy_cli/cli/clis/history_cli.py +0 -64
  40. anipy_cli/cli/clis/mal_cli.py +0 -27
  41. anipy_cli/cli/menus/__init__.py +0 -3
  42. anipy_cli/cli/menus/mal_menu.py +0 -411
  43. anipy_cli/cli/menus/menu.py +0 -102
  44. anipy_cli/cli/menus/seasonal_menu.py +0 -174
  45. anipy_cli/cli/util.py +0 -118
  46. anipy_cli/download.py +0 -454
  47. anipy_cli/history.py +0 -83
  48. anipy_cli/mal.py +0 -645
  49. anipy_cli/misc.py +0 -227
  50. anipy_cli/player/__init__.py +0 -1
  51. anipy_cli/player/player.py +0 -33
  52. anipy_cli/player/players/__init__.py +0 -3
  53. anipy_cli/player/players/base.py +0 -106
  54. anipy_cli/player/players/mpv.py +0 -19
  55. anipy_cli/player/players/mpv_contrl.py +0 -37
  56. anipy_cli/player/players/syncplay.py +0 -19
  57. anipy_cli/player/players/vlc.py +0 -18
  58. anipy_cli/query.py +0 -92
  59. anipy_cli/run_anipy_cli.py +0 -14
  60. anipy_cli/seasonal.py +0 -106
  61. anipy_cli/url_handler.py +0 -442
  62. anipy_cli/version.py +0 -1
  63. anipy_cli-2.7.17.dist-info/LICENSE +0 -674
  64. anipy_cli-2.7.17.dist-info/METADATA +0 -159
  65. anipy_cli-2.7.17.dist-info/RECORD +0 -43
  66. anipy_cli-2.7.17.dist-info/entry_points.txt +0 -2
  67. anipy_cli-2.7.17.dist-info/top_level.txt +0 -1
anipy_cli/__init__.py CHANGED
@@ -1,20 +1,2 @@
1
- """
2
- █████  ███  ██ ██ ██████  ██  ██  ██████ ██  ██ 
3
- ██   ██ ████  ██ ██ ██   ██  ██  ██   ██      ██  ██ 
4
- ███████ ██ ██  ██ ██ ██████    ████  █████ ██  ██  ██ 
5
- ██   ██ ██  ██ ██ ██ ██       ██         ██  ██  ██ 
6
- ██  ██ ██   ████ ██ ██  ██   ██████ ███████ ██ 
7
-
8
- ~ The best tool to watch and Download your favourite anime.
9
-
10
- https://github.com/sdaqo/anipy-cli
11
-
12
- """
13
- from anipy_cli.download import download
14
- from anipy_cli.url_handler import epHandler, videourl
15
- from anipy_cli import config
16
- from anipy_cli.query import query
17
- from anipy_cli.player import get_player
18
- from anipy_cli.misc import Entry, get_anime_info
19
- from anipy_cli.seasonal import Seasonal
20
- from anipy_cli.history import history
1
+ __appname__ = "anipy-cli"
2
+ __version__ = "3.8.2"
@@ -0,0 +1,229 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Dict, List, Optional, Set
3
+
4
+ from anipy_api.anime import Anime
5
+ from anipy_api.anilist import (
6
+ AniListAnime,
7
+ AniListMyListStatus,
8
+ AniListMyListStatusEnum,
9
+ AniList,
10
+ AniListAdapter,
11
+ )
12
+ from anipy_api.provider import LanguageTypeEnum, list_providers
13
+ from dataclasses_json import DataClassJsonMixin, config
14
+ from InquirerPy import inquirer
15
+
16
+ from anipy_cli.config import Config
17
+ from anipy_cli.util import error, get_prefered_providers
18
+
19
+
20
+ @dataclass
21
+ class ProviderMapping(DataClassJsonMixin):
22
+ provider: str = field(metadata=config(field_name="pv"))
23
+ name: str = field(metadata=config(field_name="na"))
24
+ identifier: str = field(metadata=config(field_name="id"))
25
+ languages: Set[LanguageTypeEnum] = field(metadata=config(field_name="la"))
26
+
27
+
28
+ @dataclass
29
+ class AniListProviderMapping(DataClassJsonMixin):
30
+ anilist_anime: AniListAnime
31
+ mappings: Dict[str, ProviderMapping]
32
+
33
+
34
+ @dataclass
35
+ class AniListLocalList(DataClassJsonMixin):
36
+ mappings: Dict[int, AniListProviderMapping]
37
+
38
+ def write(self, user_id: int):
39
+ config = Config()
40
+ local_list = config._anilist_local_user_list_path.with_stem(
41
+ f"{config._anilist_local_user_list_path.stem}_{user_id}"
42
+ )
43
+ local_list.write_text(self.to_json())
44
+
45
+ @staticmethod
46
+ def read(user_id: int) -> "AniListLocalList":
47
+ config = Config()
48
+ local_list = config._anilist_local_user_list_path.with_stem(
49
+ f"{config._anilist_local_user_list_path.stem}_{user_id}"
50
+ )
51
+
52
+ if not local_list.is_file():
53
+ local_list.parent.mkdir(exist_ok=True, parents=True)
54
+ local_list.touch()
55
+ mylist = AniListLocalList({})
56
+ mylist.write(user_id)
57
+ return mylist
58
+
59
+ try:
60
+ mylist: AniListLocalList = AniListLocalList.from_json(
61
+ local_list.read_text()
62
+ )
63
+ except KeyError:
64
+ choice = inquirer.confirm(
65
+ message=f"Your local AniList ({str(local_list)}) is not in the correct format, should it be deleted?",
66
+ default=False,
67
+ ).execute()
68
+ if choice:
69
+ local_list.unlink()
70
+ return AniListLocalList.read(user_id)
71
+ else:
72
+ error("could not read your AniList", fatal=True)
73
+
74
+ return mylist
75
+
76
+
77
+ class AniListProxy:
78
+ def __init__(self, anilist: AniList):
79
+ self.anilist = anilist
80
+ self.user_id = anilist.get_user().id
81
+ self.local_list = AniListLocalList.read(self.user_id)
82
+
83
+ def _cache_list(self, mylist: List[AniListAnime]):
84
+ config = Config()
85
+ for e in mylist:
86
+ if self.local_list.mappings.get(e.id, None):
87
+ if (
88
+ e.my_list_status
89
+ and config.tracker_ignore_tag in e.my_list_status.tags
90
+ ):
91
+ self.local_list.mappings.pop(e.id)
92
+ else:
93
+ self.local_list.mappings[e.id].anilist_anime = e
94
+ else:
95
+ self.local_list.mappings[e.id] = AniListProviderMapping(e, {})
96
+
97
+ self.local_list.write(self.user_id)
98
+
99
+ def _write_mapping(self, anilist_anime: AniListAnime, mapping: Anime):
100
+ self._cache_list([anilist_anime])
101
+
102
+ self.local_list.mappings[anilist_anime.id].mappings[
103
+ f"{mapping.provider.NAME}:{mapping.identifier}"
104
+ ] = ProviderMapping(
105
+ mapping.provider.NAME, mapping.name, mapping.identifier, mapping.languages
106
+ )
107
+
108
+ self.local_list.write(self.user_id)
109
+
110
+ def get_list(
111
+ self, status_catagories: Optional[Set[AniListMyListStatusEnum]] = None
112
+ ) -> List[AniListAnime]:
113
+ config = Config()
114
+ mylist: List[AniListAnime] = []
115
+
116
+ catagories = (
117
+ status_catagories
118
+ if status_catagories is not None
119
+ else set(
120
+ [
121
+ AniListMyListStatusEnum[s.upper()]
122
+ for s in config.tracker_status_categories
123
+ ]
124
+ )
125
+ )
126
+
127
+ for c in catagories:
128
+ mylist.extend(
129
+ filter(
130
+ lambda e: (
131
+ config.tracker_ignore_tag not in e.my_list_status.tags
132
+ if e.my_list_status
133
+ else True
134
+ ),
135
+ self.anilist.get_anime_list(c),
136
+ )
137
+ )
138
+
139
+ self._cache_list(mylist)
140
+ filtered_list = filter(
141
+ lambda x: (
142
+ x.my_list_status.status in catagories if x.my_list_status else False
143
+ ),
144
+ mylist,
145
+ )
146
+ return list(filtered_list)
147
+
148
+ def update_show(
149
+ self,
150
+ anime: AniListAnime,
151
+ status: Optional[AniListMyListStatusEnum] = None,
152
+ episode: Optional[int] = None,
153
+ tags: Set[str] = set(),
154
+ ) -> AniListMyListStatus:
155
+ config = Config()
156
+ tags |= set(config.tracker_tags)
157
+ result = self.anilist.update_anime_list(
158
+ anime.id, status=status, watched_episodes=episode, tags=list(tags)
159
+ )
160
+ anime.my_list_status = result
161
+ self._cache_list([anime])
162
+ return result
163
+
164
+ def delete_show(self, anime: AniListAnime) -> None:
165
+ self.local_list.mappings.pop(anime.id)
166
+ self.local_list.write(self.user_id)
167
+
168
+ self.anilist.remove_from_anime_list(anime.id)
169
+
170
+ def map_from_anilist(
171
+ self, anime: AniListAnime, mapping: Optional[Anime] = None
172
+ ) -> Optional[Anime]:
173
+ if mapping is not None:
174
+ self._write_mapping(anime, mapping)
175
+ return mapping
176
+
177
+ if self.local_list.mappings[anime.id].mappings:
178
+ for map in self.local_list.mappings[anime.id].mappings.values():
179
+ provider = next(
180
+ filter(lambda x: x.NAME == map.provider, list_providers()), None
181
+ )
182
+
183
+ if provider is None:
184
+ continue
185
+
186
+ return Anime(provider(), map.name, map.identifier, map.languages)
187
+
188
+ config = Config()
189
+ result = None
190
+ for p in get_prefered_providers("anilist"):
191
+ adapter = AniListAdapter(self.anilist, p)
192
+ result = adapter.from_anilist(
193
+ anime,
194
+ config.tracker_mapping_min_similarity,
195
+ config.tracker_mapping_use_filters,
196
+ config.tracker_mapping_use_alternatives,
197
+ )
198
+ if result is not None:
199
+ break
200
+
201
+ if result:
202
+ self._write_mapping(anime, result)
203
+
204
+ return result
205
+
206
+ def map_from_provider(
207
+ self, anime: Anime, mapping: Optional[AniListAnime] = None
208
+ ) -> Optional[AniListAnime]:
209
+ if mapping is not None:
210
+ self._write_mapping(mapping, anime)
211
+ return mapping
212
+
213
+ for _, m in self.local_list.mappings.items():
214
+ existing = m.mappings.get(f"{anime.provider.NAME}:{anime.identifier}", None)
215
+ if existing:
216
+ return m.anilist_anime
217
+
218
+ config = Config()
219
+ adapter = AniListAdapter(self.anilist, anime.provider)
220
+ result = adapter.from_provider(
221
+ anime,
222
+ config.tracker_mapping_min_similarity,
223
+ config.tracker_mapping_use_alternatives,
224
+ )
225
+
226
+ if result:
227
+ self._write_mapping(result, anime)
228
+
229
+ return result
anipy_cli/arg_parser.py CHANGED
@@ -1,31 +1,41 @@
1
1
  import argparse
2
- from pathlib import Path
3
2
  from dataclasses import dataclass
4
- from typing import Union, Optional
5
- from anipy_cli.version import __version__
3
+ from pathlib import Path
4
+ from typing import Optional, Union
6
5
 
6
+ from anipy_cli import __version__
7
7
 
8
- @dataclass(frozen=True)
8
+
9
+ @dataclass()
9
10
  class CliArgs:
10
11
  download: bool
11
12
  binge: bool
12
13
  history: bool
13
14
  seasonal: bool
14
15
  mal: bool
16
+ anilist: bool
15
17
  delete: bool
18
+ migrate_hist: bool
16
19
  quality: Optional[Union[str, int]]
17
20
  ffmpeg: bool
18
- no_season_search: bool
19
21
  auto_update: bool
22
+ mal_sync_seasonals: bool
23
+ anilist_sync_seasonals: bool
20
24
  optional_player: Optional[str]
25
+ search: Optional[str]
21
26
  location: Optional[Path]
27
+ verbosity: int
28
+ stack_always: bool
29
+ mal_user: Optional[str]
22
30
  mal_password: Optional[str]
23
31
  config: bool
32
+ seasonal_search: Optional[str]
33
+ subtitles: bool
24
34
 
25
35
 
26
- def parse_args() -> CliArgs:
36
+ def parse_args(override_args: Optional[list[str]] = None) -> CliArgs:
27
37
  parser = argparse.ArgumentParser(
28
- description="Play Animes from gogoanime in local video-player or Download them.",
38
+ description="Play Animes from online anime providers locally or download them, and much more.",
29
39
  add_help=False,
30
40
  )
31
41
 
@@ -88,6 +98,15 @@ def parse_args() -> CliArgs:
88
98
  "(requires MAL account credentials to be set in config).",
89
99
  )
90
100
 
101
+ actions_group.add_argument(
102
+ "-A",
103
+ "--anilist",
104
+ required=False,
105
+ dest="anilist",
106
+ action="store_true",
107
+ help="Anilist mode. Similar to seasonal mode, but using Anilist",
108
+ )
109
+
91
110
  actions_group.add_argument(
92
111
  "--delete-history",
93
112
  required=False,
@@ -96,12 +115,41 @@ def parse_args() -> CliArgs:
96
115
  help="Delete your History.",
97
116
  )
98
117
 
118
+ actions_group.add_argument(
119
+ "--migrate-history",
120
+ required=False,
121
+ dest="migrate_hist",
122
+ action="store_true",
123
+ help="Migrate your history to the current provider.",
124
+ )
125
+
126
+ options_group.add_argument(
127
+ "-s",
128
+ "--search",
129
+ required=False,
130
+ dest="search",
131
+ action="store",
132
+ help="Provide a search term to Default, Download or Binge mode in this format: {query}:{episode range}:{dub/sub}. Examples: 'frieren:1-10:sub' or 'frieren:1:sub' or 'frieren:1-3 7-12:dub', this argument may be appended to any of the modes mentioned like so: 'anipy-cli (-D/B) -s <search>'",
133
+ )
134
+
135
+ options_group.add_argument(
136
+ "-ss",
137
+ "--seasonal-search",
138
+ required=False,
139
+ dest="seasonal_search",
140
+ nargs="?", # 1 or none possible args
141
+ default=None, # Used if flag is not present (added this line for clarity, because default is always None)
142
+ const=True, # Used if flag is present, but no value
143
+ help="Provide search parameters for seasons to Default, Download, or Binge mode in this format: {year}:{season}. You can only use part of the season name if you wish. Examples: '2024:win' or '2020:fa'",
144
+ )
145
+
99
146
  options_group.add_argument(
100
147
  "-q",
101
148
  "--quality",
102
149
  action="store",
103
150
  required=False,
104
- default="auto",
151
+ default="best",
152
+ type=lambda v: int(v) if v.isdigit() else v,
105
153
  help="Change the quality of the video, accepts: best, worst or 360, 480, 720 etc. Default: best",
106
154
  )
107
155
 
@@ -114,30 +162,20 @@ def parse_args() -> CliArgs:
114
162
  help="Use ffmpeg to download m3u8 playlists, may be more stable but is way slower than internal downloader",
115
163
  )
116
164
 
117
- options_group.add_argument(
118
- "-o",
119
- "--no-seas-search",
120
- required=False,
121
- dest="no_season_search",
122
- action="store_true",
123
- help="Turn off search in season. "
124
- "Disables prompting if GoGoAnime is to be searched for anime in specific season.",
125
- )
126
-
127
165
  options_group.add_argument(
128
166
  "-a",
129
167
  "--auto-update",
130
168
  required=False,
131
169
  dest="auto_update",
132
170
  action="store_true",
133
- help="Automatically update and download all Anime in seasonals list from start EP to newest.",
171
+ help="Automatically update and download all Anime in seasonals or mal mode from start EP to newest.",
134
172
  )
135
173
 
136
174
  options_group.add_argument(
137
175
  "-p",
138
176
  "--optional-player",
139
177
  required=False,
140
- choices=["mpv", "vlc", "syncplay", "mpvnet"],
178
+ choices=["mpv", "vlc", "iina", "syncplay", "mpvnet", "mpv-controlled"],
141
179
  help="Override the player set in the config.",
142
180
  )
143
181
 
@@ -152,6 +190,33 @@ def parse_args() -> CliArgs:
152
190
  help="Override all configured download locations",
153
191
  )
154
192
 
193
+ options_group.add_argument(
194
+ "-V",
195
+ "--verbose",
196
+ required=False,
197
+ dest="verbosity",
198
+ action="count",
199
+ default=0,
200
+ help="Verbosity levels in the console: -V = 'fatal' -VV = 'warnings' -VVV = 'info'",
201
+ )
202
+
203
+ options_group.add_argument(
204
+ "--stack-always",
205
+ required=False,
206
+ dest="stack_always",
207
+ action="store_true",
208
+ help="Always show the stack trace on any log outputs.",
209
+ )
210
+
211
+ options_group.add_argument(
212
+ "-so",
213
+ "--sub-only",
214
+ required=False,
215
+ dest="subtitles",
216
+ action="store_true",
217
+ help="Download only subtitles",
218
+ )
219
+
155
220
  options_group.add_argument(
156
221
  "--mal-password",
157
222
  required=False,
@@ -159,6 +224,29 @@ def parse_args() -> CliArgs:
159
224
  action="store",
160
225
  help="Provide password for MAL login (overrides password set in config)",
161
226
  )
227
+ options_group.add_argument(
228
+ "--mal-user",
229
+ required=False,
230
+ dest="mal_user",
231
+ action="store",
232
+ help="Provide username for MAL login (overrides username set in config)",
233
+ )
234
+
235
+ options_group.add_argument(
236
+ "--mal-sync-to-seasonals",
237
+ required=False,
238
+ dest="mal_sync_seasonals",
239
+ action="store_true",
240
+ help="Automatically sync myanimelist to seasonals (only works with `-M`)",
241
+ )
242
+
243
+ options_group.add_argument(
244
+ "--anilist-sync-to-seasonals",
245
+ required=False,
246
+ dest="anilist_sync_seasonals",
247
+ action="store_true",
248
+ help="Automatically sync anilist to seasonals (only works with `-A`)",
249
+ )
162
250
 
163
251
  info_group.add_argument(
164
252
  "-h", "--help", action="help", help="show this help message and exit"
@@ -174,4 +262,4 @@ def parse_args() -> CliArgs:
174
262
  help="Print path to the config file.",
175
263
  )
176
264
 
177
- return CliArgs(**vars(parser.parse_args()))
265
+ return CliArgs(**vars(parser.parse_args(args=override_args)))
anipy_cli/cli.py ADDED
@@ -0,0 +1,98 @@
1
+ from pathlib import Path
2
+ from types import TracebackType
3
+ from typing import Optional
4
+
5
+ from pypresence.exceptions import DiscordNotFound
6
+
7
+ from anipy_api.locallist import LocalList
8
+ from anipy_cli.prompts import migrate_provider
9
+
10
+ from anipy_cli.arg_parser import CliArgs, parse_args
11
+ from anipy_cli.clis import *
12
+ from anipy_cli.colors import color, colors, cprint
13
+ from anipy_cli.util import error, DotSpinner, migrate_locallist
14
+ from anipy_cli.config import Config
15
+ from anipy_cli.discord import DiscordPresence
16
+ import anipy_cli.logger as logger
17
+
18
+
19
+ def run_cli(override_args: Optional[list[str]] = None):
20
+ args = parse_args(override_args)
21
+
22
+ logger.set_cli_verbosity(args.verbosity)
23
+ logger.set_stack_always(args.stack_always)
24
+
25
+ def fatal_handler(
26
+ exc_val: BaseException, exc_tb: TracebackType, logs_location: Path
27
+ ):
28
+ print(
29
+ color(
30
+ colors.RED,
31
+ f'A fatal error of type [{exc_val.__class__.__name__}] has occurred with message "{exc_val.args[0]}". Logs can be found at {logs_location}.',
32
+ )
33
+ )
34
+
35
+ with logger.safe(fatal_handler):
36
+ _safe_cli(args)
37
+
38
+
39
+ def _safe_cli(args: CliArgs):
40
+ config = Config()
41
+ # This updates the config, adding new values doc changes and the like.
42
+ config._create_config()
43
+
44
+ if config.dc_presence:
45
+ with DotSpinner("Intializing Discord Presence...") as s:
46
+ try:
47
+ DiscordPresence()
48
+ s.set_text(colors.GREEN, "Initialized Discord Presence")
49
+ s.ok("✔")
50
+ except DiscordNotFound:
51
+ s.set_text(
52
+ colors.RED,
53
+ "Discord is not opened, can't initialize Discord Presence",
54
+ )
55
+ s.fail("✘")
56
+ except ConnectionError:
57
+ s.set_text(
58
+ colors.RED,
59
+ "Can't Connect to discord, can't initialize Discord Presence",
60
+ )
61
+ s.fail("✘")
62
+
63
+ if args.config:
64
+ print(config._config_file)
65
+ return
66
+ elif args.delete:
67
+ try:
68
+ config._history_file_path.unlink()
69
+ cprint(colors.RED, "Done")
70
+ except FileNotFoundError:
71
+ error("no history file found")
72
+ return
73
+ elif args.migrate_hist:
74
+ history_list = LocalList(
75
+ Config()._history_file_path, migrate_cb=migrate_locallist
76
+ )
77
+ migrate_provider("default", history_list)
78
+ return
79
+
80
+ clis_dict = {
81
+ args.download: DownloadCli,
82
+ args.binge: BingeCli,
83
+ args.seasonal: SeasonalCli,
84
+ args.history: HistoryCli,
85
+ args.mal: MalCli,
86
+ args.anilist: AniListCli,
87
+ }
88
+
89
+ cli_class = clis_dict.get(True, DefaultCli)
90
+
91
+ try:
92
+ cli_class(options=args).run()
93
+ except KeyboardInterrupt:
94
+ error("interrupted", fatal=True)
95
+
96
+
97
+ if __name__ == "__main__":
98
+ run_cli()
@@ -0,0 +1,17 @@
1
+ from anipy_cli.clis.default_cli import DefaultCli
2
+ from anipy_cli.clis.history_cli import HistoryCli
3
+ from anipy_cli.clis.mal_cli import MalCli
4
+ from anipy_cli.clis.seasonal_cli import SeasonalCli
5
+ from anipy_cli.clis.binge_cli import BingeCli
6
+ from anipy_cli.clis.download_cli import DownloadCli
7
+ from anipy_cli.clis.anilist_cli import AniListCli
8
+
9
+ __all__ = [
10
+ "DefaultCli",
11
+ "HistoryCli",
12
+ "MalCli",
13
+ "AniListCli",
14
+ "SeasonalCli",
15
+ "BingeCli",
16
+ "DownloadCli",
17
+ ]
@@ -0,0 +1,62 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from anipy_api.error import AniListError
4
+ from anipy_api.anilist import AniList
5
+ from InquirerPy import inquirer
6
+
7
+ from anipy_cli.clis.base_cli import CliBase
8
+ from anipy_cli.config import Config
9
+ from anipy_cli.menus import AniListMenu
10
+ from anipy_cli.util import DotSpinner, error
11
+ import webbrowser
12
+
13
+ if TYPE_CHECKING:
14
+ from anipy_cli.arg_parser import CliArgs
15
+
16
+
17
+ class AniListCli(CliBase):
18
+ def __init__(self, options: "CliArgs"):
19
+ super().__init__(options)
20
+ self.access_token = ""
21
+ self.anilist = None
22
+
23
+ def print_header(self):
24
+ pass
25
+
26
+ def take_input(self):
27
+ config = Config()
28
+ self.access_token = config.anilist_token
29
+
30
+ if not self.access_token:
31
+ webbrowser.open(AniList.AUTH_URL)
32
+ self.access_token = inquirer.text( # type: ignore
33
+ "Paste the token from the browser for Auth",
34
+ validate=lambda x: len(x) > 1,
35
+ invalid_message="You must enter a access_token!",
36
+ long_instruction="Hint: You can save your access token in the config!",
37
+ ).execute()
38
+
39
+ def process(self):
40
+ try:
41
+ with DotSpinner("Logging into AniList..."):
42
+ self.anilist = AniList.from_implicit_grant(self.access_token)
43
+ except AniListError as e:
44
+ error(
45
+ f"{str(e)}\nCannot login to AniList, it is likely your response token is wrong",
46
+ fatal=True,
47
+ )
48
+
49
+ def show(self):
50
+ pass
51
+
52
+ def post(self):
53
+ assert self.anilist is not None
54
+
55
+ menu = AniListMenu(anilist=self.anilist, options=self.options)
56
+
57
+ if self.options.auto_update:
58
+ menu.download()
59
+ elif self.options.anilist_sync_seasonals:
60
+ menu.sync_anilist_seasonls()
61
+ else:
62
+ menu.run()
@@ -0,0 +1,34 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import TYPE_CHECKING, Optional
3
+
4
+ if TYPE_CHECKING:
5
+ from anipy_cli.arg_parser import CliArgs
6
+
7
+
8
+ class CliBase(ABC):
9
+ def __init__(self, options: "CliArgs"):
10
+ self.options = options
11
+
12
+ @abstractmethod
13
+ def print_header(self) -> Optional[bool]: ...
14
+
15
+ @abstractmethod
16
+ def take_input(self) -> Optional[bool]: ...
17
+
18
+ @abstractmethod
19
+ def process(self) -> Optional[bool]: ...
20
+
21
+ @abstractmethod
22
+ def show(self) -> Optional[bool]: ...
23
+
24
+ @abstractmethod
25
+ def post(self) -> Optional[bool]: ...
26
+
27
+ def run(self):
28
+ funcs = ["print_header", "take_input", "process", "show", "post"]
29
+ for f in funcs:
30
+ func = getattr(self, f)
31
+ ret = func()
32
+
33
+ if ret is False:
34
+ break