anipy-cli 3.5.9__py3-none-any.whl → 3.7.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.
- anipy_cli/__init__.py +1 -1
- anipy_cli/anilist_proxy.py +221 -0
- anipy_cli/arg_parser.py +39 -0
- anipy_cli/cli.py +23 -2
- anipy_cli/clis/__init__.py +2 -0
- anipy_cli/clis/anilist_cli.py +62 -0
- anipy_cli/colors.py +7 -4
- anipy_cli/config.py +47 -43
- anipy_cli/download_component.py +17 -7
- anipy_cli/logger.py +199 -0
- anipy_cli/mal_proxy.py +9 -9
- anipy_cli/menus/__init__.py +2 -1
- anipy_cli/menus/anilist_menu.py +648 -0
- anipy_cli/menus/base_menu.py +4 -5
- anipy_cli/menus/mal_menu.py +15 -15
- anipy_cli/menus/menu.py +5 -2
- anipy_cli/menus/seasonal_menu.py +4 -4
- anipy_cli/util.py +22 -5
- {anipy_cli-3.5.9.dist-info → anipy_cli-3.7.0.dist-info}/METADATA +2 -2
- anipy_cli-3.7.0.dist-info/RECORD +31 -0
- anipy_cli-3.5.9.dist-info/RECORD +0 -27
- {anipy_cli-3.5.9.dist-info → anipy_cli-3.7.0.dist-info}/WHEEL +0 -0
- {anipy_cli-3.5.9.dist-info → anipy_cli-3.7.0.dist-info}/entry_points.txt +0 -0
anipy_cli/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__appname__ = "anipy-cli"
|
|
2
|
-
__version__ = "3.
|
|
2
|
+
__version__ = "3.7.0"
|
|
@@ -0,0 +1,221 @@
|
|
|
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(local_list.read_text())
|
|
61
|
+
except KeyError:
|
|
62
|
+
choice = inquirer.confirm(
|
|
63
|
+
message=f"Your local AniList ({str(local_list)}) is not in the correct format, should it be deleted?",
|
|
64
|
+
default=False,
|
|
65
|
+
).execute()
|
|
66
|
+
if choice:
|
|
67
|
+
local_list.unlink()
|
|
68
|
+
return AniListLocalList.read(user_id)
|
|
69
|
+
else:
|
|
70
|
+
error("could not read your AniList", fatal=True)
|
|
71
|
+
|
|
72
|
+
return mylist
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class AniListProxy:
|
|
76
|
+
def __init__(self, anilist: AniList):
|
|
77
|
+
self.anilist = anilist
|
|
78
|
+
self.user_id = anilist.get_user().id
|
|
79
|
+
self.local_list = AniListLocalList.read(self.user_id)
|
|
80
|
+
|
|
81
|
+
def _cache_list(self, mylist: List[AniListAnime]):
|
|
82
|
+
config = Config()
|
|
83
|
+
for e in mylist:
|
|
84
|
+
if self.local_list.mappings.get(e.id, None):
|
|
85
|
+
if e.my_list_status and config.tracker_ignore_tag in e.my_list_status.tags:
|
|
86
|
+
self.local_list.mappings.pop(e.id)
|
|
87
|
+
else:
|
|
88
|
+
self.local_list.mappings[e.id].anilist_anime = e
|
|
89
|
+
else:
|
|
90
|
+
self.local_list.mappings[e.id] = AniListProviderMapping(e, {})
|
|
91
|
+
|
|
92
|
+
self.local_list.write(self.user_id)
|
|
93
|
+
|
|
94
|
+
def _write_mapping(self, anilist_anime: AniListAnime, mapping: Anime):
|
|
95
|
+
self._cache_list([anilist_anime])
|
|
96
|
+
|
|
97
|
+
self.local_list.mappings[anilist_anime.id].mappings[
|
|
98
|
+
f"{mapping.provider.NAME}:{mapping.identifier}"
|
|
99
|
+
] = ProviderMapping(
|
|
100
|
+
mapping.provider.NAME, mapping.name, mapping.identifier, mapping.languages
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
self.local_list.write(self.user_id)
|
|
104
|
+
|
|
105
|
+
def get_list(
|
|
106
|
+
self, status_catagories: Optional[Set[AniListMyListStatusEnum]] = None
|
|
107
|
+
) -> List[AniListAnime]:
|
|
108
|
+
config = Config()
|
|
109
|
+
mylist: List[AniListAnime] = []
|
|
110
|
+
|
|
111
|
+
catagories = (
|
|
112
|
+
status_catagories
|
|
113
|
+
if status_catagories is not None
|
|
114
|
+
else set(
|
|
115
|
+
[AniListMyListStatusEnum[s.upper()] for s in config.tracker_status_categories]
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
for c in catagories:
|
|
120
|
+
mylist.extend(
|
|
121
|
+
filter(
|
|
122
|
+
lambda e: (
|
|
123
|
+
config.tracker_ignore_tag not in e.my_list_status.tags
|
|
124
|
+
if e.my_list_status
|
|
125
|
+
else True
|
|
126
|
+
),
|
|
127
|
+
self.anilist.get_anime_list(c),
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
self._cache_list(mylist)
|
|
132
|
+
filtered_list = filter(
|
|
133
|
+
lambda x: (
|
|
134
|
+
x.my_list_status.status in catagories if x.my_list_status else False
|
|
135
|
+
),
|
|
136
|
+
mylist,
|
|
137
|
+
)
|
|
138
|
+
return list(filtered_list)
|
|
139
|
+
|
|
140
|
+
def update_show(
|
|
141
|
+
self,
|
|
142
|
+
anime: AniListAnime,
|
|
143
|
+
status: Optional[AniListMyListStatusEnum] = None,
|
|
144
|
+
episode: Optional[int] = None,
|
|
145
|
+
tags: Set[str] = set(),
|
|
146
|
+
) -> AniListMyListStatus:
|
|
147
|
+
config = Config()
|
|
148
|
+
tags |= set(config.tracker_tags)
|
|
149
|
+
result = self.anilist.update_anime_list(
|
|
150
|
+
anime.id, status=status, watched_episodes=episode, tags=list(tags)
|
|
151
|
+
)
|
|
152
|
+
anime.my_list_status = result
|
|
153
|
+
self._cache_list([anime])
|
|
154
|
+
return result
|
|
155
|
+
|
|
156
|
+
def delete_show(self, anime: AniListAnime) -> None:
|
|
157
|
+
self.local_list.mappings.pop(anime.id)
|
|
158
|
+
self.local_list.write(self.user_id)
|
|
159
|
+
|
|
160
|
+
self.anilist.remove_from_anime_list(anime.id)
|
|
161
|
+
|
|
162
|
+
def map_from_anilist(
|
|
163
|
+
self, anime: AniListAnime, mapping: Optional[Anime] = None
|
|
164
|
+
) -> Optional[Anime]:
|
|
165
|
+
if mapping is not None:
|
|
166
|
+
self._write_mapping(anime, mapping)
|
|
167
|
+
return mapping
|
|
168
|
+
|
|
169
|
+
if self.local_list.mappings[anime.id].mappings:
|
|
170
|
+
for map in self.local_list.mappings[anime.id].mappings.values():
|
|
171
|
+
provider = next(
|
|
172
|
+
filter(lambda x: x.NAME == map.provider, list_providers()), None
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if provider is None:
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
return Anime(provider(), map.name, map.identifier, map.languages)
|
|
179
|
+
|
|
180
|
+
config = Config()
|
|
181
|
+
result = None
|
|
182
|
+
for p in get_prefered_providers("anilist"):
|
|
183
|
+
adapter = AniListAdapter(self.anilist, p)
|
|
184
|
+
result = adapter.from_anilist(
|
|
185
|
+
anime,
|
|
186
|
+
config.tracker_mapping_min_similarity,
|
|
187
|
+
config.tracker_mapping_use_filters,
|
|
188
|
+
config.tracker_mapping_use_alternatives,
|
|
189
|
+
)
|
|
190
|
+
if result is not None:
|
|
191
|
+
break
|
|
192
|
+
|
|
193
|
+
if result:
|
|
194
|
+
self._write_mapping(anime, result)
|
|
195
|
+
|
|
196
|
+
return result
|
|
197
|
+
|
|
198
|
+
def map_from_provider(
|
|
199
|
+
self, anime: Anime, mapping: Optional[AniListAnime] = None
|
|
200
|
+
) -> Optional[AniListAnime]:
|
|
201
|
+
if mapping is not None:
|
|
202
|
+
self._write_mapping(mapping, anime)
|
|
203
|
+
return mapping
|
|
204
|
+
|
|
205
|
+
for _, m in self.local_list.mappings.items():
|
|
206
|
+
existing = m.mappings.get(f"{anime.provider.NAME}:{anime.identifier}", None)
|
|
207
|
+
if existing:
|
|
208
|
+
return m.anilist_anime
|
|
209
|
+
|
|
210
|
+
config = Config()
|
|
211
|
+
adapter = AniListAdapter(self.anilist, anime.provider)
|
|
212
|
+
result = adapter.from_provider(
|
|
213
|
+
anime,
|
|
214
|
+
config.tracker_mapping_min_similarity,
|
|
215
|
+
config.tracker_mapping_use_alternatives,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if result:
|
|
219
|
+
self._write_mapping(result, anime)
|
|
220
|
+
|
|
221
|
+
return result
|
anipy_cli/arg_parser.py
CHANGED
|
@@ -13,15 +13,19 @@ class CliArgs:
|
|
|
13
13
|
history: bool
|
|
14
14
|
seasonal: bool
|
|
15
15
|
mal: bool
|
|
16
|
+
anilist: bool
|
|
16
17
|
delete: bool
|
|
17
18
|
migrate_hist: bool
|
|
18
19
|
quality: Optional[Union[str, int]]
|
|
19
20
|
ffmpeg: bool
|
|
20
21
|
auto_update: bool
|
|
21
22
|
mal_sync_seasonals: bool
|
|
23
|
+
anilist_sync_seasonals: bool
|
|
22
24
|
optional_player: Optional[str]
|
|
23
25
|
search: Optional[str]
|
|
24
26
|
location: Optional[Path]
|
|
27
|
+
verbosity: int
|
|
28
|
+
stack_always: bool
|
|
25
29
|
mal_password: Optional[str]
|
|
26
30
|
config: bool
|
|
27
31
|
seasonal_search: Optional[str]
|
|
@@ -93,6 +97,15 @@ def parse_args(override_args: Optional[list[str]] = None) -> CliArgs:
|
|
|
93
97
|
"(requires MAL account credentials to be set in config).",
|
|
94
98
|
)
|
|
95
99
|
|
|
100
|
+
actions_group.add_argument(
|
|
101
|
+
"-A",
|
|
102
|
+
"--anilist",
|
|
103
|
+
required=False,
|
|
104
|
+
dest="anilist",
|
|
105
|
+
action="store_true",
|
|
106
|
+
help="Anilist mode. Similar to seasonal mode, but using Anilist"
|
|
107
|
+
)
|
|
108
|
+
|
|
96
109
|
actions_group.add_argument(
|
|
97
110
|
"--delete-history",
|
|
98
111
|
required=False,
|
|
@@ -176,6 +189,24 @@ def parse_args(override_args: Optional[list[str]] = None) -> CliArgs:
|
|
|
176
189
|
help="Override all configured download locations",
|
|
177
190
|
)
|
|
178
191
|
|
|
192
|
+
options_group.add_argument(
|
|
193
|
+
"-V",
|
|
194
|
+
"--verbose",
|
|
195
|
+
required=False,
|
|
196
|
+
dest="verbosity",
|
|
197
|
+
action="count",
|
|
198
|
+
default=0,
|
|
199
|
+
help="Verbosity levels in the console: -V = 'fatal' -VV = 'warnings' -VVV = 'info'",
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
options_group.add_argument(
|
|
203
|
+
"--stack-always",
|
|
204
|
+
required=False,
|
|
205
|
+
dest="stack_always",
|
|
206
|
+
action="store_true",
|
|
207
|
+
help="Always show the stack trace on any log outputs.",
|
|
208
|
+
)
|
|
209
|
+
|
|
179
210
|
options_group.add_argument(
|
|
180
211
|
"-so",
|
|
181
212
|
"--sub-only",
|
|
@@ -201,6 +232,14 @@ def parse_args(override_args: Optional[list[str]] = None) -> CliArgs:
|
|
|
201
232
|
help="Automatically sync myanimelist to seasonals (only works with `-M`)",
|
|
202
233
|
)
|
|
203
234
|
|
|
235
|
+
options_group.add_argument(
|
|
236
|
+
"--anilist-sync-to-seasonals",
|
|
237
|
+
required=False,
|
|
238
|
+
dest="anilist_sync_seasonals",
|
|
239
|
+
action="store_true",
|
|
240
|
+
help="Automatically sync anilist to seasonals (only works with `-A`)",
|
|
241
|
+
)
|
|
242
|
+
|
|
204
243
|
info_group.add_argument(
|
|
205
244
|
"-h", "--help", action="help", help="show this help message and exit"
|
|
206
245
|
)
|
anipy_cli/cli.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from types import TracebackType
|
|
1
3
|
from typing import Optional
|
|
2
4
|
|
|
3
5
|
from pypresence.exceptions import DiscordNotFound
|
|
@@ -5,16 +7,34 @@ from pypresence.exceptions import DiscordNotFound
|
|
|
5
7
|
from anipy_api.locallist import LocalList
|
|
6
8
|
from anipy_cli.prompts import migrate_provider
|
|
7
9
|
|
|
8
|
-
from anipy_cli.arg_parser import parse_args
|
|
10
|
+
from anipy_cli.arg_parser import CliArgs, parse_args
|
|
9
11
|
from anipy_cli.clis import *
|
|
10
|
-
from anipy_cli.colors import colors, cprint
|
|
12
|
+
from anipy_cli.colors import color, colors, cprint
|
|
11
13
|
from anipy_cli.util import error, DotSpinner, migrate_locallist
|
|
12
14
|
from anipy_cli.config import Config
|
|
13
15
|
from anipy_cli.discord import DiscordPresence
|
|
16
|
+
import anipy_cli.logger as logger
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
def run_cli(override_args: Optional[list[str]] = None):
|
|
17
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(exc_val: BaseException, exc_tb: TracebackType, logs_location: Path):
|
|
26
|
+
print(
|
|
27
|
+
color(
|
|
28
|
+
colors.RED,
|
|
29
|
+
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}.',
|
|
30
|
+
)
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
with logger.safe(fatal_handler):
|
|
34
|
+
_safe_cli(args)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _safe_cli(args: CliArgs):
|
|
18
38
|
config = Config()
|
|
19
39
|
# This updates the config, adding new values doc changes and the like.
|
|
20
40
|
config._create_config()
|
|
@@ -61,6 +81,7 @@ def run_cli(override_args: Optional[list[str]] = None):
|
|
|
61
81
|
args.seasonal: SeasonalCli,
|
|
62
82
|
args.history: HistoryCli,
|
|
63
83
|
args.mal: MalCli,
|
|
84
|
+
args.anilist: AniListCli,
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
cli_class = clis_dict.get(True, DefaultCli)
|
anipy_cli/clis/__init__.py
CHANGED
|
@@ -4,11 +4,13 @@ from anipy_cli.clis.mal_cli import MalCli
|
|
|
4
4
|
from anipy_cli.clis.seasonal_cli import SeasonalCli
|
|
5
5
|
from anipy_cli.clis.binge_cli import BingeCli
|
|
6
6
|
from anipy_cli.clis.download_cli import DownloadCli
|
|
7
|
+
from anipy_cli.clis.anilist_cli import AniListCli
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
9
10
|
"DefaultCli",
|
|
10
11
|
"HistoryCli",
|
|
11
12
|
"MalCli",
|
|
13
|
+
"AniListCli",
|
|
12
14
|
"SeasonalCli",
|
|
13
15
|
"BingeCli",
|
|
14
16
|
"DownloadCli",
|
|
@@ -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()
|
anipy_cli/colors.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class colors: # noqa: N801
|
|
2
5
|
"""Just a class for colors."""
|
|
3
6
|
|
|
4
7
|
GREEN = "\033[92m"
|
|
@@ -14,7 +17,7 @@ class colors:
|
|
|
14
17
|
RESET = "\033[0m"
|
|
15
18
|
|
|
16
19
|
|
|
17
|
-
def color(*values, sep: str = "") -> str:
|
|
20
|
+
def color(*values: Any, sep: str = "") -> str:
|
|
18
21
|
"""Decorate a string with color codes.
|
|
19
22
|
|
|
20
23
|
Basically just ensures that the color doesn't "leak"
|
|
@@ -24,13 +27,13 @@ def color(*values, sep: str = "") -> str:
|
|
|
24
27
|
return sep.join(map(str, values)) + colors.END
|
|
25
28
|
|
|
26
29
|
|
|
27
|
-
def cinput(*prompt, input_color: str = "") -> str:
|
|
30
|
+
def cinput(*prompt: Any, input_color: str = "") -> str:
|
|
28
31
|
"""An input function that handles coloring input."""
|
|
29
32
|
inp = input(color(*prompt) + input_color)
|
|
30
33
|
print(colors.END, end="")
|
|
31
34
|
return inp
|
|
32
35
|
|
|
33
36
|
|
|
34
|
-
def cprint(*values, sep: str = "", **kwargs) -> None:
|
|
37
|
+
def cprint(*values: Any, sep: str = "", **kwargs: Any) -> None:
|
|
35
38
|
"""Prints colored text."""
|
|
36
39
|
print(color(*values, sep=sep), **kwargs)
|
anipy_cli/config.py
CHANGED
|
@@ -12,8 +12,6 @@ from anipy_cli import __appname__, __version__
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class Config:
|
|
15
|
-
_EXPAND_PATHS = True
|
|
16
|
-
|
|
17
15
|
def __init__(self):
|
|
18
16
|
self._config_file, self._yaml_conf = Config._read_config()
|
|
19
17
|
|
|
@@ -44,6 +42,10 @@ class Config:
|
|
|
44
42
|
def _mal_local_user_list_path(self) -> Path:
|
|
45
43
|
return self.user_files_path / "mal_list.json"
|
|
46
44
|
|
|
45
|
+
@property
|
|
46
|
+
def _anilist_local_user_list_path(self) -> Path:
|
|
47
|
+
return self.user_files_path / "anilist_list.json"
|
|
48
|
+
|
|
47
49
|
@property
|
|
48
50
|
def download_folder_path(self) -> Path:
|
|
49
51
|
"""Path to your download folder/directory.
|
|
@@ -68,8 +70,8 @@ class Config:
|
|
|
68
70
|
def providers(self) -> Dict[str, List[str]]:
|
|
69
71
|
"""A list of pairs defining which providers will search for anime
|
|
70
72
|
in different parts of the program. Configurable areas are as follows:
|
|
71
|
-
default (and history), download (-D), seasonal (-S), binge (-B)
|
|
72
|
-
(-M) The example will show you how it is done! Please note that for seasonal
|
|
73
|
+
default (and history), download (-D), seasonal (-S), binge (-B), anilist (-A)
|
|
74
|
+
and mal (-M) The example will show you how it is done! Please note that for seasonal
|
|
73
75
|
search always the first provider that supports it is used.
|
|
74
76
|
|
|
75
77
|
For an updated list of providers look here: https://sdaqo.github.io/anipy-cli/availabilty
|
|
@@ -83,6 +85,7 @@ class Config:
|
|
|
83
85
|
seasonal: ["provider3"]
|
|
84
86
|
binge: ["provider4"]
|
|
85
87
|
mal: ["provider2", "provider3"]
|
|
88
|
+
anilist: ["provider1"]
|
|
86
89
|
"""
|
|
87
90
|
defaults = {
|
|
88
91
|
"default": ["allanime"],
|
|
@@ -90,6 +93,7 @@ class Config:
|
|
|
90
93
|
"seasonal": ["allanime"],
|
|
91
94
|
"binge": ["allanime"],
|
|
92
95
|
"mal": ["allanime"],
|
|
96
|
+
"anilist": ["allanime"],
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
value = self._get_value("providers", defaults, dict)
|
|
@@ -229,9 +233,9 @@ class Config:
|
|
|
229
233
|
"""With this option you can define scripts that run after a file
|
|
230
234
|
has been downloaded. As with the 'providers' option, you can configure
|
|
231
235
|
different behaviour, depending on which part of anipy-cli the download occurs.
|
|
232
|
-
Configurable areas are as follows: default (and history), download (-D), seasonal (-S)
|
|
233
|
-
and mal (-M). The example will show you how it is done! Please note that
|
|
234
|
-
several scripts for one area, they will run in the order you put them in the list.
|
|
236
|
+
Configurable areas are as follows: default (and history), download (-D), seasonal (-S),
|
|
237
|
+
anilist (-A) and mal (-M). The example will show you how it is done! Please note that
|
|
238
|
+
if you define several scripts for one area, they will run in the order you put them in the list.
|
|
235
239
|
You can also define a timeout (in seconds), after which a script will be terminated,
|
|
236
240
|
if set to null there will be no timeout and any script will run forever.
|
|
237
241
|
|
|
@@ -256,6 +260,7 @@ class Config:
|
|
|
256
260
|
"download": [],
|
|
257
261
|
"seasonal": [],
|
|
258
262
|
"mal": [],
|
|
263
|
+
"anilist": [],
|
|
259
264
|
"timeout": None,
|
|
260
265
|
}
|
|
261
266
|
|
|
@@ -281,6 +286,11 @@ class Config:
|
|
|
281
286
|
"""Your MyAnimeList username for MAL mode."""
|
|
282
287
|
return self._get_value("mal_user", "", str)
|
|
283
288
|
|
|
289
|
+
@property
|
|
290
|
+
def anilist_token(self) -> str:
|
|
291
|
+
"""Your AniList access token for AniList mode."""
|
|
292
|
+
return self._get_value("anilist_token", "", str)
|
|
293
|
+
|
|
284
294
|
@property
|
|
285
295
|
def mal_password(self) -> str:
|
|
286
296
|
"""Your MyAnimeList password for MAL mode.
|
|
@@ -291,55 +301,55 @@ class Config:
|
|
|
291
301
|
return self._get_value("mal_password", "", str)
|
|
292
302
|
|
|
293
303
|
@property
|
|
294
|
-
def
|
|
304
|
+
def tracker_ignore_tag(self) -> str:
|
|
295
305
|
"""All anime in your MyAnimeList with this tag will be ignored by
|
|
296
306
|
anipy-cli.
|
|
297
307
|
|
|
298
308
|
Examples:
|
|
299
|
-
|
|
300
|
-
|
|
309
|
+
tracker_ignore_tag: ignore # all anime with ignore tag will be ignored
|
|
310
|
+
tracker_ignore_tag: "" # no anime will be ignored
|
|
301
311
|
"""
|
|
302
|
-
return self._get_value("
|
|
312
|
+
return self._get_value("tracker_ignore_tag", "ignore", str)
|
|
303
313
|
|
|
304
314
|
@property
|
|
305
|
-
def
|
|
306
|
-
"""All anime in your
|
|
307
|
-
dub in
|
|
308
|
-
anipy-cli will use `preferred_type` to choose dub or sub in
|
|
315
|
+
def tracker_dub_tag(self) -> str:
|
|
316
|
+
"""All anime in your Anime Tracker with this tag will be switched over to
|
|
317
|
+
dub in tracker mode, if the dub is available. If you do not specify a tag,
|
|
318
|
+
anipy-cli will use `preferred_type` to choose dub or sub in tracker mode.
|
|
309
319
|
|
|
310
320
|
Examples:
|
|
311
|
-
|
|
312
|
-
|
|
321
|
+
tracker_dub_tag: dub # all anime with this tag will be switched to dub
|
|
322
|
+
tracker_dub_tag: "" # no anime will be switched to dub, except you have preferred_type on dub
|
|
313
323
|
"""
|
|
314
|
-
return self._get_value("
|
|
324
|
+
return self._get_value("tracker_dub_tag", "dub", str)
|
|
315
325
|
|
|
316
326
|
@property
|
|
317
|
-
def
|
|
318
|
-
"""Custom tags to tag all anime in your
|
|
327
|
+
def tracker_tags(self) -> List[str]:
|
|
328
|
+
"""Custom tags to tag all anime in your Anime Tracker that are
|
|
319
329
|
altered/added by anipy-cli.
|
|
320
330
|
|
|
321
331
|
Examples:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
332
|
+
tracker_tags: ["anipy-cli"] # tag all anime with anipy-cli
|
|
333
|
+
tracker_tags: ["anipy-cli", "important"] # tag all anime with anipy-cli and important
|
|
334
|
+
tracker_tags: null or tracker_tags: [] # Do not tag the anime
|
|
325
335
|
"""
|
|
326
|
-
return self._get_value("
|
|
336
|
+
return self._get_value("tracker_tags", [], list)
|
|
327
337
|
|
|
328
338
|
@property
|
|
329
|
-
def
|
|
330
|
-
"""Status categories of your
|
|
339
|
+
def tracker_status_categories(self) -> List[str]:
|
|
340
|
+
"""Status categories of your Anime Tracker that anipy-cli uses for
|
|
331
341
|
downloading/watching new episodes listing anime in your list and stuff
|
|
332
342
|
like that. Normally the watching catagory should be enough as you would
|
|
333
343
|
normally put anime you currently watch in the watching catagory.
|
|
334
344
|
|
|
335
345
|
Valid values are: watching, completed, on_hold, dropped, plan_to_watch
|
|
336
346
|
"""
|
|
337
|
-
return self._get_value("
|
|
347
|
+
return self._get_value("tracker_status_categories", ["watching"], list)
|
|
338
348
|
|
|
339
349
|
@property
|
|
340
|
-
def
|
|
350
|
+
def tracker_mapping_min_similarity(self) -> float:
|
|
341
351
|
"""
|
|
342
|
-
The minumum similarity between titles when mapping anime in
|
|
352
|
+
The minumum similarity between titles when mapping anime in tracker mode.
|
|
343
353
|
This is a decimal number from 0 - 1, 1 meaning 100% match and 0 meaning all characters are different.
|
|
344
354
|
If the similarity of a map is below the threshold you will be prompted for a manual map.
|
|
345
355
|
|
|
@@ -349,24 +359,24 @@ class Config:
|
|
|
349
359
|
|
|
350
360
|
If you are interested, the algorithm being used here is this: https://en.wikipedia.org/wiki/Levenshtein_distance
|
|
351
361
|
"""
|
|
352
|
-
return self._get_value("
|
|
362
|
+
return self._get_value("tracker_mapping_min_similarity", 0.8, float)
|
|
353
363
|
|
|
354
364
|
@property
|
|
355
|
-
def
|
|
365
|
+
def tracker_mapping_use_alternatives(self) -> bool:
|
|
356
366
|
"""Check alternative names when mapping anime.
|
|
357
367
|
|
|
358
368
|
If turned on this will slow down mapping but provide better
|
|
359
369
|
chances of finding a match.
|
|
360
370
|
"""
|
|
361
|
-
return self._get_value("
|
|
371
|
+
return self._get_value("tracker_mapping_use_alternatives", True, bool)
|
|
362
372
|
|
|
363
373
|
@property
|
|
364
|
-
def
|
|
374
|
+
def tracker_mapping_use_filters(self) -> bool:
|
|
365
375
|
"""Use filters (e.g. year, season etc.) of providers to narrow down the
|
|
366
376
|
results, this will lead to more accurate mapping, but provide wrong
|
|
367
377
|
results if the filters of the provider do not work properly or if anime
|
|
368
378
|
are not correctly marked with the correct data."""
|
|
369
|
-
return self._get_value("
|
|
379
|
+
return self._get_value("tracker_mapping_use_filters", True, bool)
|
|
370
380
|
|
|
371
381
|
@property
|
|
372
382
|
def auto_sync_mal_to_seasonals(self) -> bool:
|
|
@@ -412,14 +422,11 @@ class Config:
|
|
|
412
422
|
# os.path.expanduser is equivalent to Path().expanduser()
|
|
413
423
|
# But because pathlib doesn't have expandvars(), we resort
|
|
414
424
|
# to using the os module inside the Path constructor
|
|
415
|
-
|
|
416
|
-
return Path(os.path.expandvars(path)).expanduser()
|
|
417
|
-
|
|
418
|
-
return Path(path)
|
|
425
|
+
return Path(os.path.expandvars(path)).expanduser()
|
|
419
426
|
except RuntimeError:
|
|
420
427
|
return fallback
|
|
421
428
|
|
|
422
|
-
def _get_value(self, key: str, fallback, _type: Type) -> Any:
|
|
429
|
+
def _get_value(self, key: str, fallback: Any, _type: Type) -> Any:
|
|
423
430
|
value = self._yaml_conf.get(key, fallback)
|
|
424
431
|
if isinstance(value, _type):
|
|
425
432
|
return value
|
|
@@ -429,8 +436,6 @@ class Config:
|
|
|
429
436
|
def _create_config(self):
|
|
430
437
|
self._get_config_path().mkdir(exist_ok=True, parents=True)
|
|
431
438
|
self._config_file.touch()
|
|
432
|
-
|
|
433
|
-
self._EXPAND_PATHS = False
|
|
434
439
|
|
|
435
440
|
dump = ""
|
|
436
441
|
# generate config based on attrs and default values of config class
|
|
@@ -455,8 +460,7 @@ class Config:
|
|
|
455
460
|
+ yaml.dump({attribute: val}, indent=4, default_flow_style=False)
|
|
456
461
|
+ "\n"
|
|
457
462
|
)
|
|
458
|
-
|
|
459
|
-
self._EXPAND_PATHS = True
|
|
463
|
+
|
|
460
464
|
self._config_file.write_text(dump)
|
|
461
465
|
|
|
462
466
|
@staticmethod
|