From 288c75bcc7aa82fbebecf21d0edf3688c85d6c07 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Sun, 22 Oct 2023 10:51:47 +0200 Subject: [PATCH 1/8] Removed `tidal.` import. --- TIDALDL-PY/tidal_dl/__init__.py | 32 +++++++++---------- TIDALDL-PY/tidal_dl/download.py | 14 ++++---- TIDALDL-PY/tidal_dl/events.py | 12 +++---- TIDALDL-PY/tidal_dl/gui.py | 32 +++++++++---------- TIDALDL-PY/tidal_dl/lang/language.py | 48 ++++++++++++++-------------- TIDALDL-PY/tidal_dl/paths.py | 6 ++-- TIDALDL-PY/tidal_dl/printf.py | 40 +++++++++++------------ TIDALDL-PY/tidal_dl/settings.py | 8 ++--- TIDALDL-PY/tidal_dl/tidal.py | 10 +++--- 9 files changed, 101 insertions(+), 101 deletions(-) diff --git a/TIDALDL-PY/tidal_dl/__init__.py b/TIDALDL-PY/tidal_dl/__init__.py index 4959091..bc7ad07 100644 --- a/TIDALDL-PY/tidal_dl/__init__.py +++ b/TIDALDL-PY/tidal_dl/__init__.py @@ -6,20 +6,20 @@ @Author : Yaronzz @Version : 3.0 @Contact : yaronhuang@foxmail.com -@Desc : +@Desc : ''' import sys import getopt -from tidal_dl.events import * -from tidal_dl.settings import * -from tidal_dl.gui import startGui +from events import * +from settings import * +from gui import startGui def mainCommand(): try: - opts, args = getopt.getopt(sys.argv[1:], - "hvgl:o:q:r:", + opts, args = getopt.getopt(sys.argv[1:], + "hvgl:o:q:r:", ["help", "version", "gui", "link=", "output=", "quality", "resolution"]) except getopt.GetoptError as errmsg: Printf.err(vars(errmsg)['msg'] + ". Use 'tidal-dl -h' for useage.") @@ -52,11 +52,11 @@ def mainCommand(): SETTINGS.videoQuality = SETTINGS.getVideoQuality(val) SETTINGS.save() continue - + if not aigpy.path.mkdirs(SETTINGS.downloadPath): Printf.err(LANG.select.MSG_PATH_ERR + SETTINGS.downloadPath) return - + if showGui: startGui() return @@ -71,22 +71,22 @@ def main(): SETTINGS.read(getProfilePath()) TOKEN.read(getTokenPath()) TIDAL_API.apiKey = apiKey.getItem(SETTINGS.apiKeyIndex) - + if len(sys.argv) > 1: mainCommand() return - + Printf.logo() Printf.settings() - + if not apiKey.isItemValid(SETTINGS.apiKeyIndex): changeApiKey() loginByWeb() elif not loginByConfig(): loginByWeb() - + Printf.checkVersion() - + while True: Printf.choices() choice = Printf.enter(LANG.select.PRINT_ENTER_CHOICE) @@ -115,10 +115,10 @@ def main(): def test(): SETTINGS.read(getProfilePath()) TOKEN.read(getTokenPath()) - + if not loginByConfig(): loginByWeb() - + SETTINGS.audioQuality = AudioQuality.Master SETTINGS.videoFileFormat = VideoQuality.P240 SETTINGS.checkExist = False @@ -149,7 +149,7 @@ def test(): # playlist 98235845-13e8-43b4-94e2-d9f8e603cee7 # start('98235845-13e8-43b4-94e2-d9f8e603cee7') # video 155608351 188932980 https://tidal.com/browse/track/55130637 - # start("155608351")https://tidal.com/browse/track/199683732 + # start("155608351")https://tidal.com/browse/track/199683732 if __name__ == '__main__': diff --git a/TIDALDL-PY/tidal_dl/download.py b/TIDALDL-PY/tidal_dl/download.py index 37b00c3..e675f0b 100644 --- a/TIDALDL-PY/tidal_dl/download.py +++ b/TIDALDL-PY/tidal_dl/download.py @@ -6,15 +6,15 @@ @Author : Yaronzz @Version : 1.0 @Contact : yaronhuang@foxmail.com -@Desc : +@Desc : ''' import aigpy import logging -from tidal_dl.paths import * -from tidal_dl.printf import * -from tidal_dl.decryption import * -from tidal_dl.tidal import * +from paths import * +from printf import * +from decryption import * +from tidal import * from concurrent.futures import ThreadPoolExecutor @@ -114,7 +114,7 @@ def downloadVideo(video: Video, album: Album = None, playlist: Playlist = None): try: stream = TIDAL_API.getVideoStreamUrl(video.id, SETTINGS.videoQuality) path = getVideoPath(video, album, playlist) - + Printf.video(video, stream) logging.info("[DL Video] name=" + aigpy.path.getFileName(path) + "\nurl=" + stream.m3u8Url) @@ -199,7 +199,7 @@ def downloadTracks(tracks, album: Album = None, playlist : Playlist=None): if SETTINGS.saveCovers and not SETTINGS.usePlaylistFolder: downloadCover(album) return album - + if not SETTINGS.multiThread: for index, item in enumerate(tracks): itemAlbum = album diff --git a/TIDALDL-PY/tidal_dl/events.py b/TIDALDL-PY/tidal_dl/events.py index 9fd521d..6c0f5bb 100644 --- a/TIDALDL-PY/tidal_dl/events.py +++ b/TIDALDL-PY/tidal_dl/events.py @@ -6,17 +6,17 @@ @Author : Yaronzz @Version : 1.0 @Contact : yaronhuang@foxmail.com -@Desc : +@Desc : """ import aigpy import time -from tidal_dl.model import * -from tidal_dl.enums import * -from tidal_dl.tidal import * -from tidal_dl.printf import * -from tidal_dl.download import * +from model import * +from enums import * +from tidal import * +from printf import * +from download import * ''' ================================= diff --git a/TIDALDL-PY/tidal_dl/gui.py b/TIDALDL-PY/tidal_dl/gui.py index fddb6e3..be6fb2f 100644 --- a/TIDALDL-PY/tidal_dl/gui.py +++ b/TIDALDL-PY/tidal_dl/gui.py @@ -6,17 +6,17 @@ @Author : Yaronzz @Version : 1.0 @Contact : yaronhuang@foxmail.com -@Desc : +@Desc : """ import sys import aigpy import _thread import importlib -from tidal_dl.events import * -from tidal_dl.settings import * -from tidal_dl.printf import * -from tidal_dl.enums import * +from events import * +from settings import * +from printf import * +from enums import * def enableGui(): @@ -42,25 +42,25 @@ else: def __init__(self, ) -> None: super().__init__() self.initView() - + def initView(self): self.c_pathDownload = QtWidgets.QLineEdit() self.c_pathAlbumFormat = QtWidgets.QLineEdit() self.c_pathTrackFormat = QtWidgets.QLineEdit() self.c_pathVideoFormat = QtWidgets.QLineEdit() - + self.mainGrid = QtWidgets.QVBoxLayout(self) self.mainGrid.addWidget(self.c_pathDownload) self.mainGrid.addWidget(self.c_pathAlbumFormat) self.mainGrid.addWidget(self.c_pathTrackFormat) self.mainGrid.addWidget(self.c_pathVideoFormat) - + class EmittingStream(QObject): textWritten = pyqtSignal(str) def write(self, text): self.textWritten.emit(str(text)) - + class MainView(QtWidgets.QWidget): s_downloadEnd = pyqtSignal(str, bool, str) @@ -95,7 +95,7 @@ else: self.m_supportType = [Type.Album, Type.Playlist, Type.Track, Type.Video, Type.Artist] for item in self.m_supportType: self.c_combType.addItem(item.name, item) - + for item in AudioQuality: self.c_combTQuality.addItem(item.name, item) for item in VideoQuality: @@ -146,7 +146,7 @@ else: self.funcGrid.addWidget(self.c_tableInfo) self.funcGrid.addLayout(self.line2Grid) self.funcGrid.addWidget(self.c_printTextEdit) - + self.mainGrid = QtWidgets.QGridLayout(self) self.mainGrid.addLayout(self.funcGrid, 0, 0) self.mainGrid.addWidget(self.c_widgetSetting, 0, 0) @@ -237,7 +237,7 @@ else: start_type(type, item) if isinstance(item, Artist): downloading_item = item.name - else: + else: downloading_item = item.title model.s_downloadEnd.emit(downloading_item, True, '') except Exception as e: @@ -257,18 +257,18 @@ else: def checkLogin(self): if not loginByConfig(): self.__info__('Login failed. Please log in using the command line first.') - + def changeTQuality(self, index): SETTINGS.audioQuality = self.c_combTQuality.itemData(index) SETTINGS.save() - + def changeVQuality(self, index): SETTINGS.videoQuality = self.c_combVQuality.itemData(index) SETTINGS.save() - + def showSettings(self): self.c_widgetSetting.show() - + def startGui(): aigpy.cmd.enableColor(False) diff --git a/TIDALDL-PY/tidal_dl/lang/language.py b/TIDALDL-PY/tidal_dl/lang/language.py index aff7e6b..6bc1644 100644 --- a/TIDALDL-PY/tidal_dl/lang/language.py +++ b/TIDALDL-PY/tidal_dl/lang/language.py @@ -6,31 +6,31 @@ @Author : Yaronzz @Version : 1.0 @Contact : yaronhuang@foxmail.com -@Desc : +@Desc : ''' -from tidal_dl.lang.arabic import LangArabic -from tidal_dl.lang.chinese import LangChinese -from tidal_dl.lang.croatian import LangCroatian -from tidal_dl.lang.czech import LangCzech -from tidal_dl.lang.danish import LangDanish -from tidal_dl.lang.dutch import LangDutch -from tidal_dl.lang.english import LangEnglish -from tidal_dl.lang.filipino import LangFilipino -from tidal_dl.lang.french import LangFrench -from tidal_dl.lang.german import LangGerman -from tidal_dl.lang.hungarian import LangHungarian -from tidal_dl.lang.italian import LangItalian -from tidal_dl.lang.norwegian import LangNorwegian -from tidal_dl.lang.polish import LangPolish -from tidal_dl.lang.portuguese import LangPortuguese -from tidal_dl.lang.russian import LangRussian -from tidal_dl.lang.spanish import LangSpanish -from tidal_dl.lang.turkish import LangTurkish -from tidal_dl.lang.ukrainian import LangUkrainian -from tidal_dl.lang.vietnamese import LangVietnamese -from tidal_dl.lang.korean import LangKorean -from tidal_dl.lang.japanese import LangJapanese +from lang.arabic import LangArabic +from lang.chinese import LangChinese +from lang.croatian import LangCroatian +from lang.czech import LangCzech +from lang.danish import LangDanish +from lang.dutch import LangDutch +from lang.english import LangEnglish +from lang.filipino import LangFilipino +from lang.french import LangFrench +from lang.german import LangGerman +from lang.hungarian import LangHungarian +from lang.italian import LangItalian +from lang.norwegian import LangNorwegian +from lang.polish import LangPolish +from lang.portuguese import LangPortuguese +from lang.russian import LangRussian +from lang.spanish import LangSpanish +from lang.turkish import LangTurkish +from lang.ukrainian import LangUkrainian +from lang.vietnamese import LangVietnamese +from lang.korean import LangKorean +from lang.japanese import LangJapanese _ALL_LANGUAGE_ = [ ['English', LangEnglish()], @@ -66,7 +66,7 @@ class Language(object): return int(str) except: return 0 - + def setLang(self, index): index = self.__toInt__(index) if index >= 0 and index < len(_ALL_LANGUAGE_): diff --git a/TIDALDL-PY/tidal_dl/paths.py b/TIDALDL-PY/tidal_dl/paths.py index 38f2c36..ab88d43 100644 --- a/TIDALDL-PY/tidal_dl/paths.py +++ b/TIDALDL-PY/tidal_dl/paths.py @@ -6,14 +6,14 @@ @Author : Yaronzz @Version : 1.0 @Contact : yaronhuang@foxmail.com -@Desc : +@Desc : """ import os import aigpy import datetime -from tidal_dl.tidal import * -from tidal_dl.settings import * +from tidal import * +from settings import * def __fixPath__(name: str): diff --git a/TIDALDL-PY/tidal_dl/printf.py b/TIDALDL-PY/tidal_dl/printf.py index 00f0796..5349891 100644 --- a/TIDALDL-PY/tidal_dl/printf.py +++ b/TIDALDL-PY/tidal_dl/printf.py @@ -6,7 +6,7 @@ @Author : Yaronzz @Version : 3.0 @Contact : yaronhuang@foxmail.com -@Desc : +@Desc : ''' from pickle import GLOBAL import threading @@ -14,12 +14,12 @@ import aigpy import logging import prettytable -import tidal_dl.apiKey as apiKey +import apiKey as apiKey -from tidal_dl.model import * -from tidal_dl.paths import * -from tidal_dl.settings import * -from tidal_dl.lang.language import * +from model import * +from paths import * +from settings import * +from lang.language import * VERSION = '2022.10.31.1' @@ -32,9 +32,9 @@ __LOGO__ = f''' | $$ | $$| $$ | $$ /$$__ $$| $$ | $$ | $$| $$ | $$ | $$| $$$$$$$| $$$$$$$| $$ | $$$$$$$| $$ |__/ |__/ \_______/ \_______/|__/ \_______/|__/ - - https://github.com/yaronzz/Tidal-Media-Downloader - + + https://github.com/yaronzz/Tidal-Media-Downloader + {VERSION} ''' @@ -56,7 +56,7 @@ class Printf(object): for item in rows: tb.add_row(item) return tb - + @staticmethod def usage(): print("=============TIDAL-DL HELP==============") @@ -70,7 +70,7 @@ class Printf(object): ["-r or --resolution", "video resolution('P1080', 'P720', 'P480', 'P360')"] ]) print(tb) - + @staticmethod def checkVersion(): onlineVer = aigpy.pip.getLastVersion('tidal-dl') @@ -90,11 +90,11 @@ class Printf(object): [LANG.select.SETTING_PLAYLIST_FOLDER_FORMAT, data.playlistFolderFormat], [LANG.select.SETTING_TRACK_FILE_FORMAT, data.trackFileFormat], [LANG.select.SETTING_VIDEO_FILE_FORMAT, data.videoFileFormat], - + #settings - quality [LANG.select.SETTING_AUDIO_QUALITY, data.audioQuality], [LANG.select.SETTING_VIDEO_QUALITY, data.videoQuality], - + #settings - else [LANG.select.SETTING_USE_PLAYLIST_FOLDER, data.usePlaylistFolder], [LANG.select.SETTING_CHECK_EXIST, data.checkExist], @@ -135,7 +135,7 @@ class Printf(object): aigpy.cmd.colorPrint(string, aigpy.cmd.TextColor.Yellow, None) ret = input("") return ret - + @staticmethod def enterBool(string): aigpy.cmd.colorPrint(string, aigpy.cmd.TextColor.Yellow, None) @@ -180,7 +180,7 @@ class Printf(object): print(aigpy.cmd.red(LANG.select.PRINT_ERR + " ") + string) # logging.error(string) print_mutex.release() - + @staticmethod def info(string): global print_mutex @@ -295,15 +295,15 @@ class Printf(object): def apikeys(items): print("-------------API-KEYS---------------") tb = prettytable.PrettyTable() - tb.field_names = [aigpy.cmd.green('Index'), + tb.field_names = [aigpy.cmd.green('Index'), aigpy.cmd.green('Valid'), - aigpy.cmd.green('Platform'), + aigpy.cmd.green('Platform'), aigpy.cmd.green('Formats'), ] tb.align = 'l' - + for index, item in enumerate(items): - tb.add_row([str(index), + tb.add_row([str(index), aigpy.cmd.green('True') if item["valid"] == "True" else aigpy.cmd.red('False'), - item["platform"], + item["platform"], item["formats"]]) print(tb) diff --git a/TIDALDL-PY/tidal_dl/settings.py b/TIDALDL-PY/tidal_dl/settings.py index 3582d84..595200c 100644 --- a/TIDALDL-PY/tidal_dl/settings.py +++ b/TIDALDL-PY/tidal_dl/settings.py @@ -12,8 +12,8 @@ import json import aigpy import base64 -from tidal_dl.lang.language import * -from tidal_dl.enums import * +from lang.language import * +from enums import * class Settings(aigpy.model.ModelBase): @@ -61,7 +61,7 @@ class Settings(aigpy.model.ModelBase): if item.name == value: return item return VideoQuality.P360 - + def read(self, path): self._path_ = path txt = aigpy.file.getContent(self._path_) @@ -83,7 +83,7 @@ class Settings(aigpy.model.ModelBase): self.videoFileFormat = self.getDefaultPathFormat(Type.Video) if self.apiKeyIndex is None: self.apiKeyIndex = 0 - + LANG.setLang(self.language) def save(self): diff --git a/TIDALDL-PY/tidal_dl/tidal.py b/TIDALDL-PY/tidal_dl/tidal.py index 915872f..2cfa0b7 100644 --- a/TIDALDL-PY/tidal_dl/tidal.py +++ b/TIDALDL-PY/tidal_dl/tidal.py @@ -17,9 +17,9 @@ import base64 import requests from xml.etree import ElementTree -from tidal_dl.model import * -from tidal_dl.enums import * -from tidal_dl.settings import * +from model import * +from enums import * +from settings import * # SSL Warnings | retry number requests.packages.urllib3.disable_warnings() @@ -341,7 +341,7 @@ class TidalAPI(object): tracks.append(track_urls) return tracks - + def getStreamUrl(self, id, quality: AudioQuality): squality = "HI_RES" if quality == AudioQuality.Normal: @@ -378,7 +378,7 @@ class TidalAPI(object): if len(ret.urls) > 0: ret.url = ret.urls[0] return ret - + raise Exception("Can't get the streamUrl, type is " + resp.manifestMimeType) def getVideoStreamUrl(self, id, quality: VideoQuality): From 87eabab36fa922e8317e9e7b3b878279543d0d3d Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Sun, 22 Oct 2023 21:16:20 +0200 Subject: [PATCH 2/8] * Added tidalapi as dependency. * Added TreeView to display user's playlists. --- TIDALDL-PY/requirements.txt | 3 ++- TIDALDL-PY/tidal_dl/__init__.py | 3 ++- TIDALDL-PY/tidal_dl/gui.py | 26 +++++++++++++++++++++++--- TIDALDL-PY/tidal_dl/tidal.py | 21 +++++++++++++++++++++ 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/TIDALDL-PY/requirements.txt b/TIDALDL-PY/requirements.txt index 624a9f9..80711bb 100644 --- a/TIDALDL-PY/requirements.txt +++ b/TIDALDL-PY/requirements.txt @@ -9,4 +9,5 @@ lyricsgenius==3.0.1 pydub==0.25.1 PyQt5==5.15.7 qt-material==2.12 -lxml==4.7.1 \ No newline at end of file +lxml==4.7.1 +tidalapi==0.7.3 diff --git a/TIDALDL-PY/tidal_dl/__init__.py b/TIDALDL-PY/tidal_dl/__init__.py index bc7ad07..c873f6a 100644 --- a/TIDALDL-PY/tidal_dl/__init__.py +++ b/TIDALDL-PY/tidal_dl/__init__.py @@ -22,11 +22,12 @@ def mainCommand(): "hvgl:o:q:r:", ["help", "version", "gui", "link=", "output=", "quality", "resolution"]) except getopt.GetoptError as errmsg: - Printf.err(vars(errmsg)['msg'] + ". Use 'tidal-dl -h' for useage.") + Printf.err(vars(errmsg)['msg'] + ". Use 'tidal-dl -h' for usage.") return link = None showGui = False + for opt, val in opts: if opt in ('-h', '--help'): Printf.usage() diff --git a/TIDALDL-PY/tidal_dl/gui.py b/TIDALDL-PY/tidal_dl/gui.py index be6fb2f..ddb10dc 100644 --- a/TIDALDL-PY/tidal_dl/gui.py +++ b/TIDALDL-PY/tidal_dl/gui.py @@ -67,7 +67,7 @@ else: def __init__(self, ) -> None: super().__init__() self.initView() - self.setMinimumSize(600, 620) + self.setMinimumSize(800, 620) self.setWindowTitle("Tidal-dl") def __info__(self, msg): @@ -120,6 +120,16 @@ else: item = QtWidgets.QTableWidgetItem(name) self.c_tableInfo.setHorizontalHeaderItem(index, item) + # Create Tree View for playlists. + self.tree_playlists = QtWidgets.QTreeWidget() + self.tree_playlists.setAnimated(False) + self.tree_playlists.setIndentation(20) + self.tree_playlists.setSortingEnabled(True) + self.tree_playlists.resize(200, 400) + self.tree_playlists.setColumnCount(2) + self.tree_playlists.setHeaderLabels(("Name", "# Tracks")) + self.tree_playlists.setColumnWidth(0, 250) + # print self.c_printTextEdit = QtWidgets.QTextEdit() self.c_printTextEdit.setReadOnly(True) @@ -148,7 +158,8 @@ else: self.funcGrid.addWidget(self.c_printTextEdit) self.mainGrid = QtWidgets.QGridLayout(self) - self.mainGrid.addLayout(self.funcGrid, 0, 0) + self.mainGrid.addWidget(self.tree_playlists, 0, 0) + self.mainGrid.addLayout(self.funcGrid, 0, 1) self.mainGrid.addWidget(self.c_widgetSetting, 0, 0) # connect @@ -166,9 +177,9 @@ else: def search(self): self.c_tableInfo.setRowCount(0) - self.s_type = self.c_combType.currentData() self.s_text = self.c_lineSearch.text() + if self.s_text.startswith('http'): tmpType, tmpId = TIDAL_API.parseUrl(self.s_text) if tmpType == Type.Null: @@ -269,6 +280,14 @@ else: def showSettings(self): self.c_widgetSetting.show() + def tree_items_playlists(self): + playlists = TIDAL_API.get_playlists() + + for playlist in playlists: + item = QtWidgets.QTreeWidgetItem(self.tree_playlists) + item.setText(0, playlist.name) + item.setText(1, str(playlist.num_tracks)) + def startGui(): aigpy.cmd.enableColor(False) @@ -278,6 +297,7 @@ else: window = MainView() window.show() window.checkLogin() + window.tree_items_playlists() app.exec_() diff --git a/TIDALDL-PY/tidal_dl/tidal.py b/TIDALDL-PY/tidal_dl/tidal.py index 2cfa0b7..1e5c137 100644 --- a/TIDALDL-PY/tidal_dl/tidal.py +++ b/TIDALDL-PY/tidal_dl/tidal.py @@ -12,6 +12,8 @@ import json import random import re import time +from typing import Union, List + import aigpy import base64 import requests @@ -21,6 +23,8 @@ from model import * from enums import * from settings import * +import tidalapi + # SSL Warnings | retry number requests.packages.urllib3.disable_warnings() requests.adapters.DEFAULT_RETRIES = 5 @@ -157,8 +161,14 @@ class TidalAPI(object): def verifyAccessToken(self, accessToken) -> bool: header = {'authorization': 'Bearer {}'.format(accessToken)} result = requests.get('https://api.tidal.com/v1/sessions', headers=header).json() + if 'status' in result and result['status'] != 200: return False + + # Set tidalapi session. + self.session = tidalapi.session.Session() + self.session.load_oauth_session("Bearer", accessToken) + return True def refreshAccessToken(self, refreshToken) -> bool: @@ -193,6 +203,7 @@ class TidalAPI(object): self.key.userId = result['userId'] self.key.countryCode = result['countryCode'] self.key.accessToken = accessToken + return def getAlbum(self, id) -> Album: @@ -233,6 +244,7 @@ class TidalAPI(object): def search(self, text: str, type: Type, offset: int = 0, limit: int = 10) -> SearchResult: typeStr = type.name.upper() + "S" + if type == Type.Null: typeStr = "ARTISTS,ALBUMS,TRACKS,VIDEOS,PLAYLISTS" @@ -474,6 +486,15 @@ class TidalAPI(object): raise Exception("No result.") + def get_playlists(self) -> List[Union["Playlist", "UserPlaylist"]]: + playlists = self.session.user.playlists() + + return playlists + + def get_playlist_items(self, playlist_id: int) -> Union[tidalapi.Playlist, tidalapi.UserPlaylist]: + tracks = self.session.playlist(playlist_id).items() + + return tracks # Singleton TIDAL_API = TidalAPI() From 7d6f9fc6e1d3f51aa2f45da071969b89e8d59724 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Mon, 23 Oct 2023 13:41:17 +0200 Subject: [PATCH 3/8] * Playlist content will be displayed in table. * Can select multiple items to download. --- TIDALDL-PY/tidal_dl/gui.py | 43 ++++++++++++++++++++++++++---------- TIDALDL-PY/tidal_dl/tidal.py | 3 ++- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/TIDALDL-PY/tidal_dl/gui.py b/TIDALDL-PY/tidal_dl/gui.py index ddb10dc..8e6b004 100644 --- a/TIDALDL-PY/tidal_dl/gui.py +++ b/TIDALDL-PY/tidal_dl/gui.py @@ -111,7 +111,7 @@ else: self.c_tableInfo.setShowGrid(False) self.c_tableInfo.verticalHeader().setVisible(False) self.c_tableInfo.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) - self.c_tableInfo.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) + self.c_tableInfo.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) self.c_tableInfo.horizontalHeader().setStretchLastSection(True) self.c_tableInfo.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.ResizeToContents) self.c_tableInfo.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) @@ -128,12 +128,12 @@ else: self.tree_playlists.resize(200, 400) self.tree_playlists.setColumnCount(2) self.tree_playlists.setHeaderLabels(("Name", "# Tracks")) - self.tree_playlists.setColumnWidth(0, 250) + self.tree_playlists.setColumnWidth(0, 200) # print self.c_printTextEdit = QtWidgets.QTextEdit() self.c_printTextEdit.setReadOnly(True) - self.c_printTextEdit.setFixedHeight(150) + self.c_printTextEdit.setFixedHeight(100) sys.stdout = EmittingStream(textWritten=self.__output__) sys.stderr = EmittingStream(textWritten=self.__output__) @@ -158,17 +158,19 @@ else: self.funcGrid.addWidget(self.c_printTextEdit) self.mainGrid = QtWidgets.QGridLayout(self) - self.mainGrid.addWidget(self.tree_playlists, 0, 0) - self.mainGrid.addLayout(self.funcGrid, 0, 1) + self.mainGrid.addWidget(self.tree_playlists, 0, 0, 1, 2) + self.mainGrid.addLayout(self.funcGrid, 0, 2, 1, 3) self.mainGrid.addWidget(self.c_widgetSetting, 0, 0) # connect self.c_btnSearch.clicked.connect(self.search) + self.c_lineSearch.returnPressed.connect(self.search) self.c_btnDownload.clicked.connect(self.download) self.s_downloadEnd.connect(self.downloadEnd) self.c_combTQuality.currentIndexChanged.connect(self.changeTQuality) self.c_combVQuality.currentIndexChanged.connect(self.changeVQuality) self.c_btnSetting.clicked.connect(self.showSettings) + self.tree_playlists.itemClicked.connect(self.playlist_display_tracks) def addItem(self, rowIdx: int, colIdx: int, text): if isinstance(text, str): @@ -205,22 +207,25 @@ else: self.__info__('No result!') return - self.c_tableInfo.setRowCount(len(self.s_array)) - for index, item in enumerate(self.s_array): + self.set_table_search_results(self.s_array, self.s_type) + + def set_table_search_results(self, s_array, s_type): + self.c_tableInfo.setRowCount(len(s_array)) + for index, item in enumerate(s_array): self.addItem(index, 0, str(index + 1)) - if self.s_type in [Type.Album, Type.Track]: + if s_type in [Type.Album, Type.Track]: self.addItem(index, 1, item.title) self.addItem(index, 2, TIDAL_API.getArtistsName(item.artists)) self.addItem(index, 3, item.audioQuality) - elif self.s_type in [Type.Video]: + elif s_type in [Type.Video]: self.addItem(index, 1, item.title) self.addItem(index, 2, TIDAL_API.getArtistsName(item.artists)) self.addItem(index, 3, item.quality) - elif self.s_type in [Type.Playlist]: + elif s_type in [Type.Playlist]: self.addItem(index, 1, item.title) self.addItem(index, 2, '') self.addItem(index, 3, '') - elif self.s_type in [Type.Artist]: + elif s_type in [Type.Artist]: self.addItem(index, 1, item.name) self.addItem(index, 2, '') self.addItem(index, 3, '') @@ -228,10 +233,16 @@ else: def download(self): index = self.c_tableInfo.currentIndex().row() - if index < 0: + selection = self.c_tableInfo.selectionModel() + has_selection = selection.hasSelection() + + if has_selection == False: self.__info__('Please select a row first.') return + rows = self.c_tableInfo.selectionModel().selectedRows() + index = rows[0].row() + self.c_btnDownload.setEnabled(False) item_to_download = "" if isinstance(self.s_array[index], Artist): @@ -287,6 +298,14 @@ else: item = QtWidgets.QTreeWidgetItem(self.tree_playlists) item.setText(0, playlist.name) item.setText(1, str(playlist.num_tracks)) + item.setText(2, playlist.id) + + def playlist_display_tracks(self, item, column): + tracks = TIDAL_API.get_playlist_items(item.text(2)) + self.s_array = tracks + self.s_type = Type.Track + + self.set_table_search_results(tracks, Type.Track) def startGui(): aigpy.cmd.enableColor(False) diff --git a/TIDALDL-PY/tidal_dl/tidal.py b/TIDALDL-PY/tidal_dl/tidal.py index 1e5c137..b590692 100644 --- a/TIDALDL-PY/tidal_dl/tidal.py +++ b/TIDALDL-PY/tidal_dl/tidal.py @@ -492,7 +492,8 @@ class TidalAPI(object): return playlists def get_playlist_items(self, playlist_id: int) -> Union[tidalapi.Playlist, tidalapi.UserPlaylist]: - tracks = self.session.playlist(playlist_id).items() + #tracks = self.session.playlist(playlist_id).items() + tracks, videos = TIDAL_API.getItems(playlist_id, Type.Playlist) return tracks From 379eca8458558a69b8f3f447a8c558dbb36982c8 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Mon, 23 Oct 2023 14:34:14 +0200 Subject: [PATCH 4/8] Multiple title can be downloaded sequentially. --- TIDALDL-PY/tidal_dl/gui.py | 55 +++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/TIDALDL-PY/tidal_dl/gui.py b/TIDALDL-PY/tidal_dl/gui.py index 8e6b004..0739680 100644 --- a/TIDALDL-PY/tidal_dl/gui.py +++ b/TIDALDL-PY/tidal_dl/gui.py @@ -241,40 +241,47 @@ else: return rows = self.c_tableInfo.selectionModel().selectedRows() - index = rows[0].row() - self.c_btnDownload.setEnabled(False) - item_to_download = "" - if isinstance(self.s_array[index], Artist): - item_to_download = self.s_array[index].name - else: - item_to_download = self.s_array[index].title - self.c_btnDownload.setText(f"Downloading [${item_to_download}]...") + for row in rows: + index = row.row() + item = self.s_array[index] + type = self.s_type - def __thread_download__(model: MainView): - downloading_item = "" - try: - type = model.s_type - item = model.s_array[index] - start_type(type, item) - if isinstance(item, Artist): - downloading_item = item.name - else: - downloading_item = item.title - model.s_downloadEnd.emit(downloading_item, True, '') - except Exception as e: - model.s_downloadEnd.emit(downloading_item, False, str(e)) + self.c_btnDownload.setEnabled(False) + item_to_download = "" + if isinstance(item, Artist): + item_to_download = item.name + else: + item_to_download = item.title - _thread.start_new_thread(__thread_download__, (self, )) + self.c_btnDownload.setText(f"'{item_to_download}' ...") + self.download_(item, type) + + # Not race condition safe. Needs refactoring. + def download_(self, item, s_type): + downloading_item = "" + try: + type = s_type + + start_type(type, item) + + if isinstance(item, Artist): + downloading_item = item.name + else: + downloading_item = item.title + + self.s_downloadEnd.emit(downloading_item, True, '') + except Exception as e: + self.s_downloadEnd.emit(downloading_item, False, str(e)) def downloadEnd(self, title, result, msg): self.c_btnDownload.setEnabled(True) self.c_btnDownload.setText(f"Download") if result: - self.__info__(f'Download [{title}] finish') + Printf.info(f"Download '{title}' finished.") else: - self.__info__(f'Download [{title}] failed:{msg}') + Printf.err(f"Download '{title}' failed:{msg}") def checkLogin(self): if not loginByConfig(): From 56c9cf569535fe794378acb8f497acb3b3214dda Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Mon, 23 Oct 2023 15:00:06 +0200 Subject: [PATCH 5/8] Refactoring: Mainly removed unused imports. --- TIDALDL-PY/tidal_dl/download.py | 19 +++++++++---------- TIDALDL-PY/tidal_dl/events.py | 7 ------- TIDALDL-PY/tidal_dl/gui.py | 11 +++++------ TIDALDL-PY/tidal_dl/tidal.py | 25 +++++++++++-------------- 4 files changed, 25 insertions(+), 37 deletions(-) diff --git a/TIDALDL-PY/tidal_dl/download.py b/TIDALDL-PY/tidal_dl/download.py index e675f0b..c0f6fa0 100644 --- a/TIDALDL-PY/tidal_dl/download.py +++ b/TIDALDL-PY/tidal_dl/download.py @@ -8,16 +8,14 @@ @Contact : yaronhuang@foxmail.com @Desc : ''' -import aigpy -import logging - -from paths import * -from printf import * -from decryption import * -from tidal import * from concurrent.futures import ThreadPoolExecutor +from decryption import * +from printf import * +from tidal import * + + def __isSkip__(finalpath, url): if not SETTINGS.checkExist: return False @@ -164,7 +162,7 @@ def downloadTrack(track: Track, album=None, playlist=None, userProgress=None, pa tool.setPartSize(partSize) check, err = tool.start(SETTINGS.showProgress and not SETTINGS.multiThread) if not check: - Printf.err(f"DL Track[{track.title}] failed.{str(err)}") + Printf.err(f"DL Track '{track.title}' failed: {str(err)}") return False, str(err) # encrypted -> decrypt and remove encrypted file @@ -187,13 +185,14 @@ def downloadTrack(track: Track, album=None, playlist=None, userProgress=None, pa __setMetaData__(track, album, path, contributors, lyrics) Printf.success(track.title) + return True, '' except Exception as e: - Printf.err(f"DL Track[{track.title}] failed.{str(e)}") + Printf.err(f"DL Track '{track.title}' failed: {str(e)}") return False, str(e) -def downloadTracks(tracks, album: Album = None, playlist : Playlist=None): +def downloadTracks(tracks, album: Album = None, playlist: Playlist = None): def __getAlbum__(item: Track): album = TIDAL_API.getAlbum(item.album.id) if SETTINGS.saveCovers and not SETTINGS.usePlaylistFolder: diff --git a/TIDALDL-PY/tidal_dl/events.py b/TIDALDL-PY/tidal_dl/events.py index 6c0f5bb..0b46332 100644 --- a/TIDALDL-PY/tidal_dl/events.py +++ b/TIDALDL-PY/tidal_dl/events.py @@ -9,13 +9,6 @@ @Desc : """ -import aigpy -import time - -from model import * -from enums import * -from tidal import * -from printf import * from download import * ''' diff --git a/TIDALDL-PY/tidal_dl/gui.py b/TIDALDL-PY/tidal_dl/gui.py index 0739680..98d33ac 100644 --- a/TIDALDL-PY/tidal_dl/gui.py +++ b/TIDALDL-PY/tidal_dl/gui.py @@ -8,15 +8,11 @@ @Contact : yaronhuang@foxmail.com @Desc : """ -import sys -import aigpy -import _thread import importlib +import sys from events import * -from settings import * from printf import * -from enums import * def enableGui(): @@ -38,6 +34,7 @@ else: from PyQt5 import QtWidgets from qt_material import apply_stylesheet + class SettingView(QtWidgets.QWidget): def __init__(self, ) -> None: super().__init__() @@ -55,12 +52,14 @@ else: self.mainGrid.addWidget(self.c_pathTrackFormat) self.mainGrid.addWidget(self.c_pathVideoFormat) + class EmittingStream(QObject): textWritten = pyqtSignal(str) def write(self, text): self.textWritten.emit(str(text)) + class MainView(QtWidgets.QWidget): s_downloadEnd = pyqtSignal(str, bool, str) @@ -314,6 +313,7 @@ else: self.set_table_search_results(tracks, Type.Track) + def startGui(): aigpy.cmd.enableColor(False) @@ -327,7 +327,6 @@ else: app.exec_() - if __name__ == '__main__': SETTINGS.read(getProfilePath()) TOKEN.read(getTokenPath()) diff --git a/TIDALDL-PY/tidal_dl/tidal.py b/TIDALDL-PY/tidal_dl/tidal.py index b590692..394cf4f 100644 --- a/TIDALDL-PY/tidal_dl/tidal.py +++ b/TIDALDL-PY/tidal_dl/tidal.py @@ -8,23 +8,18 @@ @Contact : yaronhuang@foxmail.com @Desc : tidal api ''' -import json import random import re import time from typing import Union, List - -import aigpy -import base64 -import requests from xml.etree import ElementTree -from model import * -from enums import * -from settings import * - +import requests import tidalapi +from model import * +from settings import * + # SSL Warnings | retry number requests.packages.urllib3.disable_warnings() requests.adapters.DEFAULT_RETRIES = 5 @@ -47,7 +42,8 @@ class TidalAPI(object): if respond.url.find("playbackinfopostpaywall") != -1 and SETTINGS.downloadDelay is not False: # random sleep between 0.5 and 5 seconds and print it sleep_time = random.randint(500, 5000) / 1000 - print(f"Sleeping for {sleep_time} seconds, to mimic human behaviour and prevent too many requests error") + print( + f"Sleeping for {sleep_time} seconds, to mimic human behaviour and prevent too many requests error") time.sleep(sleep_time) if respond.status_code == 429: @@ -113,7 +109,7 @@ class TidalAPI(object): def __post__(self, path, data, auth=None, urlpre='https://auth.tidal.com/v1/oauth2'): for index in range(3): try: - result = requests.post(urlpre+path, data=data, auth=auth, verify=False).json() + result = requests.post(urlpre + path, data=data, auth=auth, verify=False).json() return result except Exception as e: if index == 2: @@ -198,7 +194,7 @@ class TidalAPI(object): if not aigpy.string.isNull(userid): if str(result['userId']) != str(userid): - raise Exception("User mismatch! Please use your own accesstoken.",) + raise Exception("User mismatch! Please use your own accesstoken.", ) self.key.userId = result['userId'] self.key.countryCode = result['countryCode'] @@ -385,7 +381,7 @@ class TidalAPI(object): ret.trackid = resp.trackid ret.soundQuality = resp.audioQuality ret.codec = aigpy.string.getSub(xmldata, 'codecs="', '"') - ret.encryptionKey = ""#manifest['keyId'] if 'keyId' in manifest else "" + ret.encryptionKey = "" # manifest['keyId'] if 'keyId' in manifest else "" ret.urls = self.parse_mpd(xmldata)[0] if len(ret.urls) > 0: ret.url = ret.urls[0] @@ -492,10 +488,11 @@ class TidalAPI(object): return playlists def get_playlist_items(self, playlist_id: int) -> Union[tidalapi.Playlist, tidalapi.UserPlaylist]: - #tracks = self.session.playlist(playlist_id).items() + # tracks = self.session.playlist(playlist_id).items() tracks, videos = TIDAL_API.getItems(playlist_id, Type.Playlist) return tracks + # Singleton TIDAL_API = TidalAPI() From 88b5b6132800ae4a823d32e4ff5f74328fbfa9e6 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Mon, 23 Oct 2023 21:55:26 +0200 Subject: [PATCH 6/8] KeyPressEvent select all items. --- TIDALDL-PY/tidal_dl/gui.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/TIDALDL-PY/tidal_dl/gui.py b/TIDALDL-PY/tidal_dl/gui.py index 98d33ac..63dd5ea 100644 --- a/TIDALDL-PY/tidal_dl/gui.py +++ b/TIDALDL-PY/tidal_dl/gui.py @@ -29,7 +29,7 @@ if not enableGui(): Printf.err("Not support gui. Please type: `pip3 install PyQt5 qt_material`") else: from PyQt5.QtCore import Qt, QObject - from PyQt5.QtGui import QTextCursor + from PyQt5.QtGui import QTextCursor, QKeyEvent from PyQt5.QtCore import pyqtSignal from PyQt5 import QtWidgets from qt_material import apply_stylesheet @@ -171,6 +171,13 @@ else: self.c_btnSetting.clicked.connect(self.showSettings) self.tree_playlists.itemClicked.connect(self.playlist_display_tracks) + def keyPressEvent(self, event: QKeyEvent): + key = event.key() + + + if event.modifiers() & Qt.MetaModifier and key == Qt.Key_A: + self.c_tableInfo.selectAll() + def addItem(self, rowIdx: int, colIdx: int, text): if isinstance(text, str): item = QtWidgets.QTableWidgetItem(text) @@ -209,7 +216,9 @@ else: self.set_table_search_results(self.s_array, self.s_type) def set_table_search_results(self, s_array, s_type): + self.c_tableInfo.clearSelection() self.c_tableInfo.setRowCount(len(s_array)) + for index, item in enumerate(s_array): self.addItem(index, 0, str(index + 1)) if s_type in [Type.Album, Type.Track]: From d88dca4da6db2faab90244c220ad7aa7c3740301 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Mon, 23 Oct 2023 21:55:57 +0200 Subject: [PATCH 7/8] Style fix. --- TIDALDL-PY/tidal_dl/gui.py | 1 - 1 file changed, 1 deletion(-) diff --git a/TIDALDL-PY/tidal_dl/gui.py b/TIDALDL-PY/tidal_dl/gui.py index 63dd5ea..3d9e372 100644 --- a/TIDALDL-PY/tidal_dl/gui.py +++ b/TIDALDL-PY/tidal_dl/gui.py @@ -174,7 +174,6 @@ else: def keyPressEvent(self, event: QKeyEvent): key = event.key() - if event.modifiers() & Qt.MetaModifier and key == Qt.Key_A: self.c_tableInfo.selectAll() From df7c44448711b349bb7a8f5f3453b0c90bf6ba8f Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Tue, 31 Oct 2023 06:08:40 +0100 Subject: [PATCH 8/8] Added playlist -> right click to download the whole playlist in "playlist style". --- TIDALDL-PY/tidal_dl/gui.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/TIDALDL-PY/tidal_dl/gui.py b/TIDALDL-PY/tidal_dl/gui.py index 3d9e372..1a2abf0 100644 --- a/TIDALDL-PY/tidal_dl/gui.py +++ b/TIDALDL-PY/tidal_dl/gui.py @@ -171,6 +171,10 @@ else: self.c_btnSetting.clicked.connect(self.showSettings) self.tree_playlists.itemClicked.connect(self.playlist_display_tracks) + # Connect the contextmenu + self.tree_playlists.setContextMenuPolicy(Qt.CustomContextMenu) + self.tree_playlists.customContextMenuRequested.connect(self.menuContextTree) + def keyPressEvent(self, event: QKeyEvent): key = event.key() @@ -252,8 +256,11 @@ else: for row in rows: index = row.row() item = self.s_array[index] - type = self.s_type + item_type = self.s_type + self.download_item(item, item_type) + + def download_item(self, item, item_type): self.c_btnDownload.setEnabled(False) item_to_download = "" if isinstance(item, Artist): @@ -262,15 +269,15 @@ else: item_to_download = item.title self.c_btnDownload.setText(f"'{item_to_download}' ...") - self.download_(item, type) + self.download_(item, item_type) # Not race condition safe. Needs refactoring. def download_(self, item, s_type): downloading_item = "" try: - type = s_type + item_type = s_type - start_type(type, item) + start_type(item_type, item) if isinstance(item, Artist): downloading_item = item.name @@ -321,6 +328,28 @@ else: self.set_table_search_results(tracks, Type.Track) + def menuContextTree(self, point): + # Infos about the node selected. + index = self.tree_playlists.indexAt(point) + + if not index.isValid(): + return + + item = self.tree_playlists.itemAt(point) + playlist = Playlist() + playlist.title = item.text(0) + playlist.uuid = item.text(2) + + # We build the menu. + menu = QtWidgets.QMenu() + action = menu.addAction("Dowload Playlist") + + menu.exec_(self.tree_playlists.mapToGlobal(point)) + + self.download_item(playlist, Type.Playlist) + + + def startGui(): aigpy.cmd.enableColor(False)