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
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"