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/prompts.py ADDED
@@ -0,0 +1,387 @@
1
+ import time
2
+ from typing import TYPE_CHECKING, Optional, List, Tuple
3
+ from InquirerPy import inquirer
4
+ from InquirerPy.base.control import Choice
5
+ from anipy_api.mal import MyAnimeListAdapter
6
+ from anipy_api.provider import (
7
+ BaseProvider,
8
+ FilterCapabilities,
9
+ Filters,
10
+ LanguageTypeEnum,
11
+ Season,
12
+ )
13
+ from anipy_api.anime import Anime
14
+
15
+ from anipy_cli.util import (
16
+ DotSpinner,
17
+ find_closest,
18
+ get_anime_season,
19
+ get_prefered_providers,
20
+ error,
21
+ parse_episode_ranges,
22
+ convert_letter_to_season,
23
+ )
24
+ from anipy_cli.colors import colors
25
+ from anipy_cli.config import Config
26
+
27
+
28
+ if TYPE_CHECKING:
29
+ from anipy_api.provider import Episode
30
+ from anipy_api.locallist import LocalList
31
+
32
+
33
+ def search_show_prompt(
34
+ mode: str, skip_season_search: bool = False
35
+ ) -> Optional["Anime"]:
36
+ if not (Config().skip_season_search or skip_season_search):
37
+ anime = season_search_pre_prompt(mode)
38
+ if anime is not None:
39
+ return anime
40
+
41
+ query = inquirer.text( # type: ignore
42
+ "Search Anime:",
43
+ long_instruction="To cancel this prompt press ctrl+z",
44
+ mandatory=False,
45
+ ).execute()
46
+
47
+ if not (query and query.strip()):
48
+ return None
49
+
50
+ with DotSpinner("Searching for ", colors.BLUE, query, "..."):
51
+ results: List[Anime] = []
52
+ for provider in get_prefered_providers(mode):
53
+ results.extend(
54
+ [
55
+ Anime.from_search_result(provider, x)
56
+ for x in provider.get_search(query)
57
+ ]
58
+ )
59
+
60
+ if len(results) == 0:
61
+ error("no search results")
62
+ # Do not prompt for season again
63
+ return search_show_prompt(mode, skip_season_search=True)
64
+
65
+ anime = inquirer.fuzzy( # type: ignore
66
+ message="Select Show:",
67
+ choices=[
68
+ Choice(value=r, name=f"{n + 1}. {repr(r)}") for n, r in enumerate(results)
69
+ ],
70
+ long_instruction=(
71
+ "\nS = Anime is available in sub\n"
72
+ "D = Anime is available in dub\n"
73
+ "First two letters indicate the provider\n"
74
+ "To skip this prompt press ctrl+z"
75
+ ),
76
+ mandatory=False,
77
+ ).execute()
78
+
79
+ return anime
80
+
81
+
82
+ def _get_season_provider(mode: str) -> Optional["BaseProvider"]:
83
+ season_provider = None
84
+ for p in get_prefered_providers(mode):
85
+ if p.FILTER_CAPS & (
86
+ FilterCapabilities.SEASON
87
+ | FilterCapabilities.YEAR
88
+ | FilterCapabilities.NO_QUERY
89
+ ):
90
+ season_provider = p
91
+ return season_provider
92
+
93
+
94
+ def season_search_pre_prompt(
95
+ mode: str, year: Optional[int] = None, season: Optional[str] = None
96
+ ) -> Optional["Anime"]:
97
+ season_provider = _get_season_provider(mode)
98
+ assume_season_search = Config().assume_season_search
99
+
100
+ # If there is no proper season provider
101
+ if season_provider is None:
102
+ if not assume_season_search:
103
+ return
104
+ # If assume search was on, and there is no proper season provider
105
+ print(
106
+ f"`assume_season_search` was set to true, but the providers ({', '.join(Config().providers[mode])}) you have selected do not have seasonal capabilities"
107
+ )
108
+ return
109
+
110
+ if assume_season_search or (year and season):
111
+ return season_search_prompt(season_provider, year, season)
112
+
113
+ should_search = inquirer.confirm("Do you want to search in season?", default=False).execute() # type: ignore
114
+
115
+ if should_search:
116
+ return season_search_prompt(season_provider)
117
+
118
+ print(
119
+ "Hint: you can set `skip_season_search` to `true` in the config to skip this prompt!"
120
+ )
121
+
122
+
123
+ def season_search_prompt(
124
+ provider: "BaseProvider", year: Optional[int] = None, season: Optional[str] = None
125
+ ) -> Optional["Anime"]:
126
+ if year is None:
127
+ curr_year = time.localtime().tm_year
128
+ year = inquirer.number( # type: ignore
129
+ message="Enter year:",
130
+ long_instruction="To skip this prompt press ctrl+z",
131
+ default=curr_year,
132
+ mandatory=False,
133
+ ).execute()
134
+
135
+ if not year or not str(year).strip():
136
+ return
137
+
138
+ if season is None:
139
+ season = inquirer.select( # type: ignore
140
+ message="Select Season:",
141
+ choices=["Winter", "Spring", "Summer", "Fall"],
142
+ instruction="The season selected by default is the current season.",
143
+ long_instruction="To skip this prompt press ctrl+z",
144
+ default=get_anime_season(time.localtime().tm_mon),
145
+ mandatory=False,
146
+ ).execute()
147
+
148
+ if season is None:
149
+ return
150
+
151
+ discovered_anime = get_anime_by_season(provider, year, Season[season.upper()])
152
+
153
+ if not discovered_anime:
154
+ error(f"No anime found in {season} {year}")
155
+ return
156
+
157
+ anime = inquirer.fuzzy( # type: ignore
158
+ message="Select Show:",
159
+ choices=[
160
+ Choice(value=r, name=f"{n + 1}. {repr(r)}")
161
+ for n, r in enumerate(discovered_anime)
162
+ ],
163
+ long_instruction=(
164
+ "\nS = Anime is available in sub\n"
165
+ "D = Anime is available in dub\n"
166
+ "First two letters indicate the provider\n"
167
+ "To skip this prompt press ctrl+z"
168
+ ),
169
+ mandatory=False,
170
+ ).execute()
171
+
172
+ return anime
173
+
174
+
175
+ def get_anime_by_season(provider: "BaseProvider", year: int, season: Season):
176
+ with DotSpinner(
177
+ "Retrieving anime in ", colors.BLUE, f"{season.name} {year}", "..."
178
+ ):
179
+ filters = Filters(year=year, season=season)
180
+ return [
181
+ Anime.from_search_result(provider, r)
182
+ for r in provider.get_search(query="", filters=filters)
183
+ ]
184
+
185
+
186
+ def pick_episode_prompt(
187
+ anime: "Anime", lang: LanguageTypeEnum, instruction: str = ""
188
+ ) -> Optional["Episode"]:
189
+ with DotSpinner("Fetching episode list for ", colors.BLUE, anime.name, "..."):
190
+ episodes = anime.get_episodes(lang)
191
+
192
+ if not episodes:
193
+ error(f"No episodes available for {anime.name}")
194
+ return None
195
+
196
+ return inquirer.fuzzy( # type: ignore
197
+ message="Select Episode:",
198
+ instruction=instruction,
199
+ choices=episodes,
200
+ long_instruction="To skip this prompt press ctrl+z",
201
+ mandatory=False,
202
+ ).execute()
203
+
204
+
205
+ def pick_episode_range_prompt(
206
+ anime: "Anime", lang: LanguageTypeEnum
207
+ ) -> List["Episode"]:
208
+ with DotSpinner("Fetching episode list for ", colors.BLUE, anime.name, "..."):
209
+ episodes = anime.get_episodes(lang)
210
+
211
+ if not episodes:
212
+ error(f"No episodes available for {anime.name}")
213
+ return []
214
+
215
+ res = inquirer.text( # type: ignore
216
+ message=f"Input Episode Range(s) from episodes {episodes[0]} to {episodes[-1]}:",
217
+ long_instruction="Type e.g. `1-10 19-20` or `3-4` or `3`\nTo skip this prompt press ctrl+z",
218
+ mandatory=False,
219
+ ).execute()
220
+
221
+ if res is None:
222
+ return []
223
+
224
+ return parse_episode_ranges(res, episodes)
225
+
226
+
227
+ def lang_prompt(anime: "Anime") -> LanguageTypeEnum:
228
+ config = Config()
229
+ preferred = (
230
+ LanguageTypeEnum[config.preferred_type.upper()]
231
+ if config.preferred_type is not None
232
+ else None
233
+ )
234
+
235
+ if preferred in anime.languages:
236
+ return preferred
237
+
238
+ if LanguageTypeEnum.DUB not in anime.languages:
239
+ return LanguageTypeEnum.SUB
240
+
241
+ if len(anime.languages) == 2:
242
+ res = inquirer.confirm("Want to watch in dub?").execute() # type: ignore
243
+ print("Hint: you can set a default in the config with `preferred_type`!")
244
+
245
+ if res:
246
+ return LanguageTypeEnum.DUB
247
+ else:
248
+ return LanguageTypeEnum.SUB
249
+ else:
250
+ return next(iter(anime.languages))
251
+
252
+
253
+ def parse_seasonal_search(mode: str, passed: str | bool) -> Optional["Anime"]:
254
+ """
255
+ Takes the mode we are in, as well as the search parameters.
256
+ Asks the user to choose an anime.
257
+
258
+ `Mode`: The provider to use.
259
+ `Passed`: The search terms passed (ex: `year:season`) or True,
260
+ if True we'll ask the user for this information
261
+ """
262
+ if isinstance(passed, bool):
263
+ if not passed:
264
+ return
265
+
266
+ provider = _get_season_provider(mode)
267
+
268
+ if not provider:
269
+ error(
270
+ "No valid provider was found for season search in the current mode",
271
+ fatal=True,
272
+ )
273
+
274
+ return season_search_prompt(provider)
275
+
276
+ options = iter(passed.split(":"))
277
+ year = next(options, None)
278
+ season = next(options, None)
279
+
280
+ if (not year) or not year.isnumeric():
281
+ error("A year was either not provided, or was not a number", fatal=True)
282
+
283
+ if not season:
284
+ error("A season was not provided", fatal=True)
285
+
286
+ season = convert_letter_to_season(season)
287
+
288
+ if not season:
289
+ error("The given season was not a valid season", fatal=True)
290
+
291
+ return season_search_pre_prompt(mode, int(year), season)
292
+
293
+
294
+ def parse_auto_search(
295
+ mode: str, passed: str
296
+ ) -> Tuple["Anime", LanguageTypeEnum, List["Episode"]]:
297
+ options = iter(passed.split(":"))
298
+ query = next(options, None)
299
+ ranges = next(options, None)
300
+ ltype = next(options, None)
301
+
302
+ if not query:
303
+ error("you provided the search parameter but no query", fatal=True)
304
+
305
+ if not ranges:
306
+ error("you provided the search parameter but no episode ranges", fatal=True)
307
+
308
+ if not (ltype == "sub" or ltype == "dub"):
309
+ ltype = Config().preferred_type
310
+
311
+ with DotSpinner("Searching for ", colors.BLUE, query, "..."):
312
+ results: List[Anime] = []
313
+ for provider in get_prefered_providers(mode):
314
+ results.extend(
315
+ [
316
+ Anime.from_search_result(provider, x)
317
+ for x in provider.get_search(query)
318
+ ]
319
+ )
320
+ if len(results) == 0:
321
+ error(f"no anime found for query {query}", fatal=True)
322
+
323
+ result = results[0]
324
+ if ltype is None:
325
+ lang = lang_prompt(result)
326
+ else:
327
+ lang = LanguageTypeEnum[ltype.upper()]
328
+
329
+ if lang not in result.languages:
330
+ error(f"{lang} is not available for {result.name}", fatal=True)
331
+
332
+ episodes = result.get_episodes(lang)
333
+ chosen = parse_episode_ranges(ranges, episodes)
334
+ if not chosen:
335
+ error("could not determine any epiosdes from search parameter", fatal=True)
336
+
337
+ return result, lang, chosen
338
+
339
+
340
+ def migrate_provider(mode: str, local_list: "LocalList"):
341
+ config = Config()
342
+ all_entries = local_list.get_all()
343
+ current_providers = list(get_prefered_providers(mode))
344
+ print(
345
+ f"Migrating to configured providers ({mode}):",
346
+ ", ".join([p.NAME for p in current_providers]),
347
+ )
348
+ for s in all_entries:
349
+ if s.provider in [p.NAME for p in current_providers]:
350
+ continue
351
+
352
+ print(f"Mapping: {s.name}")
353
+
354
+ search_results: List[Anime] = []
355
+ for p in current_providers:
356
+ search_results.extend(
357
+ [Anime.from_search_result(p, r) for r in p.get_search(s.name)]
358
+ )
359
+
360
+ best_anime = None
361
+ best_ratio = 0
362
+ for r in search_results:
363
+ titles = {r.name}
364
+ titles |= set(r.get_info().alternative_names or [])
365
+ ratio = MyAnimeListAdapter._find_best_ratio(titles, {s.name})
366
+
367
+ if ratio > best_ratio:
368
+ best_ratio = ratio
369
+ best_anime = r
370
+
371
+ if best_ratio == 1:
372
+ break
373
+
374
+ if best_anime is None:
375
+ continue
376
+
377
+ if best_ratio >= config.mal_mapping_min_similarity:
378
+ local_list.delete(s)
379
+ else:
380
+ print(f"Could not autmatically map {s.name}, you can map it manually.")
381
+ best_anime = search_show_prompt("seasonal", skip_season_search=True)
382
+ if best_anime is None:
383
+ continue
384
+ local_list.delete(s)
385
+
386
+ episode = find_closest(best_anime.get_episodes(s.language), s.episode)
387
+ local_list.update(best_anime, language=s.language, episode=episode)
anipy_cli/util.py ADDED
@@ -0,0 +1,268 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+ import subprocess as sp
5
+ from pathlib import Path
6
+ from typing import (
7
+ TYPE_CHECKING,
8
+ Any,
9
+ Iterator,
10
+ List,
11
+ Literal,
12
+ NoReturn,
13
+ Optional,
14
+ Union,
15
+ overload,
16
+ )
17
+
18
+ import anipy_cli.logger as logger
19
+
20
+ from anipy_api.anime import Anime
21
+ from anipy_api.download import Downloader, PostDownloadCallback
22
+ from anipy_api.locallist import LocalListData
23
+ from anipy_api.player import get_player
24
+ from anipy_api.provider import list_providers
25
+ from InquirerPy import inquirer
26
+ from yaspin.core import Yaspin
27
+ from yaspin.spinners import Spinners
28
+
29
+ from anipy_cli.colors import color, colors
30
+ from anipy_cli.config import Config
31
+ from anipy_cli.discord import DiscordPresence
32
+
33
+ if TYPE_CHECKING:
34
+ from anipy_api.player import PlayerBase
35
+ from anipy_api.provider import BaseProvider, Episode, ProviderStream
36
+
37
+
38
+ class DotSpinner(Yaspin):
39
+ def __init__(self, *text_and_colors: Any, **spinner_args: Any):
40
+ super().__init__(
41
+ text=color(*text_and_colors),
42
+ color="cyan",
43
+ spinner=Spinners.dots,
44
+ **spinner_args,
45
+ )
46
+
47
+ def __enter__(self) -> "DotSpinner":
48
+ self.start()
49
+ return self
50
+
51
+ def set_text(self, *text_and_colors: Any):
52
+ self.text = color(*text_and_colors)
53
+
54
+
55
+ @overload
56
+ def error(error: str, fatal: Literal[True]) -> NoReturn: ...
57
+ @overload
58
+ def error(
59
+ error: str, fatal: Literal[False] = ..., log_level: int = logging.INFO
60
+ ) -> None: ...
61
+
62
+
63
+ def error(
64
+ error: str, fatal: bool = False, log_level: int = logging.INFO
65
+ ) -> Union[NoReturn, None]:
66
+ if not fatal:
67
+ sys.stderr.write(
68
+ color(colors.RED, "anipy-cli: error: ", colors.END, f"{error}\n")
69
+ )
70
+ logger.log(log_level, error)
71
+ return
72
+
73
+ sys.stderr.write(
74
+ color(
75
+ colors.RED,
76
+ "anipy-cli: fatal error: ",
77
+ colors.END,
78
+ f"{error}, exiting\n",
79
+ )
80
+ )
81
+ logger.warn(error)
82
+ sys.exit(1)
83
+
84
+
85
+ def clear_screen():
86
+ if logger.get_console_log_level() < 60:
87
+ return
88
+ os.system("cls" if os.name == "nt" else "clear")
89
+
90
+
91
+ def get_prefered_providers(mode: str) -> Iterator["BaseProvider"]:
92
+ config = Config()
93
+ preferred_providers = config.providers[mode]
94
+
95
+ if not preferred_providers:
96
+ error(
97
+ f"you have no providers set for '{mode}' mode, look into your config",
98
+ fatal=True,
99
+ )
100
+
101
+ providers = []
102
+ for i in list_providers():
103
+ if i.NAME in preferred_providers:
104
+ url_override = config.provider_urls.get(i.NAME, None)
105
+ providers.append(i(url_override))
106
+
107
+ if not providers:
108
+ error(
109
+ f"there are no working providers for '{mode}' mode, look into your config",
110
+ fatal=True,
111
+ )
112
+
113
+ for p in providers:
114
+ yield p
115
+
116
+
117
+ def get_download_path(
118
+ anime: "Anime",
119
+ stream: "ProviderStream",
120
+ parent_directory: Optional[Path] = None,
121
+ ) -> Path:
122
+ config = Config()
123
+ download_folder = parent_directory or config.download_folder_path
124
+
125
+ anime_name = Downloader._get_valid_pathname(anime.name)
126
+ filename = config.download_name_format.format(
127
+ show_name=anime_name,
128
+ episode_number=str(stream.episode).zfill(2),
129
+ quality=stream.resolution,
130
+ provider=anime.provider.NAME,
131
+ type=stream.language,
132
+ )
133
+
134
+ filename = Downloader._get_valid_pathname(filename)
135
+
136
+ return download_folder / anime_name / filename
137
+
138
+
139
+ def get_post_download_scripts_hook(
140
+ mode: str, anime: "Anime", spinner: DotSpinner
141
+ ) -> PostDownloadCallback:
142
+ config = Config()
143
+ scripts = config.post_download_scripts[mode]
144
+ timeout = config.post_download_scripts["timeout"]
145
+
146
+ def hook(path: Path, stream: "ProviderStream"):
147
+ spinner.hide()
148
+ arguments = [
149
+ str(path),
150
+ anime.name,
151
+ str(stream.episode),
152
+ anime.provider.NAME,
153
+ str(stream.resolution),
154
+ stream.language.name,
155
+ ]
156
+ for s in scripts:
157
+ sub_proc = sp.Popen([s, *arguments])
158
+ sub_proc.wait(timeout) # type: ignore
159
+ spinner.show()
160
+
161
+ return hook
162
+
163
+
164
+ def parse_episode_ranges(ranges: str, episodes: List["Episode"]) -> List["Episode"]:
165
+ picked = set()
166
+ for r in ranges.split():
167
+ numbers = [parsenum(n) for n in r.split("-")]
168
+ if numbers[0] > numbers[-1]:
169
+ error(f"invalid range: {r}")
170
+ continue
171
+ try:
172
+ picked = picked | set(
173
+ episodes[episodes.index(numbers[0]) : episodes.index(numbers[-1]) + 1]
174
+ )
175
+ except ValueError:
176
+ error(f"range `{r}` is not contained in episodes {episodes}")
177
+ continue
178
+
179
+ return sorted(picked)
180
+
181
+
182
+ def parsenum(n: str):
183
+ try:
184
+ return int(n)
185
+ except ValueError:
186
+ return float(n)
187
+
188
+
189
+ def find_closest(episodes: List["Episode"], target: "Episode") -> "Episode":
190
+ left, right = 0, len(episodes) - 1
191
+ while left < right:
192
+ if abs(episodes[left] - target) <= abs(episodes[right] - target):
193
+ right -= 1
194
+ else:
195
+ left += 1
196
+
197
+ return episodes[left]
198
+
199
+
200
+ def get_configured_player(player_override: Optional[str] = None) -> "PlayerBase":
201
+ config = Config()
202
+ player = Path(player_override or config.player_path)
203
+ if config.dc_presence:
204
+ # If the cache size is 0, it means that DiscordPresence was
205
+ # not intialized once in the run_cli function and therefore we
206
+ # can assume that it failed to initialize beacuse of some error.
207
+ if DiscordPresence.cache_info().currsize > 0:
208
+ discord_cb = DiscordPresence().dc_presence_callback
209
+ else:
210
+ discord_cb = None
211
+ else:
212
+ discord_cb = None
213
+
214
+ if "mpv" in player.stem:
215
+ args = config.mpv_commandline_options
216
+ elif "vlc" in player.stem:
217
+ args = config.vlc_commandline_options
218
+ elif "iina" in player.stem:
219
+ args = config.iina_commandline_options
220
+ else:
221
+ args = []
222
+
223
+ return get_player(player, args, discord_cb)
224
+
225
+
226
+ def get_anime_season(month: int):
227
+ if 1 <= month <= 3:
228
+ return "Winter"
229
+ elif 4 <= month <= 6:
230
+ return "Spring"
231
+ elif 7 <= month <= 9:
232
+ return "Summer"
233
+ else:
234
+ return "Fall"
235
+
236
+
237
+ def convert_letter_to_season(letter: str) -> Optional[str]:
238
+ """
239
+ Converts the beginning of the name of a season to that season name.
240
+
241
+ Ex:
242
+ ```
243
+ win -> Winter
244
+ su -> Summer
245
+ sp -> Spring
246
+ ```
247
+
248
+ Returns None if the letter does not correspond to a season
249
+ """
250
+ for season in ["Spring", "Summer", "Fall", "Winter"]:
251
+ if season.startswith(letter.capitalize()):
252
+ return season
253
+ return
254
+
255
+
256
+ def migrate_locallist(file: Path) -> LocalListData:
257
+ error(f"{file} is in an unsuported format...")
258
+
259
+ new_list = LocalListData({})
260
+ choice = inquirer.confirm( # type: ignore
261
+ message="Should it be delted?",
262
+ default=False,
263
+ ).execute()
264
+ if choice:
265
+ file.unlink()
266
+ return new_list
267
+ else:
268
+ error("could not read {file}", fatal=True)