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