Merge pull request #1106 from exislow/gui-playlists

[GUI] Show user's playlists and allow to download them.
This commit is contained in:
Yaronzz
2023-10-31 13:34:49 +08:00
committed by GitHub
10 changed files with 254 additions and 159 deletions
+2 -1
View File
@@ -9,4 +9,5 @@ lyricsgenius==3.0.1
pydub==0.25.1 pydub==0.25.1
PyQt5==5.15.7 PyQt5==5.15.7
qt-material==2.12 qt-material==2.12
lxml==4.7.1 lxml==4.7.1
tidalapi==0.7.3
+18 -17
View File
@@ -6,27 +6,28 @@
@Author : Yaronzz @Author : Yaronzz
@Version : 3.0 @Version : 3.0
@Contact : yaronhuang@foxmail.com @Contact : yaronhuang@foxmail.com
@Desc : @Desc :
''' '''
import sys import sys
import getopt import getopt
from tidal_dl.events import * from events import *
from tidal_dl.settings import * from settings import *
from tidal_dl.gui import startGui from gui import startGui
def mainCommand(): def mainCommand():
try: try:
opts, args = getopt.getopt(sys.argv[1:], opts, args = getopt.getopt(sys.argv[1:],
"hvgl:o:q:r:", "hvgl:o:q:r:",
["help", "version", "gui", "link=", "output=", "quality", "resolution"]) ["help", "version", "gui", "link=", "output=", "quality", "resolution"])
except getopt.GetoptError as errmsg: 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 return
link = None link = None
showGui = False showGui = False
for opt, val in opts: for opt, val in opts:
if opt in ('-h', '--help'): if opt in ('-h', '--help'):
Printf.usage() Printf.usage()
@@ -52,11 +53,11 @@ def mainCommand():
SETTINGS.videoQuality = SETTINGS.getVideoQuality(val) SETTINGS.videoQuality = SETTINGS.getVideoQuality(val)
SETTINGS.save() SETTINGS.save()
continue continue
if not aigpy.path.mkdirs(SETTINGS.downloadPath): if not aigpy.path.mkdirs(SETTINGS.downloadPath):
Printf.err(LANG.select.MSG_PATH_ERR + SETTINGS.downloadPath) Printf.err(LANG.select.MSG_PATH_ERR + SETTINGS.downloadPath)
return return
if showGui: if showGui:
startGui() startGui()
return return
@@ -71,22 +72,22 @@ def main():
SETTINGS.read(getProfilePath()) SETTINGS.read(getProfilePath())
TOKEN.read(getTokenPath()) TOKEN.read(getTokenPath())
TIDAL_API.apiKey = apiKey.getItem(SETTINGS.apiKeyIndex) TIDAL_API.apiKey = apiKey.getItem(SETTINGS.apiKeyIndex)
if len(sys.argv) > 1: if len(sys.argv) > 1:
mainCommand() mainCommand()
return return
Printf.logo() Printf.logo()
Printf.settings() Printf.settings()
if not apiKey.isItemValid(SETTINGS.apiKeyIndex): if not apiKey.isItemValid(SETTINGS.apiKeyIndex):
changeApiKey() changeApiKey()
loginByWeb() loginByWeb()
elif not loginByConfig(): elif not loginByConfig():
loginByWeb() loginByWeb()
Printf.checkVersion() Printf.checkVersion()
while True: while True:
Printf.choices() Printf.choices()
choice = Printf.enter(LANG.select.PRINT_ENTER_CHOICE) choice = Printf.enter(LANG.select.PRINT_ENTER_CHOICE)
@@ -115,10 +116,10 @@ def main():
def test(): def test():
SETTINGS.read(getProfilePath()) SETTINGS.read(getProfilePath())
TOKEN.read(getTokenPath()) TOKEN.read(getTokenPath())
if not loginByConfig(): if not loginByConfig():
loginByWeb() loginByWeb()
SETTINGS.audioQuality = AudioQuality.Master SETTINGS.audioQuality = AudioQuality.Master
SETTINGS.videoFileFormat = VideoQuality.P240 SETTINGS.videoFileFormat = VideoQuality.P240
SETTINGS.checkExist = False SETTINGS.checkExist = False
@@ -149,7 +150,7 @@ def test():
# playlist 98235845-13e8-43b4-94e2-d9f8e603cee7 # playlist 98235845-13e8-43b4-94e2-d9f8e603cee7
# start('98235845-13e8-43b4-94e2-d9f8e603cee7') # start('98235845-13e8-43b4-94e2-d9f8e603cee7')
# video 155608351 188932980 https://tidal.com/browse/track/55130637 # 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__': if __name__ == '__main__':
+12 -13
View File
@@ -6,18 +6,16 @@
@Author : Yaronzz @Author : Yaronzz
@Version : 1.0 @Version : 1.0
@Contact : yaronhuang@foxmail.com @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 concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from decryption import *
from printf import *
from tidal import *
def __isSkip__(finalpath, url): def __isSkip__(finalpath, url):
if not SETTINGS.checkExist: if not SETTINGS.checkExist:
return False return False
@@ -114,7 +112,7 @@ def downloadVideo(video: Video, album: Album = None, playlist: Playlist = None):
try: try:
stream = TIDAL_API.getVideoStreamUrl(video.id, SETTINGS.videoQuality) stream = TIDAL_API.getVideoStreamUrl(video.id, SETTINGS.videoQuality)
path = getVideoPath(video, album, playlist) path = getVideoPath(video, album, playlist)
Printf.video(video, stream) Printf.video(video, stream)
logging.info("[DL Video] name=" + aigpy.path.getFileName(path) + "\nurl=" + stream.m3u8Url) logging.info("[DL Video] name=" + aigpy.path.getFileName(path) + "\nurl=" + stream.m3u8Url)
@@ -164,7 +162,7 @@ def downloadTrack(track: Track, album=None, playlist=None, userProgress=None, pa
tool.setPartSize(partSize) tool.setPartSize(partSize)
check, err = tool.start(SETTINGS.showProgress and not SETTINGS.multiThread) check, err = tool.start(SETTINGS.showProgress and not SETTINGS.multiThread)
if not check: 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) return False, str(err)
# encrypted -> decrypt and remove encrypted file # encrypted -> decrypt and remove encrypted file
@@ -187,19 +185,20 @@ def downloadTrack(track: Track, album=None, playlist=None, userProgress=None, pa
__setMetaData__(track, album, path, contributors, lyrics) __setMetaData__(track, album, path, contributors, lyrics)
Printf.success(track.title) Printf.success(track.title)
return True, '' return True, ''
except Exception as e: 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) 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): def __getAlbum__(item: Track):
album = TIDAL_API.getAlbum(item.album.id) album = TIDAL_API.getAlbum(item.album.id)
if SETTINGS.saveCovers and not SETTINGS.usePlaylistFolder: if SETTINGS.saveCovers and not SETTINGS.usePlaylistFolder:
downloadCover(album) downloadCover(album)
return album return album
if not SETTINGS.multiThread: if not SETTINGS.multiThread:
for index, item in enumerate(tracks): for index, item in enumerate(tracks):
itemAlbum = album itemAlbum = album
+2 -9
View File
@@ -6,17 +6,10 @@
@Author : Yaronzz @Author : Yaronzz
@Version : 1.0 @Version : 1.0
@Contact : yaronhuang@foxmail.com @Contact : yaronhuang@foxmail.com
@Desc : @Desc :
""" """
import aigpy from download import *
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 *
''' '''
================================= =================================
+137 -55
View File
@@ -6,17 +6,13 @@
@Author : Yaronzz @Author : Yaronzz
@Version : 1.0 @Version : 1.0
@Contact : yaronhuang@foxmail.com @Contact : yaronhuang@foxmail.com
@Desc : @Desc :
""" """
import sys
import aigpy
import _thread
import importlib import importlib
import sys
from tidal_dl.events import * from events import *
from tidal_dl.settings import * from printf import *
from tidal_dl.printf import *
from tidal_dl.enums import *
def enableGui(): def enableGui():
@@ -33,41 +29,44 @@ if not enableGui():
Printf.err("Not support gui. Please type: `pip3 install PyQt5 qt_material`") Printf.err("Not support gui. Please type: `pip3 install PyQt5 qt_material`")
else: else:
from PyQt5.QtCore import Qt, QObject from PyQt5.QtCore import Qt, QObject
from PyQt5.QtGui import QTextCursor from PyQt5.QtGui import QTextCursor, QKeyEvent
from PyQt5.QtCore import pyqtSignal from PyQt5.QtCore import pyqtSignal
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from qt_material import apply_stylesheet from qt_material import apply_stylesheet
class SettingView(QtWidgets.QWidget): class SettingView(QtWidgets.QWidget):
def __init__(self, ) -> None: def __init__(self, ) -> None:
super().__init__() super().__init__()
self.initView() self.initView()
def initView(self): def initView(self):
self.c_pathDownload = QtWidgets.QLineEdit() self.c_pathDownload = QtWidgets.QLineEdit()
self.c_pathAlbumFormat = QtWidgets.QLineEdit() self.c_pathAlbumFormat = QtWidgets.QLineEdit()
self.c_pathTrackFormat = QtWidgets.QLineEdit() self.c_pathTrackFormat = QtWidgets.QLineEdit()
self.c_pathVideoFormat = QtWidgets.QLineEdit() self.c_pathVideoFormat = QtWidgets.QLineEdit()
self.mainGrid = QtWidgets.QVBoxLayout(self) self.mainGrid = QtWidgets.QVBoxLayout(self)
self.mainGrid.addWidget(self.c_pathDownload) self.mainGrid.addWidget(self.c_pathDownload)
self.mainGrid.addWidget(self.c_pathAlbumFormat) self.mainGrid.addWidget(self.c_pathAlbumFormat)
self.mainGrid.addWidget(self.c_pathTrackFormat) self.mainGrid.addWidget(self.c_pathTrackFormat)
self.mainGrid.addWidget(self.c_pathVideoFormat) self.mainGrid.addWidget(self.c_pathVideoFormat)
class EmittingStream(QObject): class EmittingStream(QObject):
textWritten = pyqtSignal(str) textWritten = pyqtSignal(str)
def write(self, text): def write(self, text):
self.textWritten.emit(str(text)) self.textWritten.emit(str(text))
class MainView(QtWidgets.QWidget): class MainView(QtWidgets.QWidget):
s_downloadEnd = pyqtSignal(str, bool, str) s_downloadEnd = pyqtSignal(str, bool, str)
def __init__(self, ) -> None: def __init__(self, ) -> None:
super().__init__() super().__init__()
self.initView() self.initView()
self.setMinimumSize(600, 620) self.setMinimumSize(800, 620)
self.setWindowTitle("Tidal-dl") self.setWindowTitle("Tidal-dl")
def __info__(self, msg): def __info__(self, msg):
@@ -95,7 +94,7 @@ else:
self.m_supportType = [Type.Album, Type.Playlist, Type.Track, Type.Video, Type.Artist] self.m_supportType = [Type.Album, Type.Playlist, Type.Track, Type.Video, Type.Artist]
for item in self.m_supportType: for item in self.m_supportType:
self.c_combType.addItem(item.name, item) self.c_combType.addItem(item.name, item)
for item in AudioQuality: for item in AudioQuality:
self.c_combTQuality.addItem(item.name, item) self.c_combTQuality.addItem(item.name, item)
for item in VideoQuality: for item in VideoQuality:
@@ -111,7 +110,7 @@ else:
self.c_tableInfo.setShowGrid(False) self.c_tableInfo.setShowGrid(False)
self.c_tableInfo.verticalHeader().setVisible(False) self.c_tableInfo.verticalHeader().setVisible(False)
self.c_tableInfo.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) 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().setStretchLastSection(True)
self.c_tableInfo.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.ResizeToContents) self.c_tableInfo.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
self.c_tableInfo.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.c_tableInfo.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
@@ -120,10 +119,20 @@ else:
item = QtWidgets.QTableWidgetItem(name) item = QtWidgets.QTableWidgetItem(name)
self.c_tableInfo.setHorizontalHeaderItem(index, item) 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, 200)
# print # print
self.c_printTextEdit = QtWidgets.QTextEdit() self.c_printTextEdit = QtWidgets.QTextEdit()
self.c_printTextEdit.setReadOnly(True) self.c_printTextEdit.setReadOnly(True)
self.c_printTextEdit.setFixedHeight(150) self.c_printTextEdit.setFixedHeight(100)
sys.stdout = EmittingStream(textWritten=self.__output__) sys.stdout = EmittingStream(textWritten=self.__output__)
sys.stderr = EmittingStream(textWritten=self.__output__) sys.stderr = EmittingStream(textWritten=self.__output__)
@@ -146,18 +155,31 @@ else:
self.funcGrid.addWidget(self.c_tableInfo) self.funcGrid.addWidget(self.c_tableInfo)
self.funcGrid.addLayout(self.line2Grid) self.funcGrid.addLayout(self.line2Grid)
self.funcGrid.addWidget(self.c_printTextEdit) self.funcGrid.addWidget(self.c_printTextEdit)
self.mainGrid = QtWidgets.QGridLayout(self) self.mainGrid = QtWidgets.QGridLayout(self)
self.mainGrid.addLayout(self.funcGrid, 0, 0) 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) self.mainGrid.addWidget(self.c_widgetSetting, 0, 0)
# connect # connect
self.c_btnSearch.clicked.connect(self.search) self.c_btnSearch.clicked.connect(self.search)
self.c_lineSearch.returnPressed.connect(self.search)
self.c_btnDownload.clicked.connect(self.download) self.c_btnDownload.clicked.connect(self.download)
self.s_downloadEnd.connect(self.downloadEnd) self.s_downloadEnd.connect(self.downloadEnd)
self.c_combTQuality.currentIndexChanged.connect(self.changeTQuality) self.c_combTQuality.currentIndexChanged.connect(self.changeTQuality)
self.c_combVQuality.currentIndexChanged.connect(self.changeVQuality) self.c_combVQuality.currentIndexChanged.connect(self.changeVQuality)
self.c_btnSetting.clicked.connect(self.showSettings) 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()
if event.modifiers() & Qt.MetaModifier and key == Qt.Key_A:
self.c_tableInfo.selectAll()
def addItem(self, rowIdx: int, colIdx: int, text): def addItem(self, rowIdx: int, colIdx: int, text):
if isinstance(text, str): if isinstance(text, str):
@@ -166,9 +188,9 @@ else:
def search(self): def search(self):
self.c_tableInfo.setRowCount(0) self.c_tableInfo.setRowCount(0)
self.s_type = self.c_combType.currentData() self.s_type = self.c_combType.currentData()
self.s_text = self.c_lineSearch.text() self.s_text = self.c_lineSearch.text()
if self.s_text.startswith('http'): if self.s_text.startswith('http'):
tmpType, tmpId = TIDAL_API.parseUrl(self.s_text) tmpType, tmpId = TIDAL_API.parseUrl(self.s_text)
if tmpType == Type.Null: if tmpType == Type.Null:
@@ -194,22 +216,27 @@ else:
self.__info__('No result') self.__info__('No result')
return return
self.c_tableInfo.setRowCount(len(self.s_array)) self.set_table_search_results(self.s_array, self.s_type)
for index, item in enumerate(self.s_array):
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)) 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, 1, item.title)
self.addItem(index, 2, TIDAL_API.getArtistsName(item.artists)) self.addItem(index, 2, TIDAL_API.getArtistsName(item.artists))
self.addItem(index, 3, item.audioQuality) 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, 1, item.title)
self.addItem(index, 2, TIDAL_API.getArtistsName(item.artists)) self.addItem(index, 2, TIDAL_API.getArtistsName(item.artists))
self.addItem(index, 3, item.quality) 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, 1, item.title)
self.addItem(index, 2, '') self.addItem(index, 2, '')
self.addItem(index, 3, '') 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, 1, item.name)
self.addItem(index, 2, '') self.addItem(index, 2, '')
self.addItem(index, 3, '') self.addItem(index, 3, '')
@@ -217,58 +244,113 @@ else:
def download(self): def download(self):
index = self.c_tableInfo.currentIndex().row() 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.') self.__info__('Please select a row first.')
return return
self.c_btnDownload.setEnabled(False) rows = self.c_tableInfo.selectionModel().selectedRows()
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}]...")
def __thread_download__(model: MainView): for row in rows:
downloading_item = "" index = row.row()
try: item = self.s_array[index]
type = model.s_type item_type = self.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))
_thread.start_new_thread(__thread_download__, (self, )) 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):
item_to_download = item.name
else:
item_to_download = item.title
self.c_btnDownload.setText(f"'{item_to_download}' ...")
self.download_(item, item_type)
# Not race condition safe. Needs refactoring.
def download_(self, item, s_type):
downloading_item = ""
try:
item_type = s_type
start_type(item_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): def downloadEnd(self, title, result, msg):
self.c_btnDownload.setEnabled(True) self.c_btnDownload.setEnabled(True)
self.c_btnDownload.setText(f"Download") self.c_btnDownload.setText(f"Download")
if result: if result:
self.__info__(f'Download [{title}] finish') Printf.info(f"Download '{title}' finished.")
else: else:
self.__info__(f'Download [{title}] failed:{msg}') Printf.err(f"Download '{title}' failed:{msg}")
def checkLogin(self): def checkLogin(self):
if not loginByConfig(): if not loginByConfig():
self.__info__('Login failed. Please log in using the command line first.') self.__info__('Login failed. Please log in using the command line first.')
def changeTQuality(self, index): def changeTQuality(self, index):
SETTINGS.audioQuality = self.c_combTQuality.itemData(index) SETTINGS.audioQuality = self.c_combTQuality.itemData(index)
SETTINGS.save() SETTINGS.save()
def changeVQuality(self, index): def changeVQuality(self, index):
SETTINGS.videoQuality = self.c_combVQuality.itemData(index) SETTINGS.videoQuality = self.c_combVQuality.itemData(index)
SETTINGS.save() SETTINGS.save()
def showSettings(self): def showSettings(self):
self.c_widgetSetting.show() 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))
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 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(): def startGui():
aigpy.cmd.enableColor(False) aigpy.cmd.enableColor(False)
@@ -278,10 +360,10 @@ else:
window = MainView() window = MainView()
window.show() window.show()
window.checkLogin() window.checkLogin()
window.tree_items_playlists()
app.exec_() app.exec_()
if __name__ == '__main__': if __name__ == '__main__':
SETTINGS.read(getProfilePath()) SETTINGS.read(getProfilePath())
TOKEN.read(getTokenPath()) TOKEN.read(getTokenPath())
+24 -24
View File
@@ -6,31 +6,31 @@
@Author : Yaronzz @Author : Yaronzz
@Version : 1.0 @Version : 1.0
@Contact : yaronhuang@foxmail.com @Contact : yaronhuang@foxmail.com
@Desc : @Desc :
''' '''
from tidal_dl.lang.arabic import LangArabic from lang.arabic import LangArabic
from tidal_dl.lang.chinese import LangChinese from lang.chinese import LangChinese
from tidal_dl.lang.croatian import LangCroatian from lang.croatian import LangCroatian
from tidal_dl.lang.czech import LangCzech from lang.czech import LangCzech
from tidal_dl.lang.danish import LangDanish from lang.danish import LangDanish
from tidal_dl.lang.dutch import LangDutch from lang.dutch import LangDutch
from tidal_dl.lang.english import LangEnglish from lang.english import LangEnglish
from tidal_dl.lang.filipino import LangFilipino from lang.filipino import LangFilipino
from tidal_dl.lang.french import LangFrench from lang.french import LangFrench
from tidal_dl.lang.german import LangGerman from lang.german import LangGerman
from tidal_dl.lang.hungarian import LangHungarian from lang.hungarian import LangHungarian
from tidal_dl.lang.italian import LangItalian from lang.italian import LangItalian
from tidal_dl.lang.norwegian import LangNorwegian from lang.norwegian import LangNorwegian
from tidal_dl.lang.polish import LangPolish from lang.polish import LangPolish
from tidal_dl.lang.portuguese import LangPortuguese from lang.portuguese import LangPortuguese
from tidal_dl.lang.russian import LangRussian from lang.russian import LangRussian
from tidal_dl.lang.spanish import LangSpanish from lang.spanish import LangSpanish
from tidal_dl.lang.turkish import LangTurkish from lang.turkish import LangTurkish
from tidal_dl.lang.ukrainian import LangUkrainian from lang.ukrainian import LangUkrainian
from tidal_dl.lang.vietnamese import LangVietnamese from lang.vietnamese import LangVietnamese
from tidal_dl.lang.korean import LangKorean from lang.korean import LangKorean
from tidal_dl.lang.japanese import LangJapanese from lang.japanese import LangJapanese
_ALL_LANGUAGE_ = [ _ALL_LANGUAGE_ = [
['English', LangEnglish()], ['English', LangEnglish()],
@@ -66,7 +66,7 @@ class Language(object):
return int(str) return int(str)
except: except:
return 0 return 0
def setLang(self, index): def setLang(self, index):
index = self.__toInt__(index) index = self.__toInt__(index)
if index >= 0 and index < len(_ALL_LANGUAGE_): if index >= 0 and index < len(_ALL_LANGUAGE_):
+3 -3
View File
@@ -6,14 +6,14 @@
@Author : Yaronzz @Author : Yaronzz
@Version : 1.0 @Version : 1.0
@Contact : yaronhuang@foxmail.com @Contact : yaronhuang@foxmail.com
@Desc : @Desc :
""" """
import os import os
import aigpy import aigpy
import datetime import datetime
from tidal_dl.tidal import * from tidal import *
from tidal_dl.settings import * from settings import *
def __fixPath__(name: str): def __fixPath__(name: str):
+20 -20
View File
@@ -6,7 +6,7 @@
@Author : Yaronzz @Author : Yaronzz
@Version : 3.0 @Version : 3.0
@Contact : yaronhuang@foxmail.com @Contact : yaronhuang@foxmail.com
@Desc : @Desc :
''' '''
from pickle import GLOBAL from pickle import GLOBAL
import threading import threading
@@ -14,12 +14,12 @@ import aigpy
import logging import logging
import prettytable import prettytable
import tidal_dl.apiKey as apiKey import apiKey as apiKey
from tidal_dl.model import * from model import *
from tidal_dl.paths import * from paths import *
from tidal_dl.settings import * from settings import *
from tidal_dl.lang.language import * from lang.language import *
VERSION = '2022.10.31.1' 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} {VERSION}
''' '''
@@ -56,7 +56,7 @@ class Printf(object):
for item in rows: for item in rows:
tb.add_row(item) tb.add_row(item)
return tb return tb
@staticmethod @staticmethod
def usage(): def usage():
print("=============TIDAL-DL HELP==============") print("=============TIDAL-DL HELP==============")
@@ -70,7 +70,7 @@ class Printf(object):
["-r or --resolution", "video resolution('P1080', 'P720', 'P480', 'P360')"] ["-r or --resolution", "video resolution('P1080', 'P720', 'P480', 'P360')"]
]) ])
print(tb) print(tb)
@staticmethod @staticmethod
def checkVersion(): def checkVersion():
onlineVer = aigpy.pip.getLastVersion('tidal-dl') onlineVer = aigpy.pip.getLastVersion('tidal-dl')
@@ -90,11 +90,11 @@ class Printf(object):
[LANG.select.SETTING_PLAYLIST_FOLDER_FORMAT, data.playlistFolderFormat], [LANG.select.SETTING_PLAYLIST_FOLDER_FORMAT, data.playlistFolderFormat],
[LANG.select.SETTING_TRACK_FILE_FORMAT, data.trackFileFormat], [LANG.select.SETTING_TRACK_FILE_FORMAT, data.trackFileFormat],
[LANG.select.SETTING_VIDEO_FILE_FORMAT, data.videoFileFormat], [LANG.select.SETTING_VIDEO_FILE_FORMAT, data.videoFileFormat],
#settings - quality #settings - quality
[LANG.select.SETTING_AUDIO_QUALITY, data.audioQuality], [LANG.select.SETTING_AUDIO_QUALITY, data.audioQuality],
[LANG.select.SETTING_VIDEO_QUALITY, data.videoQuality], [LANG.select.SETTING_VIDEO_QUALITY, data.videoQuality],
#settings - else #settings - else
[LANG.select.SETTING_USE_PLAYLIST_FOLDER, data.usePlaylistFolder], [LANG.select.SETTING_USE_PLAYLIST_FOLDER, data.usePlaylistFolder],
[LANG.select.SETTING_CHECK_EXIST, data.checkExist], [LANG.select.SETTING_CHECK_EXIST, data.checkExist],
@@ -135,7 +135,7 @@ class Printf(object):
aigpy.cmd.colorPrint(string, aigpy.cmd.TextColor.Yellow, None) aigpy.cmd.colorPrint(string, aigpy.cmd.TextColor.Yellow, None)
ret = input("") ret = input("")
return ret return ret
@staticmethod @staticmethod
def enterBool(string): def enterBool(string):
aigpy.cmd.colorPrint(string, aigpy.cmd.TextColor.Yellow, None) 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) print(aigpy.cmd.red(LANG.select.PRINT_ERR + " ") + string)
# logging.error(string) # logging.error(string)
print_mutex.release() print_mutex.release()
@staticmethod @staticmethod
def info(string): def info(string):
global print_mutex global print_mutex
@@ -295,15 +295,15 @@ class Printf(object):
def apikeys(items): def apikeys(items):
print("-------------API-KEYS---------------") print("-------------API-KEYS---------------")
tb = prettytable.PrettyTable() tb = prettytable.PrettyTable()
tb.field_names = [aigpy.cmd.green('Index'), tb.field_names = [aigpy.cmd.green('Index'),
aigpy.cmd.green('Valid'), aigpy.cmd.green('Valid'),
aigpy.cmd.green('Platform'), aigpy.cmd.green('Platform'),
aigpy.cmd.green('Formats'), ] aigpy.cmd.green('Formats'), ]
tb.align = 'l' tb.align = 'l'
for index, item in enumerate(items): 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'), aigpy.cmd.green('True') if item["valid"] == "True" else aigpy.cmd.red('False'),
item["platform"], item["platform"],
item["formats"]]) item["formats"]])
print(tb) print(tb)
+4 -4
View File
@@ -12,8 +12,8 @@ import json
import aigpy import aigpy
import base64 import base64
from tidal_dl.lang.language import * from lang.language import *
from tidal_dl.enums import * from enums import *
class Settings(aigpy.model.ModelBase): class Settings(aigpy.model.ModelBase):
@@ -61,7 +61,7 @@ class Settings(aigpy.model.ModelBase):
if item.name == value: if item.name == value:
return item return item
return VideoQuality.P360 return VideoQuality.P360
def read(self, path): def read(self, path):
self._path_ = path self._path_ = path
txt = aigpy.file.getContent(self._path_) txt = aigpy.file.getContent(self._path_)
@@ -83,7 +83,7 @@ class Settings(aigpy.model.ModelBase):
self.videoFileFormat = self.getDefaultPathFormat(Type.Video) self.videoFileFormat = self.getDefaultPathFormat(Type.Video)
if self.apiKeyIndex is None: if self.apiKeyIndex is None:
self.apiKeyIndex = 0 self.apiKeyIndex = 0
LANG.setLang(self.language) LANG.setLang(self.language)
def save(self): def save(self):
+32 -13
View File
@@ -8,18 +8,17 @@
@Contact : yaronhuang@foxmail.com @Contact : yaronhuang@foxmail.com
@Desc : tidal api @Desc : tidal api
''' '''
import json
import random import random
import re import re
import time import time
import aigpy from typing import Union, List
import base64
import requests
from xml.etree import ElementTree from xml.etree import ElementTree
from tidal_dl.model import * import requests
from tidal_dl.enums import * import tidalapi
from tidal_dl.settings import *
from model import *
from settings import *
# SSL Warnings | retry number # SSL Warnings | retry number
requests.packages.urllib3.disable_warnings() requests.packages.urllib3.disable_warnings()
@@ -43,7 +42,8 @@ class TidalAPI(object):
if respond.url.find("playbackinfopostpaywall") != -1 and SETTINGS.downloadDelay is not False: if respond.url.find("playbackinfopostpaywall") != -1 and SETTINGS.downloadDelay is not False:
# random sleep between 0.5 and 5 seconds and print it # random sleep between 0.5 and 5 seconds and print it
sleep_time = random.randint(500, 5000) / 1000 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) time.sleep(sleep_time)
if respond.status_code == 429: if respond.status_code == 429:
@@ -109,7 +109,7 @@ class TidalAPI(object):
def __post__(self, path, data, auth=None, urlpre='https://auth.tidal.com/v1/oauth2'): def __post__(self, path, data, auth=None, urlpre='https://auth.tidal.com/v1/oauth2'):
for index in range(3): for index in range(3):
try: 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 return result
except Exception as e: except Exception as e:
if index == 2: if index == 2:
@@ -157,8 +157,14 @@ class TidalAPI(object):
def verifyAccessToken(self, accessToken) -> bool: def verifyAccessToken(self, accessToken) -> bool:
header = {'authorization': 'Bearer {}'.format(accessToken)} header = {'authorization': 'Bearer {}'.format(accessToken)}
result = requests.get('https://api.tidal.com/v1/sessions', headers=header).json() result = requests.get('https://api.tidal.com/v1/sessions', headers=header).json()
if 'status' in result and result['status'] != 200: if 'status' in result and result['status'] != 200:
return False return False
# Set tidalapi session.
self.session = tidalapi.session.Session()
self.session.load_oauth_session("Bearer", accessToken)
return True return True
def refreshAccessToken(self, refreshToken) -> bool: def refreshAccessToken(self, refreshToken) -> bool:
@@ -188,11 +194,12 @@ class TidalAPI(object):
if not aigpy.string.isNull(userid): if not aigpy.string.isNull(userid):
if str(result['userId']) != str(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.userId = result['userId']
self.key.countryCode = result['countryCode'] self.key.countryCode = result['countryCode']
self.key.accessToken = accessToken self.key.accessToken = accessToken
return return
def getAlbum(self, id) -> Album: def getAlbum(self, id) -> Album:
@@ -233,6 +240,7 @@ class TidalAPI(object):
def search(self, text: str, type: Type, offset: int = 0, limit: int = 10) -> SearchResult: def search(self, text: str, type: Type, offset: int = 0, limit: int = 10) -> SearchResult:
typeStr = type.name.upper() + "S" typeStr = type.name.upper() + "S"
if type == Type.Null: if type == Type.Null:
typeStr = "ARTISTS,ALBUMS,TRACKS,VIDEOS,PLAYLISTS" typeStr = "ARTISTS,ALBUMS,TRACKS,VIDEOS,PLAYLISTS"
@@ -341,7 +349,7 @@ class TidalAPI(object):
tracks.append(track_urls) tracks.append(track_urls)
return tracks return tracks
def getStreamUrl(self, id, quality: AudioQuality): def getStreamUrl(self, id, quality: AudioQuality):
squality = "HI_RES" squality = "HI_RES"
if quality == AudioQuality.Normal: if quality == AudioQuality.Normal:
@@ -373,12 +381,12 @@ class TidalAPI(object):
ret.trackid = resp.trackid ret.trackid = resp.trackid
ret.soundQuality = resp.audioQuality ret.soundQuality = resp.audioQuality
ret.codec = aigpy.string.getSub(xmldata, 'codecs="', '"') 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] ret.urls = self.parse_mpd(xmldata)[0]
if len(ret.urls) > 0: if len(ret.urls) > 0:
ret.url = ret.urls[0] ret.url = ret.urls[0]
return ret return ret
raise Exception("Can't get the streamUrl, type is " + resp.manifestMimeType) raise Exception("Can't get the streamUrl, type is " + resp.manifestMimeType)
def getVideoStreamUrl(self, id, quality: VideoQuality): def getVideoStreamUrl(self, id, quality: VideoQuality):
@@ -474,6 +482,17 @@ class TidalAPI(object):
raise Exception("No result.") 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()
tracks, videos = TIDAL_API.getItems(playlist_id, Type.Playlist)
return tracks
# Singleton # Singleton
TIDAL_API = TidalAPI() TIDAL_API = TidalAPI()