def __init__(self, threadNum=3): self.config = TidalConfig() self.tool = TidalTool() self.thread = ThreadTool(int(threadNum)) self.ffmpeg = FFmpegTool(mergerTimeout=45) self.progress = ProgressTool(100) self.check = CheckTool() self.showpro = False if self.config.showprogress == 'True': self.showpro = True
def __init__(self, threadNum=3): self.config = TidalConfig() self.tool = TidalTool() self.thread = ThreadTool(int(threadNum)) self.ffmpeg = FFmpegTool(mergerTimeout=45) self.progress = ProgressTool(100) self.check = CheckTool() pathHelper.mkdirs(self.config.outputdir + "/Album/") pathHelper.mkdirs(self.config.outputdir + "/Playlist/") pathHelper.mkdirs(self.config.outputdir + "/Video/") pathHelper.mkdirs(self.config.outputdir + "/Favorite/")
def __init__(self): self.config = TidalConfig() self.errmsg = "" self.tmpfileFlag = 'TIDAL_TMP_' self.ffmpeg = FFmpegTool(1) self.header = {'X-Tidal-SessionId': self.config.sessionid} self.header2 = {'X-Tidal-SessionId': self.config.sessionid2} self.header3 = None if self.config.accesstoken != '': self.header3 = {'authorization': 'Bearer {}'.format(self.config.accesstoken)}
class Download(object): def __init__(self, threadNum=3): self.config = TidalConfig() self.tool = TidalTool() self.thread = ThreadTool(int(threadNum)) self.ffmpeg = FFmpegTool() self.progress = ProgressTool(100) self.check = CheckTool() pathHelper.mkdirs(self.config.outputdir + "/Album/") pathHelper.mkdirs(self.config.outputdir + "/Track/") pathHelper.mkdirs(self.config.outputdir + "/Playlist/") pathHelper.mkdirs(self.config.outputdir + "/Video/") pathHelper.mkdirs(self.config.outputdir + "/Favorite/") def __isNeedDownload(self, path, url): curSize = fileHelper.getFileSize(path) if curSize <= 0: return True netSize = netHelper.getFileSize(url) if curSize >= netSize: return False return True # dowmload track thread def __thradfunc_dl(self, paraList): count = 1 printRet = True pstr = paraList['title'] + "(Download Err!)" redownload = True needDl = True bIsSuccess = False albumInfo = None index = None if 'redownload' in paraList: redownload = paraList['redownload'] if 'retry' in paraList: count = count + paraList['retry'] if 'show' in paraList: printRet = paraList['show'] if 'album' in paraList: albumInfo = paraList['album'] if 'index' in paraList: index = paraList['index'] if redownload is False: needDl = self.__isNeedDownload(paraList['path'], paraList['url']) if needDl: try: while count > 0: count = count - 1 check = netHelper.downloadFile(paraList['url'], paraList['path']) if check is True: if paraList['key'] == '': break key, nonce = decrypt_security_token(paraList['key']) decrypt_file(paraList['path'], key, nonce) break if check: self.tool.setTrackMetadata(paraList['trackinfo'], paraList['path'], albumInfo, index) pstr = paraList['title'] bIsSuccess = True except: pass else: pstr = paraList['title'] bIsSuccess = True if printRet: if (bIsSuccess): printSUCCESS(14, pstr) else: printErr(14, pstr) return # creat album output dir def __creatAlbumDir(self, albumInfo): # creat outputdir title = pathHelper.replaceLimitChar(albumInfo['title'], '-') author = pathHelper.replaceLimitChar(albumInfo['artist']['name'], '-') targetDir = self.config.outputdir + "/Album/" + title + '(' + author + ')' targetDir = os.path.abspath(targetDir) pathHelper.mkdirs(targetDir) # creat volumes dir count = 0 numOfVolumes = int(albumInfo['numberOfVolumes']) if numOfVolumes > 1: while count < numOfVolumes: volumeDir = targetDir + "/Volume" + str(count) pathHelper.mkdirs(volumeDir) count = count + 1 return targetDir def _getSongExtension(self, downloadUrl): if downloadUrl.find('.flac?') != -1: return '.flac' if downloadUrl.find('.m4a?') != -1: return '.m4a' if downloadUrl.find('.mp4?') != -1: return '.mp4' return '.m4a' def __getAlbumSongSavePath(self, targetDir, albumInfo, item, extension): if extension == None: extension = ".m4a" numOfVolumes = int(albumInfo['numberOfVolumes']) if numOfVolumes <= 1: filePath = targetDir + "/" + pathHelper.replaceLimitChar( item['title'], '-') + extension else: index = item['volumeNumber'] filePath = targetDir + "/Volume" + str( index - 1) + "/" + pathHelper.replaceLimitChar( item['title'], '-') + extension return filePath def __getExistFiles(self, paths): ret = [] for item in paths: if os.path.isfile(item): ret.append(item) return ret def __getVideoResolutionIndex(self, reslist): array = [] for item in reslist: subs = item.split('x') array.append(int(subs[1])) cmp = int(self.config.resolution) ret = 0 for item in array: if cmp >= item: return ret ret += 1 return len(array) - 1 def downloadAlbum(self): while True: print("----------------ALBUM------------------") sID = printChoice("Enter AlbumID(Enter '0' go back):", True, 0) if sID == 0: return aAlbumInfo = self.tool.getAlbum(sID) if self.tool.errmsg != "": printErr(0, "Get AlbumInfo Err! " + self.tool.errmsg) continue print("[Title] %s" % (aAlbumInfo['title'])) print("[SongNum] %s\n" % (aAlbumInfo['numberOfTracks'])) # Get Tracks aAlbumTracks = self.tool.getAlbumTracks(sID) if self.tool.errmsg != "": printErr(0, "Get AlbumTracks Err!" + self.tool.errmsg) return # Creat OutputDir targetDir = self.__creatAlbumDir(aAlbumInfo) # write msg string = self.tool.convertAlbumInfoToString( aAlbumInfo, aAlbumTracks) with open(targetDir + "/AlbumInfo.txt", 'w', encoding='utf-8') as fd: fd.write(string) # download cover coverPath = targetDir + '/' + pathHelper.replaceLimitChar( aAlbumInfo['title'], '-') + '.jpg' coverUrl = self.tool.getAlbumArtworkUrl(aAlbumInfo['cover']) netHelper.downloadFile(coverUrl, coverPath) # check exist files redownload = True existFiles = pathHelper.getDirFiles(targetDir) for item in existFiles: if '.txt' in item: continue if '.jpg' in item: continue check = printChoice( "Some TrackFile Exist.Is Redownload?(y/n):") if check != 'y' and check != 'yes': redownload = False break # download album tracks for item in aAlbumTracks['items']: streamInfo = self.tool.getStreamUrl(str(item['id']), self.config.quality) if self.tool.errmsg != "": printErr( 14, item['title'] + "(Get Stream Url Err!" + self.tool.errmsg + ")") continue fileType = self._getSongExtension(streamInfo['url']) filePath = self.__getAlbumSongSavePath(targetDir, aAlbumInfo, item, fileType) paraList = { 'album': aAlbumInfo, 'redownload': redownload, 'title': item['title'], 'trackinfo': item, 'url': streamInfo['url'], 'path': filePath, 'retry': 3, 'key': streamInfo['encryptionKey'] } self.thread.start(self.__thradfunc_dl, paraList) # wait all download thread self.thread.waitAll() self.tool.removeTmpFile(targetDir) return def downloadTrack(self): while True: targetDir = self.config.outputdir + "/Track/" print("----------------TRACK------------------") sID = printChoice("Enter TrackID(Enter '0' go back):", True, 0) if sID == 0: return aTrackInfo = self.tool.getTrack(sID) if self.tool.errmsg != "": printErr(0, "Get TrackInfo Err! " + self.tool.errmsg) return print("[TrackTitle ] %s" % (aTrackInfo['title'])) print("[Duration ] %s" % (aTrackInfo['duration'])) print("[TrackNumber] %s" % (aTrackInfo['trackNumber'])) print("[Version ] %s\n" % (aTrackInfo['version'])) # download streamInfo = self.tool.getStreamUrl(sID, self.config.quality) if self.tool.errmsg != "": printErr( 14, aTrackInfo['title'] + "(Get Stream Url Err!" + self.tool.errmsg + ")") continue fileType = self._getSongExtension(streamInfo['url']) filePath = targetDir + "/" + pathHelper.replaceLimitChar( aTrackInfo['title'], '-') + fileType paraList = { 'title': aTrackInfo['title'], 'trackinfo': aTrackInfo, 'url': streamInfo['url'], 'path': filePath, 'retry': 3, 'key': streamInfo['encryptionKey'] } self.thread.start(self.__thradfunc_dl, paraList) # wait all download thread self.thread.waitAll() self.tool.removeTmpFile(targetDir) return def downloadVideo(self): while True: targetDir = self.config.outputdir + "/Video/" print("----------------VIDEO------------------") sID = printChoice("Enter VideoID(Enter '0' go back):", True, 0) if sID == 0: return aVideoInfo = self.tool.getVideo(sID) if self.tool.errmsg != "": printErr(0, "Get VideoInfo Err! " + self.tool.errmsg) continue print("[Title ] %s" % (aVideoInfo['title'])) print("[Duration ] %s" % (aVideoInfo['duration'])) print("[TrackNumber] %s" % (aVideoInfo['trackNumber'])) print("[Type ] %s\n" % (aVideoInfo['type'])) # get resolution index = 0 resolutionList, urlList = self.tool.getVideoResolutionList(sID) if self.tool.errmsg != "": printErr(14, self.tool.errmsg) continue # print("-Index--Resolution--") # for item in resolutionList: # print(' ' + str(index) + " " + resolutionList[index]) # index = index + 1 # print("--------------------") # while True: # index = printChoice("Enter ResolutionIndex:", True, 0) # if index == '' or index == None or int(index) >= len(resolutionList): # printErr(0, "ResolutionIndex is err") # continue # break index = self.__getVideoResolutionIndex(resolutionList) path = targetDir + "/" + pathHelper.replaceLimitChar( aVideoInfo['title'], '-') + ".mp4" path = os.path.abspath(path) if os.access(path, 0): os.remove(path) if self.ffmpeg.mergerByM3u8_Multithreading(urlList[int(index)], path, True): printSUCCESS(14, aVideoInfo['title']) else: printErr(14, aVideoInfo['title']) return def downloadPlaylist(self): while True: targetDir = self.config.outputdir + "/Playlist/" print("--------------PLAYLIST-----------------") sID = printChoice("Enter PlayListID(Enter '0' go back):") if sID == '0': return aPlaylistInfo, aItemInfo = self.tool.getPlaylist(sID) if self.tool.errmsg != "": printErr(0, "Get PlaylistInfo Err! " + self.tool.errmsg) return print("[Title] %s" % (aPlaylistInfo['title'])) print("[Type] %s" % (aPlaylistInfo['type'])) print("[NumberOfTracks] %s" % (aPlaylistInfo['numberOfTracks'])) print("[NumberOfVideos] %s" % (aPlaylistInfo['numberOfVideos'])) print("[Duration] %s\n" % (aPlaylistInfo['duration'])) # Creat OutputDir targetDir = targetDir + pathHelper.replaceLimitChar( aPlaylistInfo['title'], '-') targetDir = os.path.abspath(targetDir) pathHelper.mkdirs(targetDir) # write msg string = self.tool.convertPlaylistInfoToString( aPlaylistInfo, aItemInfo) with open(targetDir + "/PlaylistInfo.txt", 'w', encoding='utf-8') as fd: fd.write(string) # download track bBreakFlag = False bFirstTime = True errIndex = [] index = 0 while bBreakFlag is False: self.check.clear() index = 0 for item in aItemInfo: type = item['type'] item = item['item'] if type != 'track': continue index = index + 1 if bFirstTime is False: if self.check.isInErr(index - 1, errIndex) == False: continue streamInfo = self.tool.getStreamUrl( str(item['id']), self.config.quality) if self.tool.errmsg != "": printErr( 14, item['title'] + "(Get Stream Url Err!!" + self.tool.errmsg + ")") continue fileType = self._getSongExtension(streamInfo['url']) filePath = targetDir + '/' + pathHelper.replaceLimitChar( item['title'], '-') + fileType paraList = { 'index': index, 'title': item['title'], 'trackinfo': item, 'url': streamInfo['url'], 'path': filePath, 'retry': 3, 'key': streamInfo['encryptionKey'] } self.check.addPath(filePath) if not os.path.isfile(filePath): self.thread.start(self.__thradfunc_dl, paraList) self.thread.waitAll() self.tool.removeTmpFile(targetDir) bBreakFlag = True bFirstTime = False # check isErr, errIndex = self.check.checkPaths() if isErr: check = printChoice( "[Err]\t\t" + len(errIndex) + " Tracks Download Failed.Try Again?(y/n):") if check == 'y' or check == 'Y': bBreakFlag = False # download video for item in aItemInfo: type = item['type'] item = item['item'] if type != 'video': continue filePath = targetDir + '/' + pathHelper.replaceLimitChar( item['title'], '-') + ".mp4" filePath = os.path.abspath(filePath) if os.access(filePath, 0): os.remove(filePath) videoID = item['id'] resolutionList, urlList = self.tool.getVideoResolutionList( videoID) if urlList is None: printErr(14, item['title'] + '(' + self.tool.errmsg + ')') else: selectIndex = self.__getVideoResolutionIndex( resolutionList) if self.ffmpeg.mergerByM3u8_Multithreading( urlList[selectIndex], filePath, showprogress=False): printSUCCESS(14, item['title']) else: printErr(14, item['title'] + "(Download Or Merger Err!)") return def downloadFavorite(self): targetDir = self.config.outputdir + "/Favorite/" pathHelper.mkdirs(targetDir) trackList, videoList = self.tool.getFavorite(self.config.userid) if self.tool.errmsg != "": printErr(0, "Get FavoriteList Err! " + self.tool.errmsg) return print("[NumberOfTracks] %s" % (len(trackList))) print("[NumberOfVideos] %s" % (len(videoList))) # download track for item in trackList: item = item['item'] streamInfo = self.tool.getStreamUrl(str(item['id']), self.config.quality) if self.tool.errmsg != "": printErr( 14, item['title'] + "(Get Stream Url Err!!" + self.tool.errmsg + ")") continue fileType = self._getSongExtension(streamInfo['url']) filePath = targetDir + '/' + pathHelper.replaceLimitChar( item['title'], '-') + fileType paraList = { 'title': item['title'], 'trackinfo': item, 'url': streamInfo['url'], 'path': filePath, 'retry': 3, 'key': streamInfo['encryptionKey'] } self.thread.start(self.__thradfunc_dl, paraList) self.thread.waitAll() # download video for item in videoList: item = item['item'] filePath = targetDir + '/' + pathHelper.replaceLimitChar( item['title'], '-') + ".mp4" filePath = os.path.abspath(filePath) if os.access(filePath, 0): os.remove(filePath) resolutionList, urlList = self.tool.getVideoResolutionList( item['id']) selectIndex = self.__getVideoResolutionIndex(resolutionList) if self.ffmpeg.mergerByM3u8_Multithreading(urlList[selectIndex], filePath, showprogress=False): printSUCCESS(14, item['title']) else: printErr(14, item['title']) return
def __init__(self): self.config = TidalConfig() self.errmsg = "" self.tmpfileFlag = 'TIDAL_TMP_' self.ffmpeg = FFmpegTool(1)
class TidalTool(object): def __init__(self): self.config = TidalConfig() self.errmsg = "" self.tmpfileFlag = 'TIDAL_TMP_' self.ffmpeg = FFmpegTool(1) def _get(self, url, params={}): retry = 3 sessionid = self.config.sessionid if 'soundQuality' in params: if params['soundQuality'] == 'LOSSLESS': sessionid = self.config.sessionid2 while retry > 0: retry -= 1 try: self.errmsg = "" params['countryCode'] = self.config.countrycode resp = requests.get(URL_PRE + url, headers={ 'X-Tidal-SessionId': sessionid }, params=params).json() if 'status' in resp and resp['status'] == 404 and resp[ 'subStatus'] == 2001: self.errmsg = '{}. This might be region-locked.'.format( resp['userMessage']) elif 'status' in resp and resp['status'] == 401 and resp[ 'subStatus'] == 4005: #'Asset is not ready for playback' sessionid = self.config.sessionid2 continue elif 'status' in resp and not resp['status'] == 200: self.errmsg = '{}. Get operation err!'.format( resp['userMessage']) # self.errmsg = "Get operation err!" return resp except Exception as e: if retry <= 0: self.errmsg = 'Function `Http-Get` Err! ' + str(e) return None def setTag(self, tag, srcfile, coverpath=None): path = pathHelper.getDirName(srcfile) name = pathHelper.getFileNameWithoutExtension(srcfile) ext = pathHelper.getFileExtension(srcfile) oext = ext if 'm4a' in ext or 'mp4' in ext: oext = '.mp3' if 'mp3' not in oext: coverpath = None tmpfile = path + '/' + 'TMP' + name + oext try: data = AudioSegment.from_file(srcfile, format=ext[1:]) check = data.export(tmpfile, format=oext[1:], tags=tag, cover=coverpath) check.close() except Exception as e: pathHelper.remove(tmpfile) return if fileHelper.getFileSize(tmpfile) > 0: pathHelper.remove(srcfile) os.rename(tmpfile, path + '/' + name + oext) else: pathHelper.remove(tmpfile) def setTrackMetadata_old(self, track_info, file_path, album_info, index, coverpath): tag = { 'Artist': track_info['artist']['name'], 'Album': track_info['album']['title'], 'Title': track_info['title'], 'CopyRight': track_info['copyright'], 'Track': track_info['trackNumber'] } if index is not None: tag['Track'] = str(index) if album_info is not None: tag['Date'] = album_info['releaseDate'] tag['Year'] = album_info['releaseDate'].split('-')[0] self.setTag(tag, file_path, coverpath) return def covertMp4toM4a(self, file_path): if self.config.onlym4a != "True": return file_path if '.mp4' not in file_path: return file_path if not self.ffmpeg.enable: return file_path new_path = file_path.replace('.mp4', '.m4a') pathHelper.remove(new_path) if self.ffmpeg.covertFile(file_path, new_path): pathHelper.remove(file_path) return new_path else: return file_path def _parseContributors(self, roleType, Contributors): if Contributors is None: return None try: ret = [] for item in Contributors['items']: if item['role'] == roleType: ret.append(item['name']) return ret except: return None def setTrackMetadata(self, track_info, file_path, album_info, index, coverpath, Contributors): # isrc,replayGain,releasedate obj = tagHelper.TagTool(file_path) obj.album = track_info['album']['title'] obj.title = track_info['title'] obj.artist = self._getArtists(track_info['artists']) obj.copyright = track_info['copyright'] obj.tracknumber = track_info['trackNumber'] obj.discnumber = track_info['volumeNumber'] obj.isrc = track_info['isrc'] obj.composer = self._parseContributors('Composer', Contributors) if index is not None: obj.tracknumber = str(index) if album_info is not None: obj.albumartist = self._getArtists(album_info['artists']) obj.date = album_info['releaseDate'] obj.totaldisc = album_info['numberOfVolumes'] if obj.totaldisc <= 1: obj.totaltrack = album_info['numberOfTracks'] obj.save(coverpath) return def removeTmpFile(self, path): for root, dirs, files in os.walk(path): for name in files: if self.tmpfileFlag in name: pathHelper.remove(os.path.join(root, name)) def getStreamUrl(self, track_id, quality): url = self._get('tracks/' + str(track_id) + '/streamUrl', {'soundQuality': quality}) if not url: resp = self._get( 'tracks/{}/playbackinfopostpaywall'.format(track_id), { 'audioquality': quality, 'playbackmode': 'STREAM', 'assetpresentation': 'FULL' }) if resp and 'trackId' in resp: # printWarning(14, "Redirecting: {} -> {}".format(track_id, resp['trackId'])) track_id = resp['trackId'] url = self._get('tracks/' + str(track_id) + '/streamUrl', {'soundQuality': quality}) return url def _getArtists(self, pHash): ret = [] for item in pHash: ret.append(item['name']) return ret def getIndexStr(self, index, sum): pre = "0" if sum > 99: pre = "00" if index < 10: return pre + str(index) + " " if index < 99 and sum > 99: return "0" + str(index) + " " return str(index) + " " def _fixSameTrackName(self, tracks, isOnLayer2=False): same = {} for item in tracks: if isOnLayer2: item = item['item'] if 'version' in item: if item['version'] is not None: item['title'] = item['title'] + ' - ' + item['version'] if item['title'] in same: same[item['title']] += 1 else: same[item['title']] = 1 for item in same: if same[item] <= 1: continue index = 1 for track in tracks: if track: track = track['item'] if track['title'] != item: continue track['title'] += str(index) index += 1 return tracks def getPlaylist(self, playlist_id): info = self._get('playlists/' + playlist_id) if self.errmsg != "": return None, None list = self.__getItemsList('playlists/' + playlist_id + '/items') list = self._fixSameTrackName(list, True) return info, list def getAlbumTracks(self, album_id): info = self._get('albums/' + str(album_id) + '/tracks') if self.errmsg != "": return info # sum = info['totalNumberOfItems'] for item in info['items']: if 'version' in item and item['version'] is not None: item['title'] += ' - ' + item['version'] # indexs = self._getIndexStr(item['trackNumber'],sum) # item['title'] = indexs + " " + item['title'] # info['items'] = self._fixSameTrackName(info['items']) return info def getAlbumVideos(self, album_id): # info = self._get('albums/' + str(album_id) + '/items') info = self.__getItemsList('albums/' + str(album_id) + '/items') if self.errmsg != "": return [] ret = [] for item in info: if item['type'] == 'video': ret.append(item) return ret def getTrack(self, track_id): item = self._get('tracks/' + str(track_id)) if 'version' in item and item['version'] is not None: item['title'] += ' - ' + item['version'] return item def getAlbum(self, album_id): return self._get('albums/' + str(album_id)) def getVideo(self, video_id): return self._get('videos/' + str(video_id)) def getFavorite(self, user_id): trackList = self.__getItemsList('users/' + str(user_id) + '/favorites/tracks') tracklist = self._fixSameTrackName(trackList, True) videoList = self.__getItemsList('users/' + str(user_id) + '/favorites/videos') return trackList, videoList def getArtistAlbum(self, artist_id, includeSingles): if includeSingles: items1 = self.__getItemsList( 'artists/' + str(artist_id) + '/albums', {'filter': 'EPSANDSINGLES'}) else: items1 = [] # items2 = self.__getItemsList('artists/' + str(artist_id) + '/albums',{'filter': 'COMPILATIONS'}) items3 = self.__getItemsList('artists/' + str(artist_id) + '/albums') itemall = items1 + items3 return itemall def __getItemsList(self, url, in_dirs={}): in_dirs['limit'] = 0 ret = self._get(url, in_dirs) count = ret['totalNumberOfItems'] offset = 0 limit = 100 retList = [] while offset < count: in_dirs['limit'] = limit in_dirs['offset'] = offset items = self._get(url, in_dirs) if self.errmsg != "": if self.errmsg.find('Too big page') >= 0: limit = limit - 10 continue else: return retList offset = offset + limit if retList == None: retList = items['items'] else: retList.extend(items['items']) return retList def getTrackContributors(self, track_id): return self._get('tracks/' + str(track_id) + '/contributors') def getAlbumArtworkUrl(self, coverid, size=1280): if coverid is not None: return 'https://resources.tidal.com/images/{0}/{1}x{1}.jpg'.format( coverid.replace('-', '/'), size) else: return '' def getPlaylistArtworkUrl(self, playlist_uuid, size=1280): return 'http://images.tidalhifi.com/im/im?w={1}&h={2}&uuid={0}&rows=2&cols=3&noph'.format( playlist_uuid, size, size) def getVideoResolutionList(self, video_id): info = self._get('videos/' + str(video_id) + '/streamurl') if self.errmsg != "": return None, None content = netHelper.downloadString(info['url'], None) resolutionList, urlList = self.__parseVideoMasterAll(str(content)) return resolutionList, urlList def getVideoMediaPlaylist(self, url): urlList = self.__parseVideoMediaPlaylist(url) return urlList def searchTrack(self, query): ret = self._get('search/tracks', { 'query': query, 'offset': 0, 'limit': 99 }) return ret def __parseVideoMasterAll(self, content): pattern = re.compile(r"(?<=RESOLUTION=).+?(?=\\n)") resolutionList = pattern.findall(content) pattern = re.compile(r"(?<=http).+?(?=\\n)") pList = pattern.findall(content) urlList = [] for item in pList: urlList.append("http" + item) return resolutionList, urlList def __parseVideoMediaPlaylist(self, url): content = netHelper.downloadString(url, None) pattern = re.compile(r"(?<=http).+?(?=\\n)") plist = pattern.findall(str(content)) urllist = [] for item in plist: urllist.append("http" + item) return urllist def convertAlbumInfoToString(self, aAlbumInfo, aAlbumTracks): str = "" str += "[ID] %d\n" % (aAlbumInfo['id']) str += "[Title] %s\n" % (aAlbumInfo['title']) str += "[Artists] %s\n" % (aAlbumInfo['artist']['name']) str += "[ReleaseDate] %s\n" % (aAlbumInfo['releaseDate']) str += "[SongNum] %s\n" % (aAlbumInfo['numberOfTracks']) str += "[Duration] %s\n" % (aAlbumInfo['duration']) str += '\n' i = 0 while True: if i >= int(aAlbumInfo['numberOfVolumes']): break i = i + 1 str += "===========Volume %d=============\n" % i for item in aAlbumTracks['items']: if item['volumeNumber'] != i: continue str += '{:<8}'.format("[%d]" % item['trackNumber']) str += "%s\n" % item['title'] return str def convertPlaylistInfoToString(seld, aPlaylistInfo, aTrackItems): str = "" str += "[Title] %s\n" % (aPlaylistInfo['title']) str += "[Type] %s\n" % (aPlaylistInfo['type']) str += "[NumberOfTracks] %s\n" % (aPlaylistInfo['numberOfTracks']) str += "[NumberOfVideos] %s\n" % (aPlaylistInfo['numberOfVideos']) str += "[Duration] %s\n" % (aPlaylistInfo['duration']) i = 0 str += "===========Track=============\n" for item in aTrackItems: type = item['type'] item = item['item'] if type != 'track': continue i = i + 1 str += '{:<8}'.format("[%d]" % i) + item['title'] + '\n' i = 0 str += "\n===========Video=============\n" for item in aTrackItems: type = item['type'] item = item['item'] if type != 'video': continue i = i + 1 str += '{:<8}'.format("[%d]" % i) + item['title'] + '\n' return str def parseLink(self, link): link = link.strip() if link.find('http') < 0: return None, None urlpres = ['tidal.com/', 'tidal.com/browse/'] for pre in urlpres: stype = re.findall(pre + "(.+?)/", link) if len(stype) <= 0 or stype[0] not in TYPE_ARR: continue sid = re.findall(pre + stype[0] + "/(.+)/", link) if len(sid) <= 0: sid = re.findall(pre + stype[0] + "/(.+)", link) if len(sid) <= 0: return None, None return stype[0], sid[0] return None, None def parseFile(self, path): cfp = configHelper.ParseNoEqual(path) ret = cfp if 'album' not in ret: ret['album'] = [] if 'artist' not in ret: ret['artist'] = [] if 'track' not in ret: ret['track'] = [] if 'video' not in ret: ret['video'] = [] if 'url' not in ret: ret['url'] = [] return ret
class Download(object): def __init__(self, threadNum=3): self.config = TidalConfig() self.tool = TidalTool() self.thread = ThreadTool(int(threadNum)) self.ffmpeg = FFmpegTool(mergerTimeout=45) self.progress = ProgressTool(100) self.check = CheckTool() self.showpro = False if self.config.showprogress == 'True': self.showpro = True pathHelper.mkdirs(self.config.outputdir + "/Album/") pathHelper.mkdirs(self.config.outputdir + "/Playlist/") pathHelper.mkdirs(self.config.outputdir + "/Video/") pathHelper.mkdirs(self.config.outputdir + "/Favorite/") def __isNeedDownload(self, path, url): curSize = fileHelper.getFileSize(path) if curSize <= 0: return True netSize = netHelper.getFileSize(url) if curSize >= netSize: return False return True # dowmload track thread def __thradfunc_dl(self, paraList): count = 1 printRet = True pstr = paraList['title'] + "(Download Err!)" redownload = True needDl = True bIsSuccess = False albumInfo = None index = None coverpath = None if 'redownload' in paraList: redownload = paraList['redownload'] if 'retry' in paraList: count = count + paraList['retry'] if 'show' in paraList: printRet = paraList['show'] if 'album' in paraList: albumInfo = paraList['album'] if 'index' in paraList: index = paraList['index'] if 'coverpath' in paraList: coverpath = paraList['coverpath'] if redownload is False: needDl = self.__isNeedDownload(paraList['path'], paraList['url']) # DEBUG # self.tool.setTrackMetadata(paraList['trackinfo'], paraList['path'], albumInfo, index, coverpath) showprogress = False if int(self.config.threadnum) <= 1 and self.showpro: showprogress = True Contributors = self.tool.getTrackContributors( paraList['trackinfo']['id']) if needDl: try: while count > 0: count = count - 1 check = netHelper.downloadFile(paraList['url'], paraList['path'] + '.part', showprogress=showprogress, stimeout=20) if check is True: if paraList['key'] == '': # unencrypted -> just move into place os.replace(paraList['path'] + '.part', paraList['path']) break else: # encrypted -> decrypt and remove encrypted file key, nonce = decrypt_security_token( paraList['key']) decrypt_file(paraList['path'] + '.part', paraList['path'], key, nonce) os.remove(paraList['path'] + '.part') break if check: bIsSuccess = True paraList['path'] = self.tool.covertMp4toM4a( paraList['path']) self.tool.setTrackMetadata(paraList['trackinfo'], paraList['path'], albumInfo, index, coverpath, Contributors) pstr = paraList['title'] except Exception as e: printErr(14, str(e) + " while downloading " + paraList['url']) else: pstr = paraList['title'] bIsSuccess = True if printRet: if (bIsSuccess): printSUCCESS(14, pstr) else: printErr(14, pstr) return # creat album output dir def __creatAlbumDir(self, albumInfo): # creat outputdir title = pathHelper.replaceLimitChar(albumInfo['title'], '-') author = pathHelper.replaceLimitChar(albumInfo['artist']['name'], '-') if self.config.addyear != 'No': if self.config.addyear == 'Before': title = '[' + str( datetime.strptime(albumInfo['releaseDate'], '%Y-%m-%d').year) + '] ' + title elif self.config.addyear == 'After': title = title + ' [' + str( datetime.strptime(albumInfo['releaseDate'], '%Y-%m-%d').year) + ']' else: title = title targetDir = self.config.outputdir + "/Album/" + author + '/' + title # targetDir = self.config.outputdir + "/Album/" + title + '(' + author + ')' targetDir = os.path.abspath(targetDir) pathHelper.mkdirs(targetDir) # creat volumes dir count = 1 numOfVolumes = int(albumInfo['numberOfVolumes']) if numOfVolumes > 1: while count < numOfVolumes + 1: volumeDir = targetDir + "/Volume" + str(count) pathHelper.mkdirs(volumeDir) count = count + 1 return targetDir def _getSongExtension(self, downloadUrl): if downloadUrl.find('.flac?') != -1: return '.flac' if downloadUrl.find('.m4a?') != -1: return '.m4a' if downloadUrl.find('.mp4?') != -1: return '.mp4' return '.m4a' def _IsExplicitString(self, IsExplicit): String = None if IsExplicit: String = 'Explicit' return String def __getAlbumSongSavePath(self, targetDir, albumInfo, item, extension): if extension is None: extension = ".m4a" seq = self.tool.getIndexStr(item['trackNumber'], albumInfo['numberOfTracks']) name = seq + pathHelper.replaceLimitChar(item['title'], '-') fileExplicit = self._IsExplicitString(item['explicit']) if self.config.addhyphen == 'True': name = seq + '- ' + pathHelper.replaceLimitChar(item['title'], '-') if self.config.addexplicit == "True" and fileExplicit is not None: name = name + " - " + fileExplicit seq = item['volumeNumber'] path = targetDir + "/" if int(albumInfo['numberOfVolumes']) > 1: path += 'Volume' + str(seq) + "/" maxlen = 255 if systemHelper.isLinux(): maxlen = 4090 # truncate filename when it's longer than system's # filename limit which is 255 len_sum = len(path) + len(name) + len(extension) if len_sum > maxlen: diff = maxlen - len_sum name = name[:len(name) + diff] filePath = path + name + extension checklen = len(filePath) return filePath def __getExistFiles(self, paths): ret = [] for item in paths: if os.path.isfile(item): ret.append(item) return ret def __getVideoResolutionIndex(self, reslist): array = [] # if reslist != None: # for item in reslist: # subs = item.split('x') # subs = subs[1].split(',') # array.append(int(subs[0])) for item in reslist: subs = item.split('x') subs = subs[1].split(',') array.append(int(subs[0])) cmp = int(self.config.resolution) ret = 0 for item in array: if cmp >= item: return ret ret += 1 return len(array) - 1 def downloadAlbum(self, album_id=None, redl_flag=None): while_count = 9999 while while_count > 0: while_count -= 1 if album_id is not None: while_count = 0 sID = album_id else: print("----------------ALBUM------------------") sID = printChoice("Enter AlbumID(Enter '0' go back):", True, 0) if sID == 0: return aAlbumInfo = self.tool.getAlbum(sID) if self.tool.errmsg != "": printErr(0, "Get AlbumInfo Err! " + self.tool.errmsg) continue print("[Title] %s" % (aAlbumInfo['title'])) print("[SongNum] %s\n" % (aAlbumInfo['numberOfTracks'])) # Get Tracks aAlbumTracks = self.tool.getAlbumTracks(sID) if self.tool.errmsg != "": printErr(0, "Get AlbumTracks Err!" + self.tool.errmsg) continue aAlbumVideos = self.tool.getAlbumVideos(sID) # Creat OutputDir targetDir = self.__creatAlbumDir(aAlbumInfo) # write msg string = self.tool.convertAlbumInfoToString( aAlbumInfo, aAlbumTracks) with codecs.open(targetDir + "/AlbumInfo.txt", 'w', 'utf-8') as fd: fd.write(string) # download cover coverPath = targetDir + '/' + pathHelper.replaceLimitChar( aAlbumInfo['title'], '-') + '.jpg' if aAlbumInfo['cover'] is not None: coverUrl = self.tool.getAlbumArtworkUrl(aAlbumInfo['cover']) netHelper.downloadFile(coverUrl, coverPath) # check exist files redownload = True if redl_flag is None: existFiles = pathHelper.getDirFiles(targetDir) for item in existFiles: if '.txt' in item: continue if '.jpg' in item: continue check = printChoice( "Some tracks already exist. Redownload?(y/n):") if not cmdHelper.isInputYes(check): redownload = False break else: redownload = redl_flag # download album tracks for item in aAlbumTracks['items']: streamInfo = self.tool.getStreamUrl(str(item['id']), self.config.quality) if self.tool.errmsg != "" or not streamInfo: printErr( 14, item['title'] + "(Get Stream Url Err!" + self.tool.errmsg + ")") continue fileType = self._getSongExtension(streamInfo['url']) filePath = self.__getAlbumSongSavePath(targetDir, aAlbumInfo, item, fileType) paraList = { 'album': aAlbumInfo, 'redownload': redownload, 'title': item['title'], 'trackinfo': item, 'url': streamInfo['url'], 'path': filePath, 'retry': 3, 'key': streamInfo['encryptionKey'], 'coverpath': coverPath } self.thread.start(self.__thradfunc_dl, paraList) # wait all download thread self.thread.waitAll() self.tool.removeTmpFile(targetDir) # remove cover if self.config.savephoto != 'True': pathHelper.remove(coverPath) # download video for item in aAlbumVideos: item = item['item'] filePath = targetDir + '/' + pathHelper.replaceLimitChar( item['title'], '-') + ".mp4" filePath = os.path.abspath(filePath) if os.access(filePath, 0): os.remove(filePath) try: resolutionList, urlList = self.tool.getVideoResolutionList( item['id']) selectIndex = self.__getVideoResolutionIndex( resolutionList) if self.ffmpeg.mergerByM3u8_Multithreading2( urlList[int(selectIndex)], filePath, showprogress=self.showpro): printSUCCESS(14, item['title']) else: printErr(14, item['title']) except: printErr(14, item['title']) # return return def downloadArtistAlbum(self, includeSingles=True, artistID=None): while True: print("-------------ARTIST ALBUM--------------") if artistID is not None: sID = artistID else: sID = printChoice("Enter Artist ID(Enter '0' go back):", True, 0) if sID == 0: return array = self.tool.getArtistAlbum(sID, includeSingles) if self.tool.errmsg != "": printErr(0, "Get AlbumList Err! " + self.tool.errmsg) continue redownload = True if artistID is None: check = printChoice("Skip downloaded files?(y/n):") if not cmdHelper.isInputYes(check): redownload = False for index, item in enumerate(array): print("----Album[{0}/{1}]----".format(index + 1, len(array))) self.downloadAlbum(item['id'], redownload) if artistID is not None: # Break out of the function if we are only downloading one artist's albums return def downloadTrack(self, track_id=None): while_count = 9999 while while_count > 0: while_count -= 1 if track_id is not None: while_count = 0 sID = track_id else: print("----------------TRACK------------------") sID = printChoice("Enter TrackID(Enter '0' go back):", True, 0) if sID == 0: return aTrackInfo = self.tool.getTrack(sID) if self.tool.errmsg != "": printErr(0, "Get TrackInfo Err! " + self.tool.errmsg) return aAlbumInfo = self.tool.getAlbum(aTrackInfo['album']['id']) if self.tool.errmsg != "": printErr(0, "Get TrackInfo Err! " + self.tool.errmsg) return # t = self.tool.getTrackContributors(sID) print("[AlbumTitle ] %s" % (aAlbumInfo['title'])) print("[TrackTitle ] %s" % (aTrackInfo['title'])) print("[Duration ] %s" % (aTrackInfo['duration'])) print("[TrackNumber] %s" % (aTrackInfo['trackNumber'])) print("[Explicit ] %s" % (aAlbumInfo['explicit'])) # print("[Version ] %s\n" % (aTrackInfo['version'])) # Creat OutputDir targetDir = self.__creatAlbumDir(aAlbumInfo) # download cover coverPath = targetDir + '/' + pathHelper.replaceLimitChar( aAlbumInfo['title'], '-') + '.jpg' if aAlbumInfo['cover'] is not None: coverUrl = self.tool.getAlbumArtworkUrl(aAlbumInfo['cover']) netHelper.downloadFile(coverUrl, coverPath) # download streamInfo = self.tool.getStreamUrl(sID, self.config.quality) if self.tool.errmsg != "" or not streamInfo: printErr( 14, aTrackInfo['title'] + "(Get Stream Url Err!" + self.tool.errmsg + ")") continue fileType = self._getSongExtension(streamInfo['url']) filePath = self.__getAlbumSongSavePath(targetDir, aAlbumInfo, aTrackInfo, fileType) paraList = { 'album': aAlbumInfo, 'title': aTrackInfo['title'], 'trackinfo': aTrackInfo, 'url': streamInfo['url'], 'path': filePath, 'retry': 3, 'key': streamInfo['encryptionKey'], 'coverpath': coverPath } self.thread.start(self.__thradfunc_dl, paraList) # wait all download thread self.thread.waitAll() self.tool.removeTmpFile(targetDir) # remove cover if self.config.savephoto != 'True': pathHelper.remove(coverPath) return def downloadVideo(self, video_id=None): flag = True while flag: targetDir = self.config.outputdir + "/Video/" if video_id is None: print("----------------VIDEO------------------") sID = printChoice("Enter VideoID(Enter '0' go back):", True, 0) if sID == 0: return else: flag = False sID = video_id aVideoInfo = self.tool.getVideo(sID) if self.tool.errmsg != "": printErr(0, "Get VideoInfo Err! " + self.tool.errmsg) continue print("[Title ] %s" % (aVideoInfo['title'])) print("[Duration ] %s" % (aVideoInfo['duration'])) print("[TrackNumber] %s" % (aVideoInfo['trackNumber'])) print("[Type ] %s\n" % (aVideoInfo['type'])) # get resolution index = 0 resolutionList, urlList = self.tool.getVideoResolutionList(sID) if self.tool.errmsg != "": printErr(14, self.tool.errmsg) continue index = self.__getVideoResolutionIndex(resolutionList) path = targetDir + "/" + pathHelper.replaceLimitChar( aVideoInfo['title'], '-') + ".mp4" path = os.path.abspath(path) if os.access(path, 0): os.remove(path) if self.ffmpeg.mergerByM3u8_Multithreading2( urlList[int(index)], path, True): printSUCCESS(14, aVideoInfo['title']) else: printErr(14, aVideoInfo['title']) return def downloadPlaylist(self, playlist_id=None): while True: targetDir = self.config.outputdir + "/Playlist/" if playlist_id is None: print("--------------PLAYLIST-----------------") sID = printChoice("Enter PlayListID(Enter '0' go back):") if sID == '0': return else: sID = playlist_id aPlaylistInfo, aItemInfo = self.tool.getPlaylist(sID) if self.tool.errmsg != "": printErr(0, "Get PlaylistInfo Err! " + self.tool.errmsg) return print("[Title] %s" % (aPlaylistInfo['title'])) print("[Type] %s" % (aPlaylistInfo['type'])) print("[NumberOfTracks] %s" % (aPlaylistInfo['numberOfTracks'])) print("[NumberOfVideos] %s" % (aPlaylistInfo['numberOfVideos'])) print("[Duration] %s\n" % (aPlaylistInfo['duration'])) # Creat OutputDir targetDir = targetDir + pathHelper.replaceLimitChar( aPlaylistInfo['title'], '-') targetDir = os.path.abspath(targetDir).strip() pathHelper.mkdirs(targetDir) # write msg string = self.tool.convertPlaylistInfoToString( aPlaylistInfo, aItemInfo) with codecs.open(targetDir + "/PlaylistInfo.txt", 'w', 'utf-8') as fd: fd.write(string) # download cover coverPath = targetDir + '/' + pathHelper.replaceLimitChar( aPlaylistInfo['title'], '-') + '.jpg' coverUrl = self.tool.getPlaylistArtworkUrl(aPlaylistInfo['uuid']) check = netHelper.downloadFile(coverUrl, coverPath) # download track bBreakFlag = False bFirstTime = True errIndex = [] index = 0 while bBreakFlag is False: self.check.clear() index = 0 tmpcoverpath = [] for item in aItemInfo: type = item['type'] item = item['item'] if type != 'track': continue index = index + 1 if bFirstTime is False: if self.check.isInErr(index - 1, errIndex) == False: continue streamInfo = self.tool.getStreamUrl( str(item['id']), self.config.quality) # streamInfo = self.tool.getStreamUrl(str(item['id']), 'DOLBY_ATMOS') if self.tool.errmsg != "" or not streamInfo: printErr( 14, item['title'] + "(Get Stream Url Err!!" + self.tool.errmsg + ")") continue aAlbumInfo = self.tool.getAlbum(item['album']['id']) fileType = self._getSongExtension(streamInfo['url']) # change targetDir targetDir2 = targetDir if self.config.plfile2arfolder == "True": targetDir2 = self.__creatAlbumDir(aAlbumInfo) filePath = self.__getAlbumSongSavePath( targetDir2, aAlbumInfo, item, fileType) paraList = { 'album': aAlbumInfo, 'title': item['title'], 'trackinfo': item, 'url': streamInfo['url'], 'path': filePath, 'retry': 3, 'key': streamInfo['encryptionKey'] } else: seq = self.tool.getIndexStr(index, len(aItemInfo)) filePath = targetDir2 + '/' + seq + " " + pathHelper.replaceLimitChar( item['title'], '-') + fileType paraList = { 'album': aAlbumInfo, 'index': index, 'title': item['title'], 'trackinfo': item, 'url': streamInfo['url'], 'path': filePath, 'retry': 3, 'key': streamInfo['encryptionKey'] } try: coverPath = targetDir2 + '/' + pathHelper.replaceLimitChar( aAlbumInfo['title'], '-') + '.jpg' coverUrl = self.tool.getAlbumArtworkUrl( aAlbumInfo['cover']) netHelper.downloadFile(coverUrl, coverPath) paraList['coverpath'] = coverPath tmpcoverpath.append(coverPath) except: cmdHelper.myprint( "Could not download artwork for '{}'".format( item['title']), cmdHelper.TextColor.Red, None) if self.config.onlym4a == "True": self.check.addPath(filePath.replace(".mp4", ".m4a")) else: self.check.addPath(filePath) self.thread.start(self.__thradfunc_dl, paraList) self.thread.waitAll() self.tool.removeTmpFile(targetDir) # remove cover if self.config.savephoto != 'True': for item in tmpcoverpath: pathHelper.remove(item) bBreakFlag = True bFirstTime = False # check isErr, errIndex = self.check.checkPaths() if isErr: check = printChoice( "[Err]\t\t" + str(len(errIndex)) + " Tracks Download Failed.Try Again?(y/n):") if check == 'y' or check == 'Y': bBreakFlag = False # download video for item in aItemInfo: type = item['type'] item = item['item'] if type != 'video': continue filePath = targetDir + '/' + pathHelper.replaceLimitChar( item['title'], '-') + ".mp4" filePath = os.path.abspath(filePath) if os.access(filePath, 0): os.remove(filePath) videoID = item['id'] resolutionList, urlList = self.tool.getVideoResolutionList( videoID) if urlList is None: printErr(14, item['title'] + '(' + self.tool.errmsg + ')') else: selectIndex = self.__getVideoResolutionIndex( resolutionList) if self.ffmpeg.mergerByM3u8_Multithreading2( urlList[int(selectIndex)], filePath, showprogress=self.showpro): printSUCCESS(14, item['title']) else: printErr(14, item['title'] + "(Download Or Merger Err!)") if playlist_id is not None: return return def downloadFavorite(self): targetDir = self.config.outputdir + "/Favorite/" pathHelper.mkdirs(targetDir) trackList, videoList = self.tool.getFavorite(self.config.userid) if self.tool.errmsg != "": printErr(0, "Get FavoriteList Err! " + self.tool.errmsg) return print("[NumberOfTracks] %s" % (len(trackList))) print("[NumberOfVideos] %s" % (len(videoList))) # download track for item in trackList: item = item['item'] streamInfo = self.tool.getStreamUrl(str(item['id']), self.config.quality) if self.tool.errmsg != "" or not streamInfo: printErr( 14, item['title'] + "(Get Stream Url Err!!" + self.tool.errmsg + ")") continue fileType = self._getSongExtension(streamInfo['url']) filePath = targetDir + '/' + pathHelper.replaceLimitChar( item['title'], '-') + fileType aAlbumInfo = self.tool.getAlbum(item['album']['id']) paraList = { 'album': aAlbumInfo, 'title': item['title'], 'trackinfo': item, 'url': streamInfo['url'], 'path': filePath, 'retry': 3, 'key': streamInfo['encryptionKey'] } self.thread.start(self.__thradfunc_dl, paraList) self.thread.waitAll() # download video for item in videoList: item = item['item'] filePath = targetDir + '/' + pathHelper.replaceLimitChar( item['title'], '-') + ".mp4" filePath = os.path.abspath(filePath) if os.access(filePath, 0): os.remove(filePath) resolutionList, urlList = self.tool.getVideoResolutionList( item['id']) selectIndex = self.__getVideoResolutionIndex(resolutionList) if self.ffmpeg.mergerByM3u8_Multithreading2( urlList[int(selectIndex)], filePath, showprogress=self.showpro): printSUCCESS(14, item['title']) else: printErr(14, item['title']) return def downloadUrl(self, link): stype, sid = self.tool.parseLink(link) if stype is None or sid is None: return if stype == "album": print("----------------ALBUM------------------") self.downloadAlbum(sid) elif stype == "track": print("----------------TRACK------------------") self.downloadTrack(sid) elif stype == "video": print("----------------VIDEO------------------") self.downloadVideo(sid) elif stype == "playlist": print("--------------PLAYLIST-----------------") self.downloadPlaylist(sid) elif stype == "artist": print("----------------ARTIST-----------------") self.downloadArtistAlbum(self.config.includesingle == "True", sid) def downloadByFile(self, path): if not os.path.exists(path): return arr = self.tool.parseFile(path) print("----------------FILE------------------") print("[Number of albums] %s" % (len(arr['album']))) print("[Number of artists] %s" % (len(arr['artist']))) print("[Number of tracks] %s" % (len(arr['track']))) print("[Number of videos] %s" % (len(arr['video']))) print("[Number of URLs] %s" % (len(arr['url']))) if len(arr['album']) > 0: redownload = True check = printChoice("Skip downloaded files?(y/n):") if not cmdHelper.isInputYes(check): redownload = False for index, item in enumerate(arr['album']): print("----Album[{0}/{1}]----".format(index + 1, len(arr['album']))) print("[ID] %s" % (item)) self.downloadAlbum(item, redownload) for index, item in enumerate(arr['artist']): print(index) print("----Artist[{0}/{1}]----".format(index + 1, len(arr['artist']))) print("[ID] %s" % (item)) includeSingles = self.config.includesingle == "True" self.downloadArtistAlbum(includeSingles, item) for index, item in enumerate(arr['track']): print("----Track[{0}/{1}]----".format(index + 1, len(arr['track']))) print("[ID] %s" % (item)) self.downloadTrack(item) for index, item in enumerate(arr['video']): print("----Video[{0}/{1}]----".format(index + 1, len(arr['video']))) print("[ID] %s" % (item)) self.downloadVideo(item) for index, item in enumerate(arr['url']): print("----Url[{0}/{1}]----".format(index + 1, len(arr['url']))) print("[link] %s" % (item)) stype, sid = self.tool.parseLink(item) if stype is None or sid is None: printErr(14, 'Link can`t parse!') continue print("[ID] %s" % (sid)) if stype == "album": print("[Type] %s" % ("album")) self.downloadAlbum(sid) if stype == "track": print("[Type] %s" % ("track")) self.downloadTrack(sid) if stype == "video": print("[Type] %s" % ("video")) self.downloadVideo(sid)
class Download(object): def __init__(self, threadNum=3): self.config = TidalConfig() self.tool = TidalTool() self.thread = ThreadTool(int(threadNum)) self.ffmpeg = FFmpegTool(mergerTimeout=45) self.progress = ProgressTool(100) self.check = CheckTool() self.showpro = False if self.config.showprogress == 'True': self.showpro = True pathHelper.mkdirs(self.config.outputdir + "/Album/") pathHelper.mkdirs(self.config.outputdir + "/Playlist/") pathHelper.mkdirs(self.config.outputdir + "/Video/") pathHelper.mkdirs(self.config.outputdir + "/Favorite/") def __isNeedDownload(self, path, url): curSize = fileHelper.getFileSize(path) if curSize <= 0: return True netSize = netHelper.getFileSize(url) if curSize >= netSize: return False return True # dowmload track thread def __thradfunc_dl(self, paraList): count = 1 printRet = True pstr = paraList['title'] + "(Download Err!)" redownload = True needDl = True bIsSuccess = False albumInfo = None index = None coverpath = None if 'redownload' in paraList: redownload = paraList['redownload'] if 'retry' in paraList: count = count + paraList['retry'] if 'show' in paraList: printRet = paraList['show'] if 'album' in paraList: albumInfo = paraList['album'] if 'index' in paraList: index = paraList['index'] if 'coverpath' in paraList: coverpath = paraList['coverpath'] if redownload is False: needDl = self.__isNeedDownload(paraList['path'], paraList['url']) # DEBUG # self.tool.setTrackMetadata(paraList['trackinfo'], paraList['path'], albumInfo, index, coverpath) showprogress = False if int(self.config.threadnum) <= 1 and self.showpro: showprogress = True Contributors = self.tool.getTrackContributors( paraList['trackinfo']['id']) if needDl: try: while count > 0: count = count - 1 check = netHelper.downloadFile(paraList['url'], paraList['path'], showprogress=showprogress) if check is True: if paraList['key'] == '': break key, nonce = decrypt_security_token(paraList['key']) decrypt_file(paraList['path'], key, nonce) break if check: bIsSuccess = True paraList['path'] = self.tool.covertMp4toM4a( paraList['path']) self.tool.setTrackMetadata(paraList['trackinfo'], paraList['path'], albumInfo, index, coverpath, Contributors) pstr = paraList['title'] except: pass else: pstr = paraList['title'] bIsSuccess = True if printRet: if (bIsSuccess): printSUCCESS(14, pstr) else: printErr(14, pstr) return # creat album output dir def __creatAlbumDir(self, albumInfo): # creat outputdir title = pathHelper.replaceLimitChar(albumInfo['title'], '-') author = pathHelper.replaceLimitChar(albumInfo['artist']['name'], '-') targetDir = self.config.outputdir + "/Album/" + author + '/' + title # targetDir = self.config.outputdir + "/Album/" + title + '(' + author + ')' targetDir = os.path.abspath(targetDir) pathHelper.mkdirs(targetDir) # creat volumes dir count = 1 numOfVolumes = int(albumInfo['numberOfVolumes']) if numOfVolumes > 1: while count < numOfVolumes + 1: volumeDir = targetDir + "/Volume" + str(count) pathHelper.mkdirs(volumeDir) count = count + 1 return targetDir def _getSongExtension(self, downloadUrl): if downloadUrl.find('.flac?') != -1: return '.flac' if downloadUrl.find('.m4a?') != -1: return '.m4a' if downloadUrl.find('.mp4?') != -1: return '.mp4' return '.m4a' def __getAlbumSongSavePath(self, targetDir, albumInfo, item, extension): if extension is None: extension = ".m4a" seq = self.tool.getIndexStr(item['trackNumber'], albumInfo['numberOfTracks']) name = seq + pathHelper.replaceLimitChar(item['title'], '-') if self.config.addhyphen == 'True': name = seq + '- ' + pathHelper.replaceLimitChar(item['title'], '-') seq = item['volumeNumber'] path = targetDir + "/" if int(albumInfo['numberOfVolumes']) > 1: path += 'Volume' + str(seq) + "/" filePath = path + name + extension return filePath def __getExistFiles(self, paths): ret = [] for item in paths: if os.path.isfile(item): ret.append(item) return ret def __getVideoResolutionIndex(self, reslist): array = [] for item in reslist: subs = item.split('x') subs = subs[1].split(',') array.append(int(subs[0])) cmp = int(self.config.resolution) ret = 0 for item in array: if cmp >= item: return ret ret += 1 return len(array) - 1 def downloadAlbum(self, album_id=None): while_count = 9999 while while_count > 0: while_count -= 1 if album_id is not None: while_count = 0 sID = album_id else: print("----------------ALBUM------------------") sID = printChoice("Enter AlbumID(Enter '0' go back):", True, 0) if sID == 0: return aAlbumInfo = self.tool.getAlbum(sID) if self.tool.errmsg != "": printErr(0, "Get AlbumInfo Err! " + self.tool.errmsg) continue print("[Title] %s" % (aAlbumInfo['title'])) print("[SongNum] %s\n" % (aAlbumInfo['numberOfTracks'])) # Get Tracks aAlbumTracks = self.tool.getAlbumTracks(sID) if self.tool.errmsg != "": printErr(0, "Get AlbumTracks Err!" + self.tool.errmsg) continue aAlbumVideos = self.tool.getAlbumVideos(sID) # Creat OutputDir targetDir = self.__creatAlbumDir(aAlbumInfo) # write msg string = self.tool.convertAlbumInfoToString( aAlbumInfo, aAlbumTracks) with codecs.open(targetDir + "/AlbumInfo.txt", 'w', 'utf-8') as fd: fd.write(string) # download cover coverPath = targetDir + '/' + pathHelper.replaceLimitChar( aAlbumInfo['title'], '-') + '.jpg' coverUrl = self.tool.getAlbumArtworkUrl(aAlbumInfo['cover']) netHelper.downloadFile(coverUrl, coverPath) # check exist files redownload = True existFiles = pathHelper.getDirFiles(targetDir) for item in existFiles: if '.txt' in item: continue if '.jpg' in item: continue check = printChoice( "Some TrackFile Exist.Is Redownload?(y/n):") if check != 'y' and check != 'yes': redownload = False break # download album tracks for item in aAlbumTracks['items']: streamInfo = self.tool.getStreamUrl(str(item['id']), self.config.quality) if self.tool.errmsg != "": printErr( 14, item['title'] + "(Get Stream Url Err!" + self.tool.errmsg + ")") continue fileType = self._getSongExtension(streamInfo['url']) filePath = self.__getAlbumSongSavePath(targetDir, aAlbumInfo, item, fileType) paraList = { 'album': aAlbumInfo, 'redownload': redownload, 'title': item['title'], 'trackinfo': item, 'url': streamInfo['url'], 'path': filePath, 'retry': 3, 'key': streamInfo['encryptionKey'], 'coverpath': coverPath } self.thread.start(self.__thradfunc_dl, paraList) # wait all download thread self.thread.waitAll() self.tool.removeTmpFile(targetDir) # download video for item in aAlbumVideos: item = item['item'] filePath = targetDir + '/' + pathHelper.replaceLimitChar( item['title'], '-') + ".mp4" filePath = os.path.abspath(filePath) if os.access(filePath, 0): os.remove(filePath) resolutionList, urlList = self.tool.getVideoResolutionList( item['id']) selectIndex = self.__getVideoResolutionIndex(resolutionList) if self.ffmpeg.mergerByM3u8_Multithreading( urlList[selectIndex], filePath, showprogress=self.showpro): printSUCCESS(14, item['title']) else: printErr(14, item['title']) return return def downloadArtistAlbum(self): while True: print("-------------ARTIST ALBUM--------------") sID = printChoice("Enter ArtistID(Enter '0' go back):", True, 0) if sID == 0: return array = self.tool.getArtistAlbum(sID) if self.tool.errmsg != "": printErr(0, "Get AlbumList Err! " + self.tool.errmsg) continue for index, item in enumerate(array): print("----Album[{0}/{1}]----".format(index + 1, len(array))) self.downloadAlbum(item['id']) def downloadTrack(self, track_id=None): while_count = 9999 while while_count > 0: while_count -= 1 if track_id is not None: while_count = 0 sID = track_id else: print("----------------TRACK------------------") sID = printChoice("Enter TrackID(Enter '0' go back):", True, 0) if sID == 0: return aTrackInfo = self.tool.getTrack(sID) if self.tool.errmsg != "": printErr(0, "Get TrackInfo Err! " + self.tool.errmsg) return aAlbumInfo = self.tool.getAlbum(aTrackInfo['album']['id']) if self.tool.errmsg != "": printErr(0, "Get TrackInfo Err! " + self.tool.errmsg) return # t = self.tool.getTrackContributors(sID) print("[AlbumTitle ] %s" % (aAlbumInfo['title'])) print("[TrackTitle ] %s" % (aTrackInfo['title'])) print("[Duration ] %s" % (aTrackInfo['duration'])) print("[TrackNumber] %s" % (aTrackInfo['trackNumber'])) print("[Version ] %s\n" % (aTrackInfo['version'])) # Creat OutputDir targetDir = self.__creatAlbumDir(aAlbumInfo) # download cover coverPath = targetDir + '/' + pathHelper.replaceLimitChar( aAlbumInfo['title'], '-') + '.jpg' coverUrl = self.tool.getAlbumArtworkUrl(aAlbumInfo['cover']) netHelper.downloadFile(coverUrl, coverPath) # download streamInfo = self.tool.getStreamUrl(sID, self.config.quality) if self.tool.errmsg != "": printErr( 14, aTrackInfo['title'] + "(Get Stream Url Err!" + self.tool.errmsg + ")") continue fileType = self._getSongExtension(streamInfo['url']) filePath = self.__getAlbumSongSavePath(targetDir, aAlbumInfo, aTrackInfo, fileType) # filePath = targetDir + "/" + pathHelper.replaceLimitChar(aTrackInfo['title'],'-') + fileType paraList = { 'album': aAlbumInfo, 'title': aTrackInfo['title'], 'trackinfo': aTrackInfo, 'url': streamInfo['url'], 'path': filePath, 'retry': 3, 'key': streamInfo['encryptionKey'], 'coverpath': coverPath } self.thread.start(self.__thradfunc_dl, paraList) # wait all download thread self.thread.waitAll() self.tool.removeTmpFile(targetDir) return def downloadVideo(self, video_id=None): flag = True while flag: targetDir = self.config.outputdir + "/Video/" if video_id is None: print("----------------VIDEO------------------") sID = printChoice("Enter VideoID(Enter '0' go back):", True, 0) if sID == 0: return else: flag = False sID = video_id aVideoInfo = self.tool.getVideo(sID) if self.tool.errmsg != "": printErr(0, "Get VideoInfo Err! " + self.tool.errmsg) continue print("[Title ] %s" % (aVideoInfo['title'])) print("[Duration ] %s" % (aVideoInfo['duration'])) print("[TrackNumber] %s" % (aVideoInfo['trackNumber'])) print("[Type ] %s\n" % (aVideoInfo['type'])) # get resolution index = 0 resolutionList, urlList = self.tool.getVideoResolutionList(sID) if self.tool.errmsg != "": printErr(14, self.tool.errmsg) continue index = self.__getVideoResolutionIndex(resolutionList) path = targetDir + "/" + pathHelper.replaceLimitChar( aVideoInfo['title'], '-') + ".mp4" path = os.path.abspath(path) if os.access(path, 0): os.remove(path) if self.ffmpeg.mergerByM3u8_Multithreading2( urlList[int(index)], path, True): printSUCCESS(14, aVideoInfo['title']) else: printErr(14, aVideoInfo['title']) return def downloadPlaylist(self): while True: targetDir = self.config.outputdir + "/Playlist/" print("--------------PLAYLIST-----------------") sID = printChoice("Enter PlayListID(Enter '0' go back):") if sID == '0': return aPlaylistInfo, aItemInfo = self.tool.getPlaylist(sID) if self.tool.errmsg != "": printErr(0, "Get PlaylistInfo Err! " + self.tool.errmsg) return print("[Title] %s" % (aPlaylistInfo['title'])) print("[Type] %s" % (aPlaylistInfo['type'])) print("[NumberOfTracks] %s" % (aPlaylistInfo['numberOfTracks'])) print("[NumberOfVideos] %s" % (aPlaylistInfo['numberOfVideos'])) print("[Duration] %s\n" % (aPlaylistInfo['duration'])) # Creat OutputDir targetDir = targetDir + pathHelper.replaceLimitChar( aPlaylistInfo['title'], '-') targetDir = os.path.abspath(targetDir) pathHelper.mkdirs(targetDir) # write msg string = self.tool.convertPlaylistInfoToString( aPlaylistInfo, aItemInfo) with codecs.open(targetDir + "/PlaylistInfo.txt", 'w', 'utf-8') as fd: fd.write(string) # download cover coverPath = targetDir + '/' + pathHelper.replaceLimitChar( aPlaylistInfo['title'], '-') + '.jpg' coverUrl = self.tool.getPlaylistArtworkUrl(aPlaylistInfo['uuid']) check = netHelper.downloadFile(coverUrl, coverPath) # download track bBreakFlag = False bFirstTime = True errIndex = [] index = 0 while bBreakFlag is False: self.check.clear() index = 0 for item in aItemInfo: type = item['type'] item = item['item'] if type != 'track': continue index = index + 1 if bFirstTime is False: if self.check.isInErr(index - 1, errIndex) == False: continue streamInfo = self.tool.getStreamUrl( str(item['id']), self.config.quality) if self.tool.errmsg != "": printErr( 14, item['title'] + "(Get Stream Url Err!!" + self.tool.errmsg + ")") continue fileType = self._getSongExtension(streamInfo['url']) filePath = targetDir + '/' + pathHelper.replaceLimitChar( item['title'], '-') + fileType paraList = { 'index': index, 'title': item['title'], 'trackinfo': item, 'url': streamInfo['url'], 'path': filePath, 'retry': 3, 'key': streamInfo['encryptionKey'] } self.check.addPath(filePath) if not os.path.isfile(filePath): self.thread.start(self.__thradfunc_dl, paraList) self.thread.waitAll() self.tool.removeTmpFile(targetDir) bBreakFlag = True bFirstTime = False # check isErr, errIndex = self.check.checkPaths() if isErr: check = printChoice( "[Err]\t\t" + str(len(errIndex)) + " Tracks Download Failed.Try Again?(y/n):") if check == 'y' or check == 'Y': bBreakFlag = False # download video for item in aItemInfo: type = item['type'] item = item['item'] if type != 'video': continue filePath = targetDir + '/' + pathHelper.replaceLimitChar( item['title'], '-') + ".mp4" filePath = os.path.abspath(filePath) if os.access(filePath, 0): os.remove(filePath) videoID = item['id'] resolutionList, urlList = self.tool.getVideoResolutionList( videoID) if urlList is None: printErr(14, item['title'] + '(' + self.tool.errmsg + ')') else: selectIndex = self.__getVideoResolutionIndex( resolutionList) if self.ffmpeg.mergerByM3u8_Multithreading( urlList[selectIndex], filePath, showprogress=self.showpro): printSUCCESS(14, item['title']) else: printErr(14, item['title'] + "(Download Or Merger Err!)") return def downloadFavorite(self): targetDir = self.config.outputdir + "/Favorite/" pathHelper.mkdirs(targetDir) trackList, videoList = self.tool.getFavorite(self.config.userid) if self.tool.errmsg != "": printErr(0, "Get FavoriteList Err! " + self.tool.errmsg) return print("[NumberOfTracks] %s" % (len(trackList))) print("[NumberOfVideos] %s" % (len(videoList))) # download track for item in trackList: item = item['item'] streamInfo = self.tool.getStreamUrl(str(item['id']), self.config.quality) if self.tool.errmsg != "": printErr( 14, item['title'] + "(Get Stream Url Err!!" + self.tool.errmsg + ")") continue fileType = self._getSongExtension(streamInfo['url']) filePath = targetDir + '/' + pathHelper.replaceLimitChar( item['title'], '-') + fileType paraList = { 'title': item['title'], 'trackinfo': item, 'url': streamInfo['url'], 'path': filePath, 'retry': 3, 'key': streamInfo['encryptionKey'] } self.thread.start(self.__thradfunc_dl, paraList) self.thread.waitAll() # download video for item in videoList: item = item['item'] filePath = targetDir + '/' + pathHelper.replaceLimitChar( item['title'], '-') + ".mp4" filePath = os.path.abspath(filePath) if os.access(filePath, 0): os.remove(filePath) resolutionList, urlList = self.tool.getVideoResolutionList( item['id']) selectIndex = self.__getVideoResolutionIndex(resolutionList) if self.ffmpeg.mergerByM3u8_Multithreading( urlList[selectIndex], filePath, showprogress=self.showpro): printSUCCESS(14, item['title']) else: printErr(14, item['title']) return def downloadUrl(self, link): stype, sid = self.tool.parseLink(link) if stype is None or sid is None: return if stype == "album": print("----------------ALBUM------------------") self.downloadAlbum(sid) elif stype == "track": print("----------------TRACK------------------") self.downloadTrack(sid) elif stype == "video": print("----------------VIDEO------------------") self.downloadVideo(sid) def downloadByFile(self, path): if not os.path.exists(path): return arr = self.tool.parseFile(path) print("----------------FILE------------------") print("[NumOfAlbum] %s" % (len(arr['album']))) print("[NumOfTrack] %s" % (len(arr['track']))) print("[NumOfVideo] %s" % (len(arr['video']))) print("[NumOfUrl] %s" % (len(arr['url']))) for index, item in enumerate(arr['album']): print("----Album[{0}/{1}]----".format(index + 1, len(arr['album']))) print("[ID] %s" % (item)) self.downloadAlbum(item) for index, item in enumerate(arr['track']): print("----Track[{0}/{1}]----".format(index + 1, len(arr['track']))) print("[ID] %s" % (item)) self.downloadTrack(item) for index, item in enumerate(arr['video']): print("----Video[{0}/{1}]----".format(index + 1, len(arr['video']))) print("[ID] %s" % (item)) self.downloadVideo(item) for index, item in enumerate(arr['url']): print("----Url[{0}/{1}]----".format(index + 1, len(arr['url']))) print("[link] %s" % (item)) stype, sid = self.tool.parseLink(item) if stype is None or sid is None: printErr(14, 'Link can`t parse!') continue print("[ID] %s" % (sid)) if stype == "album": print("[Type] %s" % ("album")) self.downloadAlbum(sid) if stype == "track": print("[Type] %s" % ("track")) self.downloadTrack(sid) if stype == "video": print("[Type] %s" % ("video")) self.downloadVideo(sid)