gamdl 2.6__tar.gz → 2.6.2__tar.gz
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.
- {gamdl-2.6 → gamdl-2.6.2}/PKG-INFO +1 -1
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/__init__.py +1 -1
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/cli.py +17 -4
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/downloader.py +59 -26
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/downloader_music_video.py +4 -13
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/downloader_post.py +5 -5
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/downloader_song.py +4 -21
- gamdl-2.6.2/gamdl/exceptions.py +24 -0
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/models.py +3 -1
- {gamdl-2.6 → gamdl-2.6.2}/.github/FUNDING.yml +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/.github/workflows/main.yml +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/.gitignore +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/LICENSE +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/README.md +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/__main__.py +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/apple_music_api.py +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/config_file.py +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/constants.py +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/custom_logger_formatter.py +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/enums.py +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/hardcoded_wvd.py +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/itunes_api.py +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/gamdl/utils.py +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/pyproject.toml +0 -0
- {gamdl-2.6 → gamdl-2.6.2}/requirements.txt +0 -0
|
@@ -28,6 +28,7 @@ from .enums import (
|
|
|
28
28
|
SongCodec,
|
|
29
29
|
SyncedLyricsFormat,
|
|
30
30
|
)
|
|
31
|
+
from .exceptions import *
|
|
31
32
|
from .itunes_api import ItunesApi
|
|
32
33
|
from .utils import color_text, prompt_path
|
|
33
34
|
|
|
@@ -550,22 +551,26 @@ def main(
|
|
|
550
551
|
for url_index, url in enumerate(urls, start=1):
|
|
551
552
|
url_progress = color_text(f"URL {url_index}/{len(urls)}", colorama.Style.DIM)
|
|
552
553
|
try:
|
|
553
|
-
logger.info(f'({url_progress})
|
|
554
|
+
logger.info(f'({url_progress}) Processing "{url}"')
|
|
554
555
|
url_info = downloader.parse_url_info(url)
|
|
556
|
+
|
|
555
557
|
if not url_info:
|
|
556
558
|
error_count += 1
|
|
557
559
|
logger.error(f"({url_progress}) Invalid URL, skipping")
|
|
558
560
|
continue
|
|
561
|
+
|
|
559
562
|
download_queue = downloader.get_download_queue(url_info)
|
|
560
|
-
|
|
561
|
-
if not
|
|
563
|
+
|
|
564
|
+
if not download_queue:
|
|
562
565
|
error_count += 1
|
|
563
566
|
logger.error(f"({url_progress}) Media not found, skipping")
|
|
564
567
|
continue
|
|
568
|
+
|
|
569
|
+
download_queue_medias_metadata = download_queue.medias_metadata
|
|
565
570
|
except Exception as e:
|
|
566
571
|
error_count += 1
|
|
567
572
|
logger.error(
|
|
568
|
-
f'({url_progress}) Failed to
|
|
573
|
+
f'({url_progress}) Failed to process URL "{url}", skipping',
|
|
569
574
|
exc_info=not no_exceptions,
|
|
570
575
|
)
|
|
571
576
|
continue
|
|
@@ -619,6 +624,14 @@ def main(
|
|
|
619
624
|
)
|
|
620
625
|
except KeyboardInterrupt:
|
|
621
626
|
exit(0)
|
|
627
|
+
except (
|
|
628
|
+
MediaNotStreamableException,
|
|
629
|
+
MediaFileAlreadyExistsException,
|
|
630
|
+
MediaFormatNotAvailableException,
|
|
631
|
+
) as e:
|
|
632
|
+
logger.warning(
|
|
633
|
+
f"({queue_progress}) {e}, skipping",
|
|
634
|
+
)
|
|
622
635
|
except Exception as e:
|
|
623
636
|
error_count += 1
|
|
624
637
|
logger.error(
|
|
@@ -9,6 +9,7 @@ import re
|
|
|
9
9
|
import shutil
|
|
10
10
|
import subprocess
|
|
11
11
|
import typing
|
|
12
|
+
import urllib.parse
|
|
12
13
|
import uuid
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
@@ -45,7 +46,7 @@ class Downloader:
|
|
|
45
46
|
r"("
|
|
46
47
|
r"/(?P<storefront>[a-z]{2})"
|
|
47
48
|
r"/(?P<type>artist|album|playlist|song|music-video|post)"
|
|
48
|
-
r"(?:/(?P<slug>[
|
|
49
|
+
r"(?:/(?P<slug>[^\s/]+))?"
|
|
49
50
|
r"/(?P<id>[0-9]+|pl\.[0-9a-z]{32}|pl\.u-[a-zA-Z0-9]{15})"
|
|
50
51
|
r"(?:\?i=(?P<sub_id>[0-9]+))?"
|
|
51
52
|
r")|("
|
|
@@ -130,7 +131,7 @@ class Downloader:
|
|
|
130
131
|
|
|
131
132
|
def _set_temp_path(self):
|
|
132
133
|
random_suffix = uuid.uuid4().hex[:8]
|
|
133
|
-
self.
|
|
134
|
+
self.temp_path_generated = self.temp_path / f"gamdl_temp_{random_suffix}"
|
|
134
135
|
|
|
135
136
|
def _set_exclude_tags(self):
|
|
136
137
|
self.exclude_tags = self.exclude_tags if self.exclude_tags is not None else []
|
|
@@ -161,6 +162,8 @@ class Downloader:
|
|
|
161
162
|
self.cdm = Cdm.from_device(Device.loads(HARDCODED_WVD))
|
|
162
163
|
|
|
163
164
|
def parse_url_info(self, url: str) -> UrlInfo | None:
|
|
165
|
+
url = urllib.parse.unquote(url)
|
|
166
|
+
|
|
164
167
|
url_regex_result = re.search(
|
|
165
168
|
self.VALID_URL_RE,
|
|
166
169
|
url,
|
|
@@ -184,39 +187,70 @@ class Downloader:
|
|
|
184
187
|
url_type: str,
|
|
185
188
|
id: str,
|
|
186
189
|
is_library: bool,
|
|
187
|
-
) -> DownloadQueue:
|
|
190
|
+
) -> DownloadQueue | None:
|
|
188
191
|
download_queue = DownloadQueue()
|
|
192
|
+
|
|
189
193
|
if url_type == "artist":
|
|
190
194
|
artist = self.apple_music_api.get_artist(id)
|
|
195
|
+
|
|
196
|
+
if artist is None:
|
|
197
|
+
return None
|
|
198
|
+
|
|
191
199
|
download_queue.medias_metadata = list(
|
|
192
200
|
self.get_download_queue_from_artist(artist)
|
|
193
201
|
)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
202
|
+
|
|
203
|
+
if url_type == "song":
|
|
204
|
+
song = self.apple_music_api.get_song(id)
|
|
205
|
+
|
|
206
|
+
if song is None:
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
download_queue.medias_metadata = [song]
|
|
210
|
+
|
|
211
|
+
if url_type in {"album", "albums"}:
|
|
197
212
|
if is_library:
|
|
198
213
|
album = self.apple_music_api.get_library_album(id)
|
|
199
214
|
else:
|
|
200
215
|
album = self.apple_music_api.get_album(id)
|
|
216
|
+
|
|
217
|
+
if album is None:
|
|
218
|
+
return None
|
|
219
|
+
|
|
201
220
|
download_queue.medias_metadata = [
|
|
202
221
|
track for track in album["relationships"]["tracks"]["data"]
|
|
203
222
|
]
|
|
204
|
-
|
|
223
|
+
|
|
224
|
+
if url_type == "playlist":
|
|
205
225
|
if is_library:
|
|
206
226
|
playlist = self.apple_music_api.get_library_playlist(id)
|
|
207
|
-
download_queue.medias_metadata = [
|
|
208
|
-
track for track in playlist["relationships"]["tracks"]["data"]
|
|
209
|
-
]
|
|
210
227
|
else:
|
|
211
228
|
playlist = self.apple_music_api.get_playlist(id)
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
229
|
+
|
|
230
|
+
if playlist is None:
|
|
231
|
+
return None
|
|
232
|
+
|
|
233
|
+
download_queue.medias_metadata = [
|
|
234
|
+
track for track in playlist["relationships"]["tracks"]["data"]
|
|
235
|
+
]
|
|
215
236
|
download_queue.playlist_attributes = playlist["attributes"]
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
237
|
+
|
|
238
|
+
if url_type == "music-video":
|
|
239
|
+
music_video = self.apple_music_api.get_music_video(id)
|
|
240
|
+
|
|
241
|
+
if music_video is None:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
download_queue.medias_metadata = [music_video]
|
|
245
|
+
|
|
246
|
+
if url_type == "post":
|
|
247
|
+
post = self.apple_music_api.get_post(id)
|
|
248
|
+
|
|
249
|
+
if post is None:
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
download_queue.medias_metadata = [post]
|
|
253
|
+
|
|
220
254
|
return download_queue
|
|
221
255
|
|
|
222
256
|
def get_download_queue_from_artist(
|
|
@@ -474,7 +508,7 @@ class Downloader:
|
|
|
474
508
|
tag: str,
|
|
475
509
|
file_extension: str,
|
|
476
510
|
):
|
|
477
|
-
temp_path = self.
|
|
511
|
+
temp_path = self.temp_path_generated / (f"{media_id}_{tag}" + file_extension)
|
|
478
512
|
return temp_path
|
|
479
513
|
|
|
480
514
|
def get_final_path(
|
|
@@ -639,9 +673,12 @@ class Downloader:
|
|
|
639
673
|
encoding="utf8",
|
|
640
674
|
)
|
|
641
675
|
|
|
642
|
-
def cleanup_temp_path(self):
|
|
643
|
-
if self.
|
|
644
|
-
|
|
676
|
+
def cleanup_temp_path(self, override_skip_processing_check: bool = False) -> None:
|
|
677
|
+
if self.skip_processing and not override_skip_processing_check:
|
|
678
|
+
return
|
|
679
|
+
|
|
680
|
+
if self.temp_path_generated.exists():
|
|
681
|
+
shutil.rmtree(self.temp_path_generated)
|
|
645
682
|
|
|
646
683
|
def _final_processing(
|
|
647
684
|
self,
|
|
@@ -705,11 +742,7 @@ class Downloader:
|
|
|
705
742
|
download_info.synced_lyrics_path,
|
|
706
743
|
download_info.lyrics.synced,
|
|
707
744
|
)
|
|
708
|
-
if
|
|
709
|
-
download_info.playlist_tags
|
|
710
|
-
and self.save_playlist
|
|
711
|
-
and download_info.staged_path
|
|
712
|
-
):
|
|
745
|
+
if download_info.playlist_tags and self.save_playlist:
|
|
713
746
|
playlist_file_path = self.get_playlist_file_path(
|
|
714
747
|
download_info.playlist_tags
|
|
715
748
|
)
|
|
@@ -18,6 +18,7 @@ from .enums import (
|
|
|
18
18
|
RemuxFormatMusicVideo,
|
|
19
19
|
RemuxMode,
|
|
20
20
|
)
|
|
21
|
+
from .exceptions import *
|
|
21
22
|
from .models import (
|
|
22
23
|
DecryptionKeyAv,
|
|
23
24
|
DownloadInfo,
|
|
@@ -487,11 +488,7 @@ class DownloaderMusicVideo:
|
|
|
487
488
|
colored_media_id = color_text(media_id, colorama.Style.DIM)
|
|
488
489
|
|
|
489
490
|
if not self.downloader.is_media_streamable(media_metadata):
|
|
490
|
-
|
|
491
|
-
f"[{colored_media_id}] "
|
|
492
|
-
"Music Video is not streamable or downloadable, skipping"
|
|
493
|
-
)
|
|
494
|
-
return download_info
|
|
491
|
+
raise MediaNotStreamableException()
|
|
495
492
|
|
|
496
493
|
alt_media_id = self.get_music_video_id_alt(media_metadata) or media_id
|
|
497
494
|
download_info.alt_media_id = alt_media_id
|
|
@@ -519,10 +516,7 @@ class DownloaderMusicVideo:
|
|
|
519
516
|
logger.debug(f"[{colored_media_id}] Getting stream info")
|
|
520
517
|
stream_info = self.get_stream_info_from_webplayback(webplayback)
|
|
521
518
|
if not stream_info:
|
|
522
|
-
|
|
523
|
-
f"[{colored_media_id}] Video/Audio stream with the selected codec(s) not found, skipping"
|
|
524
|
-
)
|
|
525
|
-
return download_info
|
|
519
|
+
raise MediaFormatNotAvailableException()
|
|
526
520
|
download_info.stream_info = stream_info
|
|
527
521
|
|
|
528
522
|
final_path = self.downloader.get_final_path(
|
|
@@ -543,10 +537,7 @@ class DownloaderMusicVideo:
|
|
|
543
537
|
download_info.cover_path = cover_path
|
|
544
538
|
|
|
545
539
|
if final_path.exists() and not self.downloader.overwrite:
|
|
546
|
-
|
|
547
|
-
f'[{colored_media_id}] Music Video already exists at "{final_path}", skipping'
|
|
548
|
-
)
|
|
549
|
-
return download_info
|
|
540
|
+
raise MediaFileAlreadyExistsException(final_path)
|
|
550
541
|
|
|
551
542
|
logger.debug(f"[{colored_media_id}] Getting decryption key")
|
|
552
543
|
decryption_key = self.get_decryption_key(
|
|
@@ -9,6 +9,7 @@ from InquirerPy.base.control import Choice
|
|
|
9
9
|
|
|
10
10
|
from .downloader import Downloader
|
|
11
11
|
from .enums import PostQuality
|
|
12
|
+
from .exceptions import MediaFileAlreadyExistsException, MediaNotStreamableException
|
|
12
13
|
from .models import DownloadInfo, MediaTags
|
|
13
14
|
from .utils import color_text
|
|
14
15
|
|
|
@@ -121,11 +122,7 @@ class DownloaderPost:
|
|
|
121
122
|
colored_media_id = color_text(media_id, colorama.Style.DIM)
|
|
122
123
|
|
|
123
124
|
if not self.downloader.is_media_streamable(media_metadata):
|
|
124
|
-
|
|
125
|
-
f"[{colored_media_id}] "
|
|
126
|
-
"Post Video is not streamable or downloadable, skipping"
|
|
127
|
-
)
|
|
128
|
-
return download_info
|
|
125
|
+
raise MediaNotStreamableException()
|
|
129
126
|
|
|
130
127
|
tags = self.get_tags(media_metadata)
|
|
131
128
|
final_path = self.downloader.get_final_path(
|
|
@@ -136,6 +133,9 @@ class DownloaderPost:
|
|
|
136
133
|
download_info.tags = tags
|
|
137
134
|
download_info.final_path = final_path
|
|
138
135
|
|
|
136
|
+
if final_path.exists() and not self.downloader.overwrite:
|
|
137
|
+
raise MediaFileAlreadyExistsException(final_path)
|
|
138
|
+
|
|
139
139
|
cover_url = self.downloader.get_cover_url(media_metadata)
|
|
140
140
|
cover_format = self.downloader.get_cover_format(cover_url)
|
|
141
141
|
if cover_format and self.downloader.save_cover:
|
|
@@ -19,6 +19,7 @@ from pywidevine.license_protocol_pb2 import WidevinePsshData
|
|
|
19
19
|
|
|
20
20
|
from .downloader import Downloader
|
|
21
21
|
from .enums import MediaFileFormat, RemuxMode, SongCodec, SyncedLyricsFormat
|
|
22
|
+
from .exceptions import *
|
|
22
23
|
from .models import (
|
|
23
24
|
DecryptionKey,
|
|
24
25
|
DecryptionKeyAv,
|
|
@@ -643,18 +644,7 @@ class DownloaderSong:
|
|
|
643
644
|
colored_media_id = color_text(media_id, colorama.Style.DIM)
|
|
644
645
|
|
|
645
646
|
if not self.downloader.is_media_streamable(media_metadata):
|
|
646
|
-
|
|
647
|
-
f"[{colored_media_id}] "
|
|
648
|
-
"Song is not streamable or downloadable, skipping"
|
|
649
|
-
)
|
|
650
|
-
return download_info
|
|
651
|
-
|
|
652
|
-
if not self.downloader.is_media_streamable(media_metadata):
|
|
653
|
-
logger.warning(
|
|
654
|
-
f"[{colored_media_id}] "
|
|
655
|
-
"Track is not streamable or downloadable, skipping"
|
|
656
|
-
)
|
|
657
|
-
return download_info
|
|
647
|
+
raise MediaNotStreamableException()
|
|
658
648
|
|
|
659
649
|
logger.debug(f"[{colored_media_id}] Getting lyrics")
|
|
660
650
|
lyrics = self.get_lyrics(media_metadata)
|
|
@@ -695,10 +685,7 @@ class DownloaderSong:
|
|
|
695
685
|
download_info.cover_path = cover_path
|
|
696
686
|
|
|
697
687
|
if final_path.exists() and not self.downloader.overwrite:
|
|
698
|
-
|
|
699
|
-
f'[{colored_media_id}] Song already exists at "{final_path}", skipping'
|
|
700
|
-
)
|
|
701
|
-
return download_info
|
|
688
|
+
raise MediaFileAlreadyExistsException(final_path)
|
|
702
689
|
|
|
703
690
|
logger.debug(f"[{colored_media_id}] Getting stream info")
|
|
704
691
|
if self.codec.is_legacy():
|
|
@@ -713,11 +700,7 @@ class DownloaderSong:
|
|
|
713
700
|
else:
|
|
714
701
|
stream_info = self.get_stream_info(media_metadata)
|
|
715
702
|
if not stream_info or not stream_info.audio_track.widevine_pssh:
|
|
716
|
-
|
|
717
|
-
f"[{colored_media_id}] Song is not downloadable or is not "
|
|
718
|
-
"available in the selected codec, skipping",
|
|
719
|
-
)
|
|
720
|
-
return download_info
|
|
703
|
+
raise MediaFormatNotAvailableException()
|
|
721
704
|
logger.debug(f"[{colored_media_id}] Getting decryption key")
|
|
722
705
|
decryption_key = self.get_decryption_key(
|
|
723
706
|
stream_info,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MediaNotStreamableException(Exception):
|
|
7
|
+
DEFAULT_MESSAGE = "Media is not streamable"
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
super().__init__(self.DEFAULT_MESSAGE)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MediaFileAlreadyExistsException(Exception):
|
|
14
|
+
DEFAULT_MESSAGE = "Media file already exists at '{media_path}'"
|
|
15
|
+
|
|
16
|
+
def __init__(self, media_path: Path):
|
|
17
|
+
super().__init__(self.DEFAULT_MESSAGE.format(media_path=media_path))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class MediaFormatNotAvailableException(Exception):
|
|
21
|
+
DEFAULT_MESSAGE = "Requested media format or codec not available"
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
super().__init__(self.DEFAULT_MESSAGE)
|
|
@@ -118,6 +118,8 @@ class MediaTags:
|
|
|
118
118
|
date_mp4 = self.date.strftime(date_format)
|
|
119
119
|
elif isinstance(self.date, str):
|
|
120
120
|
date_mp4 = self.date
|
|
121
|
+
else:
|
|
122
|
+
date_mp4 = None
|
|
121
123
|
|
|
122
124
|
mp4_tags = {
|
|
123
125
|
"\xa9alb": [self.album],
|
|
@@ -133,7 +135,7 @@ class MediaTags:
|
|
|
133
135
|
"cmID": [self.composer_id],
|
|
134
136
|
"soco": [self.composer_sort],
|
|
135
137
|
"cprt": [self.copyright],
|
|
136
|
-
"\xa9day": date_mp4,
|
|
138
|
+
"\xa9day": [date_mp4],
|
|
137
139
|
"disk": disc_mp4,
|
|
138
140
|
"pgap": [bool(self.gapless) if self.gapless is not None else None],
|
|
139
141
|
"\xa9gen": [self.genre],
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|