anipy-cli 2.7.30__py3-none-any.whl → 3.0.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.

Files changed (62) hide show
  1. anipy_cli/__init__.py +2 -20
  2. anipy_cli/arg_parser.py +30 -20
  3. anipy_cli/cli.py +66 -0
  4. anipy_cli/clis/__init__.py +15 -0
  5. anipy_cli/clis/base_cli.py +32 -0
  6. anipy_cli/clis/binge_cli.py +83 -0
  7. anipy_cli/clis/default_cli.py +104 -0
  8. anipy_cli/clis/download_cli.py +111 -0
  9. anipy_cli/clis/history_cli.py +93 -0
  10. anipy_cli/clis/mal_cli.py +71 -0
  11. anipy_cli/{cli/clis → clis}/seasonal_cli.py +9 -6
  12. anipy_cli/colors.py +4 -4
  13. anipy_cli/config.py +308 -87
  14. anipy_cli/discord.py +34 -0
  15. anipy_cli/mal_proxy.py +216 -0
  16. anipy_cli/menus/__init__.py +5 -0
  17. anipy_cli/{cli/menus → menus}/base_menu.py +8 -12
  18. anipy_cli/menus/mal_menu.py +660 -0
  19. anipy_cli/menus/menu.py +194 -0
  20. anipy_cli/menus/seasonal_menu.py +263 -0
  21. anipy_cli/prompts.py +231 -0
  22. anipy_cli/util.py +262 -0
  23. anipy_cli-3.0.0.dist-info/METADATA +67 -0
  24. anipy_cli-3.0.0.dist-info/RECORD +26 -0
  25. {anipy_cli-2.7.30.dist-info → anipy_cli-3.0.0.dist-info}/WHEEL +1 -2
  26. anipy_cli-3.0.0.dist-info/entry_points.txt +3 -0
  27. anipy_cli/cli/__init__.py +0 -1
  28. anipy_cli/cli/cli.py +0 -37
  29. anipy_cli/cli/clis/__init__.py +0 -6
  30. anipy_cli/cli/clis/base_cli.py +0 -43
  31. anipy_cli/cli/clis/binge_cli.py +0 -54
  32. anipy_cli/cli/clis/default_cli.py +0 -46
  33. anipy_cli/cli/clis/download_cli.py +0 -92
  34. anipy_cli/cli/clis/history_cli.py +0 -64
  35. anipy_cli/cli/clis/mal_cli.py +0 -27
  36. anipy_cli/cli/menus/__init__.py +0 -3
  37. anipy_cli/cli/menus/mal_menu.py +0 -411
  38. anipy_cli/cli/menus/menu.py +0 -108
  39. anipy_cli/cli/menus/seasonal_menu.py +0 -177
  40. anipy_cli/cli/util.py +0 -125
  41. anipy_cli/download.py +0 -467
  42. anipy_cli/history.py +0 -83
  43. anipy_cli/mal.py +0 -651
  44. anipy_cli/misc.py +0 -227
  45. anipy_cli/player/__init__.py +0 -1
  46. anipy_cli/player/player.py +0 -36
  47. anipy_cli/player/players/__init__.py +0 -3
  48. anipy_cli/player/players/base.py +0 -107
  49. anipy_cli/player/players/mpv.py +0 -19
  50. anipy_cli/player/players/mpv_contrl.py +0 -37
  51. anipy_cli/player/players/syncplay.py +0 -19
  52. anipy_cli/player/players/vlc.py +0 -18
  53. anipy_cli/query.py +0 -100
  54. anipy_cli/run_anipy_cli.py +0 -14
  55. anipy_cli/seasonal.py +0 -112
  56. anipy_cli/url_handler.py +0 -470
  57. anipy_cli/version.py +0 -1
  58. anipy_cli-2.7.30.dist-info/LICENSE +0 -674
  59. anipy_cli-2.7.30.dist-info/METADATA +0 -162
  60. anipy_cli-2.7.30.dist-info/RECORD +0 -43
  61. anipy_cli-2.7.30.dist-info/entry_points.txt +0 -2
  62. anipy_cli-2.7.30.dist-info/top_level.txt +0 -1
anipy_cli/__init__.py CHANGED
@@ -1,20 +1,2 @@
1
- """
2
- █████  ███  ██ ██ ██████  ██  ██  ██████ ██  ██ 
3
- ██   ██ ████  ██ ██ ██   ██  ██  ██   ██      ██  ██ 
4
- ███████ ██ ██  ██ ██ ██████    ████  █████ ██  ██  ██ 
5
- ██   ██ ██  ██ ██ ██ ██       ██         ██  ██  ██ 
6
- ██  ██ ██   ████ ██ ██  ██   ██████ ███████ ██ 
7
-
8
- ~ The best tool to watch and Download your favourite anime.
9
-
10
- https://github.com/sdaqo/anipy-cli
11
-
12
- """
13
- from anipy_cli.download import download
14
- from anipy_cli.url_handler import epHandler, videourl
15
- from anipy_cli import config
16
- from anipy_cli.query import query
17
- from anipy_cli.player import get_player
18
- from anipy_cli.misc import Entry, get_anime_info
19
- from anipy_cli.seasonal import Seasonal
20
- from anipy_cli.history import history
1
+ __appname__ = "anipy-cli"
2
+ __version__ = "3.0.0"
anipy_cli/arg_parser.py CHANGED
@@ -1,8 +1,9 @@
1
1
  import argparse
2
- from pathlib import Path
3
2
  from dataclasses import dataclass
4
- from typing import Union, Optional
5
- from anipy_cli.version import __version__
3
+ from pathlib import Path
4
+ from typing import Optional, Union
5
+
6
+ from anipy_cli import __version__
6
7
 
7
8
 
8
9
  @dataclass(frozen=True)
@@ -15,17 +16,18 @@ class CliArgs:
15
16
  delete: bool
16
17
  quality: Optional[Union[str, int]]
17
18
  ffmpeg: bool
18
- no_season_search: bool
19
19
  auto_update: bool
20
+ mal_sync_seasonals: bool
20
21
  optional_player: Optional[str]
22
+ search: Optional[str]
21
23
  location: Optional[Path]
22
24
  mal_password: Optional[str]
23
25
  config: bool
24
26
 
25
27
 
26
- def parse_args() -> CliArgs:
28
+ def parse_args(override_args: Optional[list[str]] = None) -> CliArgs:
27
29
  parser = argparse.ArgumentParser(
28
- description="Play Animes from gogoanime in local video-player or Download them.",
30
+ description="Play Animes from online anime providers locally or download them, and much more.",
29
31
  add_help=False,
30
32
  )
31
33
 
@@ -96,12 +98,22 @@ def parse_args() -> CliArgs:
96
98
  help="Delete your History.",
97
99
  )
98
100
 
101
+ options_group.add_argument(
102
+ "-s",
103
+ "--search",
104
+ required=False,
105
+ dest="search",
106
+ action="store",
107
+ help="Provide a search term to the 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'",
108
+ )
109
+
99
110
  options_group.add_argument(
100
111
  "-q",
101
112
  "--quality",
102
113
  action="store",
103
114
  required=False,
104
- default="auto",
115
+ default="best",
116
+ type=lambda v: int(v) if v.isdigit() else v,
105
117
  help="Change the quality of the video, accepts: best, worst or 360, 480, 720 etc. Default: best",
106
118
  )
107
119
 
@@ -114,30 +126,20 @@ def parse_args() -> CliArgs:
114
126
  help="Use ffmpeg to download m3u8 playlists, may be more stable but is way slower than internal downloader",
115
127
  )
116
128
 
117
- options_group.add_argument(
118
- "-o",
119
- "--no-seas-search",
120
- required=False,
121
- dest="no_season_search",
122
- action="store_true",
123
- help="Turn off search in season. "
124
- "Disables prompting if GoGoAnime is to be searched for anime in specific season.",
125
- )
126
-
127
129
  options_group.add_argument(
128
130
  "-a",
129
131
  "--auto-update",
130
132
  required=False,
131
133
  dest="auto_update",
132
134
  action="store_true",
133
- help="Automatically update and download all Anime in seasonals list from start EP to newest.",
135
+ help="Automatically update and download all Anime in seasonals or mal mode from start EP to newest.",
134
136
  )
135
137
 
136
138
  options_group.add_argument(
137
139
  "-p",
138
140
  "--optional-player",
139
141
  required=False,
140
- choices=["mpv", "vlc", "syncplay", "mpvnet"],
142
+ choices=["mpv", "vlc", "syncplay", "mpvnet", "mpv-controlled"],
141
143
  help="Override the player set in the config.",
142
144
  )
143
145
 
@@ -160,6 +162,14 @@ def parse_args() -> CliArgs:
160
162
  help="Provide password for MAL login (overrides password set in config)",
161
163
  )
162
164
 
165
+ options_group.add_argument(
166
+ "--mal-sync-to-seasonals",
167
+ required=False,
168
+ dest="mal_sync_seasonals",
169
+ action="store_true",
170
+ help="Automatically sync myanimelist to seasonals (only works with `-M`)",
171
+ )
172
+
163
173
  info_group.add_argument(
164
174
  "-h", "--help", action="help", help="show this help message and exit"
165
175
  )
@@ -174,4 +184,4 @@ def parse_args() -> CliArgs:
174
184
  help="Print path to the config file.",
175
185
  )
176
186
 
177
- return CliArgs(**vars(parser.parse_args()))
187
+ return CliArgs(**vars(parser.parse_args(args=override_args)))
anipy_cli/cli.py ADDED
@@ -0,0 +1,66 @@
1
+ from typing import Optional
2
+
3
+ from pypresence.exceptions import DiscordNotFound
4
+
5
+ from anipy_cli.arg_parser import parse_args
6
+ from anipy_cli.clis import *
7
+ from anipy_cli.colors import colors, cprint
8
+ from anipy_cli.util import error, DotSpinner
9
+ from anipy_cli.config import Config
10
+ from anipy_cli.discord import DiscordPresence
11
+
12
+
13
+ def run_cli(override_args: Optional[list[str]] = None):
14
+ args = parse_args(override_args)
15
+ config = Config()
16
+ # This updates the config, adding new values doc changes and the like.
17
+ config._create_config()
18
+
19
+ if config.dc_presence:
20
+ with DotSpinner("Intializing Discord Presence...") as s:
21
+ try:
22
+ DiscordPresence()
23
+ s.set_text(colors.GREEN, "Initialized Discord Presence")
24
+ s.ok("✔")
25
+ except DiscordNotFound:
26
+ s.set_text(
27
+ colors.RED,
28
+ "Discord is not opened, can't initialize Discord Presence",
29
+ )
30
+ s.fail("✘")
31
+ except ConnectionError:
32
+ s.set_text(
33
+ colors.RED,
34
+ "Can't Connect to discord, can't initialize Discord Presence",
35
+ )
36
+ s.fail("✘")
37
+
38
+ if args.config:
39
+ print(config._config_file)
40
+ return
41
+ elif args.delete:
42
+ try:
43
+ config._history_file_path.unlink()
44
+ cprint(colors.RED, "Done")
45
+ except FileNotFoundError:
46
+ error("no history file found")
47
+ return
48
+
49
+ clis_dict = {
50
+ args.download: DownloadCli,
51
+ args.binge: BingeCli,
52
+ args.seasonal: SeasonalCli,
53
+ args.history: HistoryCli,
54
+ args.mal: MalCli,
55
+ }
56
+
57
+ cli_class = clis_dict.get(True, DefaultCli)
58
+
59
+ try:
60
+ cli_class(options=args).run()
61
+ except KeyboardInterrupt:
62
+ error("interrupted", fatal=True)
63
+
64
+
65
+ if __name__ == "__main__":
66
+ run_cli()
@@ -0,0 +1,15 @@
1
+ from anipy_cli.clis.default_cli import DefaultCli
2
+ from anipy_cli.clis.history_cli import HistoryCli
3
+ from anipy_cli.clis.mal_cli import MalCli
4
+ from anipy_cli.clis.seasonal_cli import SeasonalCli
5
+ from anipy_cli.clis.binge_cli import BingeCli
6
+ from anipy_cli.clis.download_cli import DownloadCli
7
+
8
+ __all__ = [
9
+ "DefaultCli",
10
+ "HistoryCli",
11
+ "MalCli",
12
+ "SeasonalCli",
13
+ "BingeCli",
14
+ "DownloadCli",
15
+ ]
@@ -0,0 +1,32 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import TYPE_CHECKING
3
+
4
+ if TYPE_CHECKING:
5
+ from anipy_cli.arg_parser import CliArgs
6
+
7
+
8
+ class CliBase(ABC):
9
+ def __init__(self, options: "CliArgs"):
10
+ self.options = options
11
+
12
+ @abstractmethod
13
+ def print_header(self): ...
14
+
15
+ @abstractmethod
16
+ def take_input(self): ...
17
+
18
+ @abstractmethod
19
+ def process(self): ...
20
+
21
+ @abstractmethod
22
+ def show(self): ...
23
+
24
+ @abstractmethod
25
+ def post(self): ...
26
+
27
+ def run(self):
28
+ self.print_header()
29
+ self.take_input()
30
+ self.process()
31
+ self.show()
32
+ self.post()
@@ -0,0 +1,83 @@
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
+ pick_episode_range_prompt,
11
+ search_show_prompt,
12
+ lang_prompt,
13
+ parse_auto_search,
14
+ )
15
+ from anipy_cli.util import DotSpinner, get_configured_player, migrate_locallist
16
+
17
+ if TYPE_CHECKING:
18
+ from anipy_cli.arg_parser import CliArgs
19
+
20
+
21
+ class BingeCli(CliBase):
22
+ def __init__(self, options: "CliArgs"):
23
+ super().__init__(options)
24
+
25
+ self.player = get_configured_player(self.options.optional_player)
26
+ self.history_list = LocalList(
27
+ Config()._history_file_path, migrate_cb=migrate_locallist
28
+ )
29
+
30
+ self.anime = None
31
+ self.episodes = None
32
+ self.lang = None
33
+
34
+ def print_header(self):
35
+ cprint(colors.GREEN, "***Binge Mode***")
36
+
37
+ def take_input(self):
38
+ if self.options.search is not None:
39
+ self.anime, self.lang, self.episodes = parse_auto_search(
40
+ "binge", self.options.search
41
+ )
42
+ return
43
+
44
+ anime = search_show_prompt("binge")
45
+
46
+ if anime is None:
47
+ sys.exit(0)
48
+
49
+ self.lang = lang_prompt(anime)
50
+
51
+ episodes = pick_episode_range_prompt(anime, self.lang)
52
+
53
+ self.anime = anime
54
+ self.episodes = episodes
55
+
56
+ def process(self): ...
57
+
58
+ def show(self):
59
+ assert self.episodes is not None
60
+ assert self.anime is not None
61
+ assert self.lang is not None
62
+
63
+ for e in self.episodes:
64
+ with DotSpinner(
65
+ "Extracting streams for ",
66
+ colors.BLUE,
67
+ f"{self.anime.name} ({self.lang})",
68
+ colors.END,
69
+ " Episode ",
70
+ e,
71
+ "...",
72
+ ) as s:
73
+ stream = self.anime.get_video(
74
+ e, self.lang, preferred_quality=self.options.quality
75
+ )
76
+ s.ok("✔")
77
+
78
+ self.history_list.update(self.anime, episode=e, language=self.lang)
79
+ self.player.play_title(self.anime, stream)
80
+ self.player.wait()
81
+
82
+ def post(self):
83
+ self.player.kill_player()
@@ -0,0 +1,104 @@
1
+ import sys
2
+ from typing import TYPE_CHECKING, Optional
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
8
+ from anipy_cli.config import Config
9
+ from anipy_cli.menus import Menu
10
+ from anipy_cli.prompts import (
11
+ pick_episode_prompt,
12
+ search_show_prompt,
13
+ lang_prompt,
14
+ parse_auto_search,
15
+ )
16
+ from anipy_cli.util import (
17
+ DotSpinner,
18
+ get_configured_player,
19
+ migrate_locallist,
20
+ )
21
+
22
+ if TYPE_CHECKING:
23
+ from anipy_api.anime import Anime
24
+ from anipy_api.provider import Episode, ProviderStream, LanguageTypeEnum
25
+ from anipy_cli.arg_parser import CliArgs
26
+
27
+
28
+ # TODO: Add Resume feature
29
+ class DefaultCli(CliBase):
30
+ def __init__(self, options: "CliArgs"):
31
+ super().__init__(options)
32
+
33
+ self.player = get_configured_player(self.options.optional_player)
34
+ self.history_list = LocalList(
35
+ Config()._history_file_path, migrate_cb=migrate_locallist
36
+ )
37
+
38
+ self.anime: Optional["Anime"] = None
39
+ self.epsiode: Optional["Episode"] = None
40
+ self.stream: Optional["ProviderStream"] = None
41
+ self.lang: Optional["LanguageTypeEnum"] = None
42
+
43
+ def print_header(self):
44
+ pass
45
+
46
+ def take_input(self):
47
+ if self.options.search is not None:
48
+ self.anime, self.lang, episodes = parse_auto_search(
49
+ "default", self.options.search
50
+ )
51
+ self.epsiode = episodes[0]
52
+ return
53
+
54
+ anime = search_show_prompt("default")
55
+
56
+ if anime is None:
57
+ sys.exit(0)
58
+
59
+ self.lang = lang_prompt(anime)
60
+
61
+ episode = pick_episode_prompt(anime, self.lang)
62
+
63
+ if episode is None:
64
+ sys.exit(0)
65
+
66
+ self.anime = anime
67
+ self.epsiode = episode
68
+
69
+ def process(self):
70
+ assert self.anime is not None
71
+ assert self.epsiode is not None
72
+ assert self.lang is not None
73
+
74
+ with DotSpinner(
75
+ "Extracting streams for ",
76
+ colors.BLUE,
77
+ f"{self.anime.name} ({self.lang})",
78
+ " Episode ",
79
+ self.epsiode,
80
+ "...",
81
+ ):
82
+ self.stream = self.anime.get_video(
83
+ self.epsiode, self.lang, preferred_quality=self.options.quality
84
+ )
85
+
86
+ def show(self):
87
+ assert self.anime is not None
88
+ assert self.stream is not None
89
+
90
+ self.history_list.update(
91
+ self.anime, episode=self.epsiode, language=self.stream.language
92
+ )
93
+ self.player.play_title(self.anime, self.stream)
94
+
95
+ def post(self):
96
+ assert self.anime is not None
97
+ assert self.stream is not None
98
+
99
+ Menu(
100
+ options=self.options,
101
+ anime=self.anime,
102
+ stream=self.stream,
103
+ player=self.player,
104
+ ).run()
@@ -0,0 +1,111 @@
1
+ import sys
2
+ from typing import TYPE_CHECKING, Optional, List
3
+
4
+ from anipy_api.download import Downloader
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
+ pick_episode_range_prompt,
11
+ search_show_prompt,
12
+ lang_prompt,
13
+ parse_auto_search,
14
+ )
15
+ from anipy_cli.util import (
16
+ DotSpinner,
17
+ get_download_path,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from anipy_api.anime import Anime
22
+ from anipy_api.provider import Episode, LanguageTypeEnum
23
+
24
+ from anipy_cli.arg_parser import CliArgs
25
+
26
+
27
+ class DownloadCli(CliBase):
28
+ def __init__(self, options: "CliArgs"):
29
+ super().__init__(options)
30
+
31
+ self.anime: Optional["Anime"] = None
32
+ self.episodes: Optional[List["Episode"]] = None
33
+ self.lang: Optional["LanguageTypeEnum"] = None
34
+
35
+ self.dl_path = Config().download_folder_path
36
+ if options.location:
37
+ self.dl_path = options.location
38
+
39
+ def print_header(self):
40
+ cprint(colors.GREEN, "***Download Mode***")
41
+ cprint(colors.GREEN, "Downloads are stored in: ", colors.END, str(self.dl_path))
42
+
43
+ def take_input(self):
44
+ if self.options.search is not None:
45
+ self.anime, self.lang, self.episodes = parse_auto_search(
46
+ "download", self.options.search
47
+ )
48
+ return
49
+
50
+ anime = search_show_prompt("download")
51
+
52
+ if anime is None:
53
+ sys.exit(0)
54
+
55
+ self.lang = lang_prompt(anime)
56
+
57
+ episodes = pick_episode_range_prompt(anime, self.lang)
58
+
59
+ self.anime = anime
60
+ self.episodes = episodes
61
+
62
+ def process(self):
63
+ assert self.episodes is not None
64
+ assert self.anime is not None
65
+ assert self.lang is not None
66
+
67
+ config = Config()
68
+ with DotSpinner("Starting Download...") as s:
69
+
70
+ def progress_indicator(percentage: float):
71
+ s.set_text(f"Progress: {percentage:.1f}%")
72
+
73
+ def info_display(message: str):
74
+ s.write(f"> {message}")
75
+
76
+ downloader = Downloader(progress_indicator, info_display)
77
+
78
+ for e in self.episodes:
79
+ s.set_text(
80
+ "Extracting streams for ",
81
+ colors.BLUE,
82
+ f"{self.anime.name} ({self.lang})",
83
+ colors.END,
84
+ " Episode ",
85
+ e,
86
+ "...",
87
+ )
88
+
89
+ stream = self.anime.get_video(
90
+ e, self.lang, preferred_quality=self.options.quality
91
+ )
92
+
93
+ info_display(
94
+ f"Downloading Episode {stream.episode} of {self.anime.name} ({self.lang})"
95
+ )
96
+ s.set_text("Downloading...")
97
+
98
+ downloader.download(
99
+ stream,
100
+ get_download_path(
101
+ self.anime, stream, parent_directory=self.dl_path
102
+ ),
103
+ container=config.remux_to,
104
+ ffmpeg=self.options.ffmpeg or config.ffmpeg_hls,
105
+ )
106
+
107
+ def show(self):
108
+ pass
109
+
110
+ def post(self):
111
+ pass
@@ -0,0 +1,93 @@
1
+ import sys
2
+ from typing import TYPE_CHECKING, Optional
3
+
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
+ sys.exit(0)
43
+
44
+ entry = inquirer.select( # type: ignore
45
+ message="Select History Entry:",
46
+ choices=history,
47
+ long_instruction="To cancel this prompt press ctrl+z",
48
+ mandatory=False,
49
+ ).execute()
50
+
51
+ if entry is None:
52
+ sys.exit(0)
53
+
54
+ self.history_entry = entry
55
+ self.anime = Anime.from_local_list_entry(entry)
56
+
57
+ def process(self):
58
+ assert self.anime is not None
59
+ assert self.history_entry is not None
60
+
61
+ with DotSpinner(
62
+ "Extracting streams for ",
63
+ colors.BLUE,
64
+ self.history_entry,
65
+ "...",
66
+ ):
67
+ self.stream = self.anime.get_video(
68
+ self.history_entry.episode,
69
+ self.history_entry.language,
70
+ preferred_quality=self.options.quality,
71
+ )
72
+
73
+ def show(self):
74
+ assert self.anime is not None
75
+ assert self.stream is not None
76
+
77
+ self.player.play_title(self.anime, self.stream)
78
+ self.history_list.update(
79
+ self.anime,
80
+ episode=self.stream.episode,
81
+ language=self.stream.language,
82
+ )
83
+
84
+ def post(self):
85
+ assert self.anime is not None
86
+ assert self.stream is not None
87
+
88
+ Menu(
89
+ options=self.options,
90
+ anime=self.anime,
91
+ stream=self.stream,
92
+ player=self.player,
93
+ ).run()