musicdl 2.1.11__py3-none-any.whl → 2.7.3__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.
- musicdl/__init__.py +5 -5
- musicdl/modules/__init__.py +10 -3
- musicdl/modules/common/__init__.py +2 -0
- musicdl/modules/common/gdstudio.py +204 -0
- musicdl/modules/js/__init__.py +1 -0
- musicdl/modules/js/youtube/__init__.py +2 -0
- musicdl/modules/js/youtube/botguard.js +1 -0
- musicdl/modules/js/youtube/jsinterp.py +902 -0
- musicdl/modules/js/youtube/runner.js +2 -0
- musicdl/modules/sources/__init__.py +41 -10
- musicdl/modules/sources/apple.py +207 -0
- musicdl/modules/sources/base.py +256 -28
- musicdl/modules/sources/bilibili.py +118 -0
- musicdl/modules/sources/buguyy.py +148 -0
- musicdl/modules/sources/fangpi.py +153 -0
- musicdl/modules/sources/fivesing.py +108 -0
- musicdl/modules/sources/gequbao.py +148 -0
- musicdl/modules/sources/jamendo.py +108 -0
- musicdl/modules/sources/joox.py +104 -68
- musicdl/modules/sources/kugou.py +129 -76
- musicdl/modules/sources/kuwo.py +188 -68
- musicdl/modules/sources/lizhi.py +107 -0
- musicdl/modules/sources/migu.py +172 -66
- musicdl/modules/sources/mitu.py +140 -0
- musicdl/modules/sources/mp3juice.py +264 -0
- musicdl/modules/sources/netease.py +163 -115
- musicdl/modules/sources/qianqian.py +125 -77
- musicdl/modules/sources/qq.py +232 -94
- musicdl/modules/sources/tidal.py +342 -0
- musicdl/modules/sources/ximalaya.py +256 -0
- musicdl/modules/sources/yinyuedao.py +144 -0
- musicdl/modules/sources/youtube.py +238 -0
- musicdl/modules/utils/__init__.py +12 -4
- musicdl/modules/utils/appleutils.py +563 -0
- musicdl/modules/utils/data.py +107 -0
- musicdl/modules/utils/logger.py +211 -58
- musicdl/modules/utils/lyric.py +73 -0
- musicdl/modules/utils/misc.py +335 -23
- musicdl/modules/utils/modulebuilder.py +75 -0
- musicdl/modules/utils/neteaseutils.py +81 -0
- musicdl/modules/utils/qqutils.py +184 -0
- musicdl/modules/utils/quarkparser.py +105 -0
- musicdl/modules/utils/songinfoutils.py +54 -0
- musicdl/modules/utils/tidalutils.py +738 -0
- musicdl/modules/utils/youtubeutils.py +3606 -0
- musicdl/musicdl.py +184 -86
- musicdl-2.7.3.dist-info/LICENSE +203 -0
- musicdl-2.7.3.dist-info/METADATA +704 -0
- musicdl-2.7.3.dist-info/RECORD +53 -0
- {musicdl-2.1.11.dist-info → musicdl-2.7.3.dist-info}/WHEEL +5 -5
- musicdl-2.7.3.dist-info/entry_points.txt +2 -0
- musicdl/modules/sources/baiduFlac.py +0 -69
- musicdl/modules/sources/xiami.py +0 -104
- musicdl/modules/utils/downloader.py +0 -80
- musicdl-2.1.11.dist-info/LICENSE +0 -22
- musicdl-2.1.11.dist-info/METADATA +0 -82
- musicdl-2.1.11.dist-info/RECORD +0 -24
- {musicdl-2.1.11.dist-info → musicdl-2.7.3.dist-info}/top_level.txt +0 -0
- {musicdl-2.1.11.dist-info → musicdl-2.7.3.dist-info}/zip-safe +0 -0
|
@@ -0,0 +1,563 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Function:
|
|
3
|
+
Implementation of AppleMusicClient utils, refer to
|
|
4
|
+
Author:
|
|
5
|
+
Zhenchao Jin
|
|
6
|
+
WeChat Official Account (微信公众号):
|
|
7
|
+
Charles的皮卡丘
|
|
8
|
+
'''
|
|
9
|
+
import re
|
|
10
|
+
import os
|
|
11
|
+
import m3u8
|
|
12
|
+
import uuid
|
|
13
|
+
import json
|
|
14
|
+
import base64
|
|
15
|
+
import shutil
|
|
16
|
+
import datetime
|
|
17
|
+
import requests
|
|
18
|
+
import subprocess
|
|
19
|
+
from enum import Enum
|
|
20
|
+
from typing import Any
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from xml.dom import minidom
|
|
23
|
+
from xml.etree import ElementTree
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from platformdirs import user_log_dir
|
|
26
|
+
from pywidevine import PSSH, Cdm, Device
|
|
27
|
+
from pywidevine.license_protocol_pb2 import WidevinePsshData
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
'''CONSTANTS'''
|
|
31
|
+
MEDIA_TYPE_STR_MAP = {1: "Song", 6: "Music Video"}
|
|
32
|
+
MEDIA_RATING_STR_MAP = {0: "None", 1: "Explicit", 2: "Clean"}
|
|
33
|
+
LEGACY_SONG_CODECS = {"aac-legacy", "aac-he-legacy"}
|
|
34
|
+
DRM_DEFAULT_KEY_MAPPING = {
|
|
35
|
+
"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed": ("data:text/plain;base64,AAAAOHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABgSEAAAAAAAAAAAczEvZTEgICBI88aJmwY="),
|
|
36
|
+
"com.microsoft.playready": ("data:text/plain;charset=UTF-16;base64,vgEAAAEAAQC0ATwAVwBSAE0ASABFAEEARABFAFIAIAB4AG0AbABuAHMAPQAiAGgAdAB0AHAAOgAvAC8AcwBjAGgAZQBtAGEAcwAuAG0AaQBjAHIAbwBzAG8AZgB0AC4AYwBvAG0ALwBEAFIATQAvADIAMAAwADcALwAwADMALwBQAGwAYQB5AFIAZQBhAGQAeQBIAGUAYQBkAGUAcgAiACAAdgBlAHIAcwBpAG8AbgA9ACIANAAuADMALgAwAC4AMAAiAD4APABEAEEAVABBAD4APABQAFIATwBUAEUAQwBUAEkATgBGAE8APgA8AEsASQBEAFMAPgA8AEsASQBEACAAQQBMAEcASQBEAD0AIgBBAEUAUwBDAEIAQwAiACAAVgBBAEwAVQBFAD0AIgBBAEEAQQBBAEEAQQBBAEEAQQBBAEIAegBNAFMAOQBsAE0AUwBBAGcASQBBAD0APQAiAD4APAAvAEsASQBEAD4APAAvAEsASQBEAFMAPgA8AC8AUABSAE8AVABFAEMAVABJAE4ARgBPAD4APAAvAEQAQQBUAEEAPgA8AC8AVwBSAE0ASABFAEEARABFAFIAPgA="),
|
|
37
|
+
"com.apple.streamingkeydelivery": "skd://itunes.apple.com/P000000000/s1/e1",
|
|
38
|
+
}
|
|
39
|
+
MP4_FORMAT_CODECS = ["ec-3", "hvc1", "audio-atmos", "audio-ec3"]
|
|
40
|
+
SONG_CODEC_REGEX_MAP = {
|
|
41
|
+
"aac": r"audio-stereo-\d+", "aac-he": r"audio-HE-stereo-\d+", "aac-binaural": r"audio-stereo-\d+-binaural", "aac-downmix": r"audio-stereo-\d+-downmix", "aac-he-binaural": r"audio-HE-stereo-\d+-binaural",
|
|
42
|
+
"aac-he-downmix": r"audio-HE-stereo-\d+-downmix", "atmos": r"audio-atmos-.*", "ac3": r"audio-ac3-.*", "alac": r"audio-alac-.*",
|
|
43
|
+
}
|
|
44
|
+
FOURCC_MAP = {"h264": "avc1", "h265": "hvc1"}
|
|
45
|
+
UPLOADED_VIDEO_QUALITY_RANK = ["1080pHdVideo", "720pHdVideo", "sdVideoWithPlusAudio", "sdVideo", "sd480pVideo", "provisionalUploadVideo"]
|
|
46
|
+
HARDCODED_WVD = """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QrSyZcBqHO0QrdPcU35GBT7S7+IJr2AAXvnjqnb8yrtpPWN2ZW/IWUJN2z4vZ7/HV4aj3OZhkxC1DIMNyvsusUKoQQuf8gwKiEe8cFwbwFSicywlFk9la2IPe8oFShcxAzHLCCn/TIYUAvEL3/4LgaZvqWm80qCPYbgIP5HT8hPYkKWJ4WYknEWK+3InbnkzteFfGrQFCq4CCAESEGnj6Ji7LD+4o7MoHYT4jBQYjtW+kQUijgIwggEKAoIBAQDY9um1ifBRIOmkPtDZTqH+CZUBbb0eK0Cn3NHFf8MFUDzPEz+emK/OTub/hNxCJCao//pP5L8tRNUPFDrrvCBMo7Rn+iUb+mA/2yXiJ6ivqcN9Cu9i5qOU1ygon9SWZRsujFFB8nxVreY5Lzeq0283zn1Cg1stcX4tOHT7utPzFG/ReDFQt0O/GLlzVwB0d1sn3SKMO4XLjhZdncrtF9jljpg7xjMIlnWJUqxDo7TQkTytJmUl0kcM7bndBLerAdJFGaXc6oSY4eNy/IGDluLCQR3KZEQsy/mLeV1ggQ44MFr7XOM+rd+4/314q/deQbjHqjWFuVr8iIaKbq+R63ShAgMBAAEo8CISgAMii2Mw6z+Qs1bvvxGStie9tpcgoO2uAt5Zvv0CDXvrFlwnSbo+qR71Ru2IlZWVSbN5XYSIDwcwBzHjY8rNr3fgsXtSJty425djNQtF5+J2jrAhf3Q2m7EI5aohZGpD2E0cr+dVj9o8x0uJR2NWR8FVoVQSXZpad3M/4QzBLNto/tz+UKyZwa7Sc/eTQc2+ZcDS3ZEO3lGRsH864Kf/cEGvJRBBqcpJXKfG+ItqEW1AAPptjuggzmZEzRq5xTGf6or+bXrKjCpBS9G1SOyvCNF1k5z6lG8KsXhgQxL6ADHMoulxvUIihyPY5MpimdXfUdEQ5HA2EqNiNVNIO4qP007jW51yAeThOry4J22xs8RdkIClOGAauLIl0lLA4flMzW+VfQl5xYxP0E5tuhn0h+844DslU8ZF7U1dU2QprIApffXD9wgAACk26Rggy8e96z8i86/+YYyZQkc9hIdCAERrgEYCEbByzONrdRDs1MrS/ch1moV5pJv63BIKvQHGvLkaFwoMY29tcGFueV9uYW1lEgd1bmtub3duGioKCm1vZGVsX25hbWUSHEFuZHJvaWQgU0RLIGJ1aWx0IGZvciB4ODZfNjQaGwoRYXJjaGl0ZWN0dXJlX25hbWUSBng4Nl82NBodCgtkZXZpY2VfbmFtZRIOZ2VuZXJpY194ODZfNjQaIAoMcHJvZHVjdF9uYW1lEhBzZGtfcGhvbmVfeDg2XzY0GmMKCmJ1aWxkX2luZm8SVUFuZHJvaWQvc2RrX3Bob25lX3g4Nl82NC9nZW5lcmljX3g4Nl82NDo5L1BTUjEuMTgwNzIwLjAxMi80OTIzMjE0OnVzZXJkZWJ1Zy90ZXN0LWtleXMaHgoUd2lkZXZpbmVfY2RtX3ZlcnNpb24SBjE0LjAuMBokCh9vZW1fY3J5cHRvX3NlY3VyaXR5X3BhdGNoX2xldmVsEgEwMg4QASAAKA0wAEAASABQAA=="""
|
|
47
|
+
DEFAULT_SONG_DECRYPTION_KEY = "32b8ade1769e26b1ffb8986352793fc6"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
'''CoverFormat'''
|
|
51
|
+
class CoverFormat(Enum):
|
|
52
|
+
JPG = "jpg"
|
|
53
|
+
PNG = "png"
|
|
54
|
+
RAW = "raw"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
'''RemuxFormatMusicVideo'''
|
|
58
|
+
class RemuxFormatMusicVideo(Enum):
|
|
59
|
+
M4V = "m4v"
|
|
60
|
+
MP4 = "mp4"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
'''SyncedLyricsFormat'''
|
|
64
|
+
class SyncedLyricsFormat(Enum):
|
|
65
|
+
LRC = "lrc"
|
|
66
|
+
SRT = "srt"
|
|
67
|
+
TTML = "ttml"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
'''MediaType'''
|
|
71
|
+
class MediaType(Enum):
|
|
72
|
+
SONG = 1
|
|
73
|
+
MUSIC_VIDEO = 6
|
|
74
|
+
def __str__(self): return MEDIA_TYPE_STR_MAP[self.value]
|
|
75
|
+
def __int__(self): return self.value
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
'''MediaRating'''
|
|
79
|
+
class MediaRating(Enum):
|
|
80
|
+
NONE = 0
|
|
81
|
+
EXPLICIT = 1
|
|
82
|
+
CLEAN = 2
|
|
83
|
+
def __str__(self): return MEDIA_RATING_STR_MAP[self.value]
|
|
84
|
+
def __int__(self): return self.value
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
'''MediaFileFormat'''
|
|
88
|
+
class MediaFileFormat(Enum):
|
|
89
|
+
MP4 = "mp4"
|
|
90
|
+
M4V = "m4v"
|
|
91
|
+
M4A = "m4a"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
'''SongCodec'''
|
|
95
|
+
class SongCodec(Enum):
|
|
96
|
+
AAC_LEGACY = "aac-legacy"
|
|
97
|
+
AAC_HE_LEGACY = "aac-he-legacy"
|
|
98
|
+
AAC = "aac"
|
|
99
|
+
AAC_HE = "aac-he"
|
|
100
|
+
AAC_BINAURAL = "aac-binaural"
|
|
101
|
+
AAC_DOWNMIX = "aac-downmix"
|
|
102
|
+
AAC_HE_BINAURAL = "aac-he-binaural"
|
|
103
|
+
AAC_HE_DOWNMIX = "aac-he-downmix"
|
|
104
|
+
ATMOS = "atmos"
|
|
105
|
+
AC3 = "ac3"
|
|
106
|
+
ALAC = "alac"
|
|
107
|
+
def islegacy(self): return self.value in LEGACY_SONG_CODECS
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
'''MusicVideoCodec'''
|
|
111
|
+
class MusicVideoCodec(Enum):
|
|
112
|
+
H264 = "h264"
|
|
113
|
+
H265 = "h265"
|
|
114
|
+
def fourcc(self): return FOURCC_MAP[self.value]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
'''MusicVideoResolution'''
|
|
118
|
+
class MusicVideoResolution(Enum):
|
|
119
|
+
R240P = "240p"
|
|
120
|
+
R360P = "360p"
|
|
121
|
+
R480P = "480p"
|
|
122
|
+
R540P = "540p"
|
|
123
|
+
R720P = "720p"
|
|
124
|
+
R1080P = "1080p"
|
|
125
|
+
R1440P = "1440p"
|
|
126
|
+
R2160P = "2160p"
|
|
127
|
+
def __int__(self): return int(self.value[:-1])
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
'''Lyrics'''
|
|
131
|
+
@dataclass
|
|
132
|
+
class Lyrics:
|
|
133
|
+
synced: str = None
|
|
134
|
+
unsynced: str = None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
'''MediaTags'''
|
|
138
|
+
@dataclass
|
|
139
|
+
class MediaTags:
|
|
140
|
+
album: str = None
|
|
141
|
+
album_artist: str = None
|
|
142
|
+
album_id: int = None
|
|
143
|
+
album_sort: str = None
|
|
144
|
+
artist: str = None
|
|
145
|
+
artist_id: int = None
|
|
146
|
+
artist_sort: str = None
|
|
147
|
+
comment: str = None
|
|
148
|
+
compilation: bool = None
|
|
149
|
+
composer: str = None
|
|
150
|
+
composer_id: int = None
|
|
151
|
+
composer_sort: str = None
|
|
152
|
+
copyright: str = None
|
|
153
|
+
date: datetime.date | str = None
|
|
154
|
+
disc: int = None
|
|
155
|
+
disc_total: int = None
|
|
156
|
+
gapless: bool = None
|
|
157
|
+
genre: str = None
|
|
158
|
+
genre_id: int = None
|
|
159
|
+
lyrics: str = None
|
|
160
|
+
media_type: MediaType = None
|
|
161
|
+
rating: MediaRating = None
|
|
162
|
+
storefront: str = None
|
|
163
|
+
title: str = None
|
|
164
|
+
title_id: int = None
|
|
165
|
+
title_sort: str = None
|
|
166
|
+
track: int = None
|
|
167
|
+
track_total: int = None
|
|
168
|
+
xid: str = None
|
|
169
|
+
'''asmp4tags'''
|
|
170
|
+
def asmp4tags(self, date_format: str = None):
|
|
171
|
+
disc_mp4 = [self.disc if self.disc is not None else 0, self.disc_total if self.disc_total is not None else 0]
|
|
172
|
+
if disc_mp4[0] == 0 and disc_mp4[1] == 0: disc_mp4 = None
|
|
173
|
+
track_mp4 = [self.track if self.track is not None else 0, self.track_total if self.track_total is not None else 0]
|
|
174
|
+
if track_mp4[0] == 0 and track_mp4[1] == 0: track_mp4 = None
|
|
175
|
+
if isinstance(self.date, datetime.date):
|
|
176
|
+
if date_format is None: date_mp4 = self.date.isoformat()
|
|
177
|
+
else: date_mp4 = self.date.strftime(date_format)
|
|
178
|
+
elif isinstance(self.date, str):
|
|
179
|
+
date_mp4 = self.date
|
|
180
|
+
else:
|
|
181
|
+
date_mp4 = None
|
|
182
|
+
mp4_tags = {
|
|
183
|
+
"\xa9alb": self.album, "aART": self.album_artist, "plID": self.album_id, "soal": self.album_sort, "\xa9ART": self.artist, "atID": self.artist_id,
|
|
184
|
+
"soar": self.artist_sort, "\xa9cmt": self.comment, "cpil": bool(self.compilation) if self.compilation is not None else None, "\xa9wrt": self.composer,
|
|
185
|
+
"cmID": self.composer_id, "soco": self.composer_sort, "cprt": self.copyright, "\xa9day": date_mp4, "disk": disc_mp4, "pgap": bool(self.gapless) if self.gapless is not None else None,
|
|
186
|
+
"\xa9gen": self.genre, "\xa9lyr": self.lyrics, "geID": self.genre_id, "stik": int(self.media_type) if self.media_type is not None else None, "rtng": int(self.rating) if self.rating is not None else None,
|
|
187
|
+
"sfID": self.storefront, "\xa9nam": self.title, "cnID": self.title_id, "sonm": self.title_sort, "trkn": track_mp4, "xid ": self.xid,
|
|
188
|
+
}
|
|
189
|
+
return {k: ([v] if not isinstance(v, bool) else v) for k, v in mp4_tags.items() if v is not None}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
'''PlaylistTags'''
|
|
193
|
+
@dataclass
|
|
194
|
+
class PlaylistTags:
|
|
195
|
+
playlist_artist: str = None
|
|
196
|
+
playlist_id: int = None
|
|
197
|
+
playlist_title: str = None
|
|
198
|
+
playlist_track: int = None
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
'''StreamInfo'''
|
|
202
|
+
@dataclass
|
|
203
|
+
class StreamInfo:
|
|
204
|
+
stream_url: str = None
|
|
205
|
+
widevine_pssh: str = None
|
|
206
|
+
playready_pssh: str = None
|
|
207
|
+
fairplay_key: str = None
|
|
208
|
+
codec: str = None
|
|
209
|
+
width: int = None
|
|
210
|
+
height: int = None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
'''StreamInfoAv'''
|
|
214
|
+
@dataclass
|
|
215
|
+
class StreamInfoAv:
|
|
216
|
+
media_id: str = None
|
|
217
|
+
video_track: StreamInfo = None
|
|
218
|
+
audio_track: StreamInfo = None
|
|
219
|
+
file_format: MediaFileFormat = None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
'''DecryptionKey'''
|
|
223
|
+
@dataclass
|
|
224
|
+
class DecryptionKey:
|
|
225
|
+
kid: str = None
|
|
226
|
+
key: str = None
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
'''DecryptionKeyAv'''
|
|
230
|
+
@dataclass
|
|
231
|
+
class DecryptionKeyAv:
|
|
232
|
+
video_track: DecryptionKey = None
|
|
233
|
+
audio_track: DecryptionKey = None
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
'''DownloadItem'''
|
|
237
|
+
@dataclass
|
|
238
|
+
class DownloadItem:
|
|
239
|
+
media_metadata: dict = None
|
|
240
|
+
playlist_metadata: dict = None
|
|
241
|
+
random_uuid: str = None
|
|
242
|
+
lyrics: Lyrics = None
|
|
243
|
+
lyrics_results: dict = None
|
|
244
|
+
media_tags: MediaTags = None
|
|
245
|
+
playlist_tags: PlaylistTags = None
|
|
246
|
+
stream_info: StreamInfoAv = None
|
|
247
|
+
decryption_key: DecryptionKeyAv = None
|
|
248
|
+
cover_url_template: str = None
|
|
249
|
+
staged_path: str = None
|
|
250
|
+
final_path: str = None
|
|
251
|
+
playlist_file_path: str = None
|
|
252
|
+
synced_lyrics_path: str = None
|
|
253
|
+
cover_path: str = None
|
|
254
|
+
flat_filter_result: Any = None
|
|
255
|
+
error: Exception = None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
'''AppleMusicClientUtils'''
|
|
259
|
+
class AppleMusicClientUtils:
|
|
260
|
+
'''_parsedate'''
|
|
261
|
+
@staticmethod
|
|
262
|
+
def _parsedate(date: str):
|
|
263
|
+
return datetime.datetime.fromisoformat(date.split("Z")[0])
|
|
264
|
+
'''getsonglyrics'''
|
|
265
|
+
@staticmethod
|
|
266
|
+
def getsonglyrics(song_metadata: dict, synced_lyrics_format: SyncedLyricsFormat = SyncedLyricsFormat.LRC):
|
|
267
|
+
# no lyrics
|
|
268
|
+
if not song_metadata["attributes"]["hasLyrics"]: return None
|
|
269
|
+
# lyrics parser functions definition
|
|
270
|
+
def _parsettmltimestamp(timestamp_ttml: str):
|
|
271
|
+
mins_secs_ms = re.findall(r"\d+", timestamp_ttml)
|
|
272
|
+
ms, secs, mins = 0, 0, 0
|
|
273
|
+
if len(mins_secs_ms) == 2 and ":" in timestamp_ttml:
|
|
274
|
+
secs, mins = int(mins_secs_ms[-1]), int(mins_secs_ms[-2])
|
|
275
|
+
elif len(mins_secs_ms) == 1:
|
|
276
|
+
ms = int(mins_secs_ms[-1])
|
|
277
|
+
else:
|
|
278
|
+
secs = float(f"{mins_secs_ms[-2]}.{mins_secs_ms[-1]}")
|
|
279
|
+
if len(mins_secs_ms) > 2: mins = int(mins_secs_ms[-3])
|
|
280
|
+
return datetime.datetime.fromtimestamp((mins * 60) + secs + (ms / 1000), tz=datetime.timezone.utc)
|
|
281
|
+
def _getlyricslinelrc(element: ElementTree.Element):
|
|
282
|
+
timestamp_ttml, text = element.attrib.get("begin"), element.text
|
|
283
|
+
timestamp = _parsettmltimestamp(timestamp_ttml)
|
|
284
|
+
ms_new = timestamp.strftime("%f")[:-3]
|
|
285
|
+
if int(ms_new[-1]) >= 5:
|
|
286
|
+
ms = int(f"{int(ms_new[:2]) + 1}") * 10
|
|
287
|
+
timestamp += datetime.timedelta(milliseconds=ms) - datetime.timedelta(microseconds=timestamp.microsecond)
|
|
288
|
+
return f"[{timestamp.strftime('%M:%S.%f')[:-4]}]{text}"
|
|
289
|
+
def _getlyricslinesrt(index: int, element: ElementTree.Element):
|
|
290
|
+
timestamp_begin_ttml, timestamp_end_ttml, text = element.attrib.get("begin"), element.attrib.get("end"), element.text
|
|
291
|
+
timestamp_begin = _parsettmltimestamp(timestamp_begin_ttml)
|
|
292
|
+
timestamp_end = _parsettmltimestamp(timestamp_end_ttml)
|
|
293
|
+
return (
|
|
294
|
+
f"{index}\n"
|
|
295
|
+
f"{timestamp_begin.strftime('%H:%M:%S,%f')[:-3]} --> "
|
|
296
|
+
f"{timestamp_end.strftime('%H:%M:%S,%f')[:-3]}\n"
|
|
297
|
+
f"{text}\n"
|
|
298
|
+
)
|
|
299
|
+
# fetch lyrics
|
|
300
|
+
try:
|
|
301
|
+
lyrics_result = song_metadata["relationships"]["lyrics"]
|
|
302
|
+
lyrics_ttml = lyrics_result["data"][0]["attributes"]["ttml"]
|
|
303
|
+
lyrics_ttml_et = ElementTree.fromstring(lyrics_ttml)
|
|
304
|
+
unsynced_lyrics, synced_lyrics, index = [], [], 1
|
|
305
|
+
for div in lyrics_ttml_et.iter("{http://www.w3.org/ns/ttml}div"):
|
|
306
|
+
stanza = []
|
|
307
|
+
unsynced_lyrics.append(stanza)
|
|
308
|
+
for p in div.iter("{http://www.w3.org/ns/ttml}p"):
|
|
309
|
+
if p.text is not None: stanza.append(p.text)
|
|
310
|
+
if p.attrib.get("begin"):
|
|
311
|
+
if synced_lyrics_format == SyncedLyricsFormat.LRC:
|
|
312
|
+
synced_lyrics.append(_getlyricslinelrc(p))
|
|
313
|
+
if synced_lyrics_format == SyncedLyricsFormat.SRT:
|
|
314
|
+
synced_lyrics.append(_getlyricslinesrt(index, p))
|
|
315
|
+
if synced_lyrics_format == SyncedLyricsFormat.TTML:
|
|
316
|
+
if not synced_lyrics: synced_lyrics.append(minidom.parseString(lyrics_ttml).toprettyxml())
|
|
317
|
+
index += 1
|
|
318
|
+
lyrics = Lyrics(synced="\n".join(synced_lyrics + ["\n"]) if synced_lyrics else None, unsynced=("\n\n".join(["\n".join(lyric_group) for lyric_group in unsynced_lyrics]) if unsynced_lyrics else None))
|
|
319
|
+
except:
|
|
320
|
+
lyrics_result, lyrics = {}, None
|
|
321
|
+
# return
|
|
322
|
+
return lyrics, lyrics_result
|
|
323
|
+
'''getsongtags'''
|
|
324
|
+
@staticmethod
|
|
325
|
+
def getsongtags(webplayback: dict, lyrics: str | None = None):
|
|
326
|
+
webplayback_metadata = webplayback["songList"][0]["assets"][0]["metadata"]
|
|
327
|
+
tags = MediaTags(
|
|
328
|
+
album=webplayback_metadata["playlistName"], album_artist=webplayback_metadata["playlistArtistName"], album_id=int(webplayback_metadata["playlistId"]),
|
|
329
|
+
album_sort=webplayback_metadata["sort-album"], artist=webplayback_metadata["artistName"], artist_id=int(webplayback_metadata["artistId"]),
|
|
330
|
+
artist_sort=webplayback_metadata["sort-artist"], comment=webplayback_metadata.get("comments"), compilation=webplayback_metadata["compilation"],
|
|
331
|
+
composer=webplayback_metadata.get("composerName"), composer_id=(int(webplayback_metadata.get("composerId")) if webplayback_metadata.get("composerId") else None),
|
|
332
|
+
composer_sort=webplayback_metadata.get("sort-composer"), copyright=webplayback_metadata.get("copyright"),
|
|
333
|
+
date=(AppleMusicClientUtils._parsedate(webplayback_metadata["releaseDate"]) if webplayback_metadata.get("releaseDate") else None),
|
|
334
|
+
disc=webplayback_metadata["discNumber"], disc_total=webplayback_metadata["discCount"], gapless=webplayback_metadata["gapless"],
|
|
335
|
+
genre=webplayback_metadata.get("genre"), genre_id=int(webplayback_metadata["genreId"]), lyrics=lyrics if lyrics else None,
|
|
336
|
+
media_type=MediaType.SONG, rating=MediaRating(webplayback_metadata["explicit"]), storefront=webplayback_metadata["s"],
|
|
337
|
+
title=webplayback_metadata["itemName"], title_id=int(webplayback_metadata["itemId"]), title_sort=webplayback_metadata["sort-name"],
|
|
338
|
+
track=webplayback_metadata["trackNumber"], track_total=webplayback_metadata["trackCount"], xid=webplayback_metadata.get("xid"),
|
|
339
|
+
)
|
|
340
|
+
return tags
|
|
341
|
+
'''getsongstreaminfolegacy'''
|
|
342
|
+
@staticmethod
|
|
343
|
+
def getsongstreaminfolegacy(webplayback: dict, codec: SongCodec):
|
|
344
|
+
flavor = "32:ctrp64" if codec == SongCodec.AAC_HE_LEGACY else "28:ctrp256"
|
|
345
|
+
stream_info = StreamInfo()
|
|
346
|
+
stream_info.stream_url = next(i for i in webplayback["songList"][0]["assets"] if i["flavor"] == flavor)["URL"]
|
|
347
|
+
m3u8_obj = m3u8.loads(requests.get(stream_info.stream_url).text)
|
|
348
|
+
stream_info.widevine_pssh = m3u8_obj.keys[0].uri
|
|
349
|
+
stream_info_av = StreamInfoAv(media_id=webplayback["songList"][0]["songId"], audio_track=stream_info, file_format=MediaFileFormat.M4A)
|
|
350
|
+
return stream_info_av
|
|
351
|
+
'''getsongdecryptionkeylegacy'''
|
|
352
|
+
@staticmethod
|
|
353
|
+
def getsongdecryptionkeylegacy(stream_info: StreamInfoAv, cdm: Cdm, get_license_exchange_func = None, request_overrides: dict = None):
|
|
354
|
+
request_overrides = request_overrides or {}
|
|
355
|
+
stream_info_audio = stream_info.audio_track
|
|
356
|
+
try:
|
|
357
|
+
cdm_session = cdm.open()
|
|
358
|
+
widevine_pssh_data = WidevinePsshData()
|
|
359
|
+
widevine_pssh_data.algorithm = 1
|
|
360
|
+
widevine_pssh_data.key_ids.append(base64.b64decode(stream_info_audio.widevine_pssh.split(",")[1]))
|
|
361
|
+
pssh_obj = PSSH(widevine_pssh_data.SerializeToString())
|
|
362
|
+
challenge = base64.b64encode(cdm.get_license_challenge(cdm_session, pssh_obj)).decode()
|
|
363
|
+
license_resp = get_license_exchange_func(stream_info.media_id, stream_info.audio_track.widevine_pssh, challenge, request_overrides=request_overrides)
|
|
364
|
+
cdm.parse_license(cdm_session, license_resp["license"])
|
|
365
|
+
decryption_key = next(i for i in cdm.get_keys(cdm_session) if i.type == "CONTENT")
|
|
366
|
+
finally:
|
|
367
|
+
cdm.close(cdm_session)
|
|
368
|
+
decryption_key = DecryptionKeyAv(audio_track=DecryptionKey(kid=decryption_key.kid.hex, key=decryption_key.key.hex()))
|
|
369
|
+
return decryption_key
|
|
370
|
+
'''getsongstreaminfo'''
|
|
371
|
+
@staticmethod
|
|
372
|
+
def getsongstreaminfo(song_metadata: dict, codec: SongCodec):
|
|
373
|
+
m3u8_master_url: str = song_metadata["attributes"]["extendedAssetUrls"].get("enhancedHls")
|
|
374
|
+
if not m3u8_master_url: return None
|
|
375
|
+
m3u8_master_obj = m3u8.loads(requests.get(m3u8_master_url).text)
|
|
376
|
+
m3u8_master_data = m3u8_master_obj.data
|
|
377
|
+
playlist = AppleMusicClientUtils._getsongplaylistfromcodec(m3u8_master_data, codec)
|
|
378
|
+
if playlist is None: return None
|
|
379
|
+
stream_info = StreamInfo()
|
|
380
|
+
stream_info.stream_url = (f"{m3u8_master_url.rpartition('/')[0]}/{playlist['uri']}")
|
|
381
|
+
stream_info.codec = playlist["stream_info"]["codecs"]
|
|
382
|
+
is_mp4 = any(stream_info.codec.startswith(codec) for codec in MP4_FORMAT_CODECS)
|
|
383
|
+
session_key_metadata = AppleMusicClientUtils._getaudiosessionkeymetadata(m3u8_master_data)
|
|
384
|
+
if session_key_metadata:
|
|
385
|
+
asset_metadata = AppleMusicClientUtils._getassetmetadata(m3u8_master_data)
|
|
386
|
+
variant_id = playlist["stream_info"]["stable_variant_id"]
|
|
387
|
+
drm_ids = asset_metadata[variant_id]["AUDIO-SESSION-KEY-IDS"]
|
|
388
|
+
stream_info.widevine_pssh = AppleMusicClientUtils._getdrmurifromsessionkey(session_key_metadata, drm_ids, "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed")
|
|
389
|
+
stream_info.playready_pssh = AppleMusicClientUtils._getdrmurifromsessionkey(session_key_metadata, drm_ids, "com.microsoft.playready")
|
|
390
|
+
stream_info.fairplay_key = AppleMusicClientUtils._getdrmurifromsessionkey(session_key_metadata, drm_ids, "com.apple.streamingkeydelivery")
|
|
391
|
+
else:
|
|
392
|
+
m3u8_obj = m3u8.loads(requests.get(stream_info.stream_url).text)
|
|
393
|
+
stream_info.widevine_pssh = AppleMusicClientUtils._getdrmurifromm3u8keys(m3u8_obj, "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed")
|
|
394
|
+
stream_info.playready_pssh = AppleMusicClientUtils._getdrmurifromm3u8keys(m3u8_obj, "com.microsoft.playready")
|
|
395
|
+
stream_info.fairplay_key = AppleMusicClientUtils._getdrmurifromm3u8keys(m3u8_obj, "com.apple.streamingkeydelivery")
|
|
396
|
+
stream_info_av = StreamInfoAv(audio_track=stream_info, file_format=MediaFileFormat.MP4 if is_mp4 else MediaFileFormat.M4A)
|
|
397
|
+
return stream_info_av
|
|
398
|
+
'''getsongdecryptionkey'''
|
|
399
|
+
@staticmethod
|
|
400
|
+
def getsongdecryptionkey(stream_info: StreamInfoAv, cdm: Cdm, get_license_exchange_func = None, request_overrides: dict = None):
|
|
401
|
+
request_overrides = request_overrides or {}
|
|
402
|
+
def _getsongdecryptionkey(track_uri: str, track_id: str, cdm: Cdm, get_license_exchange_func = None, request_overrides: dict = None):
|
|
403
|
+
try:
|
|
404
|
+
cdm_session = cdm.open()
|
|
405
|
+
pssh_obj = PSSH(track_uri.split(",")[-1])
|
|
406
|
+
challenge = base64.b64encode(cdm.get_license_challenge(cdm_session, pssh_obj)).decode()
|
|
407
|
+
license = get_license_exchange_func(track_id, track_uri, challenge, request_overrides=request_overrides)
|
|
408
|
+
cdm.parse_license(cdm_session, license["license"])
|
|
409
|
+
decryption_key_info = next(i for i in cdm.get_keys(cdm_session) if i.type == "CONTENT")
|
|
410
|
+
finally:
|
|
411
|
+
cdm.close(cdm_session)
|
|
412
|
+
decryption_key = DecryptionKey(key=decryption_key_info.key.hex(), kid=decryption_key_info.kid.hex)
|
|
413
|
+
return decryption_key
|
|
414
|
+
decryption_key = DecryptionKeyAv(audio_track=_getsongdecryptionkey(stream_info.audio_track.widevine_pssh, stream_info.media_id, cdm, get_license_exchange_func, request_overrides))
|
|
415
|
+
return decryption_key
|
|
416
|
+
'''_getm3u8metadata'''
|
|
417
|
+
@staticmethod
|
|
418
|
+
def _getm3u8metadata(m3u8_data: dict, data_id: str):
|
|
419
|
+
for session_data in m3u8_data.get("session_data", []):
|
|
420
|
+
if session_data["data_id"] == data_id:
|
|
421
|
+
return json.loads(base64.b64decode(session_data["value"]).decode("utf-8"))
|
|
422
|
+
return None
|
|
423
|
+
'''_getaudiosessionkeymetadata'''
|
|
424
|
+
@staticmethod
|
|
425
|
+
def _getaudiosessionkeymetadata(m3u8_data: dict):
|
|
426
|
+
return AppleMusicClientUtils._getm3u8metadata(m3u8_data, "com.apple.hls.AudioSessionKeyInfo")
|
|
427
|
+
'''_getassetmetadata'''
|
|
428
|
+
@staticmethod
|
|
429
|
+
def _getassetmetadata(m3u8_data: dict):
|
|
430
|
+
return AppleMusicClientUtils._getm3u8metadata(m3u8_data, "com.apple.hls.audioAssetMetadata")
|
|
431
|
+
'''_getsongplaylistfromcodec'''
|
|
432
|
+
@staticmethod
|
|
433
|
+
def _getsongplaylistfromcodec(m3u8_data: dict, codec: SongCodec):
|
|
434
|
+
matching_playlists = [playlist for playlist in m3u8_data["playlists"] if re.fullmatch(SONG_CODEC_REGEX_MAP[codec.value], playlist["stream_info"]["audio"])]
|
|
435
|
+
if not matching_playlists: return None
|
|
436
|
+
return max(matching_playlists, key=lambda x: x["stream_info"]["average_bandwidth"])
|
|
437
|
+
'''_getdrmurifromsessionkey'''
|
|
438
|
+
@staticmethod
|
|
439
|
+
def _getdrmurifromsessionkey(drm_infos: dict, drm_ids: list, drm_key: str):
|
|
440
|
+
for drm_id in drm_ids:
|
|
441
|
+
if drm_id != "1" and drm_key in drm_infos.get(drm_id, {}):
|
|
442
|
+
return drm_infos[drm_id][drm_key]["URI"]
|
|
443
|
+
return None
|
|
444
|
+
'''_getdrmurifromm3u8keys'''
|
|
445
|
+
@staticmethod
|
|
446
|
+
def _getdrmurifromm3u8keys(m3u8_obj: m3u8.M3U8, drm_key: str):
|
|
447
|
+
default_uri = DRM_DEFAULT_KEY_MAPPING[drm_key]
|
|
448
|
+
for key in m3u8_obj.keys:
|
|
449
|
+
if key.keyformat == drm_key and key.uri != default_uri: return key.uri
|
|
450
|
+
return None
|
|
451
|
+
@staticmethod
|
|
452
|
+
def _getrandomuuid():
|
|
453
|
+
return uuid.uuid4().hex[:8]
|
|
454
|
+
'''getsongdownloaditem'''
|
|
455
|
+
@staticmethod
|
|
456
|
+
def getsongdownloaditem(song_metadata: dict, webplayback: dict, synced_lyrics_format: SyncedLyricsFormat = SyncedLyricsFormat.LRC, codec: SongCodec = SongCodec.AAC_LEGACY,
|
|
457
|
+
get_license_exchange_func = None, request_overrides: dict = None):
|
|
458
|
+
# init
|
|
459
|
+
download_item = DownloadItem()
|
|
460
|
+
download_item.media_metadata = song_metadata
|
|
461
|
+
request_overrides = request_overrides or {}
|
|
462
|
+
# lyrics
|
|
463
|
+
download_item.lyrics, download_item.lyrics_results = AppleMusicClientUtils.getsonglyrics(song_metadata, synced_lyrics_format=synced_lyrics_format)
|
|
464
|
+
# get webplayback
|
|
465
|
+
download_item.media_tags = AppleMusicClientUtils.getsongtags(webplayback, download_item.lyrics.unsynced if download_item.lyrics else None)
|
|
466
|
+
# auto set after searching
|
|
467
|
+
download_item.final_path = None
|
|
468
|
+
download_item.synced_lyrics_path = None
|
|
469
|
+
download_item.staged_path = None
|
|
470
|
+
# stream info and decryption key
|
|
471
|
+
cdm = Cdm.from_device(Device.loads(HARDCODED_WVD))
|
|
472
|
+
if codec.islegacy():
|
|
473
|
+
download_item.stream_info = AppleMusicClientUtils.getsongstreaminfolegacy(webplayback, codec)
|
|
474
|
+
download_item.decryption_key = AppleMusicClientUtils.getsongdecryptionkeylegacy(download_item.stream_info, cdm, get_license_exchange_func, request_overrides=request_overrides)
|
|
475
|
+
else:
|
|
476
|
+
download_item.stream_info = AppleMusicClientUtils.getsongstreaminfo(song_metadata, codec)
|
|
477
|
+
if (download_item.stream_info and download_item.stream_info.audio_track.widevine_pssh):
|
|
478
|
+
download_item.decryption_key = AppleMusicClientUtils.getsongdecryptionkey(download_item.stream_info, cdm, get_license_exchange_func, request_overrides=request_overrides)
|
|
479
|
+
else:
|
|
480
|
+
download_item.decryption_key = None
|
|
481
|
+
# uuid for tmp results saving
|
|
482
|
+
download_item.random_uuid = AppleMusicClientUtils._getrandomuuid()
|
|
483
|
+
# return
|
|
484
|
+
return download_item
|
|
485
|
+
'''download'''
|
|
486
|
+
@staticmethod
|
|
487
|
+
def download(download_item: DownloadItem, work_dir: str = './', silent: bool = False, codec: SongCodec = SongCodec.AAC_LEGACY, wrapper_decrypt_ip: str = "127.0.0.1:10020"):
|
|
488
|
+
ext = download_item.stream_info.file_format.value
|
|
489
|
+
encrypted_path = os.path.join(work_dir, f"{download_item.random_uuid}_encrypted.{ext}")
|
|
490
|
+
is_success = AppleMusicClientUtils.downloadstream(download_item.stream_info.audio_track.stream_url, encrypted_path, silent=silent, random_uuid=download_item.random_uuid)
|
|
491
|
+
decrypted_path = os.path.join(work_dir, f"{download_item.random_uuid}_decrypted.{ext}")
|
|
492
|
+
download_item.staged_path = os.path.join(work_dir, f"{download_item.random_uuid}_staged.{ext}")
|
|
493
|
+
is_success = AppleMusicClientUtils.decrypt(
|
|
494
|
+
encrypted_path=encrypted_path, decrypted_path=decrypted_path, final_path=download_item.final_path, decryption_key=download_item.decryption_key,
|
|
495
|
+
codec=codec, media_id=download_item.media_metadata["id"], fairplay_key=download_item.stream_info.audio_track.fairplay_key, silent=silent,
|
|
496
|
+
artist=download_item.media_tags.artist, wrapper_decrypt_ip=wrapper_decrypt_ip,
|
|
497
|
+
)
|
|
498
|
+
assert is_success
|
|
499
|
+
'''_fixkeyid'''
|
|
500
|
+
@staticmethod
|
|
501
|
+
def _fixkeyid(input_path: str):
|
|
502
|
+
count = 0
|
|
503
|
+
with open(input_path, "rb+") as file:
|
|
504
|
+
while data := file.read(4096):
|
|
505
|
+
pos, i = file.tell(), 0
|
|
506
|
+
while tenc := max(0, data.find(b"tenc", i)):
|
|
507
|
+
kid = tenc + 12
|
|
508
|
+
file.seek(max(0, pos - 4096) + kid, 0)
|
|
509
|
+
file.write(bytes.fromhex(f"{count:032}"))
|
|
510
|
+
count += 1
|
|
511
|
+
i = kid + 1
|
|
512
|
+
file.seek(pos, 0)
|
|
513
|
+
'''_remuxmp4box'''
|
|
514
|
+
@staticmethod
|
|
515
|
+
def _remuxmp4box(input_path: str, output_path: str, silent: bool = False, artist: str = ''):
|
|
516
|
+
cmd = ["MP4Box", "-quiet", "-add", input_path, "-itags", f"artist={artist}", "-keep-utc", "-new", output_path]
|
|
517
|
+
capture_output = True if silent else False
|
|
518
|
+
ret = subprocess.run(cmd, check=True, capture_output=capture_output, text=True, encoding='utf-8', errors='ignore')
|
|
519
|
+
return (ret.returncode == 0)
|
|
520
|
+
'''_decryptmp4decrypt'''
|
|
521
|
+
@staticmethod
|
|
522
|
+
def _decryptmp4decrypt(input_path: str, output_path: str, decryption_key: str, legacy: bool, silent: bool = False):
|
|
523
|
+
if legacy:
|
|
524
|
+
keys = ["--key", f"1:{decryption_key}"]
|
|
525
|
+
else:
|
|
526
|
+
AppleMusicClientUtils._fixkeyid(input_path)
|
|
527
|
+
keys = ["--key", "0" * 31 + "1" + f":{decryption_key}", "--key", "0" * 32 + f":{DEFAULT_SONG_DECRYPTION_KEY}"]
|
|
528
|
+
cmd = ["mp4decrypt", *keys, input_path, output_path]
|
|
529
|
+
capture_output = True if silent else False
|
|
530
|
+
ret = subprocess.run(cmd, check=True, capture_output=capture_output, text=True, encoding='utf-8', errors='ignore')
|
|
531
|
+
return (ret.returncode == 0)
|
|
532
|
+
'''_decryptamdecrypt'''
|
|
533
|
+
@staticmethod
|
|
534
|
+
def _decryptamdecrypt(input_path: str, output_path: str, media_id: str, fairplay_key: str, wrapper_decrypt_ip: str = "127.0.0.1:10020", silent: bool = False):
|
|
535
|
+
cmd = ['amdecrypt', wrapper_decrypt_ip, shutil.which('mp4decrypt'), media_id, fairplay_key, input_path, output_path]
|
|
536
|
+
capture_output = True if silent else False
|
|
537
|
+
ret = subprocess.run(cmd, check=True, capture_output=capture_output, text=True, encoding='utf-8', errors='ignore')
|
|
538
|
+
return (ret.returncode == 0)
|
|
539
|
+
'''decrypt'''
|
|
540
|
+
@staticmethod
|
|
541
|
+
def decrypt(encrypted_path: str, decrypted_path: str, final_path: str, decryption_key: DecryptionKeyAv, codec: SongCodec, media_id: str, fairplay_key: str, silent: bool = False, wrapper_decrypt_ip: str = "127.0.0.1:10020", artist: str = ""):
|
|
542
|
+
try:
|
|
543
|
+
is_success = AppleMusicClientUtils._decryptmp4decrypt(encrypted_path, decrypted_path, decryption_key.audio_track.key, codec.islegacy(), silent=silent)
|
|
544
|
+
is_success = AppleMusicClientUtils._remuxmp4box(decrypted_path, final_path, silent=silent, artist=artist)
|
|
545
|
+
except:
|
|
546
|
+
assert fairplay_key
|
|
547
|
+
is_success = AppleMusicClientUtils._decryptamdecrypt(encrypted_path, final_path, media_id, fairplay_key, wrapper_decrypt_ip=wrapper_decrypt_ip, silent=silent)
|
|
548
|
+
return is_success
|
|
549
|
+
'''downloadstream'''
|
|
550
|
+
@staticmethod
|
|
551
|
+
def downloadstream(stream_url: str, download_path: str, silent: bool = False, random_uuid: str = ''):
|
|
552
|
+
download_path_obj = Path(download_path)
|
|
553
|
+
download_path_obj.parent.mkdir(parents=True, exist_ok=True)
|
|
554
|
+
log_dir = user_log_dir(appname='musicdl', appauthor='zcjin')
|
|
555
|
+
log_file_path = os.path.join(log_dir, f"musicdl_{random_uuid}.log")
|
|
556
|
+
cmd = [
|
|
557
|
+
"N_m3u8DL-RE", stream_url, "--binary-merge", "--ffmpeg-binary-path", shutil.which('ffmpeg'),
|
|
558
|
+
"--save-name", download_path_obj.stem, "--save-dir", download_path_obj.parent, "--tmp-dir", download_path_obj.parent,
|
|
559
|
+
'--log-file-path', log_file_path,
|
|
560
|
+
]
|
|
561
|
+
capture_output = True if silent else False
|
|
562
|
+
ret = subprocess.run(cmd, check=True, capture_output=capture_output, text=True, encoding='utf-8', errors='ignore')
|
|
563
|
+
return (ret.returncode == 0)
|