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.

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.dev0.dist-info/METADATA +67 -0
  24. anipy_cli-3.0.0.dev0.dist-info/RECORD +26 -0
  25. {anipy_cli-2.7.31.dist-info → anipy_cli-3.0.0.dev0.dist-info}/WHEEL +1 -2
  26. anipy_cli-3.0.0.dev0.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 -35
  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_control.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.31.dist-info/LICENSE +0 -674
  59. anipy_cli-2.7.31.dist-info/METADATA +0 -162
  60. anipy_cli-2.7.31.dist-info/RECORD +0 -43
  61. anipy_cli-2.7.31.dist-info/entry_points.txt +0 -2
  62. anipy_cli-2.7.31.dist-info/top_level.txt +0 -1
anipy_cli/download.py DELETED
@@ -1,467 +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__(
28
- self, entry, quality, ffmpeg=False, dl_path: Path = None, file_name_format=""
29
- ) -> None:
30
- try:
31
- self.quality = int(quality)
32
- except ValueError:
33
- self.quality = quality
34
- self.is_audio = None
35
- self.content_audio_media = None
36
- self._m3u8_content = None
37
- self.session = None
38
- self.entry = entry
39
- self.ffmpeg = ffmpeg
40
- self.dl_path = dl_path
41
- self.file_name_format = file_name_format
42
- if dl_path is None:
43
- self.dl_path = Config().download_folder_path
44
- self.headers = {
45
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
46
- "referer": self.entry.embed_url,
47
- }
48
-
49
- def download(self):
50
- show_name = self._get_valid_pathname(self.entry.show_name)
51
- self.show_folder = self.dl_path / f"{show_name}"
52
-
53
- if Config().download_remove_dub_from_folder_name:
54
- if show_name.endswith(" (Dub)"):
55
- self.show_folder = self.dl_path / f"{show_name[:-6]}"
56
- print(self.show_folder)
57
-
58
- self.dl_path.mkdir(exist_ok=True, parents=True)
59
- self.show_folder.mkdir(exist_ok=True)
60
- self.session = requests.Session()
61
- retry = Retry(connect=3, backoff_factor=0.5)
62
- adapter = HTTPAdapter(max_retries=retry)
63
- self.session.mount("http://", adapter)
64
- self.session.mount("https://", adapter)
65
- self.session.headers.update(self.headers)
66
-
67
- fname = self._get_fname()
68
- dl_path = self.show_folder / fname
69
-
70
- if dl_path.is_file():
71
- print("-" * 20)
72
- cprint(
73
- colors.GREEN,
74
- "Skipping Already Existing: ",
75
- colors.RED,
76
- f"{self.entry.show_name} EP: {self.entry.ep} - {self.entry.quality}",
77
- )
78
- return dl_path
79
-
80
- print("-" * 20)
81
- cprint(
82
- colors.CYAN,
83
- "Downloading: ",
84
- colors.RED,
85
- f"{self.entry.show_name} EP: {self.entry.ep} - {self.entry.quality}",
86
- )
87
-
88
- if "m3u8" in self.entry.stream_url:
89
- cprint(colors.CYAN, "Type: ", colors.RED, "m3u8")
90
- if self.ffmpeg or Config().ffmpeg_hls:
91
- cprint(colors.CYAN, "Downloader: ", colors.RED, "ffmpeg")
92
- self.ffmpeg_dl()
93
- return dl_path
94
-
95
- cprint(colors.CYAN, "Downloader:", colors.RED, "internal")
96
- self.multithread_m3u8_dl()
97
- elif "mp4" in self.entry.stream_url:
98
- cprint(colors.CYAN, "Type: ", colors.RED, "mp4")
99
- self.mp4_dl(self.entry.stream_url)
100
-
101
- return dl_path
102
-
103
- def ffmpeg_dl(self):
104
- Config().user_files_path.mkdir(exist_ok=True, parents=True)
105
- Config().ffmpeg_log_path.mkdir(exist_ok=True, parents=True)
106
- fname = self._get_fname()
107
-
108
- dl_path = self.show_folder / fname
109
-
110
- ffmpeg_process = FfmpegProcess(
111
- [
112
- "ffmpeg",
113
- "-headers",
114
- f"referer:{self.entry.embed_url}",
115
- "-i",
116
- self.entry.stream_url,
117
- "-vcodec",
118
- "copy",
119
- "-acodec",
120
- "copy",
121
- "-scodec",
122
- "mov_text",
123
- "-c",
124
- "copy",
125
- str(dl_path),
126
- ]
127
- )
128
-
129
- try:
130
- ffmpeg_process.run(
131
- ffmpeg_output_file=str(
132
- Config().ffmpeg_log_path / fname.replace("mp4", "log")
133
- )
134
- )
135
- cprint(colors.CYAN, "Download finished.")
136
- except KeyboardInterrupt:
137
- error("interrupted deleting partially downloaded file")
138
- fname.unlink()
139
-
140
- def ffmpeg_merge(self, input_file, audio_input_file):
141
- Config().user_files_path.mkdir(exist_ok=True, parents=True)
142
- Config().ffmpeg_log_path.mkdir(exist_ok=True, parents=True)
143
- fname = self._get_fname()
144
-
145
- dl_path = self.show_folder / fname
146
-
147
- merged_video_ts = self.merge_ts_files(input_file)
148
- merged_audio_ts = None
149
- if audio_input_file:
150
- merged_audio_ts = self.merge_ts_files(audio_input_file, "_audio")
151
-
152
- try:
153
- cprint(colors.CYAN, "Merging Parts using Movie.py...")
154
- if audio_input_file:
155
- ffmpeg_tools.ffmpeg_merge_video_audio(
156
- merged_video_ts,
157
- merged_audio_ts,
158
- str(dl_path),
159
- vcodec="copy",
160
- acodec="copy",
161
- ffmpeg_output=False,
162
- logger="bar",
163
- )
164
- else:
165
- ffmpeg_tools.ffmpeg_merge_video_audio(
166
- merged_video_ts,
167
- merged_video_ts,
168
- str(dl_path),
169
- vcodec="copy",
170
- acodec="copy",
171
- ffmpeg_output=False,
172
- logger="bar",
173
- )
174
-
175
- cprint(colors.CYAN, "Merge finished.")
176
- except KeyboardInterrupt:
177
- error("interrupted deleting partially downloaded file")
178
- fname.unlink()
179
-
180
- def merge_ts_files(self, input_file, suffix=""):
181
- filename = f"{self.temp_folder}/{self._get_valid_pathname(self.entry.show_name)}_{self.entry.ep}_merged{suffix}.ts"
182
- # Parse playlist for filenames with ending .ts and put them into the list ts_filenames
183
- with open(input_file, "r") as playlist:
184
- ts_filenames = [
185
- line.rstrip() + f"{suffix}"
186
- for line in playlist
187
- if not line.lstrip().startswith("#")
188
- ]
189
- # open one ts_file from the list after another and append them to merged.ts
190
- with open(filename, "wb") as merged:
191
- for ts_file in ts_filenames:
192
- with open(ts_file, "rb") as mergefile:
193
- shutil.copyfileobj(mergefile, merged)
194
- return filename
195
-
196
- def mp4_dl(self, dl_link):
197
- """
198
-
199
- :param dl_link:
200
- :type dl_link:
201
- :return:
202
- :rtype:
203
- """
204
- r = self.session.get(dl_link, headers=self.headers, stream=True)
205
- response_err(r, dl_link)
206
- total = int(r.headers.get("content-length", 0))
207
- fname = self.show_folder / self._get_fname()
208
- try:
209
- with fname.open("wb") as out_file, tqdm(
210
- desc=self.entry.show_name,
211
- total=total,
212
- unit="iB",
213
- unit_scale=True,
214
- unit_divisor=1024,
215
- ) as bar:
216
- for data in r.iter_content(chunk_size=1024):
217
- size = out_file.write(data)
218
- bar.update(size)
219
- except KeyboardInterrupt:
220
- error("interrupted deleting partially downloaded file")
221
- fname.unlink()
222
-
223
- cprint(colors.CYAN, "Download finished.")
224
-
225
- def download_ts(self, m3u8_segments, retry=0):
226
- self.counter += 1
227
- audio_suffix = ""
228
- uri = urllib.parse.urljoin(m3u8_segments.base_uri, m3u8_segments.uri)
229
- if not self._is_url(uri):
230
- input(f"uri: {uri} is not an uri")
231
- return
232
-
233
- if self.is_audio:
234
- audio_suffix = "audio"
235
- filename = self._get_filename(uri, self.temp_folder, audio_suffix)
236
- headers = self.headers
237
- retry_count = 0
238
- while not Path(filename).is_file() and retry_count < 20:
239
- cprint(
240
- colors.CYAN,
241
- f"Downloading {audio_suffix} Part: {self.counter}/{self.segment_count}",
242
- end="",
243
- )
244
- print("\r", end="")
245
-
246
- try:
247
- with self.session.get(
248
- uri, timeout=10, headers=headers, stream=False
249
- ) as response:
250
- if response.status_code == 416:
251
- return
252
-
253
- response.raise_for_status()
254
-
255
- with open(filename, "wb") as fout:
256
- fout.write(response.content)
257
-
258
- except Exception as e:
259
- exit(e.__str__())
260
- retry_count += 1
261
-
262
- def multithread_m3u8_dl(self):
263
- """
264
- Multithread download
265
- function for m3u8 links.
266
- - Creates show and temp folder
267
- - Starts ThreadPoolExecutor instance
268
- and downloads all ts links
269
- - Merges ts files
270
- - Deletes temp folder
271
-
272
- :return:
273
- :rtype:
274
- """
275
-
276
- self.temp_folder = self.show_folder / f"{self.entry.ep}_temp"
277
- self.temp_folder.mkdir(exist_ok=True)
278
- self.counter = 0
279
-
280
- self._m3u8_content = self._download_m3u8(
281
- self.entry.stream_url, 10, self.headers
282
- )
283
-
284
- assert self._m3u8_content.is_variant is False
285
-
286
- try:
287
- if self.content_audio_media and not self.content_audio_media.is_variant:
288
- self.segment_count = len(self.content_audio_media.segments)
289
- self.is_audio = True
290
- with ThreadPoolExecutor(12) as pool_audio:
291
- pool_audio.map(self.download_ts, self.content_audio_media.segments)
292
- self.is_audio = False
293
- self.counter = 0
294
- self.segment_count = len(self._m3u8_content.segments)
295
- print("\n")
296
- with ThreadPoolExecutor(12) as pool_video:
297
- pool_video.map(self.download_ts, self._m3u8_content.segments)
298
- except KeyboardInterrupt:
299
- shutil.rmtree(self.temp_folder)
300
- keyboard_inter()
301
- exit()
302
-
303
- input_file = self._dump_m3u8(self._m3u8_content)
304
- audio_input_file = None
305
- if self.content_audio_media and not self.content_audio_media.is_variant:
306
- self.is_audio = True
307
- audio_input_file = self._dump_m3u8(self.content_audio_media)
308
-
309
- cprint("\n", colors.CYAN, "Parts Downloaded")
310
- try:
311
- self.ffmpeg_merge(input_file, audio_input_file)
312
- except FileNotFoundError:
313
- # This restarts the download if a file is missing
314
- error("Missing a download part, restarting download")
315
- return self.multithread_m3u8_dl()
316
-
317
- cprint("\n", colors.CYAN, "Parts Merged")
318
- shutil.rmtree(self.temp_folder)
319
-
320
- def _download_m3u8(self, uri, timeout, headers, is_audio=False):
321
- if self._is_url(uri):
322
- resp = self.session.get(uri, timeout=timeout, headers=self.headers)
323
- resp.raise_for_status()
324
- raw_content = resp.content.decode(resp.encoding or "utf-8")
325
- base_uri = urllib.parse.urljoin(uri, ".")
326
- else:
327
- with open(uri) as fin:
328
- raw_content = fin.read()
329
- base_uri = Path(uri)
330
- content = m3u8.M3U8(raw_content, base_uri=base_uri)
331
- if content.is_variant:
332
- if self.content_audio_media is not None:
333
- content.add_media(media=self.content_audio_media)
334
-
335
- # sort
336
- content.playlists.sort(key=lambda x: x.stream_info.bandwidth, reverse=True)
337
-
338
- selected_index = 0
339
- if self.quality == "worst":
340
- selected_index = len(content.playlists) - 1
341
-
342
- for index, playlist in enumerate(content.playlists):
343
- cprint(
344
- colors.GREEN,
345
- "Playlist Index: ",
346
- colors.RED,
347
- index,
348
- "\n",
349
- colors.GREEN,
350
- "Resolution at this index: ",
351
- colors.RED,
352
- playlist.stream_info.resolution,
353
- "\n\n",
354
- )
355
-
356
- if self.quality in playlist.stream_info.resolution:
357
- selected_index = index
358
-
359
- try:
360
- for media in content.media:
361
- if content.playlists[selected_index].stream_info.audio in str(
362
- media
363
- ):
364
- self.content_audio_media = media
365
-
366
- chosen_uri = content.playlists[selected_index].uri
367
- if not self._is_url(chosen_uri):
368
- chosen_uri = urllib.parse.urljoin(content.base_uri, chosen_uri)
369
- if self.content_audio_media is not None:
370
- media_uri = self.content_audio_media.uri
371
- self.content_audio_media = self._download_m3u8(
372
- media_uri, timeout, headers, True
373
- )
374
- cprint(
375
- colors.GREEN,
376
- "Quality for Download:",
377
- colors.YELLOW,
378
- content.playlists[selected_index].stream_info.resolution,
379
- )
380
- return self._download_m3u8(chosen_uri, timeout, headers)
381
-
382
- except (ValueError, IndexError):
383
- exit("Failed to get stream for chosen quality")
384
-
385
- else:
386
- self._download_key(content)
387
-
388
- return content
389
-
390
- def _dump_m3u8(self, content):
391
- audio_suffix = ""
392
- for index, segment in enumerate(content.segments):
393
- content.segments[index].uri = self._get_filename(
394
- segment.uri, self.temp_folder
395
- )
396
-
397
- if self.is_audio:
398
- audio_suffix = "_audio"
399
- filename = self._get_filename(f"master{audio_suffix}.m3u8", self.temp_folder)
400
- content.dump(filename)
401
- return filename
402
-
403
- def _download_key(self, content):
404
- for key in content.keys:
405
- if key:
406
- uri = key.absolute_uri
407
- filename = self._get_filename(uri, self.temp_folder)
408
-
409
- with self.session.get(
410
- uri, timeout=10, headers=self.headers
411
- ) as response:
412
- response.raise_for_status()
413
- with open(filename, "wb") as fout:
414
- fout.write(response.content)
415
-
416
- key.uri = filename.__str__().replace(
417
- "\\", "/"
418
- ) # ffmpeg error when using \\ in windows
419
-
420
- def _get_fname(self) -> str:
421
- """
422
- This function returns what the filename for the outputted video should be.
423
-
424
- It finds this by using data in self.entry and the Config.
425
-
426
- Returns a string which should be the filename.
427
- """
428
-
429
- show_name = self._get_valid_pathname(self.entry.show_name)
430
-
431
- file_format = (
432
- self.file_name_format
433
- if self.file_name_format
434
- else Config().download_name_format
435
- )
436
-
437
- return file_format.format(
438
- show_name=show_name,
439
- episode_number=self.entry.ep,
440
- quality=self.entry.quality,
441
- )
442
-
443
- @staticmethod
444
- def _get_valid_pathname(name: str):
445
- WIN_INVALID_CHARS = ["\\", "/", ":", "*", "?", "<", ">", "|", '"']
446
-
447
- if sys.platform == "win32":
448
- name = "".join(["" if x in WIN_INVALID_CHARS else x for x in name])
449
-
450
- name = "".join(
451
- [i for i in name if i.isascii()]
452
- ) # Verify all chars are ascii (eject if not)
453
- name = "-".join(name.split()) # Clean all white spaces, including tabs and such
454
-
455
- return name
456
-
457
- @staticmethod
458
- def _is_url(uri):
459
- return re.match(r"https?://", uri) is not None
460
-
461
- @staticmethod
462
- def _get_filename(uri, directory, suffix=""):
463
- if suffix:
464
- suffix = f"_{suffix}"
465
- basename = urllib.parse.urlparse(uri).path.split("/")[-1]
466
- filename = Path("{}/{}{}".format(directory, basename, suffix)).__str__()
467
- 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()