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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gamdl
3
- Version: 2.6
3
+ Version: 2.6.2
4
4
  Summary: A command-line app for downloading Apple Music songs, music videos and post videos.
5
5
  Author: glomatico
6
6
  Requires-Python: >=3.10
@@ -5,4 +5,4 @@ from .downloader_post import DownloaderPost
5
5
  from .downloader_song import DownloaderSong
6
6
  from .itunes_api import ItunesApi
7
7
 
8
- __version__ = "2.6"
8
+ __version__ = "2.6.2"
@@ -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}) Checking "{url}"')
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
- download_queue_medias_metadata = download_queue.medias_metadata
561
- if not download_queue_medias_metadata[0]:
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 check "{url}"',
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>[a-z0-9-]+))?"
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.temp_path = self.temp_path / f"gamdl_temp_{random_suffix}"
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
- elif url_type == "song":
195
- download_queue.medias_metadata = [self.apple_music_api.get_song(id)]
196
- elif url_type in {"album", "albums"}:
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
- elif url_type == "playlist":
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
- download_queue.medias_metadata = [
213
- track for track in playlist["relationships"]["tracks"]["data"]
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
- elif url_type == "music-video":
217
- download_queue.medias_metadata = [self.apple_music_api.get_music_video(id)]
218
- elif url_type == "post":
219
- download_queue.medias_metadata = [self.apple_music_api.get_post(id)]
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.temp_path / (f"{media_id}_{tag}" + file_extension)
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.temp_path.exists() and not self.skip_processing:
644
- shutil.rmtree(self.temp_path)
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
- logger.warning(
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
- logger.warning(
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
- logger.warning(
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
- logger.warning(
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
- logger.warning(
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
- logger.warning(
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
- logger.warning(
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