anipy-cli 3.6.0__tar.gz → 3.8.0__tar.gz
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-3.6.0 → anipy_cli-3.8.0}/PKG-INFO +4 -3
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/pyproject.toml +2 -2
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/__init__.py +1 -1
- anipy_cli-3.8.0/src/anipy_cli/anilist_proxy.py +221 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/arg_parser.py +19 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/cli.py +1 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/clis/__init__.py +2 -0
- anipy_cli-3.8.0/src/anipy_cli/clis/anilist_cli.py +62 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/config.py +50 -43
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/mal_proxy.py +9 -9
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/menus/__init__.py +2 -1
- anipy_cli-3.8.0/src/anipy_cli/menus/anilist_menu.py +648 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/menus/mal_menu.py +14 -14
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/prompts.py +2 -2
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/README.md +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/clis/base_cli.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/clis/binge_cli.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/clis/default_cli.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/clis/download_cli.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/clis/history_cli.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/clis/mal_cli.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/clis/seasonal_cli.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/colors.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/discord.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/download_component.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/logger.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/menus/base_menu.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/menus/menu.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/menus/seasonal_menu.py +0 -0
- {anipy_cli-3.6.0 → anipy_cli-3.8.0}/src/anipy_cli/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: anipy-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.8.0
|
|
4
4
|
Summary: Watch and Download anime from the comfort of your Terminal
|
|
5
5
|
License: GPL-3.0
|
|
6
6
|
Keywords: anime,cli
|
|
@@ -14,7 +14,8 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.11
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
-
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Requires-Dist: anipy-api (>=3.8.0,<4.0.0)
|
|
18
19
|
Requires-Dist: appdirs (>=1.4.4,<2.0.0)
|
|
19
20
|
Requires-Dist: inquirerpy (>=0.3.4,<0.4.0)
|
|
20
21
|
Requires-Dist: pypresence (>=4.3.0,<5.0.0)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "anipy-cli"
|
|
3
|
-
version = "3.
|
|
3
|
+
version = "3.8.0"
|
|
4
4
|
description = "Watch and Download anime from the comfort of your Terminal"
|
|
5
5
|
authors = ["sdaqo <sdaqo.dev@protonmail.com>"]
|
|
6
6
|
license = "GPL-3.0"
|
|
@@ -20,7 +20,7 @@ yaspin = "^3.0.2"
|
|
|
20
20
|
inquirerpy = "^0.3.4"
|
|
21
21
|
appdirs = "^1.4.4"
|
|
22
22
|
pypresence = "^4.3.0"
|
|
23
|
-
anipy-api = "^3.
|
|
23
|
+
anipy-api = "^3.8.0"
|
|
24
24
|
|
|
25
25
|
[tool.poetry.scripts]
|
|
26
26
|
anipy-cli = "anipy_cli.cli:run_cli"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
__appname__ = "anipy-cli"
|
|
2
|
-
__version__ = "3.
|
|
2
|
+
__version__ = "3.8.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
|
|
@@ -13,12 +13,14 @@ 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]
|
|
@@ -95,6 +97,15 @@ def parse_args(override_args: Optional[list[str]] = None) -> CliArgs:
|
|
|
95
97
|
"(requires MAL account credentials to be set in config).",
|
|
96
98
|
)
|
|
97
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
|
+
|
|
98
109
|
actions_group.add_argument(
|
|
99
110
|
"--delete-history",
|
|
100
111
|
required=False,
|
|
@@ -221,6 +232,14 @@ def parse_args(override_args: Optional[list[str]] = None) -> CliArgs:
|
|
|
221
232
|
help="Automatically sync myanimelist to seasonals (only works with `-M`)",
|
|
222
233
|
)
|
|
223
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
|
+
|
|
224
243
|
info_group.add_argument(
|
|
225
244
|
"-h", "--help", action="help", help="show this help message and exit"
|
|
226
245
|
)
|
|
@@ -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()
|
|
@@ -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,13 +70,13 @@ 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
|
|
76
78
|
|
|
77
|
-
Supported providers (as of $version): allanime, animekai (animekai is not functional for now)
|
|
79
|
+
Supported providers (as of $version): allanime, animekai (animekai is not functional for now), native (filesystem provider, set root in provider_urls config)
|
|
78
80
|
|
|
79
81
|
Examples:
|
|
80
82
|
providers:
|
|
@@ -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)
|
|
@@ -102,9 +106,12 @@ class Config:
|
|
|
102
106
|
def provider_urls(self) -> Dict[str, str]:
|
|
103
107
|
"""A list of pairs to override the default urls that providers use.
|
|
104
108
|
|
|
109
|
+
Note: the native provider accepts a path instead of a url, it defaults to ~/Videos
|
|
110
|
+
|
|
105
111
|
Examples:
|
|
106
112
|
provider_urls:
|
|
107
113
|
gogoanime: "https://gogoanime3.co"
|
|
114
|
+
native: "~/Videos"
|
|
108
115
|
provider_urls: {} # do not override any urls
|
|
109
116
|
"""
|
|
110
117
|
|
|
@@ -229,9 +236,9 @@ class Config:
|
|
|
229
236
|
"""With this option you can define scripts that run after a file
|
|
230
237
|
has been downloaded. As with the 'providers' option, you can configure
|
|
231
238
|
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.
|
|
239
|
+
Configurable areas are as follows: default (and history), download (-D), seasonal (-S),
|
|
240
|
+
anilist (-A) and mal (-M). The example will show you how it is done! Please note that
|
|
241
|
+
if you define several scripts for one area, they will run in the order you put them in the list.
|
|
235
242
|
You can also define a timeout (in seconds), after which a script will be terminated,
|
|
236
243
|
if set to null there will be no timeout and any script will run forever.
|
|
237
244
|
|
|
@@ -256,6 +263,7 @@ class Config:
|
|
|
256
263
|
"download": [],
|
|
257
264
|
"seasonal": [],
|
|
258
265
|
"mal": [],
|
|
266
|
+
"anilist": [],
|
|
259
267
|
"timeout": None,
|
|
260
268
|
}
|
|
261
269
|
|
|
@@ -281,6 +289,11 @@ class Config:
|
|
|
281
289
|
"""Your MyAnimeList username for MAL mode."""
|
|
282
290
|
return self._get_value("mal_user", "", str)
|
|
283
291
|
|
|
292
|
+
@property
|
|
293
|
+
def anilist_token(self) -> str:
|
|
294
|
+
"""Your AniList access token for AniList mode."""
|
|
295
|
+
return self._get_value("anilist_token", "", str)
|
|
296
|
+
|
|
284
297
|
@property
|
|
285
298
|
def mal_password(self) -> str:
|
|
286
299
|
"""Your MyAnimeList password for MAL mode.
|
|
@@ -291,55 +304,55 @@ class Config:
|
|
|
291
304
|
return self._get_value("mal_password", "", str)
|
|
292
305
|
|
|
293
306
|
@property
|
|
294
|
-
def
|
|
307
|
+
def tracker_ignore_tag(self) -> str:
|
|
295
308
|
"""All anime in your MyAnimeList with this tag will be ignored by
|
|
296
309
|
anipy-cli.
|
|
297
310
|
|
|
298
311
|
Examples:
|
|
299
|
-
|
|
300
|
-
|
|
312
|
+
tracker_ignore_tag: ignore # all anime with ignore tag will be ignored
|
|
313
|
+
tracker_ignore_tag: "" # no anime will be ignored
|
|
301
314
|
"""
|
|
302
|
-
return self._get_value("
|
|
315
|
+
return self._get_value("tracker_ignore_tag", "ignore", str)
|
|
303
316
|
|
|
304
317
|
@property
|
|
305
|
-
def
|
|
306
|
-
"""All anime in your
|
|
307
|
-
dub in
|
|
308
|
-
anipy-cli will use `preferred_type` to choose dub or sub in
|
|
318
|
+
def tracker_dub_tag(self) -> str:
|
|
319
|
+
"""All anime in your Anime Tracker with this tag will be switched over to
|
|
320
|
+
dub in tracker mode, if the dub is available. If you do not specify a tag,
|
|
321
|
+
anipy-cli will use `preferred_type` to choose dub or sub in tracker mode.
|
|
309
322
|
|
|
310
323
|
Examples:
|
|
311
|
-
|
|
312
|
-
|
|
324
|
+
tracker_dub_tag: dub # all anime with this tag will be switched to dub
|
|
325
|
+
tracker_dub_tag: "" # no anime will be switched to dub, except you have preferred_type on dub
|
|
313
326
|
"""
|
|
314
|
-
return self._get_value("
|
|
327
|
+
return self._get_value("tracker_dub_tag", "dub", str)
|
|
315
328
|
|
|
316
329
|
@property
|
|
317
|
-
def
|
|
318
|
-
"""Custom tags to tag all anime in your
|
|
330
|
+
def tracker_tags(self) -> List[str]:
|
|
331
|
+
"""Custom tags to tag all anime in your Anime Tracker that are
|
|
319
332
|
altered/added by anipy-cli.
|
|
320
333
|
|
|
321
334
|
Examples:
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
335
|
+
tracker_tags: ["anipy-cli"] # tag all anime with anipy-cli
|
|
336
|
+
tracker_tags: ["anipy-cli", "important"] # tag all anime with anipy-cli and important
|
|
337
|
+
tracker_tags: null or tracker_tags: [] # Do not tag the anime
|
|
325
338
|
"""
|
|
326
|
-
return self._get_value("
|
|
339
|
+
return self._get_value("tracker_tags", [], list)
|
|
327
340
|
|
|
328
341
|
@property
|
|
329
|
-
def
|
|
330
|
-
"""Status categories of your
|
|
342
|
+
def tracker_status_categories(self) -> List[str]:
|
|
343
|
+
"""Status categories of your Anime Tracker that anipy-cli uses for
|
|
331
344
|
downloading/watching new episodes listing anime in your list and stuff
|
|
332
345
|
like that. Normally the watching catagory should be enough as you would
|
|
333
346
|
normally put anime you currently watch in the watching catagory.
|
|
334
347
|
|
|
335
348
|
Valid values are: watching, completed, on_hold, dropped, plan_to_watch
|
|
336
349
|
"""
|
|
337
|
-
return self._get_value("
|
|
350
|
+
return self._get_value("tracker_status_categories", ["watching"], list)
|
|
338
351
|
|
|
339
352
|
@property
|
|
340
|
-
def
|
|
353
|
+
def tracker_mapping_min_similarity(self) -> float:
|
|
341
354
|
"""
|
|
342
|
-
The minumum similarity between titles when mapping anime in
|
|
355
|
+
The minumum similarity between titles when mapping anime in tracker mode.
|
|
343
356
|
This is a decimal number from 0 - 1, 1 meaning 100% match and 0 meaning all characters are different.
|
|
344
357
|
If the similarity of a map is below the threshold you will be prompted for a manual map.
|
|
345
358
|
|
|
@@ -349,24 +362,24 @@ class Config:
|
|
|
349
362
|
|
|
350
363
|
If you are interested, the algorithm being used here is this: https://en.wikipedia.org/wiki/Levenshtein_distance
|
|
351
364
|
"""
|
|
352
|
-
return self._get_value("
|
|
365
|
+
return self._get_value("tracker_mapping_min_similarity", 0.8, float)
|
|
353
366
|
|
|
354
367
|
@property
|
|
355
|
-
def
|
|
368
|
+
def tracker_mapping_use_alternatives(self) -> bool:
|
|
356
369
|
"""Check alternative names when mapping anime.
|
|
357
370
|
|
|
358
371
|
If turned on this will slow down mapping but provide better
|
|
359
372
|
chances of finding a match.
|
|
360
373
|
"""
|
|
361
|
-
return self._get_value("
|
|
374
|
+
return self._get_value("tracker_mapping_use_alternatives", True, bool)
|
|
362
375
|
|
|
363
376
|
@property
|
|
364
|
-
def
|
|
377
|
+
def tracker_mapping_use_filters(self) -> bool:
|
|
365
378
|
"""Use filters (e.g. year, season etc.) of providers to narrow down the
|
|
366
379
|
results, this will lead to more accurate mapping, but provide wrong
|
|
367
380
|
results if the filters of the provider do not work properly or if anime
|
|
368
381
|
are not correctly marked with the correct data."""
|
|
369
|
-
return self._get_value("
|
|
382
|
+
return self._get_value("tracker_mapping_use_filters", True, bool)
|
|
370
383
|
|
|
371
384
|
@property
|
|
372
385
|
def auto_sync_mal_to_seasonals(self) -> bool:
|
|
@@ -412,10 +425,7 @@ class Config:
|
|
|
412
425
|
# os.path.expanduser is equivalent to Path().expanduser()
|
|
413
426
|
# But because pathlib doesn't have expandvars(), we resort
|
|
414
427
|
# to using the os module inside the Path constructor
|
|
415
|
-
|
|
416
|
-
return Path(os.path.expandvars(path)).expanduser()
|
|
417
|
-
|
|
418
|
-
return Path(path)
|
|
428
|
+
return Path(os.path.expandvars(path)).expanduser()
|
|
419
429
|
except RuntimeError:
|
|
420
430
|
return fallback
|
|
421
431
|
|
|
@@ -429,8 +439,6 @@ class Config:
|
|
|
429
439
|
def _create_config(self):
|
|
430
440
|
self._get_config_path().mkdir(exist_ok=True, parents=True)
|
|
431
441
|
self._config_file.touch()
|
|
432
|
-
|
|
433
|
-
self._EXPAND_PATHS = False
|
|
434
442
|
|
|
435
443
|
dump = ""
|
|
436
444
|
# generate config based on attrs and default values of config class
|
|
@@ -455,8 +463,7 @@ class Config:
|
|
|
455
463
|
+ yaml.dump({attribute: val}, indent=4, default_flow_style=False)
|
|
456
464
|
+ "\n"
|
|
457
465
|
)
|
|
458
|
-
|
|
459
|
-
self._EXPAND_PATHS = True
|
|
466
|
+
|
|
460
467
|
self._config_file.write_text(dump)
|
|
461
468
|
|
|
462
469
|
@staticmethod
|