anipy-cli 2.7.31__py3-none-any.whl → 3.0.0.dev0__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.
- anipy_cli/__init__.py +2 -20
- anipy_cli/arg_parser.py +30 -20
- anipy_cli/cli.py +66 -0
- anipy_cli/clis/__init__.py +15 -0
- anipy_cli/clis/base_cli.py +32 -0
- anipy_cli/clis/binge_cli.py +83 -0
- anipy_cli/clis/default_cli.py +104 -0
- anipy_cli/clis/download_cli.py +111 -0
- anipy_cli/clis/history_cli.py +93 -0
- anipy_cli/clis/mal_cli.py +71 -0
- anipy_cli/{cli/clis → clis}/seasonal_cli.py +9 -6
- anipy_cli/colors.py +4 -4
- anipy_cli/config.py +308 -87
- anipy_cli/discord.py +34 -0
- anipy_cli/mal_proxy.py +216 -0
- anipy_cli/menus/__init__.py +5 -0
- anipy_cli/{cli/menus → menus}/base_menu.py +8 -12
- anipy_cli/menus/mal_menu.py +660 -0
- anipy_cli/menus/menu.py +194 -0
- anipy_cli/menus/seasonal_menu.py +263 -0
- anipy_cli/prompts.py +231 -0
- anipy_cli/util.py +262 -0
- anipy_cli-3.0.0.dev0.dist-info/METADATA +67 -0
- anipy_cli-3.0.0.dev0.dist-info/RECORD +26 -0
- {anipy_cli-2.7.31.dist-info → anipy_cli-3.0.0.dev0.dist-info}/WHEEL +1 -2
- anipy_cli-3.0.0.dev0.dist-info/entry_points.txt +3 -0
- anipy_cli/cli/__init__.py +0 -1
- anipy_cli/cli/cli.py +0 -37
- anipy_cli/cli/clis/__init__.py +0 -6
- anipy_cli/cli/clis/base_cli.py +0 -43
- anipy_cli/cli/clis/binge_cli.py +0 -54
- anipy_cli/cli/clis/default_cli.py +0 -46
- anipy_cli/cli/clis/download_cli.py +0 -92
- anipy_cli/cli/clis/history_cli.py +0 -64
- anipy_cli/cli/clis/mal_cli.py +0 -27
- anipy_cli/cli/menus/__init__.py +0 -3
- anipy_cli/cli/menus/mal_menu.py +0 -411
- anipy_cli/cli/menus/menu.py +0 -108
- anipy_cli/cli/menus/seasonal_menu.py +0 -177
- anipy_cli/cli/util.py +0 -125
- anipy_cli/download.py +0 -467
- anipy_cli/history.py +0 -83
- anipy_cli/mal.py +0 -651
- anipy_cli/misc.py +0 -227
- anipy_cli/player/__init__.py +0 -1
- anipy_cli/player/player.py +0 -35
- anipy_cli/player/players/__init__.py +0 -3
- anipy_cli/player/players/base.py +0 -107
- anipy_cli/player/players/mpv.py +0 -19
- anipy_cli/player/players/mpv_control.py +0 -37
- anipy_cli/player/players/syncplay.py +0 -19
- anipy_cli/player/players/vlc.py +0 -18
- anipy_cli/query.py +0 -100
- anipy_cli/run_anipy_cli.py +0 -14
- anipy_cli/seasonal.py +0 -112
- anipy_cli/url_handler.py +0 -470
- anipy_cli/version.py +0 -1
- anipy_cli-2.7.31.dist-info/LICENSE +0 -674
- anipy_cli-2.7.31.dist-info/METADATA +0 -162
- anipy_cli-2.7.31.dist-info/RECORD +0 -43
- anipy_cli-2.7.31.dist-info/entry_points.txt +0 -2
- anipy_cli-2.7.31.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
|
|
2
|
-
from anipy_cli.
|
|
3
|
-
from anipy_cli.
|
|
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
|
|
8
|
-
super().__init__(options
|
|
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
|
|
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
|
|
6
|
-
from
|
|
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
|
-
|
|
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
|
|
23
|
-
|
|
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
|
-
"
|
|
30
|
+
"user_files_path", Path(user_data_dir(__appname__, appauthor=False))
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
@property
|
|
32
|
-
def
|
|
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
|
-
"
|
|
52
|
+
"download_folder_path", self.user_files_path / "download"
|
|
35
53
|
)
|
|
36
54
|
|
|
37
55
|
@property
|
|
38
|
-
def
|
|
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
|
-
"
|
|
62
|
+
"seasonals_dl_path", self.download_folder_path / "seasonals"
|
|
41
63
|
)
|
|
42
64
|
|
|
43
65
|
@property
|
|
44
|
-
def
|
|
45
|
-
|
|
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
|
|
49
|
-
|
|
101
|
+
def provider_urls(self) -> Dict[str, str]:
|
|
102
|
+
"""A list of pairs to override the default urls that providers use.
|
|
50
103
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
|
87
|
-
|
|
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
|
|
91
|
-
|
|
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
|
|
95
|
-
|
|
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
|
|
99
|
-
|
|
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
|
|
103
|
-
|
|
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
|
|
107
|
-
|
|
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
|
|
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
|
|
119
|
-
|
|
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
|
|
123
|
-
|
|
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,
|
|
362
|
+
def _get_value(self, key: str, fallback, _type: Type) -> Any:
|
|
136
363
|
value = self._yaml_conf.get(key, fallback)
|
|
137
|
-
if isinstance(value,
|
|
364
|
+
if isinstance(value, _type):
|
|
138
365
|
return value
|
|
139
366
|
|
|
140
367
|
return fallback
|
|
141
368
|
|
|
142
369
|
def _create_config(self):
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
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://github.com/Dankni95/ulauncher-anime/raw/master/images/icon.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
|
+
)
|