diff --git a/TIDALDL-PY/tidal_dl/__init__.py b/TIDALDL-PY/tidal_dl/__init__.py index 23daf92..4adc183 100644 --- a/TIDALDL-PY/tidal_dl/__init__.py +++ b/TIDALDL-PY/tidal_dl/__init__.py @@ -26,44 +26,11 @@ from tidal_dl.lang.language import setLang, initLang, getLangChoicePrint from tidal_dl.printf import Printf, VERSION from tidal_dl.settings import Settings, TokenSettings, getLogPath from tidal_dl.tidal import TidalAPI -from tidal_dl.util import API +from tidal_dl.util import API, CONF, TOKEN, LANG, displayTime, loginByConfig, loginByWeb import tidal_dl.apiKey as apiKey ssl._create_default_https_context = ssl._create_unverified_context -TOKEN = TokenSettings.read() -CONF = Settings.read() -LANG = initLang(CONF.language) -API.apiKey = apiKey.getItem(CONF.apiKeyIndex) - -logging.basicConfig(filename=getLogPath(), - level=logging.INFO, - format='%(asctime)s - %(levelname)s: %(message)s') - - -def displayTime(seconds, granularity=2): - if seconds <= 0: - return "unknown" - - result = [] - intervals = ( - ('weeks', 604800), - ('days', 86400), - ('hours', 3600), - ('minutes', 60), - ('seconds', 1), - ) - - for name, count in intervals: - value = seconds // count - if value: - seconds -= value * count - if value == 1: - name = name.rstrip('s') - result.append("{} {}".format(value, name)) - return ', '.join(result[:granularity]) - - def login(): print(LANG.AUTH_START_LOGIN) msg, check = API.getDeviceCode() @@ -74,29 +41,7 @@ def login(): # print(LANG.AUTH_LOGIN_CODE.format(green(API.key.userCode))) print(LANG.AUTH_NEXT_STEP.format(green("http://" + API.key.verificationUrl + "/" + API.key.userCode), yellow(displayTime(API.key.authCheckTimeout)))) print(LANG.AUTH_WAITING) - start = time.time() - elapsed = 0 - while elapsed < API.key.authCheckTimeout: - elapsed = time.time() - start - # print("Check auth status...") - msg, check = API.checkAuthStatus() - if not check: - if msg == "pending": - time.sleep(API.key.authCheckInterval + 1) - continue - Printf.err(msg) - break - if check: - Printf.success(LANG.MSG_VALID_ACCESSTOKEN.format(displayTime(int(API.key.expiresIn)))) - TOKEN.userid = API.key.userId - TOKEN.countryCode = API.key.countryCode - TOKEN.accessToken = API.key.accessToken - TOKEN.refreshToken = API.key.refreshToken - TOKEN.expiresAfter = time.time() + int(API.key.expiresIn) - TokenSettings.save(TOKEN) - break - if elapsed >= API.key.authCheckTimeout: - Printf.err(LANG.AUTH_TIMEOUT) + loginByWeb() return @@ -139,33 +84,13 @@ def setAPIKey(): def checkLogin(): - if not isNull(TOKEN.accessToken): - # print('Checking Access Token...') #add to translations - msg, check = API.verifyAccessToken(TOKEN.accessToken) - if check: - Printf.info(LANG.MSG_VALID_ACCESSTOKEN.format(displayTime(int(TOKEN.expiresAfter - time.time())))) - return - else: - Printf.info(LANG.MSG_INVAILD_ACCESSTOKEN) - msg, check = API.refreshAccessToken(TOKEN.refreshToken) - if check: - Printf.success(LANG.MSG_VALID_ACCESSTOKEN.format(displayTime(int(API.key.expiresIn)))) - TOKEN.userid = API.key.userId - TOKEN.countryCode = API.key.countryCode - TOKEN.accessToken = API.key.accessToken - TOKEN.expiresAfter = time.time() + int(API.key.expiresIn) - TokenSettings.save(TOKEN) - return - else: - Printf.err(msg) - tmp = TokenSettings() # clears saved tokens - TokenSettings.save(tmp) + if loginByConfig(): + return login() return def checkLogout(): - global LANG login() return diff --git a/TIDALDL-PY/tidal_dl/download.py b/TIDALDL-PY/tidal_dl/download.py index 61d64dd..7a284cb 100644 --- a/TIDALDL-PY/tidal_dl/download.py +++ b/TIDALDL-PY/tidal_dl/download.py @@ -22,396 +22,13 @@ from tidal_dl.model import Track, Video, Lyrics, Mix from tidal_dl.printf import Printf from tidal_dl.settings import Settings from tidal_dl.tidal import TidalAPI -from tidal_dl.util import getVideoPath, getTrackPath, getAlbumPath, getArtistsName, convertToM4a, setMetaData, \ - isNeedDownload, API, getLyricsFromGemius +from tidal_dl.util import convert, downloadTrack, downloadVideo, encrypted, getVideoPath, getTrackPath, getAlbumPath, API def __loadAPI__(user): API.key.accessToken = user.accessToken API.key.userId = user.userid API.key.countryCode = user.countryCode - # API.key.sessionId = user.sessionid1 - - -def __loadVideoAPI__(user): - API.key.accessToken = user.accessToken - API.key.userId = user.userid - API.key.countryCode = user.countryCode - # API.key.sessionId = user.sessionid2 if not aigpy.string.isNull(user.sessionid2) else user.sessionid1 - - -# def __getIndexStr__(index): -# pre = "0" -# if index < 10: -# return pre + str(index) -# if index < 99: -# return str(index) -# return str(index) - - -# def __getExtension__(url): -# if '.flac' in url: -# return '.flac' -# if '.mp4' in url: -# return '.mp4' -# return '.m4a' - - -# def __getArtists__(array): -# ret = [] -# for item in array: -# ret.append(item.name) -# return ret - - -# def __getArtistsString__(artists): -# return ", ".join(map(lambda artist: artist.name, artists)) - - -# def __parseContributors__(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 - - -# GEMIUS = lyricsgenius.Genius('vNKbAWAE3rVY_48nRaiOrDcWNLvsxS-Z8qyG5XfEzTOtZvkTfg6P3pxOVlA2BjaW') -# -# -# def __getLyrics__(trackName, artistName, proxy): -# try: -# if not aigpy.string.isNull(proxy): -# GEMIUS._session.proxies = { -# 'http': f'http://{proxy}', -# 'https': f'http://{proxy}', -# } -# -# song = GEMIUS.search_song(trackName, artistName) -# return song.lyrics -# except: -# return "" - -# -# def __setMetaData__(track, album, filepath, contributors, lyrics): -# obj = aigpy.tag.TagTool(filepath) -# obj.album = track.album.title -# obj.title = track.title -# if not aigpy.string.isNull(track.version): -# obj.title += ' (' + track.version + ')' -# -# obj.artist = map(lambda artist: artist.name, track.artists) # __getArtists__(track.artists) -# obj.copyright = track.copyRight -# obj.tracknumber = track.trackNumber -# obj.discnumber = track.volumeNumber -# obj.composer = __parseContributors__('Composer', contributors) -# obj.isrc = track.isrc -# -# obj.albumartist = map(lambda artist: artist.name, album.artists) #__getArtists__(album.artists) -# obj.date = album.releaseDate -# obj.totaldisc = album.numberOfVolumes -# obj.lyrics = lyrics -# if obj.totaldisc <= 1: -# obj.totaltrack = album.numberOfTracks -# coverpath = API.getCoverUrl(album.cover, "1280", "1280") -# obj.save(coverpath) -# return - - -# def __convertToM4a__(filepath, codec): -# if 'ac4' in codec or 'mha1' in codec: -# return filepath -# if '.mp4' not in filepath: -# return filepath -# newpath = filepath.replace('.mp4', '.m4a') -# aigpy.path.remove(newpath) -# os.rename(filepath, newpath) -# return newpath - - -# def __stripPathParts__(stripped_path, separator): -# result = "" -# stripped_path = stripped_path.split(separator) -# for stripped_path_part in stripped_path: -# result += stripped_path_part.strip() -# if not stripped_path.index(stripped_path_part) == len(stripped_path) - 1: -# result += separator -# return result.strip() -# -# -# def __stripPath__(path): -# result = __stripPathParts__(path, "/") -# result = __stripPathParts__(result, "\\") -# return result.strip() - -# "{ArtistName}/{Flag} [{AlbumID}] [{AlbumYear}] {AlbumTitle}" -# def getAlbumPath(conf: Settings, album): -# base = conf.downloadPath + '/Album/' -# artist = aigpy.path.replaceLimitChar(__getArtistsString__(album.artists), '-') -# # album folder pre: [ME][ID] -# flag = API.getFlag(album, Type.Album, True, "") -# if conf.audioQuality != AudioQuality.Master: -# flag = flag.replace("M", "") -# if not conf.addExplicitTag: -# flag = flag.replace("E", "") -# if not aigpy.string.isNull(flag): -# flag = "[" + flag + "] " -# -# sid = str(album.id) -# # album and addyear -# albumname = aigpy.path.replaceLimitChar(album.title, '-') -# year = "" -# if album.releaseDate is not None: -# year = aigpy.string.getSubOnlyEnd(album.releaseDate, '-') -# # retpath -# retpath = conf.albumFolderFormat -# if retpath is None or len(retpath) <= 0: -# retpath = Settings.getDefaultAlbumFolderFormat() -# retpath = retpath.replace(R"{ArtistName}", artist.strip()) -# retpath = retpath.replace(R"{Flag}", flag) -# retpath = retpath.replace(R"{AlbumID}", sid) -# retpath = retpath.replace(R"{AlbumYear}", year) -# retpath = retpath.replace(R"{AlbumTitle}", albumname.strip()) -# retpath = __stripPath__(retpath.strip()) -# return base + retpath -# -# -# def __getAlbumPath2__(conf, album): -# # outputdir/Album/artist/ -# artist = aigpy.path.replaceLimitChar(__getArtistsString__(album.artists), '-').strip() -# base = conf.downloadPath + '/Album/' + artist + '/' -# -# # album folder pre: [ME][ID] -# flag = API.getFlag(album, Type.Album, True, "") -# if conf.audioQuality != AudioQuality.Master: -# flag = flag.replace("M", "") -# if not conf.addExplicitTag: -# flag = flag.replace("E", "") -# if not aigpy.string.isNull(flag): -# flag = "[" + flag + "] " -# -# sid = "[" + str(album.id) + "] " if conf.addAlbumIDBeforeFolder else "" -# -# # album and addyear -# albumname = aigpy.path.replaceLimitChar(album.title, '-').strip() -# year = "" -# if conf.addYear and album.releaseDate is not None: -# year = "[" + aigpy.string.getSubOnlyEnd(album.releaseDate, '-') + "] " -# return base + flag + sid + year + albumname + '/' -# -# -# def getPlaylistPath(conf, playlist): -# # outputdir/Playlist/ -# base = conf.downloadPath + '/Playlist/' -# # name -# name = aigpy.path.replaceLimitChar(playlist.title, '-') -# return base + name + '/' -# -# -# # "{TrackNumber} - {ArtistName} - {TrackTitle}{ExplicitFlag}" -# -# -# def getTrackPath(conf: Settings, track, stream, album=None, playlist=None): -# if album is not None: -# base = getAlbumPath(conf, album) + '/' -# if album.numberOfVolumes > 1: -# base += 'CD' + str(track.volumeNumber) + '/' -# if playlist is not None and conf.usePlaylistFolder: -# base = getPlaylistPath(conf, playlist) -# # number -# number = __getIndexStr__(track.trackNumber) -# if playlist is not None and conf.usePlaylistFolder: -# number = __getIndexStr__(track.trackNumberOnPlaylist) -# # artist -# artist = aigpy.path.replaceLimitChar(__getArtistsString__(track.artists), '-') -# # title -# title = track.title -# if not aigpy.string.isNull(track.version): -# title += ' (' + track.version + ')' -# title = aigpy.path.replaceLimitChar(title, '-') -# # get explicit -# explicit = "(Explicit)" if conf.addExplicitTag and track.explicit else '' -# # album and addyear -# albumname = aigpy.path.replaceLimitChar(album.title, '-') -# year = "" -# if album.releaseDate is not None: -# year = aigpy.string.getSubOnlyEnd(album.releaseDate, '-') -# # extension -# extension = __getExtension__(stream.url) -# retpath = conf.trackFileFormat -# if retpath is None or len(retpath) <= 0: -# retpath = Settings.getDefaultTrackFileFormat() -# retpath = retpath.replace(R"{TrackNumber}", number) -# retpath = retpath.replace(R"{ArtistName}", artist.strip()) -# retpath = retpath.replace(R"{TrackTitle}", title) -# retpath = retpath.replace(R"{ExplicitFlag}", explicit) -# retpath = retpath.replace(R"{AlbumYear}", year) -# retpath = retpath.replace(R"{AlbumTitle}", albumname.strip()) -# retpath = retpath.strip() -# return base + retpath + extension -# -# -# def __getTrackPath2__(conf, track, stream, album=None, playlist=None): -# if album is not None: -# base = getAlbumPath(conf, album) -# if album.numberOfVolumes > 1: -# base += 'CD' + str(track.volumeNumber) + '/' -# if playlist is not None and conf.usePlaylistFolder: -# base = getPlaylistPath(conf, playlist) -# -# # hyphen -# hyphen = ' - ' if conf.addHyphen else ' ' -# # get number -# number = '' -# if conf.useTrackNumber: -# number = __getIndexStr__(track.trackNumber) + hyphen -# if playlist is not None: -# number = __getIndexStr__(track.trackNumberOnPlaylist) + hyphen -# # get artist -# artist = '' -# if conf.artistBeforeTitle: -# artist = aigpy.path.replaceLimitChar(__getArtistsString__(track.artists), '-') + hyphen -# # get explicit -# explicit = "(Explicit)" if conf.addExplicitTag and track.explicit else '' -# # title -# title = track.title -# if not aigpy.string.isNull(track.version): -# title += ' - ' + track.version -# title = aigpy.path.replaceLimitChar(title, '-') -# # extension -# extension = __getExtension__(stream.url) -# return base + number + artist.strip() + title + explicit + extension -# -# -# def getVideoPath(conf, video, album=None, playlist=None): -# if album is not None and album.title is not None: -# base = getAlbumPath(conf, album) -# elif playlist is not None and conf.usePlaylistFolder: -# base = getPlaylistPath(conf, playlist) -# else: -# base = conf.downloadPath + '/Video/' -# -# # hyphen -# hyphen = ' - ' if conf.addHyphen else ' ' -# # get number -# number = '' -# if conf.useTrackNumber: -# number = __getIndexStr__(video.trackNumber) + hyphen -# # get artist -# artist = '' -# if conf.artistBeforeTitle: -# artist = aigpy.path.replaceLimitChar(__getArtistsString__(video.artists), '-') + hyphen -# # get explicit -# explicit = "(Explicit)" if conf.addExplicitTag and video.explicit else '' -# # title -# title = aigpy.path.replaceLimitChar(video.title, '-') -# # extension -# extension = ".mp4" -# return base + number + artist.strip() + title + explicit + extension - - -# def __isNeedDownload__(path, url): -# curSize = aigpy.file.getSize(path) -# if curSize <= 0: -# return True -# netSize = aigpy.net.getSize(url) -# if curSize >= netSize: -# return False -# return True - - - -def __downloadVideo__(conf, video: Video, album=None, playlist=None): - if video.allowStreaming is False: - Printf.err("Download failed! " + video.title + ' not allow streaming.') - return - - msg, stream = API.getVideoStreamUrl(video.id, conf.videoQuality) - Printf.video(video, stream) - if not aigpy.string.isNull(msg): - Printf.err(video.title + "." + msg) - return - path = getVideoPath(conf, video, album, playlist) - - logging.info("[DL Video] name=" + aigpy.path.getFileName(path) + "\nurl=" + stream.m3u8Url) - m3u8content = requests.get(stream.m3u8Url).content - if m3u8content is None: - Printf.err(video.title + ' get m3u8 content failed.') - return - - urls = aigpy.m3u8.parseTsUrls(m3u8content) - if len(urls) <= 0: - Printf.err(video.title + ' parse ts urls failed.') - logging.info("[DL Video] title=" + video.title + "\m3u8Content=" + str(m3u8content)) - return - - check, msg = aigpy.m3u8.downloadByTsUrls(urls, path) - # check, msg = aigpy.m3u8.download(stream.m3u8Url, path) - if check is True: - Printf.success(aigpy.path.getFileName(path)) - else: - Printf.err("\nDownload failed!" + msg + '(' + aigpy.path.getFileName(path) + ')') - - -def __downloadTrack__(conf: Settings, track: Track, album=None, playlist=None): - try: - # if track.allowStreaming is False: - # Printf.err("Download failed! " + track.title + ' not allow streaming.') - # return - - msg, stream = API.getStreamUrl(track.id, conf.audioQuality) - if conf.showTrackInfo: - Printf.track(track, stream) - if not aigpy.string.isNull(msg) or stream is None: - Printf.err(track.title + "." + msg) - return - path = getTrackPath(conf, track, stream, album, playlist) - - # check exist - if conf.checkExist and isNeedDownload(path, stream.url) == False: - Printf.success(aigpy.path.getFileName(path) + " (skip:already exists!)") - return - logging.info("[DL Track] name=" + aigpy.path.getFileName(path) + "\nurl=" + stream.url) - tool = aigpy.download.DownloadTool(path + '.part', [stream.url]) - check, err = tool.start(conf.showProgress) - - if not check: - Printf.err("Download failed! " + aigpy.path.getFileName(path) + ' (' + str(err) + ')') - return - # encrypted -> decrypt and remove encrypted file - if aigpy.string.isNull(stream.encryptionKey): - os.replace(path + '.part', path) - else: - key, nonce = decrypt_security_token(stream.encryptionKey) - decrypt_file(path + '.part', path, key, nonce) - os.remove(path + '.part') - - path = convertToM4a(path, stream.codec) - - # contributors - msg, contributors = API.getTrackContributors(track.id) - msg, tidalLyrics = API.getLyrics(track.id) - - lyrics = '' if tidalLyrics is None else tidalLyrics.subtitles - if conf.addLyrics and lyrics == '': - lyrics = getLyricsFromGemius(track.title, getArtistsName(track.artists), conf.lyricsServerProxy) - - if conf.lyricFile: - if tidalLyrics is None: - Printf.info(f'Failed to get lyrics from tidal!"{track.title}"') - else: - lrcPath = path.rsplit(".", 1)[0] + '.lrc' - aigpy.fileHelper.write(lrcPath, tidalLyrics.subtitles, 'w') - - setMetaData(track, album, path, contributors, lyrics) - Printf.success(aigpy.path.getFileName(path)) - except Exception as e: - Printf.err("Download failed! " + track.title + ' (' + str(e) + ')') def __downloadCover__(conf, album): @@ -462,21 +79,21 @@ def __album__(conf, obj): if conf.saveCovers: __downloadCover__(conf, obj) for item in tracks: - __downloadTrack__(conf, item, obj) + downloadTrack(item, obj) for item in videos: - __downloadVideo__(conf, item, obj) + downloadVideo(item, obj) def __track__(conf, obj): msg, album = API.getAlbum(obj.album.id) if conf.saveCovers: __downloadCover__(conf, album) - __downloadTrack__(conf, obj, album) + downloadTrack(obj, album) def __video__(conf, obj): # Printf.video(obj) - __downloadVideo__(conf, obj, obj.album) + downloadVideo(obj, obj.album) def __artist__(conf, obj): @@ -499,11 +116,11 @@ def __playlist__(conf, obj): for index, item in enumerate(tracks): mag, album = API.getAlbum(item.album.id) item.trackNumberOnPlaylist = index + 1 - __downloadTrack__(conf, item, album, obj) + downloadTrack(item, album, obj) if conf.saveCovers and not conf.usePlaylistFolder: __downloadCover__(conf, album) for item in videos: - __downloadVideo__(conf, item, None) + downloadVideo(item, None) def __mix__(conf, obj: Mix): @@ -511,11 +128,11 @@ def __mix__(conf, obj: Mix): for index, item in enumerate(obj.tracks): mag, album = API.getAlbum(item.album.id) item.trackNumberOnPlaylist = index + 1 - __downloadTrack__(conf, item, album) + downloadTrack(item, album) if conf.saveCovers and not conf.usePlaylistFolder: __downloadCover__(conf, album) for item in obj.videos: - __downloadVideo__(conf, item, None) + downloadVideo(item, None) def file(user, conf, string): @@ -558,7 +175,6 @@ def start(user, conf, string): if etype == Type.Track: __track__(conf, obj) if etype == Type.Video: - __loadVideoAPI__(user) __video__(conf, obj) if etype == Type.Artist: __artist__(conf, obj) diff --git a/TIDALDL-PY/tidal_dl/model.py b/TIDALDL-PY/tidal_dl/model.py index 35de80b..1b568f1 100644 --- a/TIDALDL-PY/tidal_dl/model.py +++ b/TIDALDL-PY/tidal_dl/model.py @@ -51,6 +51,17 @@ class Album(ModelBase): artists = Artist() +class Playlist(ModelBase): + uuid = None + title = None + numberOfTracks = 0 + numberOfVideos = 0 + description = None + duration = 0 + image = None + squareImage = None + + class Track(ModelBase): id = None title = None @@ -67,6 +78,7 @@ class Track(ModelBase): artists = Artist() album = Album() allowStreaming = False + playlist = None class Video(ModelBase): @@ -83,17 +95,7 @@ class Video(ModelBase): artists = Artist() album = Album() allowStreaming = False - - -class Playlist(ModelBase): - uuid = None - title = None - numberOfTracks = 0 - numberOfVideos = 0 - description = None - duration = 0 - image = None - squareImage = None + playlist = None class Mix(ModelBase): diff --git a/TIDALDL-PY/tidal_dl/tidal.py b/TIDALDL-PY/tidal_dl/tidal.py index 124e2cf..600d3af 100644 --- a/TIDALDL-PY/tidal_dl/tidal.py +++ b/TIDALDL-PY/tidal_dl/tidal.py @@ -397,6 +397,14 @@ class TidalAPI(object): if sid is None or sid == "": return None return "https://resources.tidal.com/images/" + sid.replace("-", "/") + "/" + width + "x" + height + ".jpg" + + def getCoverData(self, sid, width="320", height="320"): + url = self.getCoverUrl(sid, width, height) + try: + respond = requests.get(url) + return respond.content + except: + return '' def getArtistsName(self, artists=[]): array = [] diff --git a/TIDALDL-PY/tidal_dl/util.py b/TIDALDL-PY/tidal_dl/util.py index 65737db..884fdf6 100644 --- a/TIDALDL-PY/tidal_dl/util.py +++ b/TIDALDL-PY/tidal_dl/util.py @@ -11,19 +11,32 @@ import logging import os +import time +from urllib import request import aigpy import lyricsgenius import datetime +from tidal_dl import apiKey +import tidal_dl from tidal_dl.decryption import decrypt_file from tidal_dl.decryption import decrypt_security_token from tidal_dl.enums import Type, AudioQuality +from tidal_dl.lang.language import initLang from tidal_dl.model import Track, Video, Lyrics, Mix, Album from tidal_dl.printf import Printf -from tidal_dl.settings import Settings +from tidal_dl.settings import Settings, TokenSettings, getLogPath from tidal_dl.tidal import TidalAPI +TOKEN = TokenSettings.read() +CONF = Settings.read() +LANG = initLang(CONF.language) API = TidalAPI() +API.apiKey = apiKey.getItem(CONF.apiKeyIndex) + +logging.basicConfig(filename=getLogPath(), + level=logging.INFO, + format='%(asctime)s - %(levelname)s: %(message)s') def __getIndexStr__(index): @@ -49,6 +62,7 @@ def __secondsToTimeStr__(seconds): time_string = time_string[2:] return time_string + def __parseContributors__(roleType, Contributors): if Contributors is None: return None @@ -273,3 +287,216 @@ def isNeedDownload(path, url): if curSize >= netSize: return False return True + + +def encrypted(stream, srcPath, descPath): + if aigpy.string.isNull(stream.encryptionKey): + os.replace(srcPath, descPath) + else: + key, nonce = decrypt_security_token(stream.encryptionKey) + decrypt_file(srcPath, descPath, key, nonce) + os.remove(srcPath) + + +def getAudioQualityList(): + return map(lambda quality: quality.name, tidal_dl.enums.AudioQuality) + + +def getVideoQualityList(): + return map(lambda quality: quality.name, tidal_dl.enums.VideoQuality) + + +def skip(path, url): + if CONF.checkExist and isNeedDownload(path, url) is False: + return True + return False + + +def convert(srcPath, stream): + if CONF.onlyM4a: + return convertToM4a(srcPath, stream.codec) + return srcPath + + +def downloadTrack(track: Track, album=None, playlist=None, userProgress=None): + try: + msg, stream = API.getStreamUrl(track.id, CONF.audioQuality) + if not aigpy.string.isNull(msg) or stream is None: + Printf.err(track.title + "." + msg) + return False, msg + if CONF.showTrackInfo: + Printf.track(track, stream) + path = getTrackPath(CONF, track, stream, album, playlist) + + # check exist + if skip(path, stream.url): + Printf.success(aigpy.path.getFileName(path) + " (skip:already exists!)") + return True, "" + + # download + logging.info("[DL Track] name=" + aigpy.path.getFileName(path) + "\nurl=" + stream.url) + tool = aigpy.download.DownloadTool(path + '.part', [stream.url]) + tool.setUserProgress(userProgress) + check, err = tool.start(CONF.showProgress) + if not check: + Printf.err("Download failed! " + aigpy.path.getFileName(path) + ' (' + str(err) + ')') + return False, str(err) + + # encrypted -> decrypt and remove encrypted file + encrypted(stream, path + '.part', path) + + # convert + path = convert(path, stream) + + # contributors + msg, contributors = API.getTrackContributors(track.id) + msg, tidalLyrics = API.getLyrics(track.id) + + lyrics = '' if tidalLyrics is None else tidalLyrics.subtitles + if CONF.lyricFile: + if tidalLyrics is None: + Printf.info(f'Failed to get lyrics from tidal!"{track.title}"') + else: + lrcPath = path.rsplit(".", 1)[0] + '.lrc' + aigpy.fileHelper.write(lrcPath, tidalLyrics.subtitles, 'w') + + setMetaData(track, album, path, contributors, lyrics) + Printf.success(aigpy.path.getFileName(path)) + return True, "" + except Exception as e: + Printf.err("Download failed! " + track.title + ' (' + str(e) + ')') + return False, str(e) + + +def downloadVideo(video: Video, album=None, playlist=None): + msg, stream = API.getVideoStreamUrl(video.id, CONF.videoQuality) + Printf.video(video, stream) + if not aigpy.string.isNull(msg): + Printf.err(video.title + "." + msg) + return False, msg + path = getVideoPath(CONF, video, album, playlist) + + logging.info("[DL Video] name=" + aigpy.path.getFileName(path) + "\nurl=" + stream.m3u8Url) + m3u8content = request.get(stream.m3u8Url).content + if m3u8content is None: + Printf.err(video.title + ' get m3u8 content failed.') + return False, "Get m3u8 content failed" + + urls = aigpy.m3u8.parseTsUrls(m3u8content) + if len(urls) <= 0: + Printf.err(video.title + ' parse ts urls failed.') + logging.info("[DL Video] title=" + video.title + "\m3u8Content=" + str(m3u8content)) + return False, 'Parse ts urls failed.' + + check, msg = aigpy.m3u8.downloadByTsUrls(urls, path) + # check, msg = aigpy.m3u8.download(stream.m3u8Url, path) + if check is True: + Printf.success(aigpy.path.getFileName(path)) + return True, '' + else: + Printf.err("\nDownload failed!" + msg + '(' + aigpy.path.getFileName(path) + ')') + return False, msg + + +def displayTime(seconds, granularity=2): + if seconds <= 0: + return "unknown" + + result = [] + intervals = ( + ('weeks', 604800), + ('days', 86400), + ('hours', 3600), + ('minutes', 60), + ('seconds', 1), + ) + + for name, count in intervals: + value = seconds // count + if value: + seconds -= value * count + if value == 1: + name = name.rstrip('s') + result.append("{} {}".format(value, name)) + return ', '.join(result[:granularity]) + +def loginByConfig(): + if aigpy.stringHelper.isNull(TOKEN.accessToken): + return False + + msg, check = API.verifyAccessToken(TOKEN.accessToken) + if check: + Printf.info(LANG.MSG_VALID_ACCESSTOKEN.format(displayTime(int(TOKEN.expiresAfter - time.time())))) + API.key.countryCode = TOKEN.countryCode + API.key.userId = TOKEN.userid + API.key.accessToken = TOKEN.accessToken + return True + + Printf.info(LANG.MSG_INVAILD_ACCESSTOKEN) + msg, check = API.refreshAccessToken(TOKEN.refreshToken) + if check: + Printf.success(LANG.MSG_VALID_ACCESSTOKEN.format(displayTime(int(API.key.expiresIn)))) + TOKEN.userid = API.key.userId + TOKEN.countryCode = API.key.countryCode + TOKEN.accessToken = API.key.accessToken + TOKEN.expiresAfter = time.time() + int(API.key.expiresIn) + TokenSettings.save(TOKEN) + return True + else: + tmp = TokenSettings() # clears saved tokens + TokenSettings.save(tmp) + return False + +def loginByWeb(): + start = time.time() + elapsed = 0 + while elapsed < API.key.authCheckTimeout: + elapsed = time.time() - start + msg, check = API.checkAuthStatus() + if not check: + if msg == "pending": + time.sleep(API.key.authCheckInterval + 1) + continue + return False + if check: + Printf.success(LANG.MSG_VALID_ACCESSTOKEN.format(displayTime(int(API.key.expiresIn)))) + TOKEN.userid = API.key.userId + TOKEN.countryCode = API.key.countryCode + TOKEN.accessToken = API.key.accessToken + TOKEN.refreshToken = API.key.refreshToken + TOKEN.expiresAfter = time.time() + int(API.key.expiresIn) + TokenSettings.save(TOKEN) + return True + + Printf.err(LANG.AUTH_TIMEOUT) + return False + +def getArtistsNames(artists): # : list[tidal_dl.model.Artist] + ret = [] + for item in artists: + ret.append(item.name) + return ','.join(ret) + +def getDurationString(seconds: int): + m, s = divmod(seconds, 60) + h, m = divmod(m, 60) + return "%02d:%02d:%02d" % (h, m, s) + +def getBasePath(model): + if isinstance(model, tidal_dl.model.Album): + return getAlbumPath(CONF, model) + if isinstance(model, tidal_dl.model.Playlist): + return getPlaylistPath(CONF, model) + if isinstance(model, tidal_dl.model.Track): + return getAlbumPath(CONF, model) + if isinstance(model, tidal_dl.model.Video): + filePath = getVideoPath(CONF, model, model.album, model.playlist) + return aigpy.pathHelper.getDirName(filePath) + return './' + +def getFilePath(model, stream=None): + if isinstance(model, tidal_dl.model.Track): + return getTrackPath(CONF, model, stream, model.album, model.playlist) + if isinstance(model, tidal_dl.model.Video): + return getVideoPath(CONF, model, model.album, model.playlist) + return './' diff --git a/TIDALDL-PY/tidal_gui/__init__.py b/TIDALDL-PY/tidal_gui/__init__.py index 014e003..edf7452 100644 --- a/TIDALDL-PY/tidal_gui/__init__.py +++ b/TIDALDL-PY/tidal_gui/__init__.py @@ -23,11 +23,12 @@ def main(): mainView = MainModel() mainView.show() - + app.exec_() mainView.uninit() sys.exit() + if __name__ == '__main__': main() diff --git a/TIDALDL-PY/tidal_gui/control/comboBox.py b/TIDALDL-PY/tidal_gui/control/comboBox.py index 793c66b..a2cb882 100644 --- a/TIDALDL-PY/tidal_gui/control/comboBox.py +++ b/TIDALDL-PY/tidal_gui/control/comboBox.py @@ -8,12 +8,12 @@ @Contact : yaronhuang@foxmail.com @Desc : """ -from PyQt5.QtWidgets import QComboBox, QListView from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QComboBox, QListView class ComboBox(QComboBox): - def __init__(self, items: list, width: int=200): + def __init__(self, items: list, width: int = 200): super(ComboBox, self).__init__() self.setItems(items) self.setFixedWidth(width) diff --git a/TIDALDL-PY/tidal_gui/control/framelessWidget.py b/TIDALDL-PY/tidal_gui/control/framelessWidget.py index 5ede2a5..6e0118b 100644 --- a/TIDALDL-PY/tidal_gui/control/framelessWidget.py +++ b/TIDALDL-PY/tidal_gui/control/framelessWidget.py @@ -8,13 +8,7 @@ @Contact : yaronhuang@foxmail.com @Desc : """ -import ctypes.wintypes -from ctypes.wintypes import POINT -import typing -import PyQt5 -import win32api -import win32con from PyQt5.QtCore import Qt, QPoint from PyQt5.QtGui import QMouseEvent from PyQt5.QtWidgets import QWidget, QGridLayout, QHBoxLayout diff --git a/TIDALDL-PY/tidal_gui/control/listWidget.py b/TIDALDL-PY/tidal_gui/control/listWidget.py index 4ad62c1..3e8f6c5 100644 --- a/TIDALDL-PY/tidal_gui/control/listWidget.py +++ b/TIDALDL-PY/tidal_gui/control/listWidget.py @@ -8,7 +8,6 @@ @Contact : yaronhuang@foxmail.com @Desc : """ -from PyQt5.QtCore import QSize from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QListWidget, QListWidgetItem, QWidget diff --git a/TIDALDL-PY/tidal_gui/control/pushButton.py b/TIDALDL-PY/tidal_gui/control/pushButton.py index 475a48c..01fea3a 100644 --- a/TIDALDL-PY/tidal_gui/control/pushButton.py +++ b/TIDALDL-PY/tidal_gui/control/pushButton.py @@ -29,4 +29,3 @@ class PushButton(QPushButton): if iconUrl != '': self.setIcon(QIcon(iconUrl)) - diff --git a/TIDALDL-PY/tidal_gui/control/scrollWidget.py b/TIDALDL-PY/tidal_gui/control/scrollWidget.py index fff07a7..8daa827 100644 --- a/TIDALDL-PY/tidal_gui/control/scrollWidget.py +++ b/TIDALDL-PY/tidal_gui/control/scrollWidget.py @@ -9,21 +9,19 @@ @Desc : """ -from PyQt5.QtCore import QSize, Qt -from PyQt5.QtGui import QIcon -from PyQt5.QtWidgets import QListWidget, QListWidgetItem, QWidget, QScrollArea, QVBoxLayout, QPushButton, QBoxLayout +from PyQt5.QtWidgets import QWidget, QScrollArea, QVBoxLayout class ScrollWidget(QScrollArea): def __init__(self): super(ScrollWidget, self).__init__() - + self._layout = QVBoxLayout() self._layout.addStretch(1) - + self._mainW = QWidget() self._mainW.setLayout(self._layout) - + self.setWidget(self._mainW) self.setWidgetResizable(True) diff --git a/TIDALDL-PY/tidal_gui/control/tableView.py b/TIDALDL-PY/tidal_gui/control/tableView.py index 05f9349..6f14a86 100644 --- a/TIDALDL-PY/tidal_gui/control/tableView.py +++ b/TIDALDL-PY/tidal_gui/control/tableView.py @@ -10,7 +10,7 @@ """ from PyQt5.QtCore import Qt from PyQt5.QtGui import QStandardItemModel, QStandardItem -from PyQt5.QtWidgets import QTableView, QTableWidgetItem, QAbstractItemView +from PyQt5.QtWidgets import QTableView, QAbstractItemView class TableView(QTableView): @@ -25,7 +25,7 @@ class TableView(QTableView): self._model.setHeaderData(index, Qt.Horizontal, name) self.setModel(self._model) - # self.setHorizontalHeaderItem(index, QTableWidgetItem(name)) + # self.setHorizontalHeaderItem(index, QTableWidgetItem(name)) # for index in range(0, rowCount): # self.setRowHeight(index, 50) diff --git a/TIDALDL-PY/tidal_gui/control/tableWidget.py b/TIDALDL-PY/tidal_gui/control/tableWidget.py index ed17a43..51ced47 100644 --- a/TIDALDL-PY/tidal_gui/control/tableWidget.py +++ b/TIDALDL-PY/tidal_gui/control/tableWidget.py @@ -8,13 +8,13 @@ @Contact : yaronhuang@foxmail.com @Desc : """ -import aigpy -import requests import threading + from PyQt5.QtCore import Qt, QUrl -from PyQt5.QtGui import QIcon, QPixmap -from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply -from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QAbstractItemView, QLabel +from PyQt5.QtGui import QPixmap +from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest +from PyQt5.QtWidgets import QTableWidget, QTableWidgetItem, QAbstractItemView + from tidal_gui.control.label import Label, LabelStyle diff --git a/TIDALDL-PY/tidal_gui/downloader.py b/TIDALDL-PY/tidal_gui/downloader.py index 3613520..031b859 100644 --- a/TIDALDL-PY/tidal_gui/downloader.py +++ b/TIDALDL-PY/tidal_gui/downloader.py @@ -8,16 +8,18 @@ @Contact : yaronhuang@foxmail.com @Desc : """ -from PyQt5.Qt import QThread -from queue import Queue -from tidal_gui.viewModel.taskModel import TaskModel import time +from PyQt5.Qt import QThread + +from tidal_gui.viewModel.taskModel import TaskModel + + class DownloaderImp(QThread): def __init__(self): super(DownloaderImp, self).__init__() self._taskModel = None - + def run(self): print('DownloadImp start...') while not self.isInterruptionRequested(): @@ -27,12 +29,13 @@ class DownloaderImp(QThread): item.download() time.sleep(1) print('DownloadImp stop...') - + def setTaskModel(self, model: TaskModel): self._taskModel = model - + def stop(self): self.requestInterruption() self.wait() - + + downloadImp = DownloaderImp() diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/V.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/V.svg index 0a1576c..5bdc765 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/V.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/V.svg @@ -1 +1,12 @@ - \ No newline at end of file + + + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/buymeacoffee.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/buymeacoffee.svg index 9a8bd88..f231fb3 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/buymeacoffee.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/buymeacoffee.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/down.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/down.svg index a171c81..aaedf37 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/down.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/down.svg @@ -3,4 +3,8 @@ --> - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/downHover.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/downHover.svg index 24cbb35..2fc7c26 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/downHover.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/downHover.svg @@ -3,4 +3,8 @@ --> - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/github.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/github.svg index 2d1cea6..d176dd1 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/github.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/github.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/left.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/left.svg index e931f47..cb55090 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/left.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/left.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/download.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/download.svg index be5235f..7e1ccad 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/download.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/download.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/downloadHover.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/downloadHover.svg index 3eeb232..a0718a6 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/downloadHover.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/downloadHover.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/info.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/info.svg index b428605..78fb6e5 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/info.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/info.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/search.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/search.svg index 0c1900f..9cf2416 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/search.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/search.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/settings.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/settings.svg index dee79cf..ff71fe2 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/settings.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/leftTab/settings.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/paypal.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/paypal.svg index a46caba..e91041d 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/paypal.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/paypal.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/right.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/right.svg index 96bf9b1..352e296 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/right.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/right.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/search.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/search.svg index a1cffbe..fe03660 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/search.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/search.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/search2.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/search2.svg index 2235c0d..b801741 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/search2.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/search2.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/cancel.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/cancel.svg index 2f1c840..3696dee 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/cancel.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/cancel.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/delete.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/delete.svg index 45176f6..7019a8d 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/delete.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/delete.svg @@ -1 +1,9 @@ - \ No newline at end of file + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/expand.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/expand.svg index 922d4eb..3b7a650 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/expand.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/expand.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/open.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/open.svg index 6a01693..3396540 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/open.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/open.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/retry.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/retry.svg index 42d25cd..174202b 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/retry.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskItem/retry.svg @@ -1 +1,8 @@ - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskTab/complete.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskTab/complete.svg index 9397de9..ca50052 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskTab/complete.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskTab/complete.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskTab/download.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskTab/download.svg index 0e9d285..96a2ae2 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskTab/download.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskTab/download.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskTab/error.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskTab/error.svg index a2bc642..2065ba1 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/taskTab/error.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/taskTab/error.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg - 原/upHover.svg b/TIDALDL-PY/tidal_gui/resource/svg - 原/upHover.svg index 9fff595..d7e66be 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg - 原/upHover.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg - 原/upHover.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/V.svg b/TIDALDL-PY/tidal_gui/resource/svg/V.svg index 0a1576c..5bdc765 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/V.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/V.svg @@ -1 +1,12 @@ - \ No newline at end of file + + + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/buymeacoffee.svg b/TIDALDL-PY/tidal_gui/resource/svg/buymeacoffee.svg index 9a8bd88..f231fb3 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/buymeacoffee.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/buymeacoffee.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/down.svg b/TIDALDL-PY/tidal_gui/resource/svg/down.svg index a171c81..aaedf37 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/down.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/down.svg @@ -3,4 +3,8 @@ --> - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/downHover.svg b/TIDALDL-PY/tidal_gui/resource/svg/downHover.svg index 24cbb35..2fc7c26 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/downHover.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/downHover.svg @@ -3,4 +3,8 @@ --> - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/github.svg b/TIDALDL-PY/tidal_gui/resource/svg/github.svg index 2d1cea6..d176dd1 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/github.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/github.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/left.svg b/TIDALDL-PY/tidal_gui/resource/svg/left.svg index e931f47..cb55090 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/left.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/left.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/leftTab/download.svg b/TIDALDL-PY/tidal_gui/resource/svg/leftTab/download.svg index be5235f..7e1ccad 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/leftTab/download.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/leftTab/download.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/leftTab/downloadHover.svg b/TIDALDL-PY/tidal_gui/resource/svg/leftTab/downloadHover.svg index 3eeb232..a0718a6 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/leftTab/downloadHover.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/leftTab/downloadHover.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/leftTab/info.svg b/TIDALDL-PY/tidal_gui/resource/svg/leftTab/info.svg index b428605..78fb6e5 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/leftTab/info.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/leftTab/info.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/leftTab/search.svg b/TIDALDL-PY/tidal_gui/resource/svg/leftTab/search.svg index 0c1900f..9cf2416 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/leftTab/search.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/leftTab/search.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/leftTab/settings.svg b/TIDALDL-PY/tidal_gui/resource/svg/leftTab/settings.svg index dee79cf..ff71fe2 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/leftTab/settings.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/leftTab/settings.svg @@ -1 +1,10 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/paypal.svg b/TIDALDL-PY/tidal_gui/resource/svg/paypal.svg index a46caba..e91041d 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/paypal.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/paypal.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/right.svg b/TIDALDL-PY/tidal_gui/resource/svg/right.svg index 96bf9b1..352e296 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/right.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/right.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/search.svg b/TIDALDL-PY/tidal_gui/resource/svg/search.svg index a1cffbe..fe03660 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/search.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/search.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/search2.svg b/TIDALDL-PY/tidal_gui/resource/svg/search2.svg index 2235c0d..b801741 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/search2.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/search2.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/taskItem/cancel.svg b/TIDALDL-PY/tidal_gui/resource/svg/taskItem/cancel.svg index 2f1c840..3696dee 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/taskItem/cancel.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/taskItem/cancel.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/taskItem/delete.svg b/TIDALDL-PY/tidal_gui/resource/svg/taskItem/delete.svg index 45176f6..7019a8d 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/taskItem/delete.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/taskItem/delete.svg @@ -1 +1,9 @@ - \ No newline at end of file + + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/taskItem/expand.svg b/TIDALDL-PY/tidal_gui/resource/svg/taskItem/expand.svg index 922d4eb..3b7a650 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/taskItem/expand.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/taskItem/expand.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/taskItem/open.svg b/TIDALDL-PY/tidal_gui/resource/svg/taskItem/open.svg index 6a01693..3396540 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/taskItem/open.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/taskItem/open.svg @@ -1 +1,6 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/taskItem/retry.svg b/TIDALDL-PY/tidal_gui/resource/svg/taskItem/retry.svg index 42d25cd..174202b 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/taskItem/retry.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/taskItem/retry.svg @@ -1 +1,8 @@ - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/taskTab/complete.svg b/TIDALDL-PY/tidal_gui/resource/svg/taskTab/complete.svg index 9397de9..ca50052 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/taskTab/complete.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/taskTab/complete.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/taskTab/download.svg b/TIDALDL-PY/tidal_gui/resource/svg/taskTab/download.svg index 0e9d285..96a2ae2 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/taskTab/download.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/taskTab/download.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/taskTab/error.svg b/TIDALDL-PY/tidal_gui/resource/svg/taskTab/error.svg index a2bc642..2065ba1 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/taskTab/error.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/taskTab/error.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/svg/upHover.svg b/TIDALDL-PY/tidal_gui/resource/svg/upHover.svg index 9fff595..d7e66be 100644 --- a/TIDALDL-PY/tidal_gui/resource/svg/upHover.svg +++ b/TIDALDL-PY/tidal_gui/resource/svg/upHover.svg @@ -1 +1,5 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/TIDALDL-PY/tidal_gui/resource/themeDefault.qss b/TIDALDL-PY/tidal_gui/resource/themeDefault.qss index 6784334..c470a20 100644 --- a/TIDALDL-PY/tidal_gui/resource/themeDefault.qss +++ b/TIDALDL-PY/tidal_gui/resource/themeDefault.qss @@ -94,7 +94,18 @@ QWidget#AboutWidget { } - +QScrollBar::vertical{ + background:transparent; + width: 4px; + border-radius:6px; +} +QScrollBar::handle{ + background: lightgray; + border-radius:6px; +} +QScrollBar::handle:hover{background:gray;} +QScrollBar::sub-line{background:transparent;} +QScrollBar::add-line{background:transparent;} /* PushButton Style */ diff --git a/TIDALDL-PY/tidal_gui/style.py b/TIDALDL-PY/tidal_gui/style.py index 50475d2..27c5284 100644 --- a/TIDALDL-PY/tidal_gui/style.py +++ b/TIDALDL-PY/tidal_gui/style.py @@ -33,13 +33,13 @@ class ButtonStyle(Enum): PrePage = 14 NextPage = 15 - + TaskRetry = 16, TaskCancel = 17, TaskDelete = 18, TaskOpen = 19, TaskExpand = 20, - + class LabelStyle(Enum): Default = 0, diff --git a/TIDALDL-PY/tidal_gui/theme.py b/TIDALDL-PY/tidal_gui/theme.py index 0aa09b2..a6c4e19 100644 --- a/TIDALDL-PY/tidal_gui/theme.py +++ b/TIDALDL-PY/tidal_gui/theme.py @@ -10,7 +10,9 @@ """ import os + import aigpy + from tidal_gui.style import ThemeStyle _RESOURCE_PATH = './resource' @@ -20,7 +22,6 @@ else: _RESOURCE_PATH = aigpy.path.getDirName(__file__).replace('\\', '/') + "resource" - def __getParam__(line: str): key = aigpy.string.getSub(line, "--", ":") value = aigpy.string.getSub(line, ":", ";") @@ -55,9 +56,11 @@ def __getQss__(filePath: str) -> str: qss = __parseQss__(content, params) return qss + def getResourcePath(): return _RESOURCE_PATH + def getThemeQssContent(style: ThemeStyle = ThemeStyle.Default): name = "theme" + style.name + ".qss" return __getQss__(_RESOURCE_PATH + '/' + name) diff --git a/TIDALDL-PY/tidal_gui/tidalImp.py b/TIDALDL-PY/tidal_gui/tidalImp.py deleted file mode 100644 index b706f04..0000000 --- a/TIDALDL-PY/tidal_gui/tidalImp.py +++ /dev/null @@ -1,165 +0,0 @@ -#!/usr/bin/env python -# -*- encoding: utf-8 -*- -""" -@File : tidalImp.py -@Date : 2021/9/2 -@Author : Yaronzz -@Version : 1.0 -@Contact : yaronhuang@foxmail.com -@Desc : -""" -import time - -import requests -import tidal_dl.model -import tidal_dl.enums -from aigpy.stringHelper import isNull -from tidal_dl import TokenSettings, TOKEN, TidalAPI, CONF -from tidal_dl.util import getAlbumPath, getPlaylistPath, getTrackPath - - -class TidalImp(TidalAPI): - def __init__(self): - super(TidalImp, self).__init__() - - def loginByConfig(self): - if isNull(TOKEN.accessToken): - return False - - msg, check = self.verifyAccessToken(TOKEN.accessToken) - if check: - self.key.countryCode = TOKEN.countryCode - self.key.userId = TOKEN.userid - self.key.accessToken = TOKEN.accessToken - return True - - msg, check = self.refreshAccessToken(TOKEN.refreshToken) - if check: - TOKEN.userid = self.key.userId - TOKEN.countryCode = self.key.countryCode - TOKEN.accessToken = self.key.accessToken - TOKEN.expiresAfter = time.time() + int(self.key.expiresIn) - TokenSettings.save(TOKEN) - return True - else: - tmp = TokenSettings() # clears saved tokens - TokenSettings.save(tmp) - return False - - def loginByWeb(self): - start = time.time() - elapsed = 0 - while elapsed < self.key.authCheckTimeout: - elapsed = time.time() - start - msg, check = self.checkAuthStatus() - if not check: - if msg == "pending": - time.sleep(self.key.authCheckInterval + 1) - continue - return False - if check: - TOKEN.userid = self.key.userId - TOKEN.countryCode = self.key.countryCode - TOKEN.accessToken = self.key.accessToken - TOKEN.refreshToken = self.key.refreshToken - TOKEN.expiresAfter = time.time() + int(self.key.expiresIn) - TokenSettings.save(TOKEN) - return True - return False - - @staticmethod - def getArtistsNames(artists): # : list[tidal_dl.model.Artist] - ret = [] - for item in artists: - ret.append(item.name) - return ','.join(ret) - - @staticmethod - def getDurationString(seconds: int): - m, s = divmod(seconds, 60) - h, m = divmod(m, 60) - return "%02d:%02d:%02d" % (h, m, s) - - def getCoverData(self, sid, width="320", height="320"): - url = self.getCoverUrl(sid, width, height) - try: - respond = requests.get(url) - return respond.content - except: - return '' - - @staticmethod - def getAudioQualityList(): - return map(lambda quality: quality.name, tidal_dl.enums.AudioQuality) - - @staticmethod - def getVideoQualityList(): - return map(lambda quality: quality.name, tidal_dl.enums.VideoQuality) - - def getBasePath(self, model): - if isinstance(model, tidal_dl.model.Album): - return getAlbumPath(CONF, model) - if isinstance(model, tidal_dl.model.Playlist): - return getPlaylistPath(CONF, model) - if isinstance(model, tidal_dl.model.Track): - return getAlbumPath(CONF, model.album) - if isinstance(model, tidal_dl.model.Video): - return CONF.downloadPath + '/Video/' - return './' - - def getConfig(self): - return CONF - - # def getTackPath(self, basePath, track, stream, album=None, playlist=None): - # # number - # number = __getIndexStr__(track.trackNumber) - # if playlist is not None and CONF.usePlaylistFolder: - # number = __getIndexStr__(track.trackNumberOnPlaylist) - # # artist - # artist = aigpy.path.replaceLimitChar(__getArtistsString__(track.artists), '-') - # # title - # title = track.title - # if not aigpy.string.isNull(track.version): - # title += ' (' + track.version + ')' - # title = aigpy.path.replaceLimitChar(title, '-') - # # get explicit - # explicit = "(Explicit)" if CONF.addExplicitTag and track.explicit else '' - # # album and addyear - # albumname = aigpy.path.replaceLimitChar(album.title, '-') - # year = "" - # if album.releaseDate is not None: - # year = aigpy.string.getSubOnlyEnd(album.releaseDate, '-') - # # extension - # extension = __getExtension__(stream.url) - # retpath = CONF.trackFileFormat - # if retpath is None or len(retpath) <= 0: - # retpath = Settings.getDefaultTrackFileFormat() - # retpath = retpath.replace(R"{TrackNumber}", number) - # retpath = retpath.replace(R"{ArtistName}", artist.strip()) - # retpath = retpath.replace(R"{TrackTitle}", title) - # retpath = retpath.replace(R"{ExplicitFlag}", explicit) - # retpath = retpath.replace(R"{AlbumYear}", year) - # retpath = retpath.replace(R"{AlbumTitle}", albumname.strip()) - # retpath = retpath.strip() - # return basePath + retpath + extension - - # def getVideoPath(self, basePath, video): - # # hyphen - # hyphen = ' - ' if CONF.addHyphen else ' ' - # # get number - # number = '' - # if CONF.useTrackNumber: - # number = __getIndexStr__(video.trackNumber) + hyphen - # # get artist - # artist = '' - # if CONF.artistBeforeTitle: - # artist = aigpy.path.replaceLimitChar(__getArtistsString__(video.artists), '-') + hyphen - # # get explicit - # explicit = "(Explicit)" if CONF.addExplicitTag and video.explicit else '' - # # title - # title = aigpy.path.replaceLimitChar(video.title, '-') - # # extension - # extension = ".mp4" - # return basePath + number + artist.strip() + title + explicit + extension - -tidalImp = TidalImp() diff --git a/TIDALDL-PY/tidal_gui/view/downloadItemView.py b/TIDALDL-PY/tidal_gui/view/downloadItemView.py index 3b492df..46ffd83 100644 --- a/TIDALDL-PY/tidal_gui/view/downloadItemView.py +++ b/TIDALDL-PY/tidal_gui/view/downloadItemView.py @@ -8,14 +8,11 @@ @Contact : yaronhuang@foxmail.com @Desc : """ -from PyQt5.QtGui import QPixmap -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QTableWidget, QVBoxLayout, QGridLayout, QProgressBar from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QWidget, QHBoxLayout, QGridLayout, QProgressBar from tidal_gui.control.label import Label -from tidal_gui.control.layout import createHBoxLayout, createVBoxLayout -from tidal_gui.control.pushButton import PushButton -from tidal_gui.style import LabelStyle, ButtonStyle +from tidal_gui.style import LabelStyle class DownloadItemView(QWidget): @@ -38,6 +35,7 @@ class DownloadItemView(QWidget): self._progress.setTextVisible(False) self._progress.setFixedHeight(3) self._progress.setFixedWidth(300) + self._progress.setRange(0, 100) grid = QGridLayout(self) grid.addWidget(self._indexLabel, 0, 0, Qt.AlignLeft | Qt.AlignVCenter) @@ -46,14 +44,14 @@ class DownloadItemView(QWidget): grid.addWidget(self._progress, 0, 3, Qt.AlignRight | Qt.AlignVCenter) grid.addWidget(self._actionLabel, 0, 4, Qt.AlignRight | Qt.AlignVCenter) grid.addWidget(self._errLabel, 1, 1, Qt.AlignLeft | Qt.AlignVCenter) - + grid.setColumnStretch(1, 1) - + layout = QHBoxLayout() layout.setSpacing(30) layout.addWidget(self._sizeLabel) layout.addWidget(self._speedLabel) - + grid.addLayout(layout, 1, 3, Qt.AlignLeft | Qt.AlignVCenter) def setLabel(self, index, title, own): @@ -70,9 +68,9 @@ class DownloadItemView(QWidget): def setProgress(self, value): self._progress.setValue(value) pass - + def setSize(self, curSize: str, totalSize: str): self._sizeLabel.setText(f'{curSize}/{totalSize}') - + def setSpeed(self, speed: str): self._speedLabel.setText(speed) diff --git a/TIDALDL-PY/tidal_gui/view/mainView.py b/TIDALDL-PY/tidal_gui/view/mainView.py index 0c6e9c4..94eee89 100644 --- a/TIDALDL-PY/tidal_gui/view/mainView.py +++ b/TIDALDL-PY/tidal_gui/view/mainView.py @@ -87,7 +87,7 @@ class MainView(FramelessWidget): self._moveWidget.setFixedHeight(30) self._moveWidget.setObjectName("BaseWidget") return self._moveWidget - + def __initContent__(self) -> QGridLayout: self._searchView = None self._settingsView = None diff --git a/TIDALDL-PY/tidal_gui/view/searchView.py b/TIDALDL-PY/tidal_gui/view/searchView.py index 64a12a1..36080f9 100644 --- a/TIDALDL-PY/tidal_gui/view/searchView.py +++ b/TIDALDL-PY/tidal_gui/view/searchView.py @@ -9,21 +9,20 @@ @Desc : """ import threading -import tidal_dl.model + from PyQt5.QtCore import Qt, QUrl from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QTabWidget -from tidal_dl import Type +import tidal_dl.model +from tidal_dl import Type +from tidal_dl.util import API, getDurationString from tidal_gui.control.comboBox import ComboBox from tidal_gui.control.label import Label -from tidal_gui.control.layout import createHBoxLayout, createVBoxLayout from tidal_gui.control.lineEdit import LineEdit from tidal_gui.control.pushButton import PushButton -from tidal_gui.control.tableView import TableView from tidal_gui.control.tableWidget import TableWidget from tidal_gui.style import ButtonStyle, LabelStyle from tidal_gui.theme import getResourcePath -from tidal_gui.tidalImp import tidalImp class SearchView(QWidget): @@ -57,7 +56,7 @@ class SearchView(QWidget): self._prePageBtn = PushButton('', ButtonStyle.PrePage) self._nextPageBtn = PushButton('', ButtonStyle.NextPage) - + self._pageIndexEdit = LineEdit('') self._pageIndexEdit.setAlignment(Qt.AlignCenter) self._pageIndexEdit.setEnabled(False) @@ -123,12 +122,12 @@ class SearchView(QWidget): datas = [] for index, item in enumerate(items): rowData = [str(index + 1 + indexOffset), - QUrl(tidalImp.getCoverUrl(item.cover)), - tidalImp.getFlag(item, Type.Album, True), + QUrl(API.getCoverUrl(item.cover)), + API.getFlag(item, Type.Album, True), item.title, item.artists[0].name, str(item.releaseDate), - tidalImp.getDurationString(item.duration)] + API.getDurationString(item.duration)] datas.append(rowData) elif stype == Type.Track: @@ -136,11 +135,11 @@ class SearchView(QWidget): datas = [] for index, item in enumerate(items): rowData = [str(index + 1 + indexOffset), - tidalImp.getFlag(item, Type.Track, True), + API.getFlag(item, Type.Track, True), item.title, item.album.title, item.artists[0].name, - tidalImp.getDurationString(item.duration)] + getDurationString(item.duration)] datas.append(rowData) elif stype == Type.Video: @@ -148,11 +147,11 @@ class SearchView(QWidget): datas = [] for index, item in enumerate(items): rowData = [str(index + 1 + indexOffset), - QUrl(tidalImp.getCoverUrl(item.imageID)), - tidalImp.getFlag(item, Type.Video, True), + QUrl(API.getCoverUrl(item.imageID)), + API.getFlag(item, Type.Video, True), item.title, item.artists[0].name, - tidalImp.getDurationString(item.duration)] + getDurationString(item.duration)] datas.append(rowData) elif stype == Type.Playlist: @@ -160,10 +159,10 @@ class SearchView(QWidget): datas = [] for index, item in enumerate(items): rowData = [str(index + 1 + indexOffset), - QUrl(tidalImp.getCoverUrl(item.squareImage)), + QUrl(API.getCoverUrl(item.squareImage)), item.title, '', - tidalImp.getDurationString(item.duration)] + getDurationString(item.duration)] datas.append(rowData) for index, rowData in enumerate(datas): diff --git a/TIDALDL-PY/tidal_gui/view/taskItemView.py b/TIDALDL-PY/tidal_gui/view/taskItemView.py index 91b8ef6..489b7d2 100644 --- a/TIDALDL-PY/tidal_gui/view/taskItemView.py +++ b/TIDALDL-PY/tidal_gui/view/taskItemView.py @@ -8,14 +8,14 @@ @Contact : yaronhuang@foxmail.com @Desc : """ +from PyQt5.QtCore import Qt from PyQt5.QtGui import QPixmap -from PyQt5.QtWidgets import QWidget, QHBoxLayout, QTableWidget, QVBoxLayout, QListView, QGridLayout -from PyQt5.QtCore import Qt, QPoint +from PyQt5.QtWidgets import QWidget, QHBoxLayout, QVBoxLayout from tidal_gui.control.label import Label from tidal_gui.control.layout import createHBoxLayout, createVBoxLayout -from tidal_gui.control.pushButton import PushButton from tidal_gui.control.listWidget import ListWidget +from tidal_gui.control.pushButton import PushButton from tidal_gui.style import LabelStyle, ButtonStyle, ListWidgetStyle @@ -25,12 +25,12 @@ class TaskItemView(QWidget): self.__initView__() self.setObjectName('TaskItemView') self.setAttribute(Qt.WA_StyledBackground) - + def __initView__(self): layout = QVBoxLayout() layout.addLayout(self.__initHead__(), Qt.AlignTop) layout.addWidget(self.__initList__(), Qt.AlignTop) - + self.setLayout(layout) def __initHead__(self): @@ -95,4 +95,3 @@ class TaskItemView(QWidget): self._btnDelete.clicked.connect(func) elif name == 'open': self._btnOpen.clicked.connect(func) - diff --git a/TIDALDL-PY/tidal_gui/view/taskView.py b/TIDALDL-PY/tidal_gui/view/taskView.py index 6fc9eb6..e4723bf 100644 --- a/TIDALDL-PY/tidal_gui/view/taskView.py +++ b/TIDALDL-PY/tidal_gui/view/taskView.py @@ -9,8 +9,9 @@ @Desc : """ from enum import Enum + from PyQt5.QtCore import Qt, QSize -from PyQt5.QtWidgets import QWidget, QGridLayout, QListWidgetItem, QListView +from PyQt5.QtWidgets import QWidget, QGridLayout, QListWidgetItem from tidal_gui.control.label import Label from tidal_gui.control.line import Line @@ -18,7 +19,6 @@ from tidal_gui.control.listWidget import ListWidget from tidal_gui.control.scrollWidget import ScrollWidget from tidal_gui.style import LabelStyle, ListWidgetStyle from tidal_gui.theme import getResourcePath -from tidal_gui.view.taskItemView import TaskItemView class TaskListType(Enum): diff --git a/TIDALDL-PY/tidal_gui/viewModel/__init__.py b/TIDALDL-PY/tidal_gui/viewModel/__init__.py index a4ecc95..77ced6e 100644 --- a/TIDALDL-PY/tidal_gui/viewModel/__init__.py +++ b/TIDALDL-PY/tidal_gui/viewModel/__init__.py @@ -8,4 +8,3 @@ @Contact : yaronhuang@foxmail.com @Desc : """ - diff --git a/TIDALDL-PY/tidal_gui/viewModel/downloadItemModel.py b/TIDALDL-PY/tidal_gui/viewModel/downloadItemModel.py index 48b3c0b..365cf4f 100644 --- a/TIDALDL-PY/tidal_gui/viewModel/downloadItemModel.py +++ b/TIDALDL-PY/tidal_gui/viewModel/downloadItemModel.py @@ -9,27 +9,42 @@ @Desc : """ -import _thread import os -import time +from abc import ABC, ABCMeta from enum import Enum +from pickle import FALSE +from aigpy.downloadHelper import UserProgress import aigpy.stringHelper -from tidal_dl import Type -from tidal_dl.model import Album, Track, Video, Playlist -from tidal_gui.tidalImp import tidalImp +from tidal_dl.model import Track +from tidal_dl.util import downloadTrack, downloadVideo, getArtistsNames, setMetaData from tidal_gui.view.downloadItemView import DownloadItemView from tidal_gui.viewModel.viewModel import ViewModel + class DownloadStatus(Enum): Wait = 0, Running = 1, Finish = 2, Error = 3, Cancel = 4, - + + _endStatus_ = [DownloadStatus.Finish, DownloadStatus.Error, DownloadStatus.Cancel] + +class Progress(UserProgress): + def __init__(self, model): + super().__init__() + self.model = model + + def updateCurNum(self): + self.model.update(self.curNum, self.maxNum) + + def updateMaxNum(self): + pass + + class DownloadItemModel(ViewModel): def __init__(self, index, data, basePath): super(DownloadItemModel, self).__init__() @@ -37,8 +52,9 @@ class DownloadItemModel(ViewModel): self.data = data self.basePath = basePath self.isTrack = isinstance(data, Track) + self.progress = Progress(self) self.__setStatus__(DownloadStatus.Wait) - + if self.isTrack: self.__initTrack__(index) else: @@ -50,7 +66,12 @@ class DownloadItemModel(ViewModel): self.view.setAction(status.name) else: self.view.setAction(status.name + '-' + desc) - + + def __setErrStatus__(self, errmsg: str): + self.status = DownloadStatus.Error + self.view.setAction(self.status.name) + self.view.setErrmsg(errmsg) + def __initTrack__(self, index): title = self.data.title own = self.data.album.title @@ -58,84 +79,36 @@ class DownloadItemModel(ViewModel): def __initVideo__(self, index): title = self.data.title - own = tidalImp.getArtistsNames(self.data.artists) + own = getArtistsNames(self.data.artists) self.view.setLabel(index, title, own) + def update(self, curNum, maxNum): + per = curNum * 100 / maxNum + self.view.setProgress(per) + def isInWait(self): return self.status == DownloadStatus.Wait - + def stopDownload(self): if self.status not in _endStatus_: self.__setStatus__(DownloadStatus.Cancel) def retry(self): self.__setStatus__(DownloadStatus.Wait) - + def download(self): self.__setStatus__(DownloadStatus.Running) - + if self.isTrack: - if not self.__dlTrack__(): - return + check, msg = downloadTrack(self.data, self.data.album, self.data.playlist, self.progress) else: - if not self.__dlVideo__(): - return + check, msg = downloadVideo(self.data) + if check is False: + self.__setErrStatus__(msg) + else: + self.__setStatus__(DownloadStatus.Finish) + self.view.setProgress(100) self.__setStatus__(DownloadStatus.Finish) - - def __dlTrack__(self): - try: - track = self.data - conf = tidalImp.getConfig() - - msg, stream = tidalImp.getStreamUrl(track.id, conf.audioQuality) - if not aigpy.string.isNull(msg) or stream is None: - self.__setStatus__(DownloadStatus.Error) - return False - - tidalImp.getBasePath(track) - path = tidalImp.getTackPath(self.basePath, track, stream) - # # check exist - # if conf.checkExist and tidalImp.__isNeedDownload__(path, stream.url) == False: - # return True - - tool = aigpy.download.DownloadTool(path + '.part', [stream.url]) - check, err = tool.start(conf.showProgress) - - if not check: - Printf.err("Download failed! " + aigpy.path.getFileName(path) + ' (' + str(err) + ')') - return - # encrypted -> decrypt and remove encrypted file - if aigpy.string.isNull(stream.encryptionKey): - os.replace(path + '.part', path) - else: - key, nonce = decrypt_security_token(stream.encryptionKey) - decrypt_file(path + '.part', path, key, nonce) - os.remove(path + '.part') - - path = __convertToM4a__(path, stream.codec) - - # contributors - msg, contributors = API.getTrackContributors(track.id) - msg, tidalLyrics = API.getLyrics(track.id) - - lyrics = '' if tidalLyrics is None else tidalLyrics.subtitles - if conf.addLyrics and lyrics == '': - lyrics = __getLyrics__(track.title, __getArtistsString__(track.artists), conf.lyricsServerProxy) - - if conf.lyricFile: - if tidalLyrics is None: - Printf.info(f'Failed to get lyrics from tidal!"{track.title}"') - else: - lrcPath = path.rsplit(".", 1)[0] + '.lrc' - aigpy.fileHelper.write(lrcPath, tidalLyrics.subtitles, 'w') - - __setMetaData__(track, album, path, contributors, lyrics) - Printf.success(aigpy.path.getFileName(path)) - except Exception as e: - Printf.err("Download failed! " + track.title + ' (' + str(e) + ')') - - def __dlVideo__(self): - pass diff --git a/TIDALDL-PY/tidal_gui/viewModel/loginModel.py b/TIDALDL-PY/tidal_gui/viewModel/loginModel.py index 9e6058a..300a42b 100644 --- a/TIDALDL-PY/tidal_gui/viewModel/loginModel.py +++ b/TIDALDL-PY/tidal_gui/viewModel/loginModel.py @@ -9,15 +9,15 @@ @Desc : """ import _thread -import time import webbrowser -from PyQt5.QtCore import QTimer, pyqtSignal, QObject +from PyQt5.QtCore import pyqtSignal +from tidal_dl.util import API, loginByConfig, loginByWeb -from tidal_gui.tidalImp import tidalImp from tidal_gui.view.loginView import LoginView from tidal_gui.viewModel.viewModel import ViewModel + class LoginModel(ViewModel): SIGNAL_LOGIN_SUCCESS = pyqtSignal() @@ -26,7 +26,7 @@ class LoginModel(ViewModel): self.view = LoginView() self.view.connectConfirmButton(self.__openWeb__) self.SIGNAL_REFRESH_VIEW.connect(self.__refresh__) - + def __refresh__(self, stype: str, text: str): if stype == "userCode": self.view.setDeviceCode(text) @@ -37,11 +37,11 @@ class LoginModel(ViewModel): self.view.hideEnterView() self.view.setMsg(text) - def login(self, useConfig = True): + def login(self, useConfig=True): self.SIGNAL_REFRESH_VIEW.emit('showMsg', "LOGIN...") def __thread_login__(model: LoginModel, useConfig: bool): - if useConfig and tidalImp.loginByConfig(): + if useConfig and loginByConfig(): model.SIGNAL_LOGIN_SUCCESS.emit() return model.getDeviceCode() @@ -52,9 +52,9 @@ class LoginModel(ViewModel): self.SIGNAL_REFRESH_VIEW.emit('showMsg', "GET DEVICE-CODE...") def __thread_getCode__(model: LoginModel): - msg, check = tidalImp.getDeviceCode() + msg, check = API.getDeviceCode() if check: - model.SIGNAL_REFRESH_VIEW.emit('userCode', tidalImp.key.userCode) + model.SIGNAL_REFRESH_VIEW.emit('userCode', API.key.userCode) else: model.SIGNAL_REFRESH_VIEW.emit('showMsg', msg) @@ -62,10 +62,10 @@ class LoginModel(ViewModel): def __openWeb__(self): self.view.enableConfirmButton(False) - webbrowser.open('http://link.tidal.com/' + tidalImp.key.userCode, new=0, autoraise=True) + webbrowser.open('http://link.tidal.com/' + API.key.userCode, new=0, autoraise=True) def __thread_waitLogin__(model: LoginModel): - if tidalImp.loginByWeb(): + if loginByWeb(): model.SIGNAL_LOGIN_SUCCESS.emit() else: model.getDeviceCode() diff --git a/TIDALDL-PY/tidal_gui/viewModel/mainModel.py b/TIDALDL-PY/tidal_gui/viewModel/mainModel.py index e6bd947..53a437e 100644 --- a/TIDALDL-PY/tidal_gui/viewModel/mainModel.py +++ b/TIDALDL-PY/tidal_gui/viewModel/mainModel.py @@ -37,17 +37,17 @@ class MainModel(ViewModel): self.view.setAboutView(self.aboutModel.view) self.view.showPage() - + downloadImp.setTaskModel(self.taskModel) downloadImp.start() - + def uninit(self): self.taskModel.stopDownloadItem() downloadImp.stop() - def show(self, relogin:bool = False): + def show(self, relogin: bool = False): self.view.hide() - self.loginModel.login(bool(1-relogin)) + self.loginModel.login(bool(1 - relogin)) self.loginModel.show() def __loginSuccess__(self): diff --git a/TIDALDL-PY/tidal_gui/viewModel/searchModel.py b/TIDALDL-PY/tidal_gui/viewModel/searchModel.py index 9e35ee0..939ba06 100644 --- a/TIDALDL-PY/tidal_gui/viewModel/searchModel.py +++ b/TIDALDL-PY/tidal_gui/viewModel/searchModel.py @@ -12,13 +12,12 @@ import _thread import threading import aigpy.stringHelper -import tidal_dl from PyQt5.QtCore import pyqtSignal from aigpy.modelHelper import ModelBase -from tidal_dl import Type -from tidal_dl.model import Album, SearchResult -from tidal_gui.tidalImp import tidalImp +import tidal_dl +from tidal_dl import Type +from tidal_dl.util import API, getAudioQualityList, getVideoQualityList from tidal_gui.view.searchView import SearchView from tidal_gui.viewModel.viewModel import ViewModel @@ -33,8 +32,8 @@ class SearchModel(ViewModel): self.view = SearchView() self.view.setPageIndex(1, 1) - self.view.setTrackQualityItems(tidalImp.getAudioQualityList()) - self.view.setVideoQualityItems(tidalImp.getVideoQualityList()) + self.view.setTrackQualityItems(getAudioQualityList()) + self.view.setVideoQualityItems(getVideoQualityList()) self.view.connectButton('search', self.__search__) self.view.connectButton('prePage', self.__searchPre__) self.view.connectButton('nextPage', self.__searchNext__) @@ -76,7 +75,7 @@ class SearchModel(ViewModel): limit = 20 offset = (index - 1) * limit stype = tidal_dl.Type(typeIndex) - msg, model._resultData = tidalImp.search(searchText, stype, offset, limit) + msg, model._resultData = API.search(searchText, stype, offset, limit) if not aigpy.stringHelper.isNull(msg): model.SIGNAL_REFRESH_VIEW.emit('setSearchErrmsg', msg) diff --git a/TIDALDL-PY/tidal_gui/viewModel/settingsModel.py b/TIDALDL-PY/tidal_gui/viewModel/settingsModel.py index 6e7d912..c6c3440 100644 --- a/TIDALDL-PY/tidal_gui/viewModel/settingsModel.py +++ b/TIDALDL-PY/tidal_gui/viewModel/settingsModel.py @@ -20,10 +20,10 @@ class SettingsModel(ViewModel): self.view.connectButton('Logout', self.__logout__) self.view.connectButton('Cancel', self.__cancel__) self.view.connectButton('OK', self.__ok__) - + def __logout__(self): self._parent.show(True) - + def __cancel__(self): pass diff --git a/TIDALDL-PY/tidal_gui/viewModel/taskItemModel.py b/TIDALDL-PY/tidal_gui/viewModel/taskItemModel.py index a383929..623a6e8 100644 --- a/TIDALDL-PY/tidal_gui/viewModel/taskItemModel.py +++ b/TIDALDL-PY/tidal_gui/viewModel/taskItemModel.py @@ -10,17 +10,16 @@ """ import _thread import os +import time import aigpy.stringHelper + from tidal_dl import Type from tidal_dl.model import Album, Track, Video, Playlist - -from tidal_gui.tidalImp import tidalImp +from tidal_dl.util import API, getArtistsNames, getBasePath, getDurationString from tidal_gui.view.taskItemView import TaskItemView -from tidal_gui.viewModel.viewModel import ViewModel from tidal_gui.viewModel.downloadItemModel import DownloadItemModel -from PyQt5.QtWidgets import QListWidget, QListWidgetItem, QWidget, QScrollArea, QVBoxLayout, QPushButton -import time +from tidal_gui.viewModel.viewModel import ViewModel class TaskItemModel(ViewModel): @@ -47,11 +46,11 @@ class TaskItemModel(ViewModel): self.SIGNAL_REFRESH_VIEW.connect(self.__refresh__) - def __refresh__(self, stype: str, text: str): + def __refresh__(self, stype: str, obj): if stype == "setPic": - self.view.setPic(text) + self.view.setPic(obj) elif stype == "addListItem": - for index, item in enumerate(text): + for index, item in enumerate(obj): downItem = DownloadItemModel(index + 1, item, self.path) self.view.addListItem(downItem.view) self.downloadModelList.append(downItem) @@ -75,24 +74,29 @@ class TaskItemModel(ViewModel): os.startfile(self.path) def __initAlbum__(self, data: Album): - self.path = tidalImp.getBasePath(data) - + self.path = getBasePath(data) + title = data.title - desc = f"by {tidalImp.getArtistsNames(data.artists)} " \ - f"{tidalImp.getDurationString(data.duration)} " \ + desc = f"by {getArtistsNames(data.artists)} " \ + f"{getDurationString(data.duration)} " \ f"Track-{data.numberOfTracks} " \ f"Video-{data.numberOfVideos}" self.view.setLabel(title, desc) def __thread_func__(model: TaskItemModel, album: Album): - cover = tidalImp.getCoverData(album.cover, '1280', '1280') + cover = API.getCoverData(album.cover, '1280', '1280') model.SIGNAL_REFRESH_VIEW.emit('setPic', cover) - msg, tracks, videos = tidalImp.getItems(album.id, Type.Album) + msg, tracks, videos = API.getItems(album.id, Type.Album) if not aigpy.stringHelper.isNull(msg): model.view.setErrmsg(msg) return + for item in tracks: + item.album = album + for item in videos: + item.album = album + model.SIGNAL_REFRESH_VIEW.emit('addListItem', tracks + videos) print('__initAlbum__') time.sleep(1) diff --git a/TIDALDL-PY/tidal_gui/viewModel/taskModel.py b/TIDALDL-PY/tidal_gui/viewModel/taskModel.py index e83e3c3..13bc217 100644 --- a/TIDALDL-PY/tidal_gui/viewModel/taskModel.py +++ b/TIDALDL-PY/tidal_gui/viewModel/taskModel.py @@ -8,40 +8,36 @@ @Contact : yaronhuang@foxmail.com @Desc : """ -from PyQt5.QtWidgets import QPushButton, QCheckBox, QWidget -from tidal_dl import Type from tidal_dl.model import Album, Artist - -from tidal_gui.control.layout import createHBoxLayout from tidal_gui.view.taskView import TaskView, TaskListType +from tidal_gui.viewModel.downloadItemModel import DownloadItemModel from tidal_gui.viewModel.taskItemModel import TaskItemModel from tidal_gui.viewModel.viewModel import ViewModel -from tidal_gui.viewModel.downloadItemModel import DownloadItemModel class TaskModel(ViewModel): def __init__(self): super(TaskModel, self).__init__() self.view = TaskView() - + self._listMap = {} for item in map(lambda typeItem: typeItem.name, TaskListType): self._listMap[item] = [] - + self.test() - + def addTaskItem(self, data): item = TaskItemModel(data) self._listMap[TaskListType.Download.name].append(item) self.view.addItemView(TaskListType.Download, item.view) - + def getWaitDownloadItem(self) -> DownloadItemModel: for item in self._listMap[TaskListType.Download.name]: for downItem in item.downloadModelList: if downItem.isInWait(): return downItem return None - + def stopDownloadItem(self): for item in self._listMap[TaskListType.Download.name]: for downItem in item.downloadModelList: diff --git a/TIDALDL-PY/tidal_gui/viewModel/viewModel.py b/TIDALDL-PY/tidal_gui/viewModel/viewModel.py index 356d6e9..7197534 100644 --- a/TIDALDL-PY/tidal_gui/viewModel/viewModel.py +++ b/TIDALDL-PY/tidal_gui/viewModel/viewModel.py @@ -9,12 +9,12 @@ @Desc : """ from PyQt5.QtCore import QObject -from PyQt5.QtCore import QTimer, pyqtSignal +from PyQt5.QtCore import pyqtSignal class ViewModel(QObject): SIGNAL_REFRESH_VIEW = pyqtSignal(str, object) - + def __init__(self): super(ViewModel, self).__init__() self.view = None