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.
- anipy_cli/__init__.py +2 -20
- anipy_cli/anilist_proxy.py +229 -0
- anipy_cli/arg_parser.py +109 -21
- anipy_cli/cli.py +98 -0
- anipy_cli/clis/__init__.py +17 -0
- anipy_cli/clis/anilist_cli.py +62 -0
- anipy_cli/clis/base_cli.py +34 -0
- anipy_cli/clis/binge_cli.py +96 -0
- anipy_cli/clis/default_cli.py +115 -0
- anipy_cli/clis/download_cli.py +85 -0
- anipy_cli/clis/history_cli.py +96 -0
- anipy_cli/clis/mal_cli.py +71 -0
- anipy_cli/{cli/clis → clis}/seasonal_cli.py +9 -6
- anipy_cli/colors.py +14 -8
- anipy_cli/config.py +387 -90
- anipy_cli/discord.py +34 -0
- anipy_cli/download_component.py +194 -0
- anipy_cli/logger.py +200 -0
- anipy_cli/mal_proxy.py +228 -0
- anipy_cli/menus/__init__.py +6 -0
- anipy_cli/menus/anilist_menu.py +671 -0
- anipy_cli/{cli/menus → menus}/base_menu.py +9 -14
- anipy_cli/menus/mal_menu.py +657 -0
- anipy_cli/menus/menu.py +265 -0
- anipy_cli/menus/seasonal_menu.py +270 -0
- anipy_cli/prompts.py +387 -0
- anipy_cli/util.py +268 -0
- anipy_cli-3.8.2.dist-info/METADATA +71 -0
- anipy_cli-3.8.2.dist-info/RECORD +31 -0
- {anipy_cli-2.7.17.dist-info → anipy_cli-3.8.2.dist-info}/WHEEL +1 -2
- anipy_cli-3.8.2.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 -102
- anipy_cli/cli/menus/seasonal_menu.py +0 -174
- anipy_cli/cli/util.py +0 -118
- anipy_cli/download.py +0 -454
- anipy_cli/history.py +0 -83
- anipy_cli/mal.py +0 -645
- anipy_cli/misc.py +0 -227
- anipy_cli/player/__init__.py +0 -1
- anipy_cli/player/player.py +0 -33
- anipy_cli/player/players/__init__.py +0 -3
- anipy_cli/player/players/base.py +0 -106
- anipy_cli/player/players/mpv.py +0 -19
- anipy_cli/player/players/mpv_contrl.py +0 -37
- anipy_cli/player/players/syncplay.py +0 -19
- anipy_cli/player/players/vlc.py +0 -18
- anipy_cli/query.py +0 -92
- anipy_cli/run_anipy_cli.py +0 -14
- anipy_cli/seasonal.py +0 -106
- anipy_cli/url_handler.py +0 -442
- anipy_cli/version.py +0 -1
- anipy_cli-2.7.17.dist-info/LICENSE +0 -674
- anipy_cli-2.7.17.dist-info/METADATA +0 -159
- anipy_cli-2.7.17.dist-info/RECORD +0 -43
- anipy_cli-2.7.17.dist-info/entry_points.txt +0 -2
- anipy_cli-2.7.17.dist-info/top_level.txt +0 -1
anipy_cli/cli/util.py
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
from anipy_cli.colors import cprint, colors, cinput
|
|
2
|
-
from anipy_cli.misc import Entry, search_in_season_on_gogo, print_names
|
|
3
|
-
from anipy_cli.url_handler import epHandler, videourl
|
|
4
|
-
from anipy_cli.mal import MAL
|
|
5
|
-
from anipy_cli.seasonal import Seasonal
|
|
6
|
-
from anipy_cli.player import PlayerBaseType
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def binge(ep_list, quality, player: PlayerBaseType, mode="", mal_class: MAL = None):
|
|
10
|
-
"""
|
|
11
|
-
TODO: bruh what is this, let this accept a list of Entry
|
|
12
|
-
Accepts ep_list like so:
|
|
13
|
-
{"name" {'ep_urls': [], 'eps': [], 'category_url': }, "next_anime"...}
|
|
14
|
-
"""
|
|
15
|
-
cprint(colors.RED, "To quit press CTRL+C")
|
|
16
|
-
try:
|
|
17
|
-
for i in ep_list:
|
|
18
|
-
print(i)
|
|
19
|
-
show_entry = Entry()
|
|
20
|
-
show_entry.show_name = i
|
|
21
|
-
show_entry.category_url = ep_list[i]["category_url"]
|
|
22
|
-
show_entry.latest_ep = epHandler(show_entry).get_latest()
|
|
23
|
-
for url, ep in zip(ep_list[i]["ep_urls"], ep_list[i]["eps"]):
|
|
24
|
-
show_entry.ep = ep
|
|
25
|
-
show_entry.embed_url = ""
|
|
26
|
-
show_entry.ep_url = url
|
|
27
|
-
cprint(
|
|
28
|
-
colors.GREEN,
|
|
29
|
-
"Fetching links for: ",
|
|
30
|
-
colors.END,
|
|
31
|
-
show_entry.show_name,
|
|
32
|
-
colors.RED,
|
|
33
|
-
f""" | EP: {
|
|
34
|
-
show_entry.ep
|
|
35
|
-
}/{
|
|
36
|
-
show_entry.latest_ep
|
|
37
|
-
}""",
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
url_class = videourl(show_entry, quality)
|
|
41
|
-
url_class.stream_url()
|
|
42
|
-
show_entry = url_class.get_entry()
|
|
43
|
-
player.play_title(show_entry)
|
|
44
|
-
player.wait()
|
|
45
|
-
|
|
46
|
-
if mode == "seasonal":
|
|
47
|
-
Seasonal().update_show(
|
|
48
|
-
show_entry.show_name, show_entry.category_url, show_entry.ep
|
|
49
|
-
)
|
|
50
|
-
elif mode == "mal":
|
|
51
|
-
mal_class.update_watched(show_entry.show_name, show_entry.ep)
|
|
52
|
-
|
|
53
|
-
except KeyboardInterrupt:
|
|
54
|
-
player.kill_player()
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def get_season_searches(gogo=True):
|
|
58
|
-
searches = []
|
|
59
|
-
selected = []
|
|
60
|
-
season_year = None
|
|
61
|
-
season_name = None
|
|
62
|
-
while not season_year:
|
|
63
|
-
try:
|
|
64
|
-
season_year = int(cinput(colors.CYAN, "Season Year: "))
|
|
65
|
-
except ValueError:
|
|
66
|
-
print("Please enter a valid year.\n")
|
|
67
|
-
|
|
68
|
-
while not season_name:
|
|
69
|
-
season_name_input = cinput(
|
|
70
|
-
colors.CYAN, "Season Name (spring|summer|fall|winter): "
|
|
71
|
-
)
|
|
72
|
-
if season_name_input.lower() in ["spring", "summer", "fall", "winter"]:
|
|
73
|
-
season_name = season_name_input
|
|
74
|
-
|
|
75
|
-
else:
|
|
76
|
-
cprint(colors.YELLOW, "Please enter a valid season name.\n")
|
|
77
|
-
|
|
78
|
-
if gogo:
|
|
79
|
-
anime_in_season = search_in_season_on_gogo(season_year, season_name)
|
|
80
|
-
|
|
81
|
-
else:
|
|
82
|
-
anime_in_season = MAL().get_seasonal_anime(season_year, season_name)
|
|
83
|
-
|
|
84
|
-
cprint("Anime found in {} {} Season: ".format(season_year, season_name))
|
|
85
|
-
cprint(
|
|
86
|
-
colors.CYAN,
|
|
87
|
-
"Anime found in ",
|
|
88
|
-
colors.GREEN,
|
|
89
|
-
season_year,
|
|
90
|
-
colors.CYAN,
|
|
91
|
-
" ",
|
|
92
|
-
colors.YELLOW,
|
|
93
|
-
season_name,
|
|
94
|
-
colors.CYAN,
|
|
95
|
-
" Season: ",
|
|
96
|
-
)
|
|
97
|
-
anime_names = []
|
|
98
|
-
for anime in anime_in_season:
|
|
99
|
-
if gogo:
|
|
100
|
-
anime_names.append(anime["name"])
|
|
101
|
-
|
|
102
|
-
else:
|
|
103
|
-
anime_names.append(anime["node"]["title"])
|
|
104
|
-
|
|
105
|
-
print_names(anime_names)
|
|
106
|
-
selection = cinput(colors.CYAN, "Selection: (e.g. 1, 1 3 or 1-3) \n>> ")
|
|
107
|
-
if selection.__contains__("-"):
|
|
108
|
-
selection_range = selection.strip(" ").split("-")
|
|
109
|
-
for i in range(int(selection_range[0]) - 1, int(selection_range[1]) - 1, 1):
|
|
110
|
-
selected.append(i)
|
|
111
|
-
|
|
112
|
-
else:
|
|
113
|
-
for i in selection.lstrip(" ").rstrip(" ").split(" "):
|
|
114
|
-
selected.append(int(i) - 1)
|
|
115
|
-
|
|
116
|
-
for value in selected:
|
|
117
|
-
searches.append(anime_in_season[int(value)])
|
|
118
|
-
return searches
|
anipy_cli/download.py
DELETED
|
@@ -1,454 +0,0 @@
|
|
|
1
|
-
import re
|
|
2
|
-
import urllib
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
import m3u8
|
|
6
|
-
import requests
|
|
7
|
-
import shutil
|
|
8
|
-
import sys
|
|
9
|
-
|
|
10
|
-
from tqdm import tqdm
|
|
11
|
-
from requests.adapters import HTTPAdapter, Retry
|
|
12
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
13
|
-
from better_ffmpeg_progress import FfmpegProcess
|
|
14
|
-
from moviepy.editor import ffmpeg_tools
|
|
15
|
-
|
|
16
|
-
from anipy_cli.misc import response_err, error, keyboard_inter
|
|
17
|
-
from anipy_cli.colors import colors, color, cprint
|
|
18
|
-
from anipy_cli.config import Config
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class download:
|
|
22
|
-
"""
|
|
23
|
-
Download Class.
|
|
24
|
-
A entry with all fields is required.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
def __init__(self, entry, quality, ffmpeg=False, dl_path: Path = None) -> None:
|
|
28
|
-
try:
|
|
29
|
-
self.quality = int(quality)
|
|
30
|
-
except ValueError:
|
|
31
|
-
self.quality = quality
|
|
32
|
-
self.is_audio = None
|
|
33
|
-
self.content_audio_media = None
|
|
34
|
-
self._m3u8_content = None
|
|
35
|
-
self.session = None
|
|
36
|
-
self.entry = entry
|
|
37
|
-
self.ffmpeg = ffmpeg
|
|
38
|
-
self.dl_path = dl_path
|
|
39
|
-
if dl_path is None:
|
|
40
|
-
self.dl_path = Config().download_folder_path
|
|
41
|
-
self.headers = {
|
|
42
|
-
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
|
|
43
|
-
"referer": self.entry.embed_url,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
def download(self):
|
|
47
|
-
show_name = self._get_valid_pathname(self.entry.show_name)
|
|
48
|
-
show_name.strip()
|
|
49
|
-
self.show_folder = self.dl_path / f"{show_name}"
|
|
50
|
-
|
|
51
|
-
if Config().download_remove_dub_from_folder_name:
|
|
52
|
-
if show_name.endswith(" (Dub)"):
|
|
53
|
-
self.show_folder = self.dl_path / f"{show_name[:-6]}"
|
|
54
|
-
print(self.show_folder)
|
|
55
|
-
|
|
56
|
-
self.dl_path.mkdir(exist_ok=True, parents=True)
|
|
57
|
-
self.show_folder.mkdir(exist_ok=True)
|
|
58
|
-
self.session = requests.Session()
|
|
59
|
-
retry = Retry(connect=3, backoff_factor=0.5)
|
|
60
|
-
adapter = HTTPAdapter(max_retries=retry)
|
|
61
|
-
self.session.mount("http://", adapter)
|
|
62
|
-
self.session.mount("https://", adapter)
|
|
63
|
-
self.session.headers.update(self.headers)
|
|
64
|
-
|
|
65
|
-
fname = self._get_fname()
|
|
66
|
-
dl_path = self.show_folder / fname
|
|
67
|
-
|
|
68
|
-
if dl_path.is_file():
|
|
69
|
-
print("-" * 20)
|
|
70
|
-
cprint(
|
|
71
|
-
colors.GREEN,
|
|
72
|
-
"Skipping Already Existing: ",
|
|
73
|
-
colors.RED,
|
|
74
|
-
f"{self.entry.show_name} EP: {self.entry.ep} - {self.entry.quality}",
|
|
75
|
-
)
|
|
76
|
-
return dl_path
|
|
77
|
-
|
|
78
|
-
print("-" * 20)
|
|
79
|
-
cprint(
|
|
80
|
-
colors.CYAN,
|
|
81
|
-
"Downloading: ",
|
|
82
|
-
colors.RED,
|
|
83
|
-
f"{self.entry.show_name} EP: {self.entry.ep} - {self.entry.quality}",
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
if "m3u8" in self.entry.stream_url:
|
|
87
|
-
cprint(colors.CYAN, "Type: ", colors.RED, "m3u8")
|
|
88
|
-
if self.ffmpeg or Config().ffmpeg_hls:
|
|
89
|
-
cprint(colors.CYAN, "Downloader: ", colors.RED, "ffmpeg")
|
|
90
|
-
self.ffmpeg_dl()
|
|
91
|
-
return dl_path
|
|
92
|
-
|
|
93
|
-
cprint(colors.CYAN, "Downloader:", colors.RED, "internal")
|
|
94
|
-
self.multithread_m3u8_dl()
|
|
95
|
-
elif "mp4" in self.entry.stream_url:
|
|
96
|
-
cprint(colors.CYAN, "Type: ", colors.RED, "mp4")
|
|
97
|
-
self.mp4_dl(self.entry.stream_url)
|
|
98
|
-
|
|
99
|
-
return dl_path
|
|
100
|
-
|
|
101
|
-
def ffmpeg_dl(self):
|
|
102
|
-
Config().user_files_path.mkdir(exist_ok=True, parents=True)
|
|
103
|
-
Config().ffmpeg_log_path.mkdir(exist_ok=True, parents=True)
|
|
104
|
-
fname = self._get_fname()
|
|
105
|
-
|
|
106
|
-
dl_path = self.show_folder / fname
|
|
107
|
-
|
|
108
|
-
ffmpeg_process = FfmpegProcess(
|
|
109
|
-
[
|
|
110
|
-
"ffmpeg",
|
|
111
|
-
"-headers",
|
|
112
|
-
f"referer:{self.entry.embed_url}",
|
|
113
|
-
"-i",
|
|
114
|
-
self.entry.stream_url,
|
|
115
|
-
"-vcodec",
|
|
116
|
-
"copy",
|
|
117
|
-
"-acodec",
|
|
118
|
-
"copy",
|
|
119
|
-
"-scodec",
|
|
120
|
-
"mov_text",
|
|
121
|
-
"-c",
|
|
122
|
-
"copy",
|
|
123
|
-
str(dl_path),
|
|
124
|
-
]
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
try:
|
|
128
|
-
ffmpeg_process.run(
|
|
129
|
-
ffmpeg_output_file=str(
|
|
130
|
-
Config().ffmpeg_log_path / fname.replace("mp4", "log")
|
|
131
|
-
)
|
|
132
|
-
)
|
|
133
|
-
cprint(colors.CYAN, "Download finished.")
|
|
134
|
-
except KeyboardInterrupt:
|
|
135
|
-
error("interrupted deleting partially downloaded file")
|
|
136
|
-
fname.unlink()
|
|
137
|
-
|
|
138
|
-
def ffmpeg_merge(self, input_file, audio_input_file):
|
|
139
|
-
Config().user_files_path.mkdir(exist_ok=True, parents=True)
|
|
140
|
-
Config().ffmpeg_log_path.mkdir(exist_ok=True, parents=True)
|
|
141
|
-
fname = self._get_fname()
|
|
142
|
-
|
|
143
|
-
dl_path = self.show_folder / fname
|
|
144
|
-
|
|
145
|
-
merged_video_ts = self.merge_ts_files(input_file)
|
|
146
|
-
merged_audio_ts = None
|
|
147
|
-
if audio_input_file:
|
|
148
|
-
merged_audio_ts = self.merge_ts_files(audio_input_file, "_audio")
|
|
149
|
-
|
|
150
|
-
try:
|
|
151
|
-
cprint(colors.CYAN, "Merging Parts using Movie.py...")
|
|
152
|
-
if audio_input_file:
|
|
153
|
-
ffmpeg_tools.ffmpeg_merge_video_audio(
|
|
154
|
-
merged_video_ts,
|
|
155
|
-
merged_audio_ts,
|
|
156
|
-
str(dl_path),
|
|
157
|
-
vcodec="copy",
|
|
158
|
-
acodec="copy",
|
|
159
|
-
ffmpeg_output=False,
|
|
160
|
-
logger="bar",
|
|
161
|
-
)
|
|
162
|
-
else:
|
|
163
|
-
ffmpeg_tools.ffmpeg_merge_video_audio(
|
|
164
|
-
merged_video_ts,
|
|
165
|
-
merged_video_ts,
|
|
166
|
-
str(dl_path),
|
|
167
|
-
vcodec="copy",
|
|
168
|
-
acodec="copy",
|
|
169
|
-
ffmpeg_output=False,
|
|
170
|
-
logger="bar",
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
cprint(colors.CYAN, "Merge finished.")
|
|
174
|
-
except KeyboardInterrupt:
|
|
175
|
-
error("interrupted deleting partially downloaded file")
|
|
176
|
-
fname.unlink()
|
|
177
|
-
|
|
178
|
-
def merge_ts_files(self, input_file, suffix=""):
|
|
179
|
-
filename = f"{self.temp_folder}/{self.entry.show_name}_{self.entry.ep}_merged{suffix}.ts"
|
|
180
|
-
# Parse playlist for filenames with ending .ts and put them into the list ts_filenames
|
|
181
|
-
with open(input_file, "r") as playlist:
|
|
182
|
-
ts_filenames = [
|
|
183
|
-
line.rstrip() + f"{suffix}"
|
|
184
|
-
for line in playlist
|
|
185
|
-
if not line.lstrip().startswith("#")
|
|
186
|
-
]
|
|
187
|
-
# open one ts_file from the list after another and append them to merged.ts
|
|
188
|
-
with open(filename, "wb") as merged:
|
|
189
|
-
for ts_file in ts_filenames:
|
|
190
|
-
with open(ts_file, "rb") as mergefile:
|
|
191
|
-
shutil.copyfileobj(mergefile, merged)
|
|
192
|
-
return filename
|
|
193
|
-
|
|
194
|
-
def mp4_dl(self, dl_link):
|
|
195
|
-
"""
|
|
196
|
-
|
|
197
|
-
:param dl_link:
|
|
198
|
-
:type dl_link:
|
|
199
|
-
:return:
|
|
200
|
-
:rtype:
|
|
201
|
-
"""
|
|
202
|
-
r = self.session.get(dl_link, headers=self.headers, stream=True)
|
|
203
|
-
response_err(r, dl_link)
|
|
204
|
-
total = int(r.headers.get("content-length", 0))
|
|
205
|
-
fname = self.show_folder / self._get_fname()
|
|
206
|
-
try:
|
|
207
|
-
with fname.open("wb") as out_file, tqdm(
|
|
208
|
-
desc=self.entry.show_name,
|
|
209
|
-
total=total,
|
|
210
|
-
unit="iB",
|
|
211
|
-
unit_scale=True,
|
|
212
|
-
unit_divisor=1024,
|
|
213
|
-
) as bar:
|
|
214
|
-
for data in r.iter_content(chunk_size=1024):
|
|
215
|
-
size = out_file.write(data)
|
|
216
|
-
bar.update(size)
|
|
217
|
-
except KeyboardInterrupt:
|
|
218
|
-
error("interrupted deleting partially downloaded file")
|
|
219
|
-
fname.unlink()
|
|
220
|
-
|
|
221
|
-
cprint(colors.CYAN, "Download finished.")
|
|
222
|
-
|
|
223
|
-
def download_ts(self, m3u8_segments, retry=0):
|
|
224
|
-
self.counter += 1
|
|
225
|
-
audio_suffix = ""
|
|
226
|
-
uri = urllib.parse.urljoin(m3u8_segments.base_uri, m3u8_segments.uri)
|
|
227
|
-
if not self._is_url(uri):
|
|
228
|
-
input(f"uri: {uri} is not an uri")
|
|
229
|
-
return
|
|
230
|
-
|
|
231
|
-
if self.is_audio:
|
|
232
|
-
audio_suffix = "audio"
|
|
233
|
-
filename = self._get_filename(uri, self.temp_folder, audio_suffix)
|
|
234
|
-
headers = self.headers
|
|
235
|
-
retry_count = 0
|
|
236
|
-
while not Path(filename).is_file() and retry_count < 20:
|
|
237
|
-
cprint(
|
|
238
|
-
colors.CYAN,
|
|
239
|
-
f"Downloading {audio_suffix} Part: {self.counter}/{self.segment_count}",
|
|
240
|
-
end="",
|
|
241
|
-
)
|
|
242
|
-
print("\r", end="")
|
|
243
|
-
|
|
244
|
-
try:
|
|
245
|
-
with self.session.get(
|
|
246
|
-
uri, timeout=10, headers=headers, stream=False
|
|
247
|
-
) as response:
|
|
248
|
-
if response.status_code == 416:
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
response.raise_for_status()
|
|
252
|
-
|
|
253
|
-
with open(filename, "wb") as fout:
|
|
254
|
-
fout.write(response.content)
|
|
255
|
-
|
|
256
|
-
except Exception as e:
|
|
257
|
-
exit(e.__str__())
|
|
258
|
-
retry_count += 1
|
|
259
|
-
|
|
260
|
-
def multithread_m3u8_dl(self):
|
|
261
|
-
"""
|
|
262
|
-
Multithread download
|
|
263
|
-
function for m3u8 links.
|
|
264
|
-
- Creates show and temp folder
|
|
265
|
-
- Starts ThreadPoolExecutor instance
|
|
266
|
-
and downloads all ts links
|
|
267
|
-
- Merges ts files
|
|
268
|
-
- Deletes temp folder
|
|
269
|
-
|
|
270
|
-
:return:
|
|
271
|
-
:rtype:
|
|
272
|
-
"""
|
|
273
|
-
|
|
274
|
-
self.temp_folder = self.show_folder / f"{self.entry.ep}_temp"
|
|
275
|
-
self.temp_folder.mkdir(exist_ok=True)
|
|
276
|
-
self.counter = 0
|
|
277
|
-
|
|
278
|
-
self._m3u8_content = self._download_m3u8(
|
|
279
|
-
self.entry.stream_url, 10, self.headers
|
|
280
|
-
)
|
|
281
|
-
|
|
282
|
-
assert self._m3u8_content.is_variant is False
|
|
283
|
-
|
|
284
|
-
try:
|
|
285
|
-
if self.content_audio_media and not self.content_audio_media.is_variant:
|
|
286
|
-
self.segment_count = len(self.content_audio_media.segments)
|
|
287
|
-
self.is_audio = True
|
|
288
|
-
with ThreadPoolExecutor(12) as pool_audio:
|
|
289
|
-
pool_audio.map(self.download_ts, self.content_audio_media.segments)
|
|
290
|
-
self.is_audio = False
|
|
291
|
-
self.counter = 0
|
|
292
|
-
self.segment_count = len(self._m3u8_content.segments)
|
|
293
|
-
print("\n")
|
|
294
|
-
with ThreadPoolExecutor(12) as pool_video:
|
|
295
|
-
pool_video.map(self.download_ts, self._m3u8_content.segments)
|
|
296
|
-
except KeyboardInterrupt:
|
|
297
|
-
shutil.rmtree(self.temp_folder)
|
|
298
|
-
keyboard_inter()
|
|
299
|
-
exit()
|
|
300
|
-
|
|
301
|
-
input_file = self._dump_m3u8(self._m3u8_content)
|
|
302
|
-
audio_input_file = None
|
|
303
|
-
if self.content_audio_media and not self.content_audio_media.is_variant:
|
|
304
|
-
self.is_audio = True
|
|
305
|
-
audio_input_file = self._dump_m3u8(self.content_audio_media)
|
|
306
|
-
|
|
307
|
-
cprint("\n", colors.CYAN, "Parts Downloaded")
|
|
308
|
-
try:
|
|
309
|
-
self.ffmpeg_merge(input_file, audio_input_file)
|
|
310
|
-
except FileNotFoundError:
|
|
311
|
-
# This restarts the download if a file is missing
|
|
312
|
-
error("Missing a download part, restarting download")
|
|
313
|
-
return self.multithread_m3u8_dl()
|
|
314
|
-
|
|
315
|
-
cprint("\n", colors.CYAN, "Parts Merged")
|
|
316
|
-
shutil.rmtree(self.temp_folder)
|
|
317
|
-
|
|
318
|
-
def _download_m3u8(self, uri, timeout, headers, is_audio=False):
|
|
319
|
-
if self._is_url(uri):
|
|
320
|
-
resp = self.session.get(uri, timeout=timeout, headers=self.headers)
|
|
321
|
-
resp.raise_for_status()
|
|
322
|
-
raw_content = resp.content.decode(resp.encoding or "utf-8")
|
|
323
|
-
base_uri = urllib.parse.urljoin(uri, ".")
|
|
324
|
-
else:
|
|
325
|
-
with open(uri) as fin:
|
|
326
|
-
raw_content = fin.read()
|
|
327
|
-
base_uri = Path(uri)
|
|
328
|
-
content = m3u8.M3U8(raw_content, base_uri=base_uri)
|
|
329
|
-
if content.is_variant:
|
|
330
|
-
if self.content_audio_media is not None:
|
|
331
|
-
content.add_media(media=self.content_audio_media)
|
|
332
|
-
|
|
333
|
-
# sort
|
|
334
|
-
content.playlists.sort(key=lambda x: x.stream_info.bandwidth, reverse=True)
|
|
335
|
-
|
|
336
|
-
selected_index = 0
|
|
337
|
-
if self.quality == "worst":
|
|
338
|
-
selected_index = len(content.playlists) - 1
|
|
339
|
-
|
|
340
|
-
for index, playlist in enumerate(content.playlists):
|
|
341
|
-
cprint(
|
|
342
|
-
colors.GREEN,
|
|
343
|
-
"Playlist Index: ",
|
|
344
|
-
colors.RED,
|
|
345
|
-
index,
|
|
346
|
-
"\n",
|
|
347
|
-
colors.GREEN,
|
|
348
|
-
"Resolution at this index: ",
|
|
349
|
-
colors.RED,
|
|
350
|
-
playlist.stream_info.resolution,
|
|
351
|
-
"\n\n",
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
if self.quality in playlist.stream_info.resolution:
|
|
355
|
-
selected_index = index
|
|
356
|
-
|
|
357
|
-
try:
|
|
358
|
-
for media in content.media:
|
|
359
|
-
if content.playlists[selected_index].stream_info.audio in str(
|
|
360
|
-
media
|
|
361
|
-
):
|
|
362
|
-
self.content_audio_media = media
|
|
363
|
-
|
|
364
|
-
chosen_uri = content.playlists[selected_index].uri
|
|
365
|
-
if not self._is_url(chosen_uri):
|
|
366
|
-
chosen_uri = urllib.parse.urljoin(content.base_uri, chosen_uri)
|
|
367
|
-
if self.content_audio_media is not None:
|
|
368
|
-
media_uri = self.content_audio_media.uri
|
|
369
|
-
self.content_audio_media = self._download_m3u8(
|
|
370
|
-
media_uri, timeout, headers, True
|
|
371
|
-
)
|
|
372
|
-
cprint(
|
|
373
|
-
colors.GREEN,
|
|
374
|
-
"Quality for Download:",
|
|
375
|
-
colors.YELLOW,
|
|
376
|
-
content.playlists[selected_index].stream_info.resolution,
|
|
377
|
-
)
|
|
378
|
-
return self._download_m3u8(chosen_uri, timeout, headers)
|
|
379
|
-
|
|
380
|
-
except (ValueError, IndexError):
|
|
381
|
-
exit("Failed to get stream for chosen quality")
|
|
382
|
-
|
|
383
|
-
else:
|
|
384
|
-
self._download_key(content)
|
|
385
|
-
|
|
386
|
-
return content
|
|
387
|
-
|
|
388
|
-
def _dump_m3u8(self, content):
|
|
389
|
-
audio_suffix = ""
|
|
390
|
-
for index, segment in enumerate(content.segments):
|
|
391
|
-
content.segments[index].uri = self._get_filename(
|
|
392
|
-
segment.uri, self.temp_folder
|
|
393
|
-
)
|
|
394
|
-
|
|
395
|
-
if self.is_audio:
|
|
396
|
-
audio_suffix = "_audio"
|
|
397
|
-
filename = self._get_filename(f"master{audio_suffix}.m3u8", self.temp_folder)
|
|
398
|
-
content.dump(filename)
|
|
399
|
-
return filename
|
|
400
|
-
|
|
401
|
-
def _download_key(self, content):
|
|
402
|
-
for key in content.keys:
|
|
403
|
-
if key:
|
|
404
|
-
uri = key.absolute_uri
|
|
405
|
-
filename = self._get_filename(uri, self.temp_folder)
|
|
406
|
-
|
|
407
|
-
with self.session.get(
|
|
408
|
-
uri, timeout=10, headers=self.headers
|
|
409
|
-
) as response:
|
|
410
|
-
response.raise_for_status()
|
|
411
|
-
with open(filename, "wb") as fout:
|
|
412
|
-
fout.write(response.content)
|
|
413
|
-
|
|
414
|
-
key.uri = filename.__str__().replace(
|
|
415
|
-
"\\", "/"
|
|
416
|
-
) # ffmpeg error when using \\ in windows
|
|
417
|
-
|
|
418
|
-
def _get_fname(self) -> str:
|
|
419
|
-
"""
|
|
420
|
-
This function returns what the filename for the outputed video should be.
|
|
421
|
-
|
|
422
|
-
It finds this by using data in self.entry and the Config.
|
|
423
|
-
|
|
424
|
-
Returns a string which should be the filename.
|
|
425
|
-
"""
|
|
426
|
-
|
|
427
|
-
show_name = self._get_valid_pathname(self.entry.show_name)
|
|
428
|
-
|
|
429
|
-
return Config().download_name_format.format(
|
|
430
|
-
show_name=show_name,
|
|
431
|
-
episode_number=self.entry.ep,
|
|
432
|
-
quality=self.entry.quality,
|
|
433
|
-
)
|
|
434
|
-
|
|
435
|
-
@staticmethod
|
|
436
|
-
def _get_valid_pathname(name):
|
|
437
|
-
WIN_INVALID_CHARS = ["\\", "/", ":", "*", "?", "<", ">", "|", '"']
|
|
438
|
-
|
|
439
|
-
if sys.platform == "win32":
|
|
440
|
-
name = "".join(["" if x in WIN_INVALID_CHARS else x for x in name])
|
|
441
|
-
|
|
442
|
-
return name
|
|
443
|
-
|
|
444
|
-
@staticmethod
|
|
445
|
-
def _is_url(uri):
|
|
446
|
-
return re.match(r"https?://", uri) is not None
|
|
447
|
-
|
|
448
|
-
@staticmethod
|
|
449
|
-
def _get_filename(uri, directory, suffix=""):
|
|
450
|
-
if suffix:
|
|
451
|
-
suffix = f"_{suffix}"
|
|
452
|
-
basename = urllib.parse.urlparse(uri).path.split("/")[-1]
|
|
453
|
-
filename = Path("{}/{}{}".format(directory, basename, suffix)).__str__()
|
|
454
|
-
return filename
|
anipy_cli/history.py
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
from anipy_cli.misc import error, read_json
|
|
5
|
-
from anipy_cli.config import Config
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class history:
|
|
9
|
-
"""
|
|
10
|
-
Class for history.
|
|
11
|
-
Following entry fields are required
|
|
12
|
-
for writing to history file:
|
|
13
|
-
- show_name
|
|
14
|
-
- category_url
|
|
15
|
-
- ep_url
|
|
16
|
-
- ep
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
def __init__(self, entry) -> None:
|
|
20
|
-
self.entry = entry
|
|
21
|
-
|
|
22
|
-
def read_save_data(self):
|
|
23
|
-
self.json = read_json(Config().history_file_path)
|
|
24
|
-
|
|
25
|
-
return self.json
|
|
26
|
-
|
|
27
|
-
def check_duplicate(self):
|
|
28
|
-
"""
|
|
29
|
-
Check if show is already in
|
|
30
|
-
history file.
|
|
31
|
-
"""
|
|
32
|
-
for i in self.json:
|
|
33
|
-
if i == self.entry.show_name:
|
|
34
|
-
self.dup = True
|
|
35
|
-
return 1
|
|
36
|
-
|
|
37
|
-
self.dup = False
|
|
38
|
-
|
|
39
|
-
def prepend_json(self):
|
|
40
|
-
"""Moves data to the top of a json file"""
|
|
41
|
-
new_data = self.json[self.entry.show_name]
|
|
42
|
-
self.json.pop(self.entry.show_name)
|
|
43
|
-
new_data = {self.entry.show_name: (new_data)}
|
|
44
|
-
self.json = {**new_data, **self.json}
|
|
45
|
-
|
|
46
|
-
def update_hist(self):
|
|
47
|
-
self.json[self.entry.show_name]["ep"] = self.entry.ep
|
|
48
|
-
self.json[self.entry.show_name]["ep-link"] = self.entry.ep_url
|
|
49
|
-
|
|
50
|
-
def write_hist(self):
|
|
51
|
-
"""
|
|
52
|
-
Write json that looks something like this
|
|
53
|
-
{"some-anime":
|
|
54
|
-
{
|
|
55
|
-
"ep": 1,
|
|
56
|
-
"ep-link": "https://ep-link",
|
|
57
|
-
"category_url": "https://"
|
|
58
|
-
}
|
|
59
|
-
"another-anime": ...}
|
|
60
|
-
"""
|
|
61
|
-
|
|
62
|
-
self.read_save_data()
|
|
63
|
-
self.check_duplicate()
|
|
64
|
-
if self.dup:
|
|
65
|
-
self.update_hist()
|
|
66
|
-
else:
|
|
67
|
-
add_data = {
|
|
68
|
-
self.entry.show_name: {
|
|
69
|
-
"ep": self.entry.ep,
|
|
70
|
-
"ep-link": self.entry.ep_url,
|
|
71
|
-
"category-link": self.entry.category_url,
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
self.json.update(add_data)
|
|
75
|
-
|
|
76
|
-
self.prepend_json()
|
|
77
|
-
|
|
78
|
-
try:
|
|
79
|
-
with Config().history_file_path.open("w") as f:
|
|
80
|
-
json.dump(self.json, f, indent=4)
|
|
81
|
-
except PermissionError:
|
|
82
|
-
error("Unable to write to history file due permissions.")
|
|
83
|
-
sys.exit()
|