anipy-cli 2.7.31__py3-none-any.whl → 3.0.0.dev0__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 +2 -20
- anipy_cli/arg_parser.py +30 -20
- anipy_cli/cli.py +66 -0
- anipy_cli/clis/__init__.py +15 -0
- anipy_cli/clis/base_cli.py +32 -0
- anipy_cli/clis/binge_cli.py +83 -0
- anipy_cli/clis/default_cli.py +104 -0
- anipy_cli/clis/download_cli.py +111 -0
- anipy_cli/clis/history_cli.py +93 -0
- anipy_cli/clis/mal_cli.py +71 -0
- anipy_cli/{cli/clis → clis}/seasonal_cli.py +9 -6
- anipy_cli/colors.py +4 -4
- anipy_cli/config.py +308 -87
- anipy_cli/discord.py +34 -0
- anipy_cli/mal_proxy.py +216 -0
- anipy_cli/menus/__init__.py +5 -0
- anipy_cli/{cli/menus → menus}/base_menu.py +8 -12
- anipy_cli/menus/mal_menu.py +660 -0
- anipy_cli/menus/menu.py +194 -0
- anipy_cli/menus/seasonal_menu.py +263 -0
- anipy_cli/prompts.py +231 -0
- anipy_cli/util.py +262 -0
- anipy_cli-3.0.0.dev0.dist-info/METADATA +67 -0
- anipy_cli-3.0.0.dev0.dist-info/RECORD +26 -0
- {anipy_cli-2.7.31.dist-info → anipy_cli-3.0.0.dev0.dist-info}/WHEEL +1 -2
- anipy_cli-3.0.0.dev0.dist-info/entry_points.txt +3 -0
- anipy_cli/cli/__init__.py +0 -1
- anipy_cli/cli/cli.py +0 -37
- anipy_cli/cli/clis/__init__.py +0 -6
- anipy_cli/cli/clis/base_cli.py +0 -43
- anipy_cli/cli/clis/binge_cli.py +0 -54
- anipy_cli/cli/clis/default_cli.py +0 -46
- anipy_cli/cli/clis/download_cli.py +0 -92
- anipy_cli/cli/clis/history_cli.py +0 -64
- anipy_cli/cli/clis/mal_cli.py +0 -27
- anipy_cli/cli/menus/__init__.py +0 -3
- anipy_cli/cli/menus/mal_menu.py +0 -411
- anipy_cli/cli/menus/menu.py +0 -108
- anipy_cli/cli/menus/seasonal_menu.py +0 -177
- anipy_cli/cli/util.py +0 -125
- anipy_cli/download.py +0 -467
- anipy_cli/history.py +0 -83
- anipy_cli/mal.py +0 -651
- anipy_cli/misc.py +0 -227
- anipy_cli/player/__init__.py +0 -1
- anipy_cli/player/player.py +0 -35
- anipy_cli/player/players/__init__.py +0 -3
- anipy_cli/player/players/base.py +0 -107
- anipy_cli/player/players/mpv.py +0 -19
- anipy_cli/player/players/mpv_control.py +0 -37
- anipy_cli/player/players/syncplay.py +0 -19
- anipy_cli/player/players/vlc.py +0 -18
- anipy_cli/query.py +0 -100
- anipy_cli/run_anipy_cli.py +0 -14
- anipy_cli/seasonal.py +0 -112
- anipy_cli/url_handler.py +0 -470
- anipy_cli/version.py +0 -1
- anipy_cli-2.7.31.dist-info/LICENSE +0 -674
- anipy_cli-2.7.31.dist-info/METADATA +0 -162
- anipy_cli-2.7.31.dist-info/RECORD +0 -43
- anipy_cli-2.7.31.dist-info/entry_points.txt +0 -2
- anipy_cli-2.7.31.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.dev0"
|
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
|
|
5
|
-
from
|
|
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
|
|
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="
|
|
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
|
|
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()
|