anipy-cli 2.7.30__py3-none-any.whl → 3.0.0__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.

Potentially problematic release.


This version of anipy-cli might be problematic. Click here for more details.

Files changed (62) hide show
  1. anipy_cli/__init__.py +2 -20
  2. anipy_cli/arg_parser.py +30 -20
  3. anipy_cli/cli.py +66 -0
  4. anipy_cli/clis/__init__.py +15 -0
  5. anipy_cli/clis/base_cli.py +32 -0
  6. anipy_cli/clis/binge_cli.py +83 -0
  7. anipy_cli/clis/default_cli.py +104 -0
  8. anipy_cli/clis/download_cli.py +111 -0
  9. anipy_cli/clis/history_cli.py +93 -0
  10. anipy_cli/clis/mal_cli.py +71 -0
  11. anipy_cli/{cli/clis → clis}/seasonal_cli.py +9 -6
  12. anipy_cli/colors.py +4 -4
  13. anipy_cli/config.py +308 -87
  14. anipy_cli/discord.py +34 -0
  15. anipy_cli/mal_proxy.py +216 -0
  16. anipy_cli/menus/__init__.py +5 -0
  17. anipy_cli/{cli/menus → menus}/base_menu.py +8 -12
  18. anipy_cli/menus/mal_menu.py +660 -0
  19. anipy_cli/menus/menu.py +194 -0
  20. anipy_cli/menus/seasonal_menu.py +263 -0
  21. anipy_cli/prompts.py +231 -0
  22. anipy_cli/util.py +262 -0
  23. anipy_cli-3.0.0.dist-info/METADATA +67 -0
  24. anipy_cli-3.0.0.dist-info/RECORD +26 -0
  25. {anipy_cli-2.7.30.dist-info → anipy_cli-3.0.0.dist-info}/WHEEL +1 -2
  26. anipy_cli-3.0.0.dist-info/entry_points.txt +3 -0
  27. anipy_cli/cli/__init__.py +0 -1
  28. anipy_cli/cli/cli.py +0 -37
  29. anipy_cli/cli/clis/__init__.py +0 -6
  30. anipy_cli/cli/clis/base_cli.py +0 -43
  31. anipy_cli/cli/clis/binge_cli.py +0 -54
  32. anipy_cli/cli/clis/default_cli.py +0 -46
  33. anipy_cli/cli/clis/download_cli.py +0 -92
  34. anipy_cli/cli/clis/history_cli.py +0 -64
  35. anipy_cli/cli/clis/mal_cli.py +0 -27
  36. anipy_cli/cli/menus/__init__.py +0 -3
  37. anipy_cli/cli/menus/mal_menu.py +0 -411
  38. anipy_cli/cli/menus/menu.py +0 -108
  39. anipy_cli/cli/menus/seasonal_menu.py +0 -177
  40. anipy_cli/cli/util.py +0 -125
  41. anipy_cli/download.py +0 -467
  42. anipy_cli/history.py +0 -83
  43. anipy_cli/mal.py +0 -651
  44. anipy_cli/misc.py +0 -227
  45. anipy_cli/player/__init__.py +0 -1
  46. anipy_cli/player/player.py +0 -36
  47. anipy_cli/player/players/__init__.py +0 -3
  48. anipy_cli/player/players/base.py +0 -107
  49. anipy_cli/player/players/mpv.py +0 -19
  50. anipy_cli/player/players/mpv_contrl.py +0 -37
  51. anipy_cli/player/players/syncplay.py +0 -19
  52. anipy_cli/player/players/vlc.py +0 -18
  53. anipy_cli/query.py +0 -100
  54. anipy_cli/run_anipy_cli.py +0 -14
  55. anipy_cli/seasonal.py +0 -112
  56. anipy_cli/url_handler.py +0 -470
  57. anipy_cli/version.py +0 -1
  58. anipy_cli-2.7.30.dist-info/LICENSE +0 -674
  59. anipy_cli-2.7.30.dist-info/METADATA +0 -162
  60. anipy_cli-2.7.30.dist-info/RECORD +0 -43
  61. anipy_cli-2.7.30.dist-info/entry_points.txt +0 -2
  62. anipy_cli-2.7.30.dist-info/top_level.txt +0 -1
@@ -0,0 +1,71 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from anipy_api.error import MyAnimeListError
4
+ from anipy_api.mal import MyAnimeList
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 MALMenu
10
+ from anipy_cli.util import DotSpinner, error
11
+
12
+ if TYPE_CHECKING:
13
+ from anipy_cli.arg_parser import CliArgs
14
+
15
+
16
+ class MalCli(CliBase):
17
+ def __init__(self, options: "CliArgs"):
18
+ super().__init__(options)
19
+ self.user = ""
20
+ self.password = ""
21
+ self.mal = None
22
+
23
+ def print_header(self):
24
+ pass
25
+
26
+ def take_input(self):
27
+ config = Config()
28
+ self.user = config.mal_user
29
+ self.password = self.options.mal_password or config.mal_password
30
+
31
+ if not self.user:
32
+ self.user = inquirer.text( # type: ignore
33
+ "Your MyAnimeList Username: ",
34
+ validate=lambda x: len(x) > 1,
35
+ invalid_message="You must enter a username!",
36
+ long_instruction="Hint: You can save your username and password in the config!",
37
+ ).execute()
38
+
39
+ if not self.password:
40
+ self.password = inquirer.secret( # type: ignore
41
+ "Your MyAnimeList Password: ",
42
+ transformer=lambda _: "[hidden]",
43
+ validate=lambda x: len(x) > 1,
44
+ invalid_message="You must enter a password!",
45
+ long_instruction="Hint: You can also pass the password via the `--mal-password` option!",
46
+ ).execute()
47
+
48
+ def process(self):
49
+ try:
50
+ with DotSpinner("Logging into MyAnimeList..."):
51
+ self.mal = MyAnimeList.from_password_grant(self.user, self.password)
52
+ except MyAnimeListError as e:
53
+ error(
54
+ f"{str(e)}\nCannot login to MyAnimeList, it is likely your credentials are wrong",
55
+ fatal=True,
56
+ )
57
+
58
+ def show(self):
59
+ pass
60
+
61
+ def post(self):
62
+ assert self.mal is not None
63
+
64
+ menu = MALMenu(mal=self.mal, options=self.options)
65
+
66
+ if self.options.auto_update:
67
+ menu.download()
68
+ elif self.options.mal_sync_seasonals:
69
+ menu.sync_mal_seasonls()
70
+ else:
71
+ menu.run()
@@ -1,11 +1,14 @@
1
- from anipy_cli.arg_parser import CliArgs
2
- from anipy_cli.cli.menus import SeasonalMenu
3
- from anipy_cli.cli.clis.base_cli import CliBase
1
+ from typing import TYPE_CHECKING
2
+ from anipy_cli.menus import SeasonalMenu
3
+ from anipy_cli.clis.base_cli import CliBase
4
+
5
+ if TYPE_CHECKING:
6
+ from anipy_cli.arg_parser import CliArgs
4
7
 
5
8
 
6
9
  class SeasonalCli(CliBase):
7
- def __init__(self, options: CliArgs, rpc_client=None):
8
- super().__init__(options, rpc_client)
10
+ def __init__(self, options: "CliArgs"):
11
+ super().__init__(options)
9
12
 
10
13
  def print_header(self):
11
14
  pass
@@ -20,7 +23,7 @@ class SeasonalCli(CliBase):
20
23
  pass
21
24
 
22
25
  def post(self):
23
- menu = SeasonalMenu(self.options, self.rpc_client)
26
+ menu = SeasonalMenu(self.options)
24
27
 
25
28
  if self.options.auto_update:
26
29
  menu.download_latest()
anipy_cli/colors.py CHANGED
@@ -1,7 +1,5 @@
1
1
  class colors:
2
- """
3
- Just a class for colors
4
- """
2
+ """Just a class for colors."""
5
3
 
6
4
  GREEN = "\033[92m"
7
5
  ERROR = "\033[93m"
@@ -18,9 +16,11 @@ class colors:
18
16
 
19
17
  def color(*values, sep: str = "") -> str:
20
18
  """Decorate a string with color codes.
19
+
21
20
  Basically just ensures that the color doesn't "leak"
22
21
  from the text.
23
- format: color(color1, text1, color2, text2...)"""
22
+ format: color(color1, text1, color2, text2...)
23
+ """
24
24
  return sep.join(map(str, values)) + colors.END
25
25
 
26
26
 
anipy_cli/config.py CHANGED
@@ -1,13 +1,14 @@
1
- import os
2
- import yaml
3
1
  import functools
2
+ import inspect
3
+ import os
4
4
  from pathlib import Path
5
- from sys import platform
6
- from sys import exit as sys_exit
5
+ from string import Template
6
+ from typing import Any, Dict, List, Optional, Tuple, Type
7
7
 
8
+ import yaml
9
+ from appdirs import user_config_dir, user_data_dir
8
10
 
9
- class SysNotFoundError(Exception):
10
- pass
11
+ from anipy_cli import __appname__, __version__
11
12
 
12
13
 
13
14
  class Config:
@@ -19,108 +20,334 @@ class Config:
19
20
  self._create_config() # Create config file
20
21
 
21
22
  @property
22
- def _anipy_cli_folder(self):
23
- return Path(Path(__file__).parent)
23
+ def user_files_path(self) -> Path:
24
+ """Path to user files, this includes history, seasonals files and more.
25
+
26
+ You may use `~` or environment vars in your path.
27
+ """
24
28
 
25
- @property
26
- def download_folder_path(self):
27
29
  return self._get_path_value(
28
- "download_folder_path", self._anipy_cli_folder / "download"
30
+ "user_files_path", Path(user_data_dir(__appname__, appauthor=False))
29
31
  )
30
32
 
31
33
  @property
32
- def seasonals_dl_path(self):
34
+ def _history_file_path(self) -> Path:
35
+ return self.user_files_path / "history.json"
36
+
37
+ @property
38
+ def _seasonal_file_path(self) -> Path:
39
+ return self.user_files_path / "seasonals.json"
40
+
41
+ @property
42
+ def _mal_local_user_list_path(self) -> Path:
43
+ return self.user_files_path / "mal_list.json"
44
+
45
+ @property
46
+ def download_folder_path(self) -> Path:
47
+ """Path to your download folder/directory.
48
+
49
+ You may use `~` or environment vars in your path.
50
+ """
33
51
  return self._get_path_value(
34
- "seasonals_dl_path", self.download_folder_path / "seasonals"
52
+ "download_folder_path", self.user_files_path / "download"
35
53
  )
36
54
 
37
55
  @property
38
- def user_files_path(self):
56
+ def seasonals_dl_path(self) -> Path:
57
+ """Path to your seasonal downloads directory.
58
+
59
+ You may use `~` or environment vars in your path.
60
+ """
39
61
  return self._get_path_value(
40
- "user_files_path", self._anipy_cli_folder / "user_files"
62
+ "seasonals_dl_path", self.download_folder_path / "seasonals"
41
63
  )
42
64
 
43
65
  @property
44
- def history_file_path(self):
45
- return self.user_files_path / "history.json"
66
+ def providers(self) -> Dict[str, List[str]]:
67
+ """A list of pairs defining which providers will search for anime
68
+ in different parts of the program. Configurable areas are as follows:
69
+ default (and history), download (-D), seasonal (-S), binge (-B) and mal
70
+ (-M) The example will show you how it is done! Please note that for seasonal
71
+ search always the first provider that supports it is used.
72
+
73
+ For an updated list of providers look here: https://sdaqo.github.io/anipy-cli/availabilty
74
+
75
+ Supported providers (as of $version): gogoanime
76
+
77
+ Examples:
78
+ providers:
79
+ default: ["provider1"] # used in default mode and for the history
80
+ download: ["provider2"]
81
+ seasonal: ["provider3"]
82
+ binge: ["provider4"]
83
+ mal: ["provider2", "provider3"]
84
+ """
85
+ defaults = {
86
+ "default": ["gogoanime"],
87
+ "download": ["gogoanime"],
88
+ "history": ["gogoanime"],
89
+ "seasonal": ["gogoanime"],
90
+ "binge": ["gogoanime"],
91
+ "mal": ["gogoanime"],
92
+ }
93
+
94
+ value = self._get_value("providers", defaults, dict)
95
+
96
+ # Merge Dicts
97
+ defaults.update(value)
98
+ return defaults
46
99
 
47
100
  @property
48
- def seasonal_file_path(self):
49
- return self.user_files_path / "seasonals.json"
101
+ def provider_urls(self) -> Dict[str, str]:
102
+ """A list of pairs to override the default urls that providers use.
50
103
 
51
- @property
52
- def gogoanime_url(self):
53
- return self._get_value("gogoanime_url", "https://gogoanime.gg/", str)
104
+ Examples:
105
+ provider_urls:
106
+ gogoanime: "https://gogoanime3.co"
107
+ provider_urls: {} # do not override any urls
108
+ """
109
+
110
+ return self._get_value("provider_urls", {}, dict)
54
111
 
55
112
  @property
56
- def player_path(self):
57
- return self._get_value("player_path", "mpv", str)
113
+ def player_path(self) -> Path:
114
+ """
115
+ Path to your video player.
116
+ For a list of supported players look here: https://sdaqo.github.io/anipy-cli/availabilty
117
+
118
+ Supported players (as of $version): mpv, vlc, syncplay, mpvnet, mpv-controlled
119
+
120
+ Info for mpv-controlled:
121
+ Reuse the mpv window instead of closing and reopening.
122
+ This uses python-mpv, which uses libmpv, on linux this is (normally) preinstalled
123
+ with mpv, on windows you have to get the mpv-2.dll file from here:
124
+ https://sourceforge.net/projects/mpv-player-windows/files/libmpv/
125
+
126
+ Examples:
127
+ player_path: /usr/bin/syncplay # full path
128
+ player_path: syncplay # if in PATH this also works
129
+ player_path: C:\\\\Programms\\mpv\\mpv.exe # on windows path with .exe
130
+ player_path: mpv-controlled # recycle your mpv windows!
131
+ """
132
+ return self._get_path_value("player_path", Path("mpv"))
58
133
 
59
134
  @property
60
- def mpv_commandline_options(self):
135
+ def mpv_commandline_options(self) -> List[str]:
136
+ """Extra commandline arguments for mpv and derivative.
137
+
138
+ Examples:
139
+ mpv_commandline_options: ["--keep-open=no", "--fs=yes"]
140
+ """
61
141
  return self._get_value("mpv_commandline_options", ["--keep-open=no"], list)
62
142
 
63
143
  @property
64
- def vlc_commandline_options(self):
144
+ def vlc_commandline_options(self) -> List[str]:
145
+ """Extra commandline arguments for vlc.
146
+
147
+ Examples:
148
+ vlc_commandline_options: ["--fullscreen"]
149
+ """
65
150
  return self._get_value("vlc_commandline_options", [], list)
66
151
 
67
152
  @property
68
- def reuse_mpv_window(self):
153
+ def reuse_mpv_window(self) -> bool:
154
+ """DEPRECATED This option was deprecated in 3.0.0, please use `mpv-
155
+ controlled` in the `player_path` instead!
156
+
157
+ Reuse the mpv window instead of closing and reopening. This uses
158
+ python-mpv, which uses libmpv, on linux this is (normally)
159
+ preinstalled with mpv, on windows you have to get the mpv-2.dll
160
+ file from here:
161
+ https://sourceforge.net/projects/mpv-player-windows/files/libmpv/
162
+ """
69
163
  return self._get_value("reuse_mpv_window", False, bool)
70
164
 
71
165
  @property
72
- def ffmpeg_hls(self):
166
+ def ffmpeg_hls(self) -> bool:
167
+ """Always use ffmpeg to download m3u8 playlists instead of the internal
168
+ downloader.
169
+
170
+ To temporarily enable this use the `--ffmpeg` command line flag.
171
+ """
73
172
  return self._get_value("ffmpeg_hls", False, bool)
74
173
 
75
174
  @property
76
- def ffmpeg_log_path(self):
77
- return self.user_files_path / "ffmpeg_log"
175
+ def remux_to(self) -> Optional[str]:
176
+ """
177
+ Remux resulting download to a specific container using ffmpeg.
178
+ You can use about any conatainer supported by ffmpeg: `.your-container`.
179
+
180
+ Examples:
181
+ remux_to: .mkv # remux all downloads to .mkv container
182
+ remux_to .mp4 # downloads with ffmpeg default to a .mp4 container,
183
+ with this option the internal downloader's downloads also get remuxed
184
+ remux_to: null or remux_to: "" # do not remux
185
+ """
186
+ return self._get_value("remux_to", None, str)
78
187
 
79
188
  @property
80
- def download_name_format(self):
81
- return self._get_value(
82
- "download_name_format", "{show_name}_{episode_number}.mp4", str
189
+ def download_name_format(self) -> str:
190
+ """
191
+ Specify the name format of a download, available fields are:
192
+ show_name: name of the show/anime
193
+ episode_number: number of the episode
194
+ quality: quality/resolution of the video
195
+ provider: provider used to download
196
+ type: this field is populated with `dub` if the episode is in dub format or `sub` otherwise
197
+
198
+ The fields should be set in curly braces i.e. `{field_name}`.
199
+ Do not add a suffix (e.g. '.mp4') here, if you want to change this
200
+ look at the `remux_to` config option.
201
+
202
+ You have to at least use episode_number in the format or else, while downloading,
203
+ perceding episodes of the same series will be skipped because the file name will be the same.
204
+
205
+ Examples:
206
+ download_name_format: "[{provider}] {show_name} E{episode_number} [{type}][{quality}p]"
207
+ download_name_format: "{show_name}_{episode_number}"
208
+
209
+ """
210
+
211
+ # Remove suffix for past 3.0.0 versions
212
+ value = self._get_value(
213
+ "download_name_format", "{show_name}_{episode_number}", str
83
214
  )
215
+ return str(Path(value).with_suffix(""))
216
+
217
+ @property
218
+ def dc_presence(self) -> bool:
219
+ """Activate discord presence, only works with discord open."""
220
+ return self._get_value("dc_presence", False, bool)
84
221
 
85
222
  @property
86
- def download_remove_dub_from_folder_name(self):
87
- return self._get_value("download_remove_dub_from_folder_name", False, bool)
223
+ def auto_open_dl_defaultcli(self) -> bool:
224
+ """This automatically opens the downloaded file if downloaded through
225
+ the `d` option in the default cli."""
226
+ return self._get_value("auto_open_dl_defaultcli", True, bool)
88
227
 
89
228
  @property
90
- def dc_presence(self):
91
- return self._get_value("dc_presence", False, bool)
229
+ def mal_user(self) -> str:
230
+ """Your MyAnimeList username for MAL mode."""
231
+ return self._get_value("mal_user", "", str)
92
232
 
93
233
  @property
94
- def auto_open_dl_defaultcli(self):
95
- return self._get_value("auto_open_dl_defaultcli", False, bool)
234
+ def mal_password(self) -> str:
235
+ """Your MyAnimeList password for MAL mode.
236
+
237
+ The password may also be passed via the `--mal-password <pwd>`
238
+ commandline option.
239
+ """
240
+ return self._get_value("mal_password", "", str)
96
241
 
97
242
  @property
98
- def mal_local_user_list_path(self):
99
- return self.user_files_path / "mal_list.json"
243
+ def mal_ignore_tag(self) -> str:
244
+ """All anime in your MyAnimeList with this tag will be ignored by
245
+ anipy-cli.
246
+
247
+ Examples:
248
+ mal_ignore_tag: ignore # all anime with ignore tag will be ignored
249
+ mal_ignore_tag: "" # no anime will be ignored
250
+ """
251
+ return self._get_value("mal_ignore_tag", "ignore", str)
100
252
 
101
253
  @property
102
- def mal_user(self):
103
- return self._get_value("mal_user", "", str)
254
+ def mal_dub_tag(self) -> str:
255
+ """All anime in your MyAnimeList with this tag will be switched over to
256
+ dub in MAL mode, if the dub is available. If you do not specify a tag,
257
+ anipy-cli will use `preferred_type` to choose dub or sub in MAL mode.
258
+
259
+ Examples:
260
+ mal_dub_tag: dub # all anime with this tag will be switched to dub
261
+ mal_dub_tag: "" # no anime will be switched to dub, except you have preferred_type on dub
262
+ """
263
+ return self._get_value("mal_dub_tag", "dub", str)
104
264
 
105
265
  @property
106
- def mal_password(self):
107
- return self._get_value("mal_password", "", str)
266
+ def mal_tags(self) -> List[str]:
267
+ """Custom tags to tag all anime in your MyAnimeList that are
268
+ altered/added by anipy-cli.
269
+
270
+ Examples:
271
+ mal_tags: ["anipy-cli"] # tag all anime with anipy-cli
272
+ mal_tags: ["anipy-cli", "important"] # tag all anime with anipy-cli and important
273
+ mal_tags: null or mal_tags: [] # Do not tag the anime
274
+ """
275
+ return self._get_value("mal_tags", [], list)
108
276
 
109
277
  @property
110
- def auto_sync_mal_to_seasonals(self):
278
+ def mal_status_categories(self) -> List[str]:
279
+ """Status categories of your MyAnimeList that anipy-cli uses for
280
+ downloading/watching new episodes listing anime in your list and stuff
281
+ like that. Normally the watching catagory should be enough as you would
282
+ normally put anime you currently watch in the watching catagory.
283
+
284
+ Valid values are: watching, completed, on_hold, dropped, plan_to_watch
285
+ """
286
+ return self._get_value("mal_status_categories", ["watching"], list)
287
+
288
+ @property
289
+ def mal_mapping_min_similarity(self) -> float:
290
+ """
291
+ The minumum similarity between titles when mapping anime in MAL mode.
292
+ This is a decimal number from 0 - 1, 1 meaning 100% match and 0 meaning all characters are different.
293
+ If the similarity of a map is below the threshold you will be prompted for a manual map.
294
+
295
+ So in summary:
296
+ higher number: more exact matching, but more manual mapping
297
+ lower number: less exact matching, but less manual mapping
298
+
299
+ If you are interested, the algorithm being used here is this: https://en.wikipedia.org/wiki/Levenshtein_distance
300
+ """
301
+ return self._get_value("mal_mapping_min_similarity", 0.8, float)
302
+
303
+ @property
304
+ def mal_mapping_use_alternatives(self) -> bool:
305
+ """Check alternative names when mapping anime.
306
+
307
+ If turned on this will slow down mapping but provide better
308
+ chances of finding a match.
309
+ """
310
+ return self._get_value("mal_mapping_use_alternatives", True, bool)
311
+
312
+ @property
313
+ def mal_mapping_use_filters(self) -> bool:
314
+ """Use filters (e.g. year, season etc.) of providers to narrow down the
315
+ results, this will lead to more accurate mapping, but provide wrong
316
+ results if the filters of the provider do not work properly or if anime
317
+ are not correctly marked with the correct data."""
318
+ return self._get_value("mal_mapping_use_filters", True, bool)
319
+
320
+ @property
321
+ def auto_sync_mal_to_seasonals(self) -> bool:
322
+ """DEPRECATED This option was deprecated in 3.0.0, please consider
323
+ using the `--mal-sync-seasonals` cli option in compination with `-M`
324
+ instead.
325
+
326
+ Automatically sync MyAnimeList to Seasonals list.
327
+ """
111
328
  return self._get_value("auto_sync_mal_to_seasonals", False, bool)
112
329
 
113
330
  @property
114
- def auto_map_mal_to_gogo(self):
331
+ def auto_map_mal_to_gogo(self) -> bool:
115
332
  return self._get_value("auto_map_mal_to_gogo", False, bool)
116
333
 
117
334
  @property
118
- def mal_status_categories(self):
119
- return self._get_value("mal_status_categories", list(["watching"]), list)
335
+ def preferred_type(self) -> Optional[str]:
336
+ """Specify which anime types (dub or sub) you prefer. If this is
337
+ specified, you will not be asked to switch to dub anymore. You can
338
+ however always switch to either in the menu.
339
+
340
+ Examples:
341
+ preferred_type: sub
342
+ preferred_type: dub
343
+ preferred_type: null or preferred_type: "" # always ask
344
+ """
345
+ return self._get_value("preferred_type", None, str)
120
346
 
121
347
  @property
122
- def anime_types(self):
123
- return self._get_value("anime_types", list(["sub", "dub"]), list)
348
+ def skip_season_search(self) -> bool:
349
+ """If this is set to true you will not be prompted to search in season."""
350
+ return self._get_value("skip_season_search", False, bool)
124
351
 
125
352
  def _get_path_value(self, key: str, fallback: Path) -> Path:
126
353
  path = self._get_value(key, fallback, str)
@@ -129,42 +356,47 @@ class Config:
129
356
  # But because pathlib doesn't have expandvars(), we resort
130
357
  # to using the os module inside the Path constructor
131
358
  return Path(os.path.expandvars(path)).expanduser()
132
- except:
359
+ except RuntimeError:
133
360
  return fallback
134
361
 
135
- def _get_value(self, key: str, fallback, typ: object):
362
+ def _get_value(self, key: str, fallback, _type: Type) -> Any:
136
363
  value = self._yaml_conf.get(key, fallback)
137
- if isinstance(value, typ):
364
+ if isinstance(value, _type):
138
365
  return value
139
366
 
140
367
  return fallback
141
368
 
142
369
  def _create_config(self):
143
- try:
144
- self._get_config_path().mkdir(exist_ok=True, parents=True)
145
- config_options = {}
146
- # generate config based on attrs and default values of config class
147
- for attribute, value in Config.__dict__.items():
148
- if attribute.startswith("_"):
149
- continue
150
-
151
- if isinstance(value, property):
152
- val = self.__getattribute__(attribute)
153
- config_options[attribute] = (
154
- str(val) if isinstance(val, Path) else val
155
- )
156
- self._config_file.touch()
157
- with open(self._config_file, "w") as file:
158
- yaml.dump(
159
- yaml.dump(config_options, file, indent=4, default_flow_style=False)
370
+ self._get_config_path().mkdir(exist_ok=True, parents=True)
371
+ self._config_file.touch()
372
+
373
+ dump = ""
374
+ # generate config based on attrs and default values of config class
375
+ for attribute, value in Config.__dict__.items():
376
+ if attribute.startswith("_"):
377
+ continue
378
+
379
+ if isinstance(value, property):
380
+ doc = inspect.getdoc(value)
381
+ if doc:
382
+ # Add docstrings
383
+ doc = Template(doc).safe_substitute(version=__version__)
384
+ doc = "\n".join([f"# {line}" for line in doc.split("\n")])
385
+ dump = dump + doc + "\n"
386
+
387
+ val = self.__getattribute__(attribute)
388
+ val = str(val) if isinstance(val, Path) else val
389
+ dump = (
390
+ dump
391
+ + yaml.dump({attribute: val}, indent=4, default_flow_style=False)
392
+ + "\n"
160
393
  )
161
- except PermissionError as e:
162
- print(f"Failed to create config file: {repr(e)}")
163
- sys_exit(1)
394
+
395
+ self._config_file.write_text(dump)
164
396
 
165
397
  @staticmethod
166
398
  @functools.lru_cache
167
- def _read_config():
399
+ def _read_config() -> Tuple[Path, dict[str, Any]]:
168
400
  config_file = Config._get_config_path() / "config.yaml"
169
401
  try:
170
402
  with config_file.open("r") as conf:
@@ -177,15 +409,4 @@ class Config:
177
409
 
178
410
  @staticmethod
179
411
  def _get_config_path() -> Path:
180
- linux_path = Path().home() / ".config" / "anipy-cli"
181
- windows_path = Path().home() / "AppData" / "Local" / "anipy-cli"
182
- macos_path = Path().home() / ".config" / "anipy-cli"
183
-
184
- if platform == "linux":
185
- return linux_path
186
- elif platform == "darwin":
187
- return macos_path
188
- elif platform == "win32":
189
- return windows_path
190
- else:
191
- raise SysNotFoundError(platform)
412
+ return Path(user_config_dir(__appname__, appauthor=False))
anipy_cli/discord.py ADDED
@@ -0,0 +1,34 @@
1
+ import time
2
+ import functools
3
+ from typing import TYPE_CHECKING
4
+
5
+ from pypresence import Presence
6
+
7
+ if TYPE_CHECKING:
8
+ from anipy_api.provider import ProviderStream
9
+ from anipy_api.anime import Anime
10
+
11
+
12
+ @functools.lru_cache(maxsize=None)
13
+ class DiscordPresence(object):
14
+ def __init__(self):
15
+ self.rpc_client = Presence(966365883691855942)
16
+ self.rpc_client.connect()
17
+
18
+ def dc_presence_callback(self, anime: "Anime", stream: "ProviderStream"):
19
+ anime_info = anime.get_info()
20
+ self.rpc_client.update(
21
+ details=f"Watching {anime.name} via anipy-cli",
22
+ state=f"Episode {stream.episode}/{anime.get_episodes(stream.language)[-1]}",
23
+ large_image=anime_info.image or "",
24
+ small_image="https://raw.githubusercontent.com/sdaqo/anipy-cli/master/docs/assets/anipy-logo-dark-compact.png",
25
+ large_text=anime.name,
26
+ small_text="anipy-cli",
27
+ start=int(time.time()),
28
+ buttons=[
29
+ {
30
+ "label": "Check out anipy-cli",
31
+ "url": "https://github.com/sdaqo/anipy-cli",
32
+ }
33
+ ],
34
+ )