anipy-cli 2.7.17__py3-none-any.whl → 3.8.2__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.
Files changed (67) hide show
  1. anipy_cli/__init__.py +2 -20
  2. anipy_cli/anilist_proxy.py +229 -0
  3. anipy_cli/arg_parser.py +109 -21
  4. anipy_cli/cli.py +98 -0
  5. anipy_cli/clis/__init__.py +17 -0
  6. anipy_cli/clis/anilist_cli.py +62 -0
  7. anipy_cli/clis/base_cli.py +34 -0
  8. anipy_cli/clis/binge_cli.py +96 -0
  9. anipy_cli/clis/default_cli.py +115 -0
  10. anipy_cli/clis/download_cli.py +85 -0
  11. anipy_cli/clis/history_cli.py +96 -0
  12. anipy_cli/clis/mal_cli.py +71 -0
  13. anipy_cli/{cli/clis → clis}/seasonal_cli.py +9 -6
  14. anipy_cli/colors.py +14 -8
  15. anipy_cli/config.py +387 -90
  16. anipy_cli/discord.py +34 -0
  17. anipy_cli/download_component.py +194 -0
  18. anipy_cli/logger.py +200 -0
  19. anipy_cli/mal_proxy.py +228 -0
  20. anipy_cli/menus/__init__.py +6 -0
  21. anipy_cli/menus/anilist_menu.py +671 -0
  22. anipy_cli/{cli/menus → menus}/base_menu.py +9 -14
  23. anipy_cli/menus/mal_menu.py +657 -0
  24. anipy_cli/menus/menu.py +265 -0
  25. anipy_cli/menus/seasonal_menu.py +270 -0
  26. anipy_cli/prompts.py +387 -0
  27. anipy_cli/util.py +268 -0
  28. anipy_cli-3.8.2.dist-info/METADATA +71 -0
  29. anipy_cli-3.8.2.dist-info/RECORD +31 -0
  30. {anipy_cli-2.7.17.dist-info → anipy_cli-3.8.2.dist-info}/WHEEL +1 -2
  31. anipy_cli-3.8.2.dist-info/entry_points.txt +3 -0
  32. anipy_cli/cli/__init__.py +0 -1
  33. anipy_cli/cli/cli.py +0 -37
  34. anipy_cli/cli/clis/__init__.py +0 -6
  35. anipy_cli/cli/clis/base_cli.py +0 -43
  36. anipy_cli/cli/clis/binge_cli.py +0 -54
  37. anipy_cli/cli/clis/default_cli.py +0 -46
  38. anipy_cli/cli/clis/download_cli.py +0 -92
  39. anipy_cli/cli/clis/history_cli.py +0 -64
  40. anipy_cli/cli/clis/mal_cli.py +0 -27
  41. anipy_cli/cli/menus/__init__.py +0 -3
  42. anipy_cli/cli/menus/mal_menu.py +0 -411
  43. anipy_cli/cli/menus/menu.py +0 -102
  44. anipy_cli/cli/menus/seasonal_menu.py +0 -174
  45. anipy_cli/cli/util.py +0 -118
  46. anipy_cli/download.py +0 -454
  47. anipy_cli/history.py +0 -83
  48. anipy_cli/mal.py +0 -645
  49. anipy_cli/misc.py +0 -227
  50. anipy_cli/player/__init__.py +0 -1
  51. anipy_cli/player/player.py +0 -33
  52. anipy_cli/player/players/__init__.py +0 -3
  53. anipy_cli/player/players/base.py +0 -106
  54. anipy_cli/player/players/mpv.py +0 -19
  55. anipy_cli/player/players/mpv_contrl.py +0 -37
  56. anipy_cli/player/players/syncplay.py +0 -19
  57. anipy_cli/player/players/vlc.py +0 -18
  58. anipy_cli/query.py +0 -92
  59. anipy_cli/run_anipy_cli.py +0 -14
  60. anipy_cli/seasonal.py +0 -106
  61. anipy_cli/url_handler.py +0 -442
  62. anipy_cli/version.py +0 -1
  63. anipy_cli-2.7.17.dist-info/LICENSE +0 -674
  64. anipy_cli-2.7.17.dist-info/METADATA +0 -159
  65. anipy_cli-2.7.17.dist-info/RECORD +0 -43
  66. anipy_cli-2.7.17.dist-info/entry_points.txt +0 -2
  67. anipy_cli-2.7.17.dist-info/top_level.txt +0 -1
@@ -0,0 +1,96 @@
1
+ import sys
2
+ from typing import TYPE_CHECKING
3
+
4
+ from anipy_api.locallist import LocalList
5
+
6
+ from anipy_cli.clis.base_cli import CliBase
7
+ from anipy_cli.colors import colors, cprint
8
+ from anipy_cli.config import Config
9
+ from anipy_cli.prompts import (
10
+ parse_seasonal_search,
11
+ pick_episode_range_prompt,
12
+ search_show_prompt,
13
+ lang_prompt,
14
+ parse_auto_search,
15
+ )
16
+ from anipy_cli.util import DotSpinner, get_configured_player, migrate_locallist, error
17
+
18
+ if TYPE_CHECKING:
19
+ from anipy_cli.arg_parser import CliArgs
20
+
21
+
22
+ class BingeCli(CliBase):
23
+ def __init__(self, options: "CliArgs"):
24
+ super().__init__(options)
25
+
26
+ self.player = get_configured_player(self.options.optional_player)
27
+ self.history_list = LocalList(
28
+ Config()._history_file_path, migrate_cb=migrate_locallist
29
+ )
30
+
31
+ self.anime = None
32
+ self.episodes = None
33
+ self.lang = None
34
+
35
+ def print_header(self):
36
+ cprint(colors.GREEN, "***Binge Mode***")
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
+
47
+ def take_input(self):
48
+ if self.options.search is not None:
49
+ self.anime, self.lang, self.episodes = parse_auto_search(
50
+ "binge", self.options.search
51
+ )
52
+ return
53
+
54
+ anime = self._get_anime_from_user()
55
+
56
+ if anime is None:
57
+ sys.exit(0)
58
+
59
+ self.lang = lang_prompt(anime)
60
+
61
+ episodes = pick_episode_range_prompt(anime, self.lang)
62
+
63
+ self.anime = anime
64
+ self.episodes = episodes
65
+
66
+ def process(self): ...
67
+
68
+ def show(self):
69
+ assert self.episodes is not None
70
+ assert self.anime is not None
71
+ assert self.lang is not None
72
+
73
+ for e in self.episodes:
74
+ with DotSpinner(
75
+ "Extracting streams for ",
76
+ colors.BLUE,
77
+ f"{self.anime.name} ({self.lang})",
78
+ colors.END,
79
+ " Episode ",
80
+ e,
81
+ "...",
82
+ ) as s:
83
+ stream = self.anime.get_video(
84
+ e, self.lang, preferred_quality=self.options.quality
85
+ )
86
+ if stream is None:
87
+ error("Could not find stream for requested episode, skipping")
88
+ continue
89
+ s.ok("✔")
90
+
91
+ self.history_list.update(self.anime, episode=e, language=self.lang)
92
+ self.player.play_title(self.anime, stream)
93
+ self.player.wait()
94
+
95
+ def post(self):
96
+ self.player.kill_player()
@@ -0,0 +1,115 @@
1
+ from typing import TYPE_CHECKING, Optional
2
+
3
+ from anipy_api.locallist import LocalList
4
+
5
+ from anipy_cli.clis.base_cli import CliBase
6
+ from anipy_cli.colors import colors
7
+ from anipy_cli.config import Config
8
+ from anipy_cli.menus import Menu
9
+ from anipy_cli.prompts import (
10
+ lang_prompt,
11
+ parse_auto_search,
12
+ parse_seasonal_search,
13
+ pick_episode_prompt,
14
+ search_show_prompt,
15
+ )
16
+ from anipy_cli.util import DotSpinner, error, get_configured_player, migrate_locallist
17
+
18
+ if TYPE_CHECKING:
19
+ from anipy_api.anime import Anime
20
+ from anipy_api.provider import Episode, LanguageTypeEnum, ProviderStream
21
+
22
+ from anipy_cli.arg_parser import CliArgs
23
+
24
+
25
+ # TODO: Add Resume feature
26
+ class DefaultCli(CliBase):
27
+ def __init__(self, options: "CliArgs"):
28
+ super().__init__(options)
29
+
30
+ self.player = get_configured_player(self.options.optional_player)
31
+ self.history_list = LocalList(
32
+ Config()._history_file_path, migrate_cb=migrate_locallist
33
+ )
34
+
35
+ self.anime: Optional["Anime"] = None
36
+ self.epsiode: Optional["Episode"] = None
37
+ self.stream: Optional["ProviderStream"] = None
38
+ self.lang: Optional["LanguageTypeEnum"] = None
39
+
40
+ def print_header(self):
41
+ pass
42
+
43
+ def _get_anime_from_user(self):
44
+ if (ss := self.options.seasonal_search) is not None:
45
+ return parse_seasonal_search(
46
+ "default",
47
+ ss,
48
+ )
49
+
50
+ return search_show_prompt("default")
51
+
52
+ def take_input(self):
53
+ if self.options.search is not None:
54
+ self.anime, self.lang, episodes = parse_auto_search(
55
+ "default", self.options.search
56
+ )
57
+ self.epsiode = episodes[0]
58
+ return
59
+
60
+ anime = self._get_anime_from_user()
61
+
62
+ if anime is None:
63
+ return False
64
+
65
+ self.lang = lang_prompt(anime)
66
+
67
+ episode = pick_episode_prompt(anime, self.lang)
68
+
69
+ if episode is None:
70
+ return False
71
+
72
+ self.anime = anime
73
+ self.epsiode = episode
74
+
75
+ def process(self):
76
+ assert self.anime is not None
77
+ assert self.epsiode is not None
78
+ assert self.lang is not None
79
+
80
+ with DotSpinner(
81
+ "Extracting streams for ",
82
+ colors.BLUE,
83
+ f"{self.anime.name} ({self.lang})",
84
+ " Episode ",
85
+ self.epsiode,
86
+ "...",
87
+ ):
88
+ self.stream = self.anime.get_video(
89
+ self.epsiode, self.lang, preferred_quality=self.options.quality
90
+ )
91
+ if not self.stream:
92
+ error(
93
+ f"Could not find any streams for {self.anime.name} ({self.lang}) Episode {self.epsiode}",
94
+ fatal=True,
95
+ )
96
+
97
+ def show(self):
98
+ assert self.anime is not None
99
+ assert self.stream is not None
100
+
101
+ self.history_list.update(
102
+ self.anime, episode=self.epsiode, language=self.stream.language
103
+ )
104
+ self.player.play_title(self.anime, self.stream)
105
+
106
+ def post(self):
107
+ assert self.anime is not None
108
+ assert self.stream is not None
109
+
110
+ Menu(
111
+ options=self.options,
112
+ anime=self.anime,
113
+ stream=self.stream,
114
+ player=self.player,
115
+ ).run()
@@ -0,0 +1,85 @@
1
+ from typing import TYPE_CHECKING, Optional, List
2
+
3
+ from anipy_cli.download_component import DownloadComponent
4
+
5
+ from anipy_cli.clis.base_cli import CliBase
6
+ from anipy_cli.colors import colors, cprint
7
+ from anipy_cli.config import Config
8
+ from anipy_cli.prompts import (
9
+ parse_seasonal_search,
10
+ pick_episode_range_prompt,
11
+ search_show_prompt,
12
+ lang_prompt,
13
+ parse_auto_search,
14
+ )
15
+
16
+ if TYPE_CHECKING:
17
+ from anipy_api.anime import Anime
18
+ from anipy_api.provider import Episode, LanguageTypeEnum
19
+
20
+ from anipy_cli.arg_parser import CliArgs
21
+
22
+
23
+ class DownloadCli(CliBase):
24
+ def __init__(self, options: "CliArgs"):
25
+ super().__init__(options)
26
+
27
+ self.anime: Optional["Anime"] = None
28
+ self.episodes: Optional[List["Episode"]] = None
29
+ self.lang: Optional["LanguageTypeEnum"] = None
30
+
31
+ self.dl_path = Config().download_folder_path
32
+ if options.location:
33
+ self.dl_path = options.location
34
+
35
+ def print_header(self):
36
+ cprint(colors.GREEN, "***Download Mode***")
37
+ cprint(colors.GREEN, "Downloads are stored in: ", colors.END, str(self.dl_path))
38
+
39
+ def _get_anime_from_user(self):
40
+ if (ss := self.options.seasonal_search) is not None:
41
+ return parse_seasonal_search(
42
+ "download",
43
+ ss,
44
+ )
45
+
46
+ return search_show_prompt("download")
47
+
48
+ def take_input(self):
49
+ if self.options.search is not None:
50
+ self.anime, self.lang, self.episodes = parse_auto_search(
51
+ "download", self.options.search
52
+ )
53
+ return
54
+
55
+ anime = self._get_anime_from_user()
56
+
57
+ if anime is None:
58
+ return False
59
+
60
+ self.lang = lang_prompt(anime)
61
+
62
+ episodes = pick_episode_range_prompt(anime, self.lang)
63
+
64
+ self.anime = anime
65
+ self.episodes = episodes
66
+
67
+ def process(self):
68
+ assert self.episodes is not None
69
+ assert self.anime is not None
70
+ assert self.lang is not None
71
+
72
+ errors = DownloadComponent(
73
+ self.options, self.dl_path, "download"
74
+ ).download_anime(
75
+ [(self.anime, self.lang, self.episodes)],
76
+ only_skip_ep_on_err=True,
77
+ sub_only=self.options.subtitles,
78
+ )
79
+ DownloadComponent.serve_download_errors(errors, only_skip_ep_on_err=True)
80
+
81
+ def show(self):
82
+ pass
83
+
84
+ def post(self):
85
+ pass
@@ -0,0 +1,96 @@
1
+ from typing import TYPE_CHECKING, Optional
2
+
3
+ from InquirerPy.base.control import Choice
4
+ from anipy_api.anime import Anime
5
+ from anipy_api.locallist import LocalList, LocalListEntry
6
+ from InquirerPy import inquirer
7
+
8
+ from anipy_cli.clis.base_cli import CliBase
9
+ from anipy_cli.colors import colors
10
+ from anipy_cli.config import Config
11
+ from anipy_cli.menus import Menu
12
+ from anipy_cli.util import DotSpinner, get_configured_player, migrate_locallist
13
+
14
+ if TYPE_CHECKING:
15
+ from anipy_api.provider import ProviderStream
16
+
17
+ from anipy_cli.arg_parser import CliArgs
18
+
19
+
20
+ class HistoryCli(CliBase):
21
+ def __init__(self, options: "CliArgs"):
22
+ super().__init__(options)
23
+
24
+ self.player = get_configured_player(self.options.optional_player)
25
+ self.history_list = LocalList(
26
+ Config()._history_file_path, migrate_cb=migrate_locallist
27
+ )
28
+
29
+ self.anime: Optional[Anime] = None
30
+ self.history_entry: Optional["LocalListEntry"] = None
31
+ self.stream: Optional["ProviderStream"] = None
32
+
33
+ def print_header(self):
34
+ pass
35
+
36
+ def take_input(self):
37
+ history = self.history_list.get_all()
38
+ history.sort(key=lambda h: h.timestamp, reverse=True)
39
+
40
+ if not history:
41
+ print("You have no History, exiting")
42
+ return False
43
+
44
+ entry = inquirer.fuzzy( # type: ignore
45
+ message="Select History Entry:",
46
+ choices=[
47
+ Choice(value=h, name=f"{n + 1}. {repr(h)}")
48
+ for n, h in enumerate(history)
49
+ ],
50
+ long_instruction="To cancel this prompt press ctrl+z",
51
+ mandatory=False,
52
+ ).execute()
53
+
54
+ if entry is None:
55
+ return False
56
+
57
+ self.history_entry = LocalListEntry.from_dict(entry)
58
+ self.anime = Anime.from_local_list_entry(self.history_entry)
59
+
60
+ def process(self):
61
+ assert self.anime is not None
62
+ assert self.history_entry is not None
63
+
64
+ with DotSpinner(
65
+ "Extracting streams for ",
66
+ colors.BLUE,
67
+ self.history_entry,
68
+ "...",
69
+ ):
70
+ self.stream = self.anime.get_video(
71
+ self.history_entry.episode,
72
+ self.history_entry.language,
73
+ preferred_quality=self.options.quality,
74
+ )
75
+
76
+ def show(self):
77
+ assert self.anime is not None
78
+ assert self.stream is not None
79
+
80
+ self.player.play_title(self.anime, self.stream)
81
+ self.history_list.update(
82
+ self.anime,
83
+ episode=self.stream.episode,
84
+ language=self.stream.language,
85
+ )
86
+
87
+ def post(self):
88
+ assert self.anime is not None
89
+ assert self.stream is not None
90
+
91
+ Menu(
92
+ options=self.options,
93
+ anime=self.anime,
94
+ stream=self.stream,
95
+ player=self.player,
96
+ ).run()
@@ -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 = self.options.mal_user or config.mal_user
29
+ self.password = self.options.mal_password or config.mal_password
30
+
31
+ if not self.user:
32
+ self.user = inquirer.text( # type: ignore
33
+ "Your MyAnimeList Username:",
34
+ validate=lambda x: len(x) > 1,
35
+ invalid_message="You must enter a username!",
36
+ long_instruction="Hint: You can save your username and password in the config!",
37
+ ).execute()
38
+
39
+ if not self.password:
40
+ self.password = inquirer.secret( # type: ignore
41
+ "Your MyAnimeList Password:",
42
+ transformer=lambda _: "[hidden]",
43
+ validate=lambda x: len(x) > 1,
44
+ invalid_message="You must enter a password!",
45
+ long_instruction="Hint: You can also pass the password via the `--mal-password` option!",
46
+ ).execute()
47
+
48
+ def process(self):
49
+ try:
50
+ with DotSpinner("Logging into MyAnimeList..."):
51
+ self.mal = MyAnimeList.from_password_grant(self.user, self.password)
52
+ except MyAnimeListError as e:
53
+ error(
54
+ f"{str(e)}\nCannot login to MyAnimeList, it is likely your credentials are wrong",
55
+ fatal=True,
56
+ )
57
+
58
+ def show(self):
59
+ pass
60
+
61
+ def post(self):
62
+ assert self.mal is not None
63
+
64
+ menu = MALMenu(mal=self.mal, options=self.options)
65
+
66
+ if self.options.auto_update:
67
+ menu.download()
68
+ elif self.options.mal_sync_seasonals:
69
+ menu.sync_mal_seasonls()
70
+ else:
71
+ menu.run()
@@ -1,11 +1,14 @@
1
- from anipy_cli.arg_parser import CliArgs
2
- from anipy_cli.cli.menus import SeasonalMenu
3
- from anipy_cli.cli.clis.base_cli import CliBase
1
+ from typing import TYPE_CHECKING
2
+ from anipy_cli.menus import SeasonalMenu
3
+ from anipy_cli.clis.base_cli import CliBase
4
+
5
+ if TYPE_CHECKING:
6
+ from anipy_cli.arg_parser import CliArgs
4
7
 
5
8
 
6
9
  class SeasonalCli(CliBase):
7
- def __init__(self, options: CliArgs, rpc_client=None):
8
- super().__init__(options, rpc_client)
10
+ def __init__(self, options: "CliArgs"):
11
+ super().__init__(options)
9
12
 
10
13
  def print_header(self):
11
14
  pass
@@ -20,7 +23,7 @@ class SeasonalCli(CliBase):
20
23
  pass
21
24
 
22
25
  def post(self):
23
- menu = SeasonalMenu(self.options, self.rpc_client)
26
+ menu = SeasonalMenu(self.options)
24
27
 
25
28
  if self.options.auto_update:
26
29
  menu.download_latest()
anipy_cli/colors.py CHANGED
@@ -1,7 +1,8 @@
1
- class colors:
2
- """
3
- Just a class for colors
4
- """
1
+ from typing import Any
2
+
3
+
4
+ class colors: # noqa: N801
5
+ """Just a class for colors."""
5
6
 
6
7
  GREEN = "\033[92m"
7
8
  ERROR = "\033[93m"
@@ -11,23 +12,28 @@ class colors:
11
12
  CYAN = "\u001b[36m"
12
13
  RED = "\u001b[31m"
13
14
  END = "\x1b[0m"
15
+ BOLD = "\033[1m"
16
+ UNDERLINE = "\033[4m"
17
+ RESET = "\033[0m"
14
18
 
15
19
 
16
- def color(*values, sep: str = "") -> str:
20
+ def color(*values: Any, sep: str = "") -> str:
17
21
  """Decorate a string with color codes.
22
+
18
23
  Basically just ensures that the color doesn't "leak"
19
24
  from the text.
20
- format: color(color1, text1, color2, text2...)"""
25
+ format: color(color1, text1, color2, text2...)
26
+ """
21
27
  return sep.join(map(str, values)) + colors.END
22
28
 
23
29
 
24
- def cinput(*prompt, input_color: str = "") -> str:
30
+ def cinput(*prompt: Any, input_color: str = "") -> str:
25
31
  """An input function that handles coloring input."""
26
32
  inp = input(color(*prompt) + input_color)
27
33
  print(colors.END, end="")
28
34
  return inp
29
35
 
30
36
 
31
- def cprint(*values, sep: str = "", **kwargs) -> None:
37
+ def cprint(*values: Any, sep: str = "", **kwargs: Any) -> None:
32
38
  """Prints colored text."""
33
39
  print(color(*values, sep=sep), **kwargs)