anipy-cli 3.1.9__py3-none-any.whl → 3.1.10__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 CHANGED
@@ -1,2 +1,2 @@
1
1
  __appname__ = "anipy-cli"
2
- __version__ = "3.1.9"
2
+ __version__ = "3.1.10"
anipy_cli/arg_parser.py CHANGED
@@ -23,6 +23,7 @@ class CliArgs:
23
23
  location: Optional[Path]
24
24
  mal_password: Optional[str]
25
25
  config: bool
26
+ seasonal_search: Optional[str]
26
27
 
27
28
 
28
29
  def parse_args(override_args: Optional[list[str]] = None) -> CliArgs:
@@ -107,6 +108,17 @@ def parse_args(override_args: Optional[list[str]] = None) -> CliArgs:
107
108
  help="Provide a search term to Default, Download or Binge mode in this format: {query}:{episode range}:{dub/sub}. Examples: 'frieren:1-10:sub' or 'frieren:1:sub' or 'frieren:1-3 7-12:dub', this argument may be appended to any of the modes mentioned like so: 'anipy-cli (-D/B) -s <search>'",
108
109
  )
109
110
 
111
+ options_group.add_argument(
112
+ "-ss",
113
+ "--seasonal-search",
114
+ required=False,
115
+ dest="seasonal_search",
116
+ nargs="?", # 1 or none possible args
117
+ default=None, # Used if flag is not present (added this line for clarity, because default is always None)
118
+ const=True, # Used if flag is present, but no value
119
+ help="Provide search parameters for seasons to Default, Download, or Binge mode in this format: {year}:{season}. You can only use part of the season name if you wish. Examples: '2024:win' or '2020:fa'",
120
+ )
121
+
110
122
  options_group.add_argument(
111
123
  "-q",
112
124
  "--quality",
@@ -7,6 +7,7 @@ from anipy_cli.clis.base_cli import CliBase
7
7
  from anipy_cli.colors import colors, cprint
8
8
  from anipy_cli.config import Config
9
9
  from anipy_cli.prompts import (
10
+ parse_seasonal_search,
10
11
  pick_episode_range_prompt,
11
12
  search_show_prompt,
12
13
  lang_prompt,
@@ -34,6 +35,15 @@ class BingeCli(CliBase):
34
35
  def print_header(self):
35
36
  cprint(colors.GREEN, "***Binge Mode***")
36
37
 
38
+ def _get_anime_from_user(self):
39
+ if (ss := self.options.seasonal_search) is not None:
40
+ return parse_seasonal_search(
41
+ "binge",
42
+ ss,
43
+ )
44
+
45
+ return search_show_prompt("binge")
46
+
37
47
  def take_input(self):
38
48
  if self.options.search is not None:
39
49
  self.anime, self.lang, self.episodes = parse_auto_search(
@@ -41,7 +51,7 @@ class BingeCli(CliBase):
41
51
  )
42
52
  return
43
53
 
44
- anime = search_show_prompt("binge")
54
+ anime = self._get_anime_from_user()
45
55
 
46
56
  if anime is None:
47
57
  sys.exit(0)
@@ -11,6 +11,7 @@ from anipy_cli.prompts import (
11
11
  search_show_prompt,
12
12
  lang_prompt,
13
13
  parse_auto_search,
14
+ parse_seasonal_search,
14
15
  )
15
16
  from anipy_cli.util import (
16
17
  DotSpinner,
@@ -42,6 +43,15 @@ class DefaultCli(CliBase):
42
43
  def print_header(self):
43
44
  pass
44
45
 
46
+ def _get_anime_from_user(self):
47
+ if (ss := self.options.seasonal_search) is not None:
48
+ return parse_seasonal_search(
49
+ "default",
50
+ ss,
51
+ )
52
+
53
+ return search_show_prompt("default")
54
+
45
55
  def take_input(self):
46
56
  if self.options.search is not None:
47
57
  self.anime, self.lang, episodes = parse_auto_search(
@@ -50,7 +60,7 @@ class DefaultCli(CliBase):
50
60
  self.epsiode = episodes[0]
51
61
  return
52
62
 
53
- anime = search_show_prompt("default")
63
+ anime = self._get_anime_from_user()
54
64
 
55
65
  if anime is None:
56
66
  return False
@@ -6,6 +6,7 @@ from anipy_cli.clis.base_cli import CliBase
6
6
  from anipy_cli.colors import colors, cprint
7
7
  from anipy_cli.config import Config
8
8
  from anipy_cli.prompts import (
9
+ parse_seasonal_search,
9
10
  pick_episode_range_prompt,
10
11
  search_show_prompt,
11
12
  lang_prompt,
@@ -39,6 +40,15 @@ class DownloadCli(CliBase):
39
40
  cprint(colors.GREEN, "***Download Mode***")
40
41
  cprint(colors.GREEN, "Downloads are stored in: ", colors.END, str(self.dl_path))
41
42
 
43
+ def _get_anime_from_user(self):
44
+ if (ss := self.options.seasonal_search) is not None:
45
+ return parse_seasonal_search(
46
+ "download",
47
+ ss,
48
+ )
49
+
50
+ return search_show_prompt("download")
51
+
42
52
  def take_input(self):
43
53
  if self.options.search is not None:
44
54
  self.anime, self.lang, self.episodes = parse_auto_search(
@@ -46,7 +56,7 @@ class DownloadCli(CliBase):
46
56
  )
47
57
  return
48
58
 
49
- anime = search_show_prompt("download")
59
+ anime = self._get_anime_from_user()
50
60
 
51
61
  if anime is None:
52
62
  return False
anipy_cli/config.py CHANGED
@@ -358,6 +358,12 @@ class Config:
358
358
  """If this is set to true you will not be prompted to search in season."""
359
359
  return self._get_value("skip_season_search", False, bool)
360
360
 
361
+ @property
362
+ def assume_season_search(self) -> bool:
363
+ """If this is set to true, the system will assume you want to search in season.
364
+ If skip_season_search is true, this will be ignored)"""
365
+ return self._get_value("assume_season_search", False, bool)
366
+
361
367
  def _get_path_value(self, key: str, fallback: Path) -> Path:
362
368
  path = self._get_value(key, fallback, str)
363
369
  try:
@@ -385,21 +391,23 @@ class Config:
385
391
  if attribute.startswith("_"):
386
392
  continue
387
393
 
388
- if isinstance(value, property):
389
- doc = inspect.getdoc(value)
390
- if doc:
391
- # Add docstrings
392
- doc = Template(doc).safe_substitute(version=__version__)
393
- doc = "\n".join([f"# {line}" for line in doc.split("\n")])
394
- dump = dump + doc + "\n"
395
-
396
- val = self.__getattribute__(attribute)
397
- val = str(val) if isinstance(val, Path) else val
398
- dump = (
399
- dump
400
- + yaml.dump({attribute: val}, indent=4, default_flow_style=False)
401
- + "\n"
402
- )
394
+ if not isinstance(value, property):
395
+ continue
396
+
397
+ doc = inspect.getdoc(value)
398
+ if doc:
399
+ # Add docstrings
400
+ doc = Template(doc).safe_substitute(version=__version__)
401
+ doc = "\n".join([f"# {line}" for line in doc.split("\n")])
402
+ dump = dump + doc + "\n"
403
+
404
+ val = self.__getattribute__(attribute)
405
+ val = str(val) if isinstance(val, Path) else val
406
+ dump = (
407
+ dump
408
+ + yaml.dump({attribute: val}, indent=4, default_flow_style=False)
409
+ + "\n"
410
+ )
403
411
 
404
412
  self._config_file.write_text(dump)
405
413
 
anipy_cli/prompts.py CHANGED
@@ -17,6 +17,7 @@ from anipy_cli.util import (
17
17
  get_prefered_providers,
18
18
  error,
19
19
  parse_episode_ranges,
20
+ convert_letter_to_season,
20
21
  )
21
22
  from anipy_cli.colors import colors
22
23
  from anipy_cli.config import Config
@@ -30,22 +31,9 @@ def search_show_prompt(
30
31
  mode: str, skip_season_search: bool = False
31
32
  ) -> Optional["Anime"]:
32
33
  if not (Config().skip_season_search or skip_season_search):
33
- season_provider = None
34
- for p in get_prefered_providers(mode):
35
- if p.FILTER_CAPS & (
36
- FilterCapabilities.SEASON
37
- | FilterCapabilities.YEAR
38
- | FilterCapabilities.NO_QUERY
39
- ):
40
- season_provider = p
41
- if season_provider is not None:
42
- should_search = inquirer.confirm("Do you want to search in season?", default=False).execute() # type: ignore
43
- if not should_search:
44
- print(
45
- "Hint: you can set `skip_season_search` to `true` in the config to skip this prompt!"
46
- )
47
- else:
48
- return season_search_prompt(season_provider)
34
+ anime = season_search_pre_prompt(mode)
35
+ if anime is not None:
36
+ return anime
49
37
 
50
38
  query = inquirer.text( # type: ignore
51
39
  "Search Anime:",
@@ -88,41 +76,86 @@ def search_show_prompt(
88
76
  return anime
89
77
 
90
78
 
91
- def season_search_prompt(provider: "BaseProvider") -> Optional["Anime"]:
92
- year = inquirer.number( # type: ignore
93
- message="Enter year:",
94
- long_instruction="To skip this prompt press ctrl+z",
95
- default=time.localtime().tm_year,
96
- mandatory=False,
97
- ).execute()
79
+ def _get_season_provider(mode: str) -> Optional["BaseProvider"]:
80
+ season_provider = None
81
+ for p in get_prefered_providers(mode):
82
+ if p.FILTER_CAPS & (
83
+ FilterCapabilities.SEASON
84
+ | FilterCapabilities.YEAR
85
+ | FilterCapabilities.NO_QUERY
86
+ ):
87
+ season_provider = p
88
+ return season_provider
89
+
90
+
91
+ def season_search_pre_prompt(
92
+ mode: str, year: Optional[int] = None, season: Optional[str] = None
93
+ ) -> Optional["Anime"]:
94
+ season_provider = _get_season_provider(mode)
95
+ assume_season_search = Config().assume_season_search
96
+
97
+ # If there is no proper season provider
98
+ if season_provider is None:
99
+ if not assume_season_search:
100
+ return
101
+ # If assume search was on, and there is no proper season provider
102
+ print(
103
+ f"`assume_season_search` was set to true, but the providers ({', '.join(Config().providers[mode])}) you have selected do not have seasonal capabilities"
104
+ )
105
+ return
106
+
107
+ if assume_season_search or (year and season):
108
+ return season_search_prompt(season_provider, year, season)
109
+
110
+ should_search = inquirer.confirm("Do you want to search in season?", default=False).execute() # type: ignore
111
+
112
+ if should_search:
113
+ return season_search_prompt(season_provider)
114
+
115
+ print(
116
+ "Hint: you can set `skip_season_search` to `true` in the config to skip this prompt!"
117
+ )
118
+
119
+
120
+ def season_search_prompt(
121
+ provider: "BaseProvider", year: Optional[int] = None, season: Optional[str] = None
122
+ ) -> Optional["Anime"]:
123
+ if year is None:
124
+ curr_year = time.localtime().tm_year
125
+ year = inquirer.number( # type: ignore
126
+ message="Enter year:",
127
+ long_instruction="To skip this prompt press ctrl+z",
128
+ default=curr_year,
129
+ mandatory=False,
130
+ ).execute()
98
131
 
99
132
  if year is None:
100
133
  return
101
134
 
102
- season = inquirer.select( # type: ignore
103
- message="Select Season:",
104
- choices=["Winter", "Spring", "Summer", "Fall"],
105
- instruction="The season selected by default is the current season.",
106
- long_instruction="To skip this prompt press ctrl+z",
107
- default=get_anime_season(time.localtime().tm_mon),
108
- mandatory=False,
109
- ).execute()
135
+ if season is None:
136
+ season = inquirer.select( # type: ignore
137
+ message="Select Season:",
138
+ choices=["Winter", "Spring", "Summer", "Fall"],
139
+ instruction="The season selected by default is the current season.",
140
+ long_instruction="To skip this prompt press ctrl+z",
141
+ default=get_anime_season(time.localtime().tm_mon),
142
+ mandatory=False,
143
+ ).execute()
110
144
 
111
145
  if season is None:
112
146
  return
113
147
 
114
- season = Season[season.upper()]
148
+ discovered_anime = get_anime_by_season(provider, year, Season[season.upper()])
115
149
 
116
- filters = Filters(year=year, season=season)
117
- results = [
118
- Anime.from_search_result(provider, r)
119
- for r in provider.get_search(query="", filters=filters)
120
- ]
150
+ if not discovered_anime:
151
+ error(f"No anime found in {season} {year}")
152
+ return
121
153
 
122
154
  anime = inquirer.fuzzy( # type: ignore
123
155
  message="Select Show:",
124
156
  choices=[
125
- Choice(value=r, name=f"{n + 1}. {repr(r)}") for n, r in enumerate(results)
157
+ Choice(value=r, name=f"{n + 1}. {repr(r)}")
158
+ for n, r in enumerate(discovered_anime)
126
159
  ],
127
160
  long_instruction=(
128
161
  "\nS = Anime is available in sub\n"
@@ -136,6 +169,17 @@ def season_search_prompt(provider: "BaseProvider") -> Optional["Anime"]:
136
169
  return anime
137
170
 
138
171
 
172
+ def get_anime_by_season(provider: "BaseProvider", year: int, season: Season):
173
+ with DotSpinner(
174
+ "Retrieving anime in ", colors.BLUE, f"{season.name} {year}", "..."
175
+ ):
176
+ filters = Filters(year=year, season=season)
177
+ return [
178
+ Anime.from_search_result(provider, r)
179
+ for r in provider.get_search(query="", filters=filters)
180
+ ]
181
+
182
+
139
183
  def pick_episode_prompt(
140
184
  anime: "Anime", lang: LanguageTypeEnum, instruction: str = ""
141
185
  ) -> Optional["Episode"]:
@@ -203,6 +247,47 @@ def lang_prompt(anime: "Anime") -> LanguageTypeEnum:
203
247
  return next(iter(anime.languages))
204
248
 
205
249
 
250
+ def parse_seasonal_search(mode: str, passed: str | bool) -> Optional["Anime"]:
251
+ """
252
+ Takes the mode we are in, as well as the search parameters.
253
+ Asks the user to choose an anime.
254
+
255
+ `Mode`: The provider to use.
256
+ `Passed`: The search terms passed (ex: `year:season`) or True,
257
+ if True we'll ask the user for this information
258
+ """
259
+ if isinstance(passed, bool):
260
+ if not passed:
261
+ return
262
+
263
+ provider = _get_season_provider(mode)
264
+
265
+ if not provider:
266
+ error(
267
+ "No valid provider was found for season search in the current mode",
268
+ fatal=True,
269
+ )
270
+
271
+ return season_search_prompt(provider)
272
+
273
+ options = iter(passed.split(":"))
274
+ year = next(options, None)
275
+ season = next(options, None)
276
+
277
+ if (not year) or not year.isnumeric():
278
+ error("A year was either not provided, or was not a number", fatal=True)
279
+
280
+ if not season:
281
+ error("A season was not provided", fatal=True)
282
+
283
+ season = convert_letter_to_season(season)
284
+
285
+ if not season:
286
+ error("The given season was not a valid season", fatal=True)
287
+
288
+ return season_search_pre_prompt(mode, int(year), season)
289
+
290
+
206
291
  def parse_auto_search(
207
292
  mode: str, passed: str
208
293
  ) -> Tuple["Anime", LanguageTypeEnum, List["Episode"]]:
anipy_cli/util.py CHANGED
@@ -182,6 +182,25 @@ def get_anime_season(month):
182
182
  return "Fall"
183
183
 
184
184
 
185
+ def convert_letter_to_season(letter: str) -> Optional[str]:
186
+ """
187
+ Converts the beginning of the name of a season to that season name.
188
+
189
+ Ex:
190
+ ```
191
+ win -> Winter
192
+ su -> Summer
193
+ sp -> Spring
194
+ ```
195
+
196
+ Returns None if the letter does not correspond to a season
197
+ """
198
+ for season in ["Spring", "Summer", "Fall", "Winter"]:
199
+ if season.startswith(letter.capitalize()):
200
+ return season
201
+ return
202
+
203
+
185
204
  def migrate_locallist(file: Path) -> LocalListData:
186
205
  import json
187
206
  import re
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: anipy-cli
3
- Version: 3.1.9
3
+ Version: 3.1.10
4
4
  Summary: Watch and Download anime from the comfort of your Terminal
5
5
  Home-page: https://sdaqo.github.io/anipy-cli
6
6
  License: GPL-3.0
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3.9
14
14
  Classifier: Programming Language :: Python :: 3.10
15
15
  Classifier: Programming Language :: Python :: 3.11
16
16
  Classifier: Programming Language :: Python :: 3.12
17
- Requires-Dist: anipy-api (>=3.1.9,<4.0.0)
17
+ Requires-Dist: anipy-api (>=3.1.10,<4.0.0)
18
18
  Requires-Dist: appdirs (>=1.4.4,<2.0.0)
19
19
  Requires-Dist: inquirerpy (>=0.3.4,<0.4.0)
20
20
  Requires-Dist: pypresence (>=4.3.0,<5.0.0)
@@ -1,16 +1,16 @@
1
- anipy_cli/__init__.py,sha256=VRAJFmcL_vqPQ-4cWly3a0ofKrKu8aVStq-AU9BG49k,48
2
- anipy_cli/arg_parser.py,sha256=6MdLjJANPh5u8QBvrmvsa1LREFT2yWY7BidhTETQep0,5402
1
+ anipy_cli/__init__.py,sha256=40gJ2USi07bG9RFib1WnzE0QeSpDq-G7wK1iRR35250,49
2
+ anipy_cli/arg_parser.py,sha256=Y_p5Sh7LR0yeqfhFVXkyAbZMjDR1-NIoY_TVuPwUnwk,6009
3
3
  anipy_cli/cli.py,sha256=Ah5BE-u_SjcuzU5dQ5cSHtUloVRApCWwTK5i49yQtH8,1922
4
4
  anipy_cli/clis/__init__.py,sha256=Y00uiPWiMvvRImxJMvfLA55BOkMUOrrx5vJUNvquNsY,411
5
5
  anipy_cli/clis/base_cli.py,sha256=xPr_J8hKs7LkDLvmK6zyL1ZTZRpyC2IuFss8KsaDstU,817
6
- anipy_cli/clis/binge_cli.py,sha256=eSvdOo_BRSb8-Xp48ZDcelqUBlDPdyQqu8OV0vc4szo,2281
7
- anipy_cli/clis/default_cli.py,sha256=yuHyDqTgnq84I522mUkY1An5c114qqIYW-Xbm1SsDl4,2806
8
- anipy_cli/clis/download_cli.py,sha256=y_8Txs6SSi60BvPCUcKDZg6zlN-w-eatM_O5ld0pzcQ,3187
6
+ anipy_cli/clis/binge_cli.py,sha256=ioZ-V0WfGYBqETFkd8epGrT9dPHwsRJ1qvIdqf4waIs,2551
7
+ anipy_cli/clis/default_cli.py,sha256=aJrJwtwdD7l-Z3dMjSHlvMvgTVnwA3_OXwS-9DZQIy8,3078
8
+ anipy_cli/clis/download_cli.py,sha256=XO6_B2Sm2P_u34MOvvXungFZWBDPGaO937GxNidpvdE,3460
9
9
  anipy_cli/clis/history_cli.py,sha256=2ccv6BpQQpUhY4K-KM7lO9qxVLXBrmCY5lec6czipSE,2863
10
10
  anipy_cli/clis/mal_cli.py,sha256=_tSLgDUOa6GOZNyCncSSzaVj088y5GAKkHVRSndLLxk,2258
11
11
  anipy_cli/clis/seasonal_cli.py,sha256=GV2TQNm9UotG1cxfYbrFFgg7Jmy8SFa7w_GlFtPdRVE,616
12
12
  anipy_cli/colors.py,sha256=voXC7z1Fs9tHg4zzNTNMIrt9k-EVgJ3_xEf5KiW2xgo,916
13
- anipy_cli/config.py,sha256=RFYY388Hn62FxAem2X0E9vMVbO63nr4LB7481ZgfwFg,15698
13
+ anipy_cli/config.py,sha256=R26KvF-mUDQ9uTqViHJn1gsrCk9TCj2ndVXW9jmQH9c,15955
14
14
  anipy_cli/discord.py,sha256=c6mdqnEdblzZBYs3cGP66oDeS4ySm59OfTRP-R-Duls,1160
15
15
  anipy_cli/mal_proxy.py,sha256=wIsku2_dl8vKD2K99L63OlzA3L5fl-VmyeiXC9VrxI4,6981
16
16
  anipy_cli/menus/__init__.py,sha256=aIzbphxAW-QGfZwR1DIegFZuTJp1O3tSUnai0f0f4lY,185
@@ -18,9 +18,9 @@ anipy_cli/menus/base_menu.py,sha256=g5b9Z7SpvCxcq_vqObcPzxLwcXeGPltLgSwa0sEzyfk,
18
18
  anipy_cli/menus/mal_menu.py,sha256=tJYq5J3k89_0BKFiWavn9Gqh5Z7uXtoUFqJaa3fT4o4,24105
19
19
  anipy_cli/menus/menu.py,sha256=UQJ1hpyDT0i03ecIjBbFRp4PFh6FTNHDhSwSBSAhQEI,7860
20
20
  anipy_cli/menus/seasonal_menu.py,sha256=Y64dRs554n-O51L5Cf-Jwtmdt2w1MM2XAErqBwsNgDM,9176
21
- anipy_cli/prompts.py,sha256=seNeErTP1om1wpRUN-rbUUI8bAGRmu48ScQjztzKkeE,7564
22
- anipy_cli/util.py,sha256=-WXc26vsdjuBsDaKAxezhBjupOdatAdu3MeRDRuTW64,7585
23
- anipy_cli-3.1.9.dist-info/METADATA,sha256=qcLQSqxDcjkftCNmp8d6ErviH2DWHXP0EXbFOglF5vY,3418
24
- anipy_cli-3.1.9.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
25
- anipy_cli-3.1.9.dist-info/entry_points.txt,sha256=86iXpcm_ECFndrt0JAI2mqYfXC2Ar7mGi0iOaxCrNP0,51
26
- anipy_cli-3.1.9.dist-info/RECORD,,
21
+ anipy_cli/prompts.py,sha256=lQDrb8IlhsQhOpLG4sflamprTSn-PlCy41tGpXIowdo,10156
22
+ anipy_cli/util.py,sha256=YiiWaX8O-yxTT53kBA1wlTJesLudjeto6E3ZdtDMgAw,8022
23
+ anipy_cli-3.1.10.dist-info/METADATA,sha256=O4nJywIMGn8nE3Tq4wQbZJ1_4chx-rpxonPX7rn21KE,3420
24
+ anipy_cli-3.1.10.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
25
+ anipy_cli-3.1.10.dist-info/entry_points.txt,sha256=86iXpcm_ECFndrt0JAI2mqYfXC2Ar7mGi0iOaxCrNP0,51
26
+ anipy_cli-3.1.10.dist-info/RECORD,,