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/url_handler.py
DELETED
|
@@ -1,442 +0,0 @@
|
|
|
1
|
-
import sys
|
|
2
|
-
import json
|
|
3
|
-
import requests
|
|
4
|
-
import re
|
|
5
|
-
import base64
|
|
6
|
-
import functools
|
|
7
|
-
import m3u8
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from urllib.parse import urlparse, parse_qsl, urlencode, urljoin
|
|
10
|
-
from bs4 import BeautifulSoup
|
|
11
|
-
from requests.adapters import HTTPAdapter, Retry
|
|
12
|
-
from Cryptodome.Cipher import AES
|
|
13
|
-
|
|
14
|
-
from anipy_cli.misc import response_err, error, loc_err, parsenum, Entry
|
|
15
|
-
from anipy_cli.colors import cinput, color, colors
|
|
16
|
-
from anipy_cli.config import Config
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class epHandler:
|
|
20
|
-
"""
|
|
21
|
-
Class for handling episodes and stuff.
|
|
22
|
-
Requires at least the category_url field of the
|
|
23
|
-
entry to be filled.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
def __init__(self, entry: Entry) -> None:
|
|
27
|
-
self.entry = entry
|
|
28
|
-
self.movie_id = None
|
|
29
|
-
self.ep_list = None
|
|
30
|
-
|
|
31
|
-
def get_entry(self) -> Entry:
|
|
32
|
-
"""
|
|
33
|
-
Returns the entry with which was
|
|
34
|
-
previously passed to this class.
|
|
35
|
-
"""
|
|
36
|
-
return self.entry
|
|
37
|
-
|
|
38
|
-
def _load_eps_list(self):
|
|
39
|
-
if self.ep_list:
|
|
40
|
-
return self.ep_list
|
|
41
|
-
|
|
42
|
-
if not self.movie_id:
|
|
43
|
-
r = requests.get(self.entry.category_url, timeout=2)
|
|
44
|
-
self.movie_id = re.search(
|
|
45
|
-
r'<input.+?value="(\d+)" id="movie_id"', r.text
|
|
46
|
-
).group(1)
|
|
47
|
-
|
|
48
|
-
res = requests.get(
|
|
49
|
-
"https://ajax.gogo-load.com/ajax/load-list-episode",
|
|
50
|
-
params={"ep_start": 0, "ep_end": 9999, "id": self.movie_id},
|
|
51
|
-
timeout=2,
|
|
52
|
-
)
|
|
53
|
-
|
|
54
|
-
response_err(res, res.url)
|
|
55
|
-
ep_list = [
|
|
56
|
-
{
|
|
57
|
-
"ep": re.search(
|
|
58
|
-
r"\d+([\.]\d+)?", x.find("div", attrs={"class": "name"}).text
|
|
59
|
-
).group(0),
|
|
60
|
-
"link": Config().gogoanime_url + x.find("a")["href"].strip(),
|
|
61
|
-
}
|
|
62
|
-
for x in BeautifulSoup(res.text, "html.parser").find_all("li")
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
ep_list.reverse()
|
|
66
|
-
|
|
67
|
-
self.ep_list = ep_list
|
|
68
|
-
|
|
69
|
-
return ep_list
|
|
70
|
-
|
|
71
|
-
def gen_eplink(self):
|
|
72
|
-
"""
|
|
73
|
-
Generate episode url
|
|
74
|
-
from ep and category url, will look something like this:
|
|
75
|
-
https://gogoanime.film/category/hyouka
|
|
76
|
-
to
|
|
77
|
-
https://gogoanime.film/hyouka-episode-1
|
|
78
|
-
"""
|
|
79
|
-
ep_list = self._load_eps_list()
|
|
80
|
-
|
|
81
|
-
filtered = list(filter(lambda x: x["ep"] == str(self.entry.ep), ep_list))
|
|
82
|
-
|
|
83
|
-
if not filtered:
|
|
84
|
-
error(f"Episode {self.entry.ep} does not exist.")
|
|
85
|
-
sys.exit()
|
|
86
|
-
|
|
87
|
-
self.entry.ep_url = filtered[0]["link"]
|
|
88
|
-
|
|
89
|
-
return self.entry
|
|
90
|
-
|
|
91
|
-
def get_special_list(self):
|
|
92
|
-
"""
|
|
93
|
-
Get List of Special Episodes (.5)
|
|
94
|
-
"""
|
|
95
|
-
|
|
96
|
-
ep_list = self._load_eps_list()
|
|
97
|
-
return list(filter(lambda x: re.match(r"^-?\d+(?:\.\d+)$", x["ep"]), ep_list))
|
|
98
|
-
|
|
99
|
-
def get_latest(self):
|
|
100
|
-
"""
|
|
101
|
-
Fetch latest episode avalible
|
|
102
|
-
from a show and return it.
|
|
103
|
-
"""
|
|
104
|
-
|
|
105
|
-
ep_list = self._load_eps_list()
|
|
106
|
-
|
|
107
|
-
if not ep_list:
|
|
108
|
-
self.entry.latest_ep = 0
|
|
109
|
-
return 0
|
|
110
|
-
else:
|
|
111
|
-
latest = ep_list[-1]["ep"]
|
|
112
|
-
self.entry.latest_ep = parsenum(latest)
|
|
113
|
-
return parsenum(latest)
|
|
114
|
-
|
|
115
|
-
def get_first(self):
|
|
116
|
-
ep_list = self._load_eps_list()
|
|
117
|
-
if not ep_list:
|
|
118
|
-
return 0
|
|
119
|
-
else:
|
|
120
|
-
return ep_list[0]["ep"]
|
|
121
|
-
|
|
122
|
-
def _do_prompt(self, prompt="Episode"):
|
|
123
|
-
ep_range = f" [{self.get_first()}-{self.get_latest()}]"
|
|
124
|
-
|
|
125
|
-
specials = self.get_special_list()
|
|
126
|
-
if specials:
|
|
127
|
-
ep_range += " Special Eps: "
|
|
128
|
-
ep_range += ", ".join([x["ep"] for x in specials])
|
|
129
|
-
|
|
130
|
-
return cinput(prompt, colors.GREEN, ep_range, colors.END, "\n>> ", input_color=colors.CYAN)
|
|
131
|
-
|
|
132
|
-
def _validate_ep(self, ep: str):
|
|
133
|
-
"""
|
|
134
|
-
See if Episode is in episode list.
|
|
135
|
-
Pass an arg to special to accept this
|
|
136
|
-
character even though it is not in the episode list.
|
|
137
|
-
"""
|
|
138
|
-
|
|
139
|
-
ep_list = self._load_eps_list()
|
|
140
|
-
|
|
141
|
-
is_in_list = bool(list(filter(lambda x: ep == x["ep"], ep_list)))
|
|
142
|
-
|
|
143
|
-
return is_in_list
|
|
144
|
-
|
|
145
|
-
def pick_ep(self):
|
|
146
|
-
"""
|
|
147
|
-
Cli function to pick an episode from 1 to
|
|
148
|
-
the latest available.
|
|
149
|
-
"""
|
|
150
|
-
|
|
151
|
-
self.get_latest()
|
|
152
|
-
|
|
153
|
-
while True:
|
|
154
|
-
which_episode = self._do_prompt()
|
|
155
|
-
try:
|
|
156
|
-
if self._validate_ep(which_episode):
|
|
157
|
-
self.entry.ep = parsenum(which_episode)
|
|
158
|
-
self.gen_eplink()
|
|
159
|
-
break
|
|
160
|
-
else:
|
|
161
|
-
error("Number out of range.")
|
|
162
|
-
|
|
163
|
-
except:
|
|
164
|
-
error("Invalid Input")
|
|
165
|
-
|
|
166
|
-
return self.entry
|
|
167
|
-
|
|
168
|
-
def pick_ep_seasonal(self):
|
|
169
|
-
"""
|
|
170
|
-
Cli function to pick an episode from 0 to
|
|
171
|
-
the latest available.
|
|
172
|
-
"""
|
|
173
|
-
|
|
174
|
-
self.get_latest()
|
|
175
|
-
|
|
176
|
-
while True:
|
|
177
|
-
which_episode = self._do_prompt(
|
|
178
|
-
"Last Episode you watched (put 0 to start at the beginning) "
|
|
179
|
-
)
|
|
180
|
-
try:
|
|
181
|
-
if self._validate_ep(which_episode) or int(which_episode) == 0:
|
|
182
|
-
self.entry.ep = int(which_episode)
|
|
183
|
-
if int(which_episode) != 0:
|
|
184
|
-
self.gen_eplink()
|
|
185
|
-
else:
|
|
186
|
-
self.entry.ep_url = None
|
|
187
|
-
break
|
|
188
|
-
else:
|
|
189
|
-
error("Number out of range.")
|
|
190
|
-
|
|
191
|
-
except:
|
|
192
|
-
error("Invalid Input")
|
|
193
|
-
|
|
194
|
-
return self.entry
|
|
195
|
-
|
|
196
|
-
def pick_range(self):
|
|
197
|
-
"""
|
|
198
|
-
Accept a range of episodes
|
|
199
|
-
and return it.
|
|
200
|
-
Input/output would be
|
|
201
|
-
something like this:
|
|
202
|
-
3-5 -> [3, 4, 5]
|
|
203
|
-
3 -> [3]
|
|
204
|
-
"""
|
|
205
|
-
self.entry.latest_ep = self.get_latest()
|
|
206
|
-
while True:
|
|
207
|
-
which_episode = self._do_prompt(prompt="Episode (Range with '-')")
|
|
208
|
-
|
|
209
|
-
which_episode = which_episode.split("-")
|
|
210
|
-
|
|
211
|
-
if len(which_episode) == 1:
|
|
212
|
-
try:
|
|
213
|
-
if self._validate_ep(which_episode[0]):
|
|
214
|
-
return which_episode
|
|
215
|
-
except:
|
|
216
|
-
error("invalid input")
|
|
217
|
-
elif len(which_episode) == 2:
|
|
218
|
-
try:
|
|
219
|
-
ep_list = self._load_eps_list()
|
|
220
|
-
|
|
221
|
-
first_index = ep_list.index(
|
|
222
|
-
list(filter(lambda x: x["ep"] == which_episode[0], ep_list))[0]
|
|
223
|
-
)
|
|
224
|
-
|
|
225
|
-
second_index = ep_list.index(
|
|
226
|
-
list(filter(lambda x: x["ep"] == which_episode[1], ep_list))[0]
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
ep_list = ep_list[first_index : second_index + 1]
|
|
230
|
-
|
|
231
|
-
if not ep_list:
|
|
232
|
-
error("invlid input1")
|
|
233
|
-
else:
|
|
234
|
-
return [x["ep"] for x in ep_list]
|
|
235
|
-
|
|
236
|
-
except Exception as e:
|
|
237
|
-
error(f"invalid input {e}")
|
|
238
|
-
else:
|
|
239
|
-
error("invalid input")
|
|
240
|
-
|
|
241
|
-
def next_ep(self):
|
|
242
|
-
"""
|
|
243
|
-
Increment ep and return the entry.
|
|
244
|
-
"""
|
|
245
|
-
self.get_latest()
|
|
246
|
-
if self.entry.ep == self.entry.latest_ep:
|
|
247
|
-
error("no more episodes")
|
|
248
|
-
return self.entry
|
|
249
|
-
else:
|
|
250
|
-
self.entry.ep += 1
|
|
251
|
-
self.gen_eplink()
|
|
252
|
-
return self.entry
|
|
253
|
-
|
|
254
|
-
def prev_ep(self):
|
|
255
|
-
"""
|
|
256
|
-
Decrement ep and return the entry.
|
|
257
|
-
"""
|
|
258
|
-
if self.entry.ep == 1:
|
|
259
|
-
error("no more episodes")
|
|
260
|
-
return self.entry
|
|
261
|
-
else:
|
|
262
|
-
self.entry.ep -= 1
|
|
263
|
-
self.gen_eplink()
|
|
264
|
-
return self.entry
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
class videourl:
|
|
268
|
-
"""
|
|
269
|
-
Class that fetches embed and
|
|
270
|
-
stream url.
|
|
271
|
-
"""
|
|
272
|
-
|
|
273
|
-
def __init__(self, entry: Entry, quality) -> None:
|
|
274
|
-
self.entry = entry
|
|
275
|
-
self.qual = quality.lower().strip("p")
|
|
276
|
-
self.session = requests.Session()
|
|
277
|
-
retry = Retry(connect=3, backoff_factor=0.5)
|
|
278
|
-
adapter = HTTPAdapter(max_retries=retry)
|
|
279
|
-
self.session.mount("http://", adapter)
|
|
280
|
-
self.session.mount("https://", adapter)
|
|
281
|
-
self.ajax_url = "/encrypt-ajax.php?"
|
|
282
|
-
self.enc_key_api = "https://raw.githubusercontent.com/justfoolingaround/animdl-provider-benchmarks/master/api/gogoanime.json"
|
|
283
|
-
self.mode = AES.MODE_CBC
|
|
284
|
-
self.size = AES.block_size
|
|
285
|
-
self.padder = "\x08\x0e\x03\x08\t\x03\x04\t"
|
|
286
|
-
self.pad = lambda s: s + chr(len(s) % 16) * (16 - len(s) % 16)
|
|
287
|
-
|
|
288
|
-
def get_entry(self) -> Entry:
|
|
289
|
-
"""
|
|
290
|
-
Returns the entry with stream and emebed url fields filled
|
|
291
|
-
which was previously passed to this class.
|
|
292
|
-
"""
|
|
293
|
-
return self.entry
|
|
294
|
-
|
|
295
|
-
def embed_url(self):
|
|
296
|
-
r = self.session.get(self.entry.ep_url)
|
|
297
|
-
response_err(r, self.entry.ep_url)
|
|
298
|
-
soup = BeautifulSoup(r.content, "html.parser")
|
|
299
|
-
link = soup.find("a", {"class": "active", "rel": "1"})
|
|
300
|
-
loc_err(link, self.entry.ep_url, "embed-url")
|
|
301
|
-
self.entry.embed_url = f'https:{link["data-video"]}'
|
|
302
|
-
|
|
303
|
-
@functools.lru_cache()
|
|
304
|
-
def get_enc_keys(self):
|
|
305
|
-
page = self.session.get(self.entry.embed_url).text
|
|
306
|
-
|
|
307
|
-
keys = re.findall(r"(?:container|videocontent)-(\d+)", page)
|
|
308
|
-
|
|
309
|
-
if not keys:
|
|
310
|
-
return {}
|
|
311
|
-
|
|
312
|
-
key, iv, second_key = keys
|
|
313
|
-
|
|
314
|
-
return {
|
|
315
|
-
"key": key.encode(),
|
|
316
|
-
"second_key": second_key.encode(),
|
|
317
|
-
"iv": iv.encode(),
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
def aes_encrypt(self, data, key, iv):
|
|
321
|
-
return base64.b64encode(
|
|
322
|
-
AES.new(key, self.mode, iv=iv).encrypt(self.pad(data).encode())
|
|
323
|
-
)
|
|
324
|
-
|
|
325
|
-
def aes_decrypt(self, data, key, iv):
|
|
326
|
-
return (
|
|
327
|
-
AES.new(key, self.mode, iv=iv)
|
|
328
|
-
.decrypt(base64.b64decode(data))
|
|
329
|
-
.strip(b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10")
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
def get_data(self):
|
|
333
|
-
r = self.session.get(self.entry.embed_url)
|
|
334
|
-
soup = BeautifulSoup(r.content, "html.parser")
|
|
335
|
-
crypto = soup.find("script", {"data-name": "episode"})
|
|
336
|
-
loc_err(crypto, self.entry.embed_url, "token")
|
|
337
|
-
return crypto["data-value"]
|
|
338
|
-
|
|
339
|
-
def stream_url(self):
|
|
340
|
-
"""
|
|
341
|
-
Fetches stream url and executes
|
|
342
|
-
quality function.
|
|
343
|
-
"""
|
|
344
|
-
if not self.entry.embed_url:
|
|
345
|
-
self.embed_url()
|
|
346
|
-
|
|
347
|
-
enc_keys = self.get_enc_keys()
|
|
348
|
-
|
|
349
|
-
parsed = urlparse(self.entry.embed_url)
|
|
350
|
-
self.ajax_url = parsed.scheme + "://" + parsed.netloc + self.ajax_url
|
|
351
|
-
|
|
352
|
-
data = self.aes_decrypt(
|
|
353
|
-
self.get_data(), enc_keys["key"], enc_keys["iv"]
|
|
354
|
-
).decode()
|
|
355
|
-
data = dict(parse_qsl(data))
|
|
356
|
-
|
|
357
|
-
id = urlparse(self.entry.embed_url).query
|
|
358
|
-
id = dict(parse_qsl(id))["id"]
|
|
359
|
-
enc_id = self.aes_encrypt(id, enc_keys["key"], enc_keys["iv"]).decode()
|
|
360
|
-
data.update(id=enc_id)
|
|
361
|
-
|
|
362
|
-
headers = {
|
|
363
|
-
"x-requested-with": "XMLHttpRequest",
|
|
364
|
-
"referer": self.entry.embed_url,
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
r = self.session.post(
|
|
368
|
-
self.ajax_url + urlencode(data) + f"&alias={id}",
|
|
369
|
-
headers=headers,
|
|
370
|
-
)
|
|
371
|
-
|
|
372
|
-
response_err(r, r.url)
|
|
373
|
-
|
|
374
|
-
json_resp = json.loads(
|
|
375
|
-
self.aes_decrypt(
|
|
376
|
-
r.json().get("data"), enc_keys["second_key"], enc_keys["iv"]
|
|
377
|
-
)
|
|
378
|
-
)
|
|
379
|
-
|
|
380
|
-
source_data = [x for x in json_resp["source"]]
|
|
381
|
-
self.quality(source_data)
|
|
382
|
-
|
|
383
|
-
def quality(self, json_data):
|
|
384
|
-
"""
|
|
385
|
-
Get quality options from
|
|
386
|
-
JSON repons and change
|
|
387
|
-
stream url to the either
|
|
388
|
-
the quality option that was picked,
|
|
389
|
-
or the best one avalible.
|
|
390
|
-
"""
|
|
391
|
-
self.entry.quality = ""
|
|
392
|
-
|
|
393
|
-
streams = []
|
|
394
|
-
for i in json_data:
|
|
395
|
-
if "m3u8" in i["file"] or i["type"] == "hls":
|
|
396
|
-
type = "hls"
|
|
397
|
-
else:
|
|
398
|
-
type = "mp4"
|
|
399
|
-
|
|
400
|
-
quality = i["label"].replace(" P", "").lower()
|
|
401
|
-
|
|
402
|
-
streams.append({"file": i["file"], "type": type, "quality": quality})
|
|
403
|
-
|
|
404
|
-
filtered_q_user = list(filter(lambda x: x["quality"] == self.qual, streams))
|
|
405
|
-
|
|
406
|
-
if filtered_q_user:
|
|
407
|
-
stream = list(filtered_q_user)[0]
|
|
408
|
-
elif self.qual == "best" or self.qual == None:
|
|
409
|
-
stream = streams[-1]
|
|
410
|
-
elif self.qual == "worst":
|
|
411
|
-
stream = streams[0]
|
|
412
|
-
else:
|
|
413
|
-
stream = streams[-1]
|
|
414
|
-
|
|
415
|
-
self.entry.quality = stream["quality"]
|
|
416
|
-
self.entry.stream_url = stream["file"]
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
def extract_m3u8_streams(uri):
|
|
420
|
-
if re.match(r"https?://", uri):
|
|
421
|
-
resp = requests.get(uri)
|
|
422
|
-
resp.raise_for_status()
|
|
423
|
-
raw_content = resp.content.decode(resp.encoding or "utf-8")
|
|
424
|
-
base_uri = urljoin(uri, ".")
|
|
425
|
-
else:
|
|
426
|
-
with open(uri) as fin:
|
|
427
|
-
raw_content = fin.read()
|
|
428
|
-
base_uri = Path(uri)
|
|
429
|
-
|
|
430
|
-
content = m3u8.M3U8(raw_content, base_uri=base_uri)
|
|
431
|
-
content.playlists.sort(key=lambda x: x.stream_info.bandwidth)
|
|
432
|
-
streams = []
|
|
433
|
-
for playlist in content.playlists:
|
|
434
|
-
streams.append(
|
|
435
|
-
{
|
|
436
|
-
"file": urljoin(content.base_uri, playlist.uri),
|
|
437
|
-
"type": "hls",
|
|
438
|
-
"quality": str(playlist.stream_info.resolution[1]),
|
|
439
|
-
}
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
return streams
|
anipy_cli/version.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "2.7.17"
|