30
.github/workflows/build.yml
vendored
@@ -49,22 +49,22 @@ jobs:
|
||||
# cp -rf tidal_gui/resource dist/
|
||||
# working-directory: TIDALDL-PY
|
||||
|
||||
- name: Build tidal-gui
|
||||
shell: bash
|
||||
if: ${{ matrix.os != 'macos-latest' }}
|
||||
run: |
|
||||
cp -rf guiStatic.in MANIFEST.in
|
||||
pyinstaller -D tidal_gui/__init__.py -w -n tidal-gui
|
||||
working-directory: TIDALDL-PY
|
||||
# - name: Build tidal-gui
|
||||
# shell: bash
|
||||
# if: ${{ matrix.os != 'macos-latest' }}
|
||||
# run: |
|
||||
# cp -rf guiStatic.in MANIFEST.in
|
||||
# pyinstaller -D tidal_gui/__init__.py -w -n tidal-gui
|
||||
# working-directory: TIDALDL-PY
|
||||
|
||||
- name: Gzip tidal-gui
|
||||
shell: bash
|
||||
if: ${{ matrix.os != 'macos-latest' }}
|
||||
run: |
|
||||
cp -rf ../tidal_gui/resource ./tidal-gui/
|
||||
tar -zcvf tidal-gui.tar.gz tidal-gui
|
||||
rm -rf tidal-gui
|
||||
working-directory: TIDALDL-PY/dist
|
||||
# - name: Gzip tidal-gui
|
||||
# shell: bash
|
||||
# if: ${{ matrix.os != 'macos-latest' }}
|
||||
# run: |
|
||||
# cp -rf ../tidal_gui/resource ./tidal-gui/
|
||||
# tar -zcvf tidal-gui.tar.gz tidal-gui
|
||||
# rm -rf tidal-gui
|
||||
# working-directory: TIDALDL-PY/dist
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
|
||||
3
.gitignore
vendored
@@ -175,3 +175,6 @@ TIDALDL-PY/exe/tidal_dl_win.tar.gz
|
||||
TIDALDL-PY/tidal-gui.spec
|
||||
.history/
|
||||
.gitignore
|
||||
/TIDALDL-PY/tidal_dl_old
|
||||
/TIDALDL-PY/tidal_gui_old
|
||||
/TIDALDL-PY/tidal_gui
|
||||
|
||||
1
.vscode/launch.json
vendored
@@ -16,6 +16,7 @@
|
||||
"env": {
|
||||
"PYTHONPATH": "${workspaceRoot}/TIDALDL-PY/"
|
||||
},
|
||||
"justMyCode": false
|
||||
},
|
||||
{
|
||||
"name": "Python: common line",
|
||||
|
||||
53
README.md
@@ -31,26 +31,23 @@
|
||||
<br>
|
||||
</p>
|
||||
|
||||
## 📺 Installation
|
||||
| Name | platform | Install |
|
||||
| ----------------------------- | --------------------------------- | ------------------------------------------------------------ |
|
||||
| tidal-dl (cli) | Windows \ Linux \ Macos \ Android | ```pip3 install tidal-dl --upgrade```<br />[Detailed Description](https://yaronzz.com/post/tidal_dl_installation/#Install) |
|
||||
| tidal-gui | Windows | [GUI Repository](https://github.com/yaronzz/Tidal-Media-Downloader-PRO) |
|
||||
| tidal-gui(**Cross-platform**) | Windows \ Linux \ Macos | ```pip3 install tidal-gui --upgrade``` |
|
||||
## 📺 Installation
|
||||
|
||||
| Name | platform | Install |
|
||||
| ----------------------------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| tidal-dl (cli) | Windows \ Linux \ Macos \ Android | ``pip3 install tidal-dl --upgrade``<br />[Detailed Description](https://yaronzz.com/post/tidal_dl_installation/#Install) |
|
||||
| tidal-gui | Windows | [GUI Repository](https://github.com/yaronzz/Tidal-Media-Downloader-PRO) |
|
||||
| tidal-gui(**Cross-platform**) | Windows \ Linux \ Macos | ``pip3 install tidal-gui --upgrade`` |
|
||||
|
||||
### Nightly Builds
|
||||
|
||||
|Download nightly builds from continuous integration: | [![Build Status][Build]][Actions]
|
||||
|-------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
|
||||
[Actions]: https://github.com/yaronzz/Tidal-Media-Downloader/actions
|
||||
[Build]: https://github.com/yaronzz/Tidal-Media-Downloader/workflows/Tidal%20Media%20Downloader/badge.svg
|
||||
| Download nightly builds from continuous integration: | [Build Status][Actions] |
|
||||
| ---------------------------------------------------- | ----------------------- |
|
||||
|
||||
## 🤖 Features
|
||||
|
||||
- Download album \ track \ video \ playlist \ artist-albums
|
||||
|
||||
- Add metadata to songs
|
||||
|
||||
- Selectable video resolution and track quality
|
||||
|
||||
## 💽 User Interface
|
||||
@@ -89,7 +86,7 @@
|
||||
| {ArtistName} | The Beatles |
|
||||
| {ArtistsName} | The Beatles |
|
||||
| {TrackTitle} | I Saw Her Standing There (Remastered 2009) |
|
||||
| {ExplicitFlag} | (*Explicit*) |
|
||||
| {ExplicitFlag} | (*Explicit*) |
|
||||
| {AlbumYear} | 1963 |
|
||||
| {AlbumTitle} | Please Please Me (Remastered) |
|
||||
| {AudioQuality} | LOSSLESS |
|
||||
@@ -99,25 +96,26 @@
|
||||
|
||||
### Video
|
||||
|
||||
| Tag | Example Value |
|
||||
| ----------------- | ------------------------------------------ |
|
||||
| {VideoNumber} | 00 |
|
||||
| {ArtistName} | DMX |
|
||||
| {ArtistsName} | DMX, Westside Gunn |
|
||||
| {VideoTitle} | Hood Blues |
|
||||
| {ExplicitFlag} | (*Explicit*) |
|
||||
| {VideoYear} | 2021 |
|
||||
| {TrackID} | 188932980 |
|
||||
| Tag | Example Value |
|
||||
| -------------- | ------------------ |
|
||||
| {VideoNumber} | 00 |
|
||||
| {ArtistName} | DMX |
|
||||
| {ArtistsName} | DMX, Westside Gunn |
|
||||
| {VideoTitle} | Hood Blues |
|
||||
| {ExplicitFlag} | (*Explicit*) |
|
||||
| {VideoYear} | 2021 |
|
||||
| {TrackID} | 188932980 |
|
||||
|
||||
## ☕ Support
|
||||
|
||||
If you really like my projects and want to support me, you can buy me a coffee and star this project.
|
||||
If you really like my projects and want to support me, you can buy me a coffee and star this project.
|
||||
|
||||
<a href="https://www.buymeacoffee.com/yaronzz" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/arial-orange.png" alt="Buy Me A Coffee" style="height: 51px !important;width: 217px !important;" ></a>
|
||||
|
||||
## 🎂 Contributors
|
||||
This project exists thanks to all the people who contribute.
|
||||
<a href="https://github.com/yaronzz/Tidal-Media-Downloader/graphs/contributors"><img src="https://contributors-img.web.app/image?repo=yaronzz/Tidal-Media-Downloader" /></a>
|
||||
|
||||
This project exists thanks to all the people who contribute.
|
||||
`<a href="https://github.com/yaronzz/Tidal-Media-Downloader/graphs/contributors"><img src="https://contributors-img.web.app/image?repo=yaronzz/Tidal-Media-Downloader" />``</a>`
|
||||
|
||||
## 🎨 Libraries and reference
|
||||
|
||||
@@ -127,8 +125,9 @@ This project exists thanks to all the people who contribute.
|
||||
- [tidal-wiki](https://github.com/Fokka-Engineering/TIDAL/wiki)
|
||||
|
||||
## 📜 Disclaimer
|
||||
|
||||
- Private use only.
|
||||
- Need a Tidal-HIFI subscription.
|
||||
- Need a Tidal-HIFI subscription.
|
||||
- You should not use this method to distribute or pirate music.
|
||||
- It may be illegal to use this in your country, so be informed.
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ prettytable==3.1.1
|
||||
mutagen==1.45.1
|
||||
psutil==5.9.0
|
||||
pycryptodome==3.14.1
|
||||
aigpy==2022.6.15.1
|
||||
aigpy==2022.6.22.1
|
||||
lyricsgenius==3.0.1
|
||||
pydub==0.25.1
|
||||
PyQt5==5.15.7
|
||||
|
||||
@@ -13,7 +13,11 @@ setup(
|
||||
packages=find_packages(exclude=['tidal_gui*']),
|
||||
include_package_data=False,
|
||||
platforms="any",
|
||||
install_requires=["aigpy>=2022.6.15.1", "requests>=2.22.0",
|
||||
"pycryptodome", "pydub", "prettytable", "lyricsgenius"],
|
||||
install_requires=["aigpy>=2022.6.22.1",
|
||||
"requests>=2.22.0",
|
||||
"pycryptodome",
|
||||
"pydub",
|
||||
"prettytable",
|
||||
"PyQt5"],
|
||||
entry_points={'console_scripts': ['tidal-dl = tidal_dl:main', ]}
|
||||
)
|
||||
|
||||
@@ -1,246 +1,148 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
'''
|
||||
@File : __init__.py
|
||||
@Time : 2020/11/08
|
||||
@Author : Yaronzz
|
||||
@Version : 2.1
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
'''
|
||||
import base64
|
||||
import getopt
|
||||
import logging
|
||||
import ssl
|
||||
import sys
|
||||
import time
|
||||
|
||||
from aigpy.cmdHelper import green, yellow
|
||||
from aigpy.pathHelper import mkdirs
|
||||
from aigpy.pipHelper import getLastVersion
|
||||
from aigpy.stringHelper import isNull
|
||||
from aigpy.systemHelper import cmpVersion
|
||||
|
||||
from tidal_dl.download import start
|
||||
from tidal_dl.enums import AudioQuality, VideoQuality, Type
|
||||
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, CONF, TOKEN, LANG, displayTime, loginByConfig, loginByWeb
|
||||
import tidal_dl.apiKey as apiKey
|
||||
|
||||
ssl._create_default_https_context = ssl._create_unverified_context
|
||||
|
||||
def login():
|
||||
print(LANG.AUTH_START_LOGIN)
|
||||
msg, check = API.getDeviceCode()
|
||||
if not check:
|
||||
Printf.err(msg)
|
||||
return
|
||||
|
||||
# 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)
|
||||
loginByWeb()
|
||||
return
|
||||
|
||||
|
||||
def setAccessToken():
|
||||
while True:
|
||||
print("-------------AccessToken---------------")
|
||||
token = Printf.enter("accessToken('0' go back):")
|
||||
if token == '0':
|
||||
return
|
||||
msg, check = API.loginByAccessToken(token, TOKEN.userid)
|
||||
if not check:
|
||||
Printf.err(msg)
|
||||
continue
|
||||
break
|
||||
|
||||
print("-------------RefreshToken---------------")
|
||||
refreshToken = Printf.enter("refreshToken('0' to skip):")
|
||||
if refreshToken == '0':
|
||||
refreshToken = TOKEN.refreshToken
|
||||
|
||||
TOKEN.accessToken = token
|
||||
TOKEN.refreshToken = refreshToken
|
||||
TOKEN.expiresAfter = 0
|
||||
TOKEN.countryCode = API.key.countryCode
|
||||
TokenSettings.save(TOKEN)
|
||||
|
||||
|
||||
def setAPIKey():
|
||||
global LANG
|
||||
item = apiKey.getItem(CONF.apiKeyIndex)
|
||||
ver = apiKey.getVersion()
|
||||
Printf.info(f'Current APIKeys: {str(CONF.apiKeyIndex)} {item["platform"]}-{item["formats"]}')
|
||||
Printf.info(f'Current Version: {str(ver)}')
|
||||
Printf.apikeys(apiKey.getItems())
|
||||
index = int(Printf.enterLimit("APIKEY index:", LANG.MSG_INPUT_ERR, apiKey.getLimitIndexs()))
|
||||
|
||||
if index != CONF.apiKeyIndex:
|
||||
CONF.apiKeyIndex = index
|
||||
Settings.save(CONF)
|
||||
API.apiKey = apiKey.getItem(index)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def checkLogin():
|
||||
if loginByConfig():
|
||||
return
|
||||
login()
|
||||
return
|
||||
|
||||
|
||||
def checkLogout():
|
||||
login()
|
||||
return
|
||||
|
||||
|
||||
def changeSettings():
|
||||
global LANG
|
||||
Printf.settings(CONF)
|
||||
choice = Printf.enter(LANG.CHANGE_START_SETTINGS)
|
||||
if choice == '0':
|
||||
return
|
||||
|
||||
CONF.downloadPath = Printf.enterPath(LANG.CHANGE_DOWNLOAD_PATH, LANG.MSG_PATH_ERR, '0', CONF.downloadPath)
|
||||
CONF.audioQuality = AudioQuality(int(Printf.enterLimit(
|
||||
LANG.CHANGE_AUDIO_QUALITY, LANG.MSG_INPUT_ERR, ['0', '1', '2', '3'])))
|
||||
CONF.videoQuality = VideoQuality(int(Printf.enterLimit(
|
||||
LANG.CHANGE_VIDEO_QUALITY, LANG.MSG_INPUT_ERR, ['1080', '720', '480', '360'])))
|
||||
CONF.onlyM4a = Printf.enter(LANG.CHANGE_ONLYM4A) == '1'
|
||||
CONF.checkExist = Printf.enter(LANG.CHANGE_CHECK_EXIST) == '1'
|
||||
CONF.includeEP = Printf.enter(LANG.CHANGE_INCLUDE_EP) == '1'
|
||||
CONF.saveCovers = Printf.enter(LANG.CHANGE_SAVE_COVERS) == '1'
|
||||
CONF.showProgress = Printf.enter(LANG.CHANGE_SHOW_PROGRESS) == '1'
|
||||
CONF.saveAlbumInfo = Printf.enter(LANG.CHANGE_SAVE_ALBUM_INFO) == '1'
|
||||
CONF.showTrackInfo = Printf.enter(LANG.CHANGE_SHOW_TRACKINFO) == '1'
|
||||
CONF.usePlaylistFolder = Printf.enter(LANG.SETTING_USE_PLAYLIST_FOLDER + "('0'-No,'1'-Yes):") == '1'
|
||||
CONF.language = Printf.enter(LANG.CHANGE_LANGUAGE + "(" + getLangChoicePrint() + "):")
|
||||
CONF.albumFolderFormat = Printf.enterFormat(
|
||||
LANG.CHANGE_ALBUM_FOLDER_FORMAT, CONF.albumFolderFormat, Settings.getDefaultAlbumFolderFormat())
|
||||
CONF.trackFileFormat = Printf.enterFormat(LANG.CHANGE_TRACK_FILE_FORMAT,
|
||||
CONF.trackFileFormat, Settings.getDefaultTrackFileFormat())
|
||||
CONF.videoFileFormat = Printf.enterFormat(LANG.CHANGE_VIDEO_FILE_FORMAT,
|
||||
CONF.videoFileFormat, Settings.getDefaultVideoFileFormat())
|
||||
CONF.addLyrics = Printf.enter(LANG.CHANGE_ADD_LYRICS) == '1'
|
||||
CONF.lyricsServerProxy = Printf.enterFormat(
|
||||
LANG.CHANGE_LYRICS_SERVER_PROXY, CONF.lyricsServerProxy, CONF.lyricsServerProxy)
|
||||
CONF.lyricFile = Printf.enter(LANG.CHANGE_ADD_LRC_FILE) == '1'
|
||||
CONF.addTypeFolder = Printf.enter(LANG.CHANGE_ADD_TYPE_FOLDER) == '1'
|
||||
|
||||
LANG = setLang(CONF.language)
|
||||
Settings.save(CONF)
|
||||
|
||||
|
||||
def mainCommand():
|
||||
try:
|
||||
opts, args = getopt.getopt(sys.argv[1:], "hvl:o:q:r:", ["help", "version",
|
||||
"link=", "output=", "quality", "resolution"])
|
||||
except getopt.GetoptError as errmsg:
|
||||
Printf.err(vars(errmsg)['msg'] + ". Use 'tidal-dl -h' for useage.")
|
||||
return
|
||||
|
||||
link = None
|
||||
for opt, val in opts:
|
||||
if opt in ('-h', '--help'):
|
||||
Printf.usage()
|
||||
continue
|
||||
if opt in ('-v', '--version'):
|
||||
Printf.logo()
|
||||
continue
|
||||
if opt in ('-l', '--link'):
|
||||
checkLogin()
|
||||
link = val
|
||||
continue
|
||||
if opt in ('-o', '--output'):
|
||||
CONF.downloadPath = val
|
||||
Settings.save(CONF)
|
||||
continue
|
||||
if opt in ('-q', '--quality'):
|
||||
CONF.audioQuality = Settings.getAudioQuality(val)
|
||||
Settings.save(CONF)
|
||||
continue
|
||||
if opt in ('-r', '--resolution'):
|
||||
CONF.videoQuality = Settings.getVideoQuality(val)
|
||||
Settings.save(CONF)
|
||||
continue
|
||||
|
||||
if not mkdirs(CONF.downloadPath):
|
||||
Printf.err(LANG.MSG_PATH_ERR + CONF.downloadPath)
|
||||
return
|
||||
|
||||
if link is not None:
|
||||
Printf.info(LANG.SETTING_DOWNLOAD_PATH + ':' + CONF.downloadPath)
|
||||
start(TOKEN, CONF, link)
|
||||
|
||||
|
||||
def debug():
|
||||
checkLogin()
|
||||
API.key.accessToken = TOKEN.accessToken
|
||||
API.key.userId = TOKEN.userid
|
||||
API.key.countryCode = TOKEN.countryCode
|
||||
# https://api.tidal.com/v1/mixes/{01453963b7dbd41c8b82ccb678d127/items?countryCode={country}
|
||||
API.getMix("01453963b7dbd41c8b82ccb678d127")
|
||||
# msg, result = API.search('Mojito', Type.Null, 0, 10)
|
||||
msg, lyric = API.getLyrics('144909909')
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) > 1:
|
||||
mainCommand()
|
||||
return
|
||||
|
||||
Printf.logo()
|
||||
Printf.settings(CONF)
|
||||
|
||||
checkLogin()
|
||||
|
||||
onlineVer = getLastVersion('tidal-dl')
|
||||
if not isNull(onlineVer):
|
||||
icmp = cmpVersion(onlineVer, VERSION)
|
||||
if icmp > 0:
|
||||
Printf.info(LANG.PRINT_LATEST_VERSION + ' ' + onlineVer)
|
||||
|
||||
while True:
|
||||
Printf.choices()
|
||||
choice = Printf.enter(LANG.PRINT_ENTER_CHOICE)
|
||||
if choice == "0":
|
||||
return
|
||||
elif choice == "1":
|
||||
checkLogin()
|
||||
elif choice == "2":
|
||||
changeSettings()
|
||||
elif choice == "3":
|
||||
checkLogout()
|
||||
elif choice == "4":
|
||||
setAccessToken()
|
||||
elif choice == '5':
|
||||
if setAPIKey():
|
||||
checkLogout()
|
||||
elif choice == "10": # test track
|
||||
start(TOKEN, CONF, '70973230')
|
||||
elif choice == "11": # test video
|
||||
start(TOKEN, CONF, '188932980')
|
||||
elif choice == "12": # test album
|
||||
start(TOKEN, CONF, '58138532')
|
||||
elif choice == "13": # test playlist
|
||||
start(TOKEN, CONF, '98235845-13e8-43b4-94e2-d9f8e603cee7')
|
||||
elif choice == "14": # test playlist
|
||||
start(TOKEN, CONF, '01453963b7dbd41c8b82ccb678d127')
|
||||
else:
|
||||
start(TOKEN, CONF, choice)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# debug()
|
||||
main()
|
||||
# test example
|
||||
# track 70973230 77798028 212657
|
||||
# video 155608351 188932980
|
||||
# album 58138532 77803199 21993753 79151897 56288918
|
||||
# playlist 98235845-13e8-43b4-94e2-d9f8e603cee7
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
'''
|
||||
@File : __init__.py
|
||||
@Time : 2020/11/08
|
||||
@Author : Yaronzz
|
||||
@Version : 3.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
'''
|
||||
import sys
|
||||
import getopt
|
||||
|
||||
from tidal_dl.events import *
|
||||
from tidal_dl.settings import *
|
||||
from tidal_dl.gui import *
|
||||
|
||||
|
||||
def mainCommand():
|
||||
try:
|
||||
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.")
|
||||
return
|
||||
|
||||
link = None
|
||||
showGui = False
|
||||
for opt, val in opts:
|
||||
if opt in ('-h', '--help'):
|
||||
Printf.usage()
|
||||
return
|
||||
if opt in ('-v', '--version'):
|
||||
Printf.logo()
|
||||
return
|
||||
if opt in ('-g', '--gui'):
|
||||
showGui = True
|
||||
return
|
||||
if opt in ('-l', '--link'):
|
||||
link = val
|
||||
continue
|
||||
if opt in ('-o', '--output'):
|
||||
SETTINGS.downloadPath = val
|
||||
SETTINGS.save()
|
||||
continue
|
||||
if opt in ('-q', '--quality'):
|
||||
SETTINGS.audioQuality = SETTINGS.getAudioQuality(val)
|
||||
SETTINGS.save()
|
||||
continue
|
||||
if opt in ('-r', '--resolution'):
|
||||
SETTINGS.videoQuality = SETTINGS.getVideoQuality(val)
|
||||
SETTINGS.save()
|
||||
continue
|
||||
|
||||
if not aigpy.path.mkdirs(SETTINGS.downloadPath):
|
||||
Printf.err(LANG.MSG_PATH_ERR + SETTINGS.downloadPath)
|
||||
return
|
||||
|
||||
if showGui:
|
||||
startGui()
|
||||
return
|
||||
|
||||
if link is not None:
|
||||
if not loginByConfig():
|
||||
loginByWeb()
|
||||
Printf.info(LANG.SETTING_DOWNLOAD_PATH + ':' + SETTINGS.downloadPath)
|
||||
start(link)
|
||||
|
||||
def main():
|
||||
SETTINGS.read(getProfilePath())
|
||||
TOKEN.read(getTokenPath())
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
mainCommand()
|
||||
return
|
||||
|
||||
Printf.logo()
|
||||
Printf.settings()
|
||||
|
||||
if not loginByConfig():
|
||||
loginByWeb()
|
||||
|
||||
Printf.checkVersion()
|
||||
|
||||
while True:
|
||||
Printf.choices()
|
||||
choice = Printf.enter(LANG.PRINT_ENTER_CHOICE)
|
||||
if choice == "0":
|
||||
return
|
||||
elif choice == "1":
|
||||
if not loginByConfig():
|
||||
loginByWeb()
|
||||
elif choice == "2":
|
||||
loginByWeb()
|
||||
elif choice == "3":
|
||||
loginByAccessToken()
|
||||
elif choice == "4":
|
||||
changePathSettings()
|
||||
elif choice == "5":
|
||||
changeQualitySettings()
|
||||
elif choice == "6":
|
||||
changeSettings()
|
||||
elif choice == "7":
|
||||
if changeApiKey():
|
||||
loginByWeb()
|
||||
else:
|
||||
start(choice)
|
||||
|
||||
|
||||
def test():
|
||||
SETTINGS.read(getProfilePath())
|
||||
TOKEN.read(getTokenPath())
|
||||
|
||||
if not loginByConfig():
|
||||
loginByWeb()
|
||||
|
||||
SETTINGS.audioQuality = AudioQuality.Normal
|
||||
SETTINGS.videoFileFormat = VideoQuality.P240
|
||||
SETTINGS.checkExist = False
|
||||
SETTINGS.includeEP = True
|
||||
SETTINGS.saveCovers = True
|
||||
SETTINGS.lyricFile = True
|
||||
SETTINGS.showProgress = True
|
||||
SETTINGS.showTrackInfo = True
|
||||
SETTINGS.saveAlbumInfo = True
|
||||
SETTINGS.downloadPath = "./download/"
|
||||
SETTINGS.usePlaylistFolder = True
|
||||
SETTINGS.albumFolderFormat = R"{ArtistName}/{Flag} {AlbumTitle} [{AlbumID}] [{AlbumYear}]"
|
||||
SETTINGS.trackFileFormat = R"{TrackNumber} - {ArtistName} - {TrackTitle}{ExplicitFlag}"
|
||||
SETTINGS.videoFileFormat = R"{VideoNumber} - {ArtistName} - {VideoTitle}{ExplicitFlag}"
|
||||
|
||||
Printf.settings()
|
||||
# test example
|
||||
# https://tidal.com/browse/track/70973230
|
||||
# track 70973230 77798028 212657
|
||||
start('70973230')
|
||||
# album 58138532 77803199 21993753 79151897 56288918
|
||||
# start('58138532')
|
||||
# playlist 98235845-13e8-43b4-94e2-d9f8e603cee7
|
||||
# start('98235845-13e8-43b4-94e2-d9f8e603cee7')
|
||||
# video 155608351 188932980
|
||||
# start("155608351")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# test()
|
||||
main()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@File : apiKey.py
|
||||
@Date : 2021/11/30
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Version : 3.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
@@ -68,7 +68,6 @@ __ERROR_KEY__ = {
|
||||
},
|
||||
|
||||
|
||||
|
||||
def getNum():
|
||||
return len(__API_KEYS__['keys'])
|
||||
|
||||
|
||||
@@ -8,203 +8,185 @@
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
'''
|
||||
import requests
|
||||
import logging
|
||||
import os
|
||||
import datetime
|
||||
|
||||
import aigpy
|
||||
import lyricsgenius
|
||||
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.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 convert, downloadTrack, downloadVideo, encrypted, getVideoPath, getTrackPath, getAlbumPath, API
|
||||
import logging
|
||||
|
||||
from tidal_dl.paths import *
|
||||
from tidal_dl.printf import *
|
||||
from tidal_dl.decryption import *
|
||||
from tidal_dl.tidal import *
|
||||
|
||||
|
||||
def __loadAPI__(user):
|
||||
API.key.accessToken = user.accessToken
|
||||
API.key.userId = user.userid
|
||||
API.key.countryCode = user.countryCode
|
||||
def __isSkip__(finalpath, url):
|
||||
if not SETTINGS.checkExist:
|
||||
return False
|
||||
curSize = aigpy.file.getSize(finalpath)
|
||||
if curSize <= 0:
|
||||
return False
|
||||
netSize = aigpy.net.getSize(url)
|
||||
return curSize >= netSize
|
||||
|
||||
|
||||
def __downloadCover__(conf, album):
|
||||
if album == None:
|
||||
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 __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
|
||||
|
||||
|
||||
def __setMetaData__(track: Track, album: 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 = list(map(lambda artist: artist.name, 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 = list(map(lambda artist: artist.name, album.artists))
|
||||
obj.date = album.releaseDate
|
||||
obj.totaldisc = album.numberOfVolumes
|
||||
obj.lyrics = lyrics
|
||||
if obj.totaldisc <= 1:
|
||||
obj.totaltrack = album.numberOfTracks
|
||||
coverpath = TIDAL_API.getCoverUrl(album.cover, "1280", "1280")
|
||||
obj.save(coverpath)
|
||||
|
||||
|
||||
def downloadCover(album):
|
||||
if album is None:
|
||||
return
|
||||
path = getAlbumPath(conf, album) + '/cover.jpg'
|
||||
url = API.getCoverUrl(album.cover, "1280", "1280")
|
||||
if url is not None:
|
||||
aigpy.net.downloadFile(url, path)
|
||||
path = getAlbumPath(album) + '/cover.jpg'
|
||||
url = TIDAL_API.getCoverUrl(album.cover, "1280", "1280")
|
||||
aigpy.net.downloadFile(url, path)
|
||||
|
||||
|
||||
def __saveAlbumInfo__(conf, album, tracks):
|
||||
if album == None:
|
||||
def downloadAlbumInfo(album, tracks):
|
||||
if album is None:
|
||||
return
|
||||
path = getAlbumPath(conf, album) + '/AlbumInfo.txt'
|
||||
|
||||
|
||||
path = getAlbumPath(album)
|
||||
aigpy.path.mkdirs(path)
|
||||
|
||||
path += '/AlbumInfo.txt'
|
||||
infos = ""
|
||||
infos += "[ID] %s\n" % (str(album.id))
|
||||
infos += "[Title] %s\n" % (str(album.title))
|
||||
infos += "[Artists] %s\n" % (str(album.artist.name))
|
||||
infos += "[Artists] %s\n" % (TIDAL_API.getArtistsName(album.artists))
|
||||
infos += "[ReleaseDate] %s\n" % (str(album.releaseDate))
|
||||
infos += "[SongNum] %s\n" % (str(album.numberOfTracks))
|
||||
infos += "[Duration] %s\n" % (str(album.duration))
|
||||
infos += '\n'
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
if i >= int(album.numberOfVolumes):
|
||||
break
|
||||
i = i + 1
|
||||
infos += "===========CD %d=============\n" % i
|
||||
for index in range(0, album.numberOfVolumes):
|
||||
volumeNumber = index + 1
|
||||
infos += f"===========CD {volumeNumber}=============\n"
|
||||
for item in tracks:
|
||||
if item.volumeNumber != i:
|
||||
if item.volumeNumber != volumeNumber:
|
||||
continue
|
||||
infos += '{:<8}'.format("[%d]" % item.trackNumber)
|
||||
infos += "%s\n" % item.title
|
||||
aigpy.file.write(path, infos, "w+")
|
||||
|
||||
|
||||
def __album__(conf, obj):
|
||||
Printf.album(obj)
|
||||
msg, tracks, videos = API.getItems(obj.id, Type.Album)
|
||||
if not aigpy.string.isNull(msg):
|
||||
Printf.err(msg)
|
||||
return
|
||||
if conf.saveAlbumInfo:
|
||||
__saveAlbumInfo__(conf, obj, tracks)
|
||||
if conf.saveCovers:
|
||||
__downloadCover__(conf, obj)
|
||||
for item in tracks:
|
||||
downloadTrack(item, obj)
|
||||
for item in videos:
|
||||
downloadVideo(item, obj)
|
||||
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)
|
||||
|
||||
m3u8content = requests.get(stream.m3u8Url).content
|
||||
if m3u8content is None:
|
||||
Printf.err(f"DL Video[{video.title}] getM3u8 failed.{str(e)}")
|
||||
return False
|
||||
|
||||
urls = aigpy.m3u8.parseTsUrls(m3u8content)
|
||||
if len(urls) <= 0:
|
||||
Printf.err(f"DL Video[{video.title}] getTsUrls failed.{str(e)}")
|
||||
return False
|
||||
|
||||
check, msg = aigpy.m3u8.downloadByTsUrls(urls, path)
|
||||
if check:
|
||||
Printf.success(video.title)
|
||||
return True
|
||||
else:
|
||||
Printf.err(f"DL Video[{video.title}] failed.{msg}")
|
||||
return False
|
||||
except Exception as e:
|
||||
Printf.err(f"DL Video[{video.title}] failed.{str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def __track__(conf, obj):
|
||||
msg, album = API.getAlbum(obj.album.id)
|
||||
if conf.saveCovers:
|
||||
__downloadCover__(conf, album)
|
||||
downloadTrack(obj, album)
|
||||
def downloadTrack(track: Track, album=None, playlist=None, userProgress=None, partSize=1048576):
|
||||
try:
|
||||
stream = TIDAL_API.getStreamUrl(track.id, SETTINGS.audioQuality)
|
||||
path = getTrackPath(track, stream, album, playlist)
|
||||
|
||||
if SETTINGS.showTrackInfo:
|
||||
Printf.track(track, stream)
|
||||
|
||||
def __video__(conf, obj):
|
||||
# Printf.video(obj)
|
||||
downloadVideo(obj, obj.album)
|
||||
if userProgress is not None:
|
||||
userProgress.updateStream(stream)
|
||||
|
||||
# check exist
|
||||
if __isSkip__(path, stream.url):
|
||||
Printf.success(aigpy.path.getFileName(path) + " (skip:already exists!)")
|
||||
return True
|
||||
|
||||
def __artist__(conf, obj):
|
||||
msg, albums = API.getArtistAlbums(obj.id, conf.includeEP)
|
||||
Printf.artist(obj, len(albums))
|
||||
if not aigpy.string.isNull(msg):
|
||||
Printf.err(msg)
|
||||
return
|
||||
for item in albums:
|
||||
__album__(conf, item)
|
||||
# download
|
||||
logging.info("[DL Track] name=" + aigpy.path.getFileName(path) + "\nurl=" + stream.url)
|
||||
|
||||
tool = aigpy.download.DownloadTool(path + '.part', [stream.url])
|
||||
tool.setUserProgress(userProgress)
|
||||
tool.setPartSize(partSize)
|
||||
check, err = tool.start(SETTINGS.showProgress)
|
||||
if not check:
|
||||
Printf.err(f"DL Track[{track.title}] failed.{str(err)}")
|
||||
return False
|
||||
|
||||
def __playlist__(conf, obj):
|
||||
Printf.playlist(obj)
|
||||
msg, tracks, videos = API.getItems(obj.uuid, Type.Playlist)
|
||||
if not aigpy.string.isNull(msg):
|
||||
Printf.err(msg)
|
||||
return
|
||||
# encrypted -> decrypt and remove encrypted file
|
||||
__encrypted__(stream, path + '.part', path)
|
||||
|
||||
dictNewFiles = {}
|
||||
for index, item in enumerate(tracks):
|
||||
mag, album = API.getAlbum(item.album.id)
|
||||
item.trackNumberOnPlaylist = index + 1
|
||||
downloadTrack(item, album, obj, dictNewFiles=dictNewFiles)
|
||||
if conf.saveCovers and not conf.usePlaylistFolder:
|
||||
__downloadCover__(conf, album)
|
||||
# contributors
|
||||
try:
|
||||
contributors = TIDAL_API.getTrackContributors(track.id)
|
||||
except:
|
||||
contributors = None
|
||||
|
||||
if len(dictNewFiles) > 0:
|
||||
targetDir = aigpy.path.getDirName(dictNewFiles[list(dictNewFiles.keys())[0]]) # trick to get the target directory
|
||||
resFiles = aigpy.path.getFiles(targetDir)
|
||||
killFiles = []
|
||||
errFiles = []
|
||||
# lyrics
|
||||
try:
|
||||
lyrics = TIDAL_API.getLyrics(track.id).subtitles
|
||||
if SETTINGS.lyricFile:
|
||||
lrcPath = path.rsplit(".", 1)[0] + '.lrc'
|
||||
aigpy.fileHelper.write(lrcPath, lyrics, 'w')
|
||||
except:
|
||||
lyrics = ''
|
||||
|
||||
def bareFnList(fullFn: str):
|
||||
# strip ext so .m4a and corresponding .lrc are handled together
|
||||
return os.path.splitext(os.path.basename(fullFn))[0]
|
||||
|
||||
for fname in resFiles:
|
||||
# strip ext so .m4a and corresponding .lrc are handled together
|
||||
if not os.path.splitext(os.path.basename(fname))[0] in list(map(bareFnList, list(dictNewFiles.keys()))) :
|
||||
try:
|
||||
os.remove(fname)
|
||||
killFiles.append(fname)
|
||||
except:
|
||||
errFiles.append(fname)
|
||||
|
||||
if len(killFiles) > 0:
|
||||
Printf.success("List of Orphan files deleted :\n" + "\n".join(killFiles))
|
||||
if len(errFiles) > 0:
|
||||
Printf.err("List of Orphan files failed to delete (read-only?) :\n" + "\n".join(errFiles))
|
||||
|
||||
for item in videos:
|
||||
downloadVideo(item, None)
|
||||
|
||||
|
||||
def __mix__(conf, obj: Mix):
|
||||
Printf.mix(obj)
|
||||
for index, item in enumerate(obj.tracks):
|
||||
mag, album = API.getAlbum(item.album.id)
|
||||
item.trackNumberOnPlaylist = index + 1
|
||||
downloadTrack(item, album)
|
||||
if conf.saveCovers and not conf.usePlaylistFolder:
|
||||
__downloadCover__(conf, album)
|
||||
for item in obj.videos:
|
||||
downloadVideo(item, None)
|
||||
|
||||
|
||||
def file(user, conf, string):
|
||||
txt = aigpy.file.getContent(string)
|
||||
if aigpy.string.isNull(txt):
|
||||
Printf.err("Nothing can read!")
|
||||
return
|
||||
array = txt.split('\n')
|
||||
for item in array:
|
||||
if aigpy.string.isNull(item):
|
||||
continue
|
||||
if item[0] == '#':
|
||||
continue
|
||||
if item[0] == '[':
|
||||
continue
|
||||
start(user, conf, item)
|
||||
|
||||
|
||||
def start(user, conf, string):
|
||||
__loadAPI__(user)
|
||||
if aigpy.string.isNull(string):
|
||||
Printf.err('Please enter something.')
|
||||
return
|
||||
|
||||
strings = string.split(" ")
|
||||
for item in strings:
|
||||
if aigpy.string.isNull(item):
|
||||
continue
|
||||
if os.path.exists(item):
|
||||
file(user, conf, item)
|
||||
return
|
||||
|
||||
msg, etype, obj = API.getByString(item)
|
||||
if etype == Type.Null or not aigpy.string.isNull(msg):
|
||||
Printf.err(msg + " [" + item + "]")
|
||||
return
|
||||
|
||||
if etype == Type.Album:
|
||||
__album__(conf, obj)
|
||||
if etype == Type.Track:
|
||||
__track__(conf, obj)
|
||||
if etype == Type.Video:
|
||||
__video__(conf, obj)
|
||||
if etype == Type.Artist:
|
||||
__artist__(conf, obj)
|
||||
if etype == Type.Playlist:
|
||||
__playlist__(conf, obj)
|
||||
if etype == Type.Mix:
|
||||
__mix__(conf, obj)
|
||||
__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)}")
|
||||
return False
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@File : enums.py
|
||||
@Time : 2020/08/08
|
||||
@Author : Yaronzz
|
||||
@Version : 2.0
|
||||
@Version : 3.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
'''
|
||||
|
||||
333
TIDALDL-PY/tidal_dl/events.py
Normal file
@@ -0,0 +1,333 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : events.py
|
||||
@Date : 2022/06/10
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@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 *
|
||||
|
||||
'''
|
||||
=================================
|
||||
START DOWNLOAD
|
||||
=================================
|
||||
'''
|
||||
|
||||
|
||||
def start_album(obj: Album):
|
||||
Printf.album(obj)
|
||||
tracks, videos = TIDAL_API.getItems(obj.id, Type.Album)
|
||||
if SETTINGS.saveAlbumInfo:
|
||||
downloadAlbumInfo(obj, tracks)
|
||||
if SETTINGS.saveCovers:
|
||||
downloadCover(obj)
|
||||
for item in tracks:
|
||||
downloadTrack(item, obj)
|
||||
for item in videos:
|
||||
downloadVideo(item, obj)
|
||||
|
||||
|
||||
def start_track(obj: Track):
|
||||
album = TIDAL_API.getAlbum(obj.album.id)
|
||||
if SETTINGS.saveCovers:
|
||||
downloadCover(album)
|
||||
downloadTrack(obj, album)
|
||||
|
||||
|
||||
def start_video(obj: Video):
|
||||
# Printf.video(obj)
|
||||
downloadVideo(obj, obj.album)
|
||||
|
||||
|
||||
def start_artist(obj: Artist):
|
||||
albums = TIDAL_API.getArtistAlbums(obj.id, SETTINGS.includeEP)
|
||||
Printf.artist(obj, len(albums))
|
||||
for item in albums:
|
||||
start_album(item)
|
||||
|
||||
|
||||
def start_playlist(obj: Playlist):
|
||||
Printf.playlist(obj)
|
||||
tracks, videos = TIDAL_API.getItems(obj.uuid, Type.Playlist)
|
||||
|
||||
for index, item in enumerate(tracks):
|
||||
album = TIDAL_API.getAlbum(item.album.id)
|
||||
item.trackNumberOnPlaylist = index + 1
|
||||
downloadTrack(item, album, obj)
|
||||
if SETTINGS.saveCovers and not SETTINGS.usePlaylistFolder:
|
||||
downloadCover(album)
|
||||
for item in videos:
|
||||
downloadVideo(item, None)
|
||||
|
||||
|
||||
def start_mix(obj: Mix):
|
||||
Printf.mix(obj)
|
||||
for index, item in enumerate(obj.tracks):
|
||||
album = TIDAL_API.getAlbum(item.album.id)
|
||||
item.trackNumberOnPlaylist = index + 1
|
||||
downloadTrack(item, album)
|
||||
if SETTINGS.saveCovers and not SETTINGS.usePlaylistFolder:
|
||||
downloadCover(album)
|
||||
|
||||
for item in obj.videos:
|
||||
downloadVideo(item, None)
|
||||
|
||||
|
||||
def start_file(string):
|
||||
txt = aigpy.file.getContent(string)
|
||||
if aigpy.string.isNull(txt):
|
||||
Printf.err("Nothing can read!")
|
||||
return
|
||||
array = txt.split('\n')
|
||||
for item in array:
|
||||
if aigpy.string.isNull(item):
|
||||
continue
|
||||
if item[0] == '#':
|
||||
continue
|
||||
if item[0] == '[':
|
||||
continue
|
||||
start(item)
|
||||
|
||||
|
||||
def start_type(etype: Type, obj):
|
||||
if etype == Type.Album:
|
||||
start_album(obj)
|
||||
elif etype == Type.Track:
|
||||
start_track(obj)
|
||||
elif etype == Type.Video:
|
||||
start_video(obj)
|
||||
elif etype == Type.Artist:
|
||||
start_artist(obj)
|
||||
elif etype == Type.Playlist:
|
||||
start_playlist(obj)
|
||||
elif etype == Type.Mix:
|
||||
start_mix(obj)
|
||||
|
||||
def start(string):
|
||||
if aigpy.string.isNull(string):
|
||||
Printf.err('Please enter something.')
|
||||
return
|
||||
|
||||
strings = string.split(" ")
|
||||
for item in strings:
|
||||
if aigpy.string.isNull(item):
|
||||
continue
|
||||
if os.path.exists(item):
|
||||
start_file(item)
|
||||
return
|
||||
|
||||
try:
|
||||
etype, obj = TIDAL_API.getByString(item)
|
||||
except Exception as e:
|
||||
Printf.err(str(e) + " [" + item + "]")
|
||||
return
|
||||
|
||||
try:
|
||||
start_type(etype, obj)
|
||||
except Exception as e:
|
||||
Printf.err(str(e))
|
||||
|
||||
'''
|
||||
=================================
|
||||
CHANGE SETTINGS
|
||||
=================================
|
||||
'''
|
||||
|
||||
|
||||
def changePathSettings():
|
||||
Printf.settings(SETTINGS)
|
||||
SETTINGS.downloadPath = Printf.enterPath(
|
||||
LANG.CHANGE_DOWNLOAD_PATH,
|
||||
LANG.MSG_PATH_ERR,
|
||||
'0',
|
||||
SETTINGS.downloadPath)
|
||||
SETTINGS.albumFolderFormat = Printf.enterFormat(
|
||||
LANG.CHANGE_ALBUM_FOLDER_FORMAT,
|
||||
SETTINGS.albumFolderFormat,
|
||||
SETTINGS.getDefaultAlbumFolderFormat())
|
||||
SETTINGS.trackFileFormat = Printf.enterFormat(
|
||||
LANG.CHANGE_TRACK_FILE_FORMAT,
|
||||
SETTINGS.trackFileFormat,
|
||||
SETTINGS.getDefaultTrackFileFormat())
|
||||
SETTINGS.videoFileFormat = Printf.enterFormat(
|
||||
LANG.CHANGE_VIDEO_FILE_FORMAT,
|
||||
SETTINGS.videoFileFormat,
|
||||
SETTINGS.getDefaultVideoFileFormat())
|
||||
SETTINGS.save()
|
||||
|
||||
|
||||
def changeQualitySettings():
|
||||
Printf.settings(SETTINGS)
|
||||
SETTINGS.audioQuality = AudioQuality(
|
||||
int(Printf.enterLimit(LANG.CHANGE_AUDIO_QUALITY,
|
||||
LANG.MSG_INPUT_ERR,
|
||||
['0', '1', '2', '3'])))
|
||||
SETTINGS.videoQuality = VideoQuality(
|
||||
int(Printf.enterLimit(LANG.CHANGE_VIDEO_QUALITY,
|
||||
LANG.MSG_INPUT_ERR,
|
||||
['1080', '720', '480', '360'])))
|
||||
SETTINGS.save()
|
||||
|
||||
|
||||
def changeSettings():
|
||||
Printf.settings(SETTINGS)
|
||||
SETTINGS.showProgress = Printf.enterBool(LANG.CHANGE_SHOW_PROGRESS)
|
||||
SETTINGS.showTrackInfo = Printf.enterBool(LANG.CHANGE_SHOW_TRACKINFO)
|
||||
SETTINGS.checkExist = Printf.enterBool(LANG.CHANGE_CHECK_EXIST)
|
||||
SETTINGS.includeEP = Printf.enterBool(LANG.CHANGE_INCLUDE_EP)
|
||||
SETTINGS.saveCovers = Printf.enterBool(LANG.CHANGE_SAVE_COVERS)
|
||||
SETTINGS.saveAlbumInfo = Printf.enterBool(LANG.CHANGE_SAVE_ALBUM_INFO)
|
||||
SETTINGS.lyricFile = Printf.enterBool(LANG.CHANGE_ADD_LRC_FILE)
|
||||
SETTINGS.usePlaylistFolder = Printf.enterBool(LANG.SETTING_USE_PLAYLIST_FOLDER + "('0'-No,'1'-Yes):")
|
||||
SETTINGS.language = Printf.enter(LANG.CHANGE_LANGUAGE + "(" + getLangChoicePrint() + "):")
|
||||
LANG = setLang(SETTINGS.language)
|
||||
SETTINGS.save()
|
||||
|
||||
|
||||
def changeApiKey():
|
||||
item = apiKey.getItem(SETTINGS.apiKeyIndex)
|
||||
ver = apiKey.getVersion()
|
||||
|
||||
Printf.info(f'Current APIKeys: {str(SETTINGS.apiKeyIndex)} {item["platform"]}-{item["formats"]}')
|
||||
Printf.info(f'Current Version: {str(ver)}')
|
||||
Printf.apikeys(apiKey.getItems())
|
||||
index = int(Printf.enterLimit("APIKEY index:", LANG.MSG_INPUT_ERR, apiKey.getLimitIndexs()))
|
||||
|
||||
if index != SETTINGS.apiKeyIndex:
|
||||
SETTINGS.apiKeyIndex = index
|
||||
SETTINGS.save()
|
||||
TIDAL_API.apiKey = apiKey.getItem(index)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
'''
|
||||
=================================
|
||||
LOGIN
|
||||
=================================
|
||||
'''
|
||||
|
||||
|
||||
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 loginByWeb():
|
||||
try:
|
||||
print(LANG.AUTH_START_LOGIN)
|
||||
# get device code
|
||||
url = TIDAL_API.getDeviceCode()
|
||||
|
||||
print(LANG.AUTH_NEXT_STEP.format(
|
||||
aigpy.cmd.green(url),
|
||||
aigpy.cmd.yellow(__displayTime__(TIDAL_API.key.authCheckTimeout))))
|
||||
print(LANG.AUTH_WAITING)
|
||||
|
||||
start = time.time()
|
||||
elapsed = 0
|
||||
while elapsed < TIDAL_API.key.authCheckTimeout:
|
||||
elapsed = time.time() - start
|
||||
if not TIDAL_API.checkAuthStatus():
|
||||
time.sleep(TIDAL_API.key.authCheckInterval + 1)
|
||||
continue
|
||||
|
||||
Printf.success(LANG.MSG_VALID_ACCESSTOKEN.format(
|
||||
__displayTime__(int(TIDAL_API.key.expiresIn))))
|
||||
|
||||
TOKEN.userid = TIDAL_API.key.userId
|
||||
TOKEN.countryCode = TIDAL_API.key.countryCode
|
||||
TOKEN.accessToken = TIDAL_API.key.accessToken
|
||||
TOKEN.refreshToken = TIDAL_API.key.refreshToken
|
||||
TOKEN.expiresAfter = time.time() + int(TIDAL_API.key.expiresIn)
|
||||
TOKEN.save()
|
||||
return True
|
||||
|
||||
raise Exception(LANG.AUTH_TIMEOUT)
|
||||
except Exception as e:
|
||||
Printf.err(f"Login failed.{str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def loginByConfig():
|
||||
try:
|
||||
if aigpy.string.isNull(TOKEN.accessToken):
|
||||
return False
|
||||
|
||||
if TIDAL_API.verifyAccessToken(TOKEN.accessToken):
|
||||
Printf.info(LANG.MSG_VALID_ACCESSTOKEN.format(
|
||||
__displayTime__(int(TOKEN.expiresAfter - time.time()))))
|
||||
|
||||
TIDAL_API.key.countryCode = TOKEN.countryCode
|
||||
TIDAL_API.key.userId = TOKEN.userid
|
||||
TIDAL_API.key.accessToken = TOKEN.accessToken
|
||||
return True
|
||||
|
||||
Printf.info(LANG.MSG_INVALID_ACCESSTOKEN)
|
||||
if TIDAL_API.refreshAccessToken(TOKEN.refreshToken):
|
||||
Printf.success(LANG.MSG_VALID_ACCESSTOKEN.format(
|
||||
__displayTime__(int(TIDAL_API.key.expiresIn))))
|
||||
|
||||
TOKEN.userid = TIDAL_API.key.userId
|
||||
TOKEN.countryCode = TIDAL_API.key.countryCode
|
||||
TOKEN.accessToken = TIDAL_API.key.accessToken
|
||||
TOKEN.expiresAfter = time.time() + int(TIDAL_API.key.expiresIn)
|
||||
TOKEN.save()
|
||||
return True
|
||||
else:
|
||||
TokenSettings().save()
|
||||
return False
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
|
||||
def loginByAccessToken():
|
||||
try:
|
||||
print("-------------AccessToken---------------")
|
||||
token = Printf.enter("accessToken('0' go back):")
|
||||
if token == '0':
|
||||
return
|
||||
TIDAL_API.loginByAccessToken(token, TOKEN.userid)
|
||||
except Exception as e:
|
||||
Printf.err(str(e))
|
||||
return
|
||||
|
||||
print("-------------RefreshToken---------------")
|
||||
refreshToken = Printf.enter("refreshToken('0' to skip):")
|
||||
if refreshToken == '0':
|
||||
refreshToken = TOKEN.refreshToken
|
||||
|
||||
TOKEN.accessToken = token
|
||||
TOKEN.refreshToken = refreshToken
|
||||
TOKEN.expiresAfter = 0
|
||||
TOKEN.countryCode = TIDAL_API.key.countryCode
|
||||
TOKEN.save()
|
||||
180
TIDALDL-PY/tidal_dl/gui.py
Normal file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : test.py
|
||||
@Date : 2022/03/28
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import _thread
|
||||
|
||||
from tidal_dl.events import *
|
||||
from tidal_dl.settings import *
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5 import QtWidgets
|
||||
from qt_material import apply_stylesheet
|
||||
|
||||
|
||||
class MainView(QtWidgets.QWidget):
|
||||
s_downloadEnd = pyqtSignal(str, bool, str)
|
||||
|
||||
def __init__(self, ) -> None:
|
||||
super().__init__()
|
||||
self.initView()
|
||||
self.setMinimumSize(600, 500)
|
||||
self.setWindowTitle("Tidal-dl")
|
||||
|
||||
def __info__(self, msg):
|
||||
QtWidgets.QMessageBox.information(self,
|
||||
'Info',
|
||||
msg,
|
||||
QtWidgets.QMessageBox.Yes)
|
||||
|
||||
def initView(self):
|
||||
self.c_lineSearch = QtWidgets.QLineEdit()
|
||||
self.c_btnSearch = QtWidgets.QPushButton("Search")
|
||||
self.c_btnDownload = QtWidgets.QPushButton("Download")
|
||||
|
||||
self.m_supportType = [Type.Album, Type.Playlist, Type.Track, Type.Video]
|
||||
self.c_combType = QtWidgets.QComboBox()
|
||||
for item in self.m_supportType:
|
||||
self.c_combType.addItem(item.name, item)
|
||||
|
||||
columnNames = ['#', 'Title', 'Artists', 'Quality']
|
||||
self.c_tableInfo = QtWidgets.QTableWidget()
|
||||
self.c_tableInfo.setColumnCount(len(columnNames))
|
||||
self.c_tableInfo.setRowCount(0)
|
||||
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.horizontalHeader().setStretchLastSection(True)
|
||||
self.c_tableInfo.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
|
||||
self.c_tableInfo.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
self.c_tableInfo.setFocusPolicy(Qt.NoFocus)
|
||||
for index, name in enumerate(columnNames):
|
||||
item = QtWidgets.QTableWidgetItem(name)
|
||||
self.c_tableInfo.setHorizontalHeaderItem(index, item)
|
||||
|
||||
self.lineGrid = QtWidgets.QHBoxLayout()
|
||||
self.lineGrid.addWidget(self.c_combType)
|
||||
self.lineGrid.addWidget(self.c_lineSearch)
|
||||
self.lineGrid.addWidget(self.c_btnSearch)
|
||||
|
||||
self.mainGrid = QtWidgets.QVBoxLayout(self)
|
||||
self.mainGrid.addLayout(self.lineGrid)
|
||||
self.mainGrid.addWidget(self.c_tableInfo)
|
||||
self.mainGrid.addWidget(self.c_btnDownload)
|
||||
|
||||
self.c_btnSearch.clicked.connect(self.search)
|
||||
self.c_btnDownload.clicked.connect(self.download)
|
||||
self.s_downloadEnd.connect(self.downloadEnd)
|
||||
|
||||
def addItem(self, rowIdx: int, colIdx: int, text):
|
||||
if isinstance(text, str):
|
||||
item = QtWidgets.QTableWidgetItem(text)
|
||||
self.c_tableInfo.setItem(rowIdx, colIdx, item)
|
||||
|
||||
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:
|
||||
self.__info__('Url not support!')
|
||||
return
|
||||
elif tmpType not in self.m_supportType:
|
||||
self.__info__(f'Type[{tmpType.name}] not support!')
|
||||
return
|
||||
|
||||
tmpData = TIDAL_API.getTypeData(tmpId, tmpType)
|
||||
if tmpData is None:
|
||||
self.__info__('Url is wrong!')
|
||||
return
|
||||
self.s_type = tmpType
|
||||
self.s_array = [tmpData]
|
||||
self.s_result = None
|
||||
self.c_combType.setCurrentText(tmpType.name)
|
||||
else:
|
||||
self.s_result = TIDAL_API.search(self.s_text, self.s_type)
|
||||
self.s_array = TIDAL_API.getSearchResultItems(self.s_result, self.s_type)
|
||||
|
||||
if len(self.s_array) <= 0:
|
||||
self.__info__('No result!')
|
||||
return
|
||||
|
||||
self.c_tableInfo.setRowCount(len(self.s_array))
|
||||
for index, item in enumerate(self.s_array):
|
||||
self.addItem(index, 0, str(index + 1))
|
||||
if self.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]:
|
||||
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]:
|
||||
self.addItem(index, 1, item.title)
|
||||
self.addItem(index, 2, '')
|
||||
self.addItem(index, 3, '')
|
||||
|
||||
def download(self):
|
||||
index = self.c_tableInfo.currentIndex().row()
|
||||
if index < 0:
|
||||
self.__info__('Please select a row first.')
|
||||
return
|
||||
|
||||
self.c_btnDownload.setEnabled(False)
|
||||
self.c_btnDownload.setText(f"Downloading [{self.s_array[index].title}]...")
|
||||
|
||||
def __thread_download__(model: MainView):
|
||||
try:
|
||||
type = model.s_type
|
||||
item = model.s_array[index]
|
||||
start_type(type, item)
|
||||
model.s_downloadEnd.emit(item.title, True, '')
|
||||
except Exception as e:
|
||||
model.s_downloadEnd.emit(item.title, False, str(e))
|
||||
|
||||
_thread.start_new_thread(__thread_download__, (self, ))
|
||||
|
||||
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')
|
||||
else:
|
||||
self.__info__(f'Download [{title}] failed:{msg}')
|
||||
|
||||
def checkLogin(self):
|
||||
if not loginByConfig():
|
||||
self.__info__('Login failed. Please log in using the command line first.')
|
||||
|
||||
|
||||
def startGui():
|
||||
os.chdir(sys.path[0])
|
||||
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
apply_stylesheet(app, theme='dark_blue.xml')
|
||||
|
||||
window = MainView()
|
||||
window.show()
|
||||
window.checkLogin()
|
||||
|
||||
app.exec_()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
SETTINGS.read(getProfilePath())
|
||||
TOKEN.read(getTokenPath())
|
||||
startGui()
|
||||
@@ -31,12 +31,7 @@ from tidal_dl.lang.vietnamese import LangVietnamese
|
||||
from tidal_dl.lang.korean import LangKorean
|
||||
from tidal_dl.lang.japanese import LangJapanese
|
||||
|
||||
LANG = None
|
||||
|
||||
|
||||
def initLang(index): # 初始化
|
||||
global LANG
|
||||
return setLang(index)
|
||||
LANG = LangEnglish()
|
||||
|
||||
|
||||
def setLang(index):
|
||||
@@ -88,11 +83,6 @@ def setLang(index):
|
||||
return LANG
|
||||
|
||||
|
||||
def getLang():
|
||||
global LANG
|
||||
return LANG
|
||||
|
||||
|
||||
def getLangName(index):
|
||||
if str(index) == '0':
|
||||
return "English"
|
||||
|
||||
@@ -1,201 +0,0 @@
|
||||
|
||||
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : m3u8dl.py
|
||||
@Date : 2021/11/01
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact :
|
||||
@Desc :
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import queue
|
||||
import base64
|
||||
import platform
|
||||
import requests
|
||||
import urllib3
|
||||
import aigpy
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
|
||||
class ThreadPoolExecutorWithQueueSizeLimit(ThreadPoolExecutor):
|
||||
def __init__(self, max_workers=None, *args, **kwargs):
|
||||
super().__init__(max_workers, *args, **kwargs)
|
||||
self._work_queue = queue.Queue(max_workers * 2)
|
||||
|
||||
|
||||
def make_sum():
|
||||
ts_num = 0
|
||||
while True:
|
||||
yield ts_num
|
||||
ts_num += 1
|
||||
|
||||
|
||||
class M3u8Download:
|
||||
def __init__(self, url, name, file_path, max_workers=64, num_retries=5, base64_key=None):
|
||||
self._url = url
|
||||
self._name = name
|
||||
self._max_workers = max_workers
|
||||
self._num_retries = num_retries
|
||||
self._file_path = file_path
|
||||
self._front_url = None
|
||||
self._ts_url_list = []
|
||||
self._success_sum = 0
|
||||
self._ts_sum = 0
|
||||
self._key = base64.b64decode(base64_key.encode()) if base64_key else None
|
||||
self._headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) \
|
||||
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.105 Safari/537.36'}
|
||||
self._part_file_paths = []
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
|
||||
def start(self):
|
||||
dir_path = aigpy.path.getDirName(self._file_path)
|
||||
aigpy.path.mkdirs(dir_path)
|
||||
|
||||
self.get_m3u8_info(self._url, self._num_retries)
|
||||
with ThreadPoolExecutorWithQueueSizeLimit(self._max_workers) as pool:
|
||||
for k, ts_url in enumerate(self._ts_url_list):
|
||||
file_path = os.path.join(self._file_path, str(k))
|
||||
self._part_file_paths.append(file_path)
|
||||
pool.submit(self.download_ts, ts_url, file_path, self._num_retries)
|
||||
if self._success_sum == self._ts_sum:
|
||||
self.output_ts()
|
||||
# self.output_mp4()
|
||||
self.delete_file()
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_m3u8_info(self, m3u8_url, num_retries):
|
||||
|
||||
try:
|
||||
with requests.get(m3u8_url, timeout=(3, 30), verify=False, headers=self._headers) as res:
|
||||
self._front_url = res.url.split(res.request.path_url)[0]
|
||||
if "EXT-X-STREAM-INF" in res.text:
|
||||
for line in res.text.split('\n'):
|
||||
if "#" in line:
|
||||
continue
|
||||
elif line.startswith('http'):
|
||||
self._url = line
|
||||
elif line.startswith('/'):
|
||||
self._url = self._front_url + line
|
||||
else:
|
||||
self._url = self._url.rsplit("/", 1)[0] + '/' + line
|
||||
self.get_m3u8_info(self._url, self._num_retries)
|
||||
else:
|
||||
m3u8_text_str = res.text
|
||||
self.get_ts_url(m3u8_text_str)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
if num_retries > 0:
|
||||
self.get_m3u8_info(m3u8_url, num_retries - 1)
|
||||
|
||||
def get_ts_url(self, m3u8_text_str):
|
||||
if not os.path.exists(self._file_path):
|
||||
os.mkdir(self._file_path)
|
||||
new_m3u8_str = ''
|
||||
ts = make_sum()
|
||||
for line in m3u8_text_str.split('\n'):
|
||||
if "#" in line:
|
||||
if "EXT-X-KEY" in line and "URI=" in line:
|
||||
if os.path.exists(os.path.join(self._file_path, 'key')):
|
||||
continue
|
||||
key = self.download_key(line, 5)
|
||||
if key:
|
||||
new_m3u8_str += f'{key}\n'
|
||||
continue
|
||||
new_m3u8_str += f'{line}\n'
|
||||
if "EXT-X-ENDLIST" in line:
|
||||
break
|
||||
else:
|
||||
if line.startswith('http'):
|
||||
self._ts_url_list.append(line)
|
||||
elif line.startswith('/'):
|
||||
self._ts_url_list.append(self._front_url + line)
|
||||
else:
|
||||
self._ts_url_list.append(self._url.rsplit("/", 1)[0] + '/' + line)
|
||||
new_m3u8_str += (os.path.join(self._file_path, str(next(ts))) + '\n')
|
||||
self._ts_sum = next(ts)
|
||||
with open(self._file_path + '.m3u8', "wb") as f:
|
||||
if platform.system() == 'Windows':
|
||||
f.write(new_m3u8_str.encode('gbk'))
|
||||
else:
|
||||
f.write(new_m3u8_str.encode('utf-8'))
|
||||
|
||||
def download_ts(self, ts_url, name, num_retries):
|
||||
ts_url = ts_url.split('\n')[0]
|
||||
try:
|
||||
if not os.path.exists(name):
|
||||
with requests.get(ts_url, stream=True, timeout=(5, 60), verify=False, headers=self._headers) as res:
|
||||
if res.status_code == 200:
|
||||
with open(name, "wb") as ts:
|
||||
for chunk in res.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
ts.write(chunk)
|
||||
self._success_sum += 1
|
||||
sys.stdout.write('\r[%-25s](%d/%d)' % ("*" * (100 * self._success_sum // self._ts_sum // 4),
|
||||
self._success_sum, self._ts_sum))
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
self.download_ts(ts_url, name, num_retries - 1)
|
||||
else:
|
||||
self._success_sum += 1
|
||||
except Exception:
|
||||
if os.path.exists(name):
|
||||
os.remove(name)
|
||||
if num_retries > 0:
|
||||
self.download_ts(ts_url, name, num_retries - 1)
|
||||
|
||||
def download_key(self, key_line, num_retries):
|
||||
mid_part = re.search(r"URI=[\'|\"].*?[\'|\"]", key_line).group()
|
||||
may_key_url = mid_part[5:-1]
|
||||
if self._key:
|
||||
with open(os.path.join(self._file_path, 'key'), 'wb') as f:
|
||||
f.write(self._key)
|
||||
return f'{key_line.split(mid_part)[0]}URI="./{self._name}/key"'
|
||||
if may_key_url.startswith('http'):
|
||||
true_key_url = may_key_url
|
||||
elif may_key_url.startswith('/'):
|
||||
true_key_url = self._front_url + may_key_url
|
||||
else:
|
||||
true_key_url = self._url.rsplit("/", 1)[0] + '/' + may_key_url
|
||||
try:
|
||||
with requests.get(true_key_url, timeout=(5, 30), verify=False, headers=self._headers) as res:
|
||||
with open(os.path.join(self._file_path, 'key'), 'wb') as f:
|
||||
f.write(res.content)
|
||||
return f'{key_line.split(mid_part)[0]}URI="./{self._name}/key"{key_line.split(mid_part)[-1]}'
|
||||
except Exception as e:
|
||||
print(e)
|
||||
if os.path.exists(os.path.join(self._file_path, 'key')):
|
||||
os.remove(os.path.join(self._file_path, 'key'))
|
||||
print("加密视频,无法加载key,揭秘失败")
|
||||
if num_retries > 0:
|
||||
self.download_key(key_line, num_retries - 1)
|
||||
|
||||
def output_mp4(self):
|
||||
cmd = f"ffmpeg -allowed_extensions ALL -i '{self._file_path}.m3u8' -acodec \
|
||||
copy -vcodec copy -f mp4 '{self._file_path}.mp4'"
|
||||
os.system(cmd)
|
||||
|
||||
def output_ts(self):
|
||||
with open(f'{self._file_path}.ts', 'wb') as output:
|
||||
for item_path in self._part_file_paths:
|
||||
content = aigpy.file.getContent(item_path, True)
|
||||
output.write(content)
|
||||
|
||||
def delete_file(self):
|
||||
file = os.listdir(self._file_path)
|
||||
for item in file:
|
||||
os.remove(os.path.join(self._file_path, item))
|
||||
os.removedirs(self._file_path)
|
||||
os.remove(self._file_path + '.m3u8')
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -4,14 +4,13 @@
|
||||
@File : model.py
|
||||
@Time : 2020/08/08
|
||||
@Author : Yaronzz
|
||||
@Version : 2.0
|
||||
@Version : 3.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
'''
|
||||
from aigpy.modelHelper import ModelBase
|
||||
import aigpy
|
||||
|
||||
|
||||
class StreamUrl(ModelBase):
|
||||
class StreamUrl(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.trackid = None
|
||||
@@ -21,7 +20,7 @@ class StreamUrl(ModelBase):
|
||||
self.soundQuality = None
|
||||
|
||||
|
||||
class VideoStreamUrl(ModelBase):
|
||||
class VideoStreamUrl(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.codec = None
|
||||
@@ -30,7 +29,7 @@ class VideoStreamUrl(ModelBase):
|
||||
self.m3u8Url = None
|
||||
|
||||
|
||||
class Artist(ModelBase):
|
||||
class Artist(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.id = None
|
||||
@@ -39,7 +38,7 @@ class Artist(ModelBase):
|
||||
self.picture = None
|
||||
|
||||
|
||||
class Album(ModelBase):
|
||||
class Album(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.id = None
|
||||
@@ -59,7 +58,7 @@ class Album(ModelBase):
|
||||
self.artists = Artist()
|
||||
|
||||
|
||||
class Playlist(ModelBase):
|
||||
class Playlist(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.uuid = None
|
||||
@@ -72,7 +71,7 @@ class Playlist(ModelBase):
|
||||
self.squareImage = None
|
||||
|
||||
|
||||
class Track(ModelBase):
|
||||
class Track(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.id = None
|
||||
@@ -93,7 +92,7 @@ class Track(ModelBase):
|
||||
self.playlist = None
|
||||
|
||||
|
||||
class Video(ModelBase):
|
||||
class Video(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.id = None
|
||||
@@ -112,7 +111,7 @@ class Video(ModelBase):
|
||||
self.playlist = None
|
||||
|
||||
|
||||
class Mix(ModelBase):
|
||||
class Mix(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.id = None
|
||||
@@ -120,7 +119,7 @@ class Mix(ModelBase):
|
||||
self.videos = Video()
|
||||
|
||||
|
||||
class Lyrics(ModelBase):
|
||||
class Lyrics(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.trackId = None
|
||||
@@ -131,7 +130,7 @@ class Lyrics(ModelBase):
|
||||
self.subtitles = None
|
||||
|
||||
|
||||
class SearchDataBase(ModelBase):
|
||||
class SearchDataBase(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.limit = 0
|
||||
@@ -169,7 +168,7 @@ class SearchPlaylists(SearchDataBase):
|
||||
self.items = Playlist()
|
||||
|
||||
|
||||
class SearchResult(ModelBase):
|
||||
class SearchResult(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.artists = SearchArtists()
|
||||
@@ -178,3 +177,31 @@ class SearchResult(ModelBase):
|
||||
self.videos = SearchVideos()
|
||||
self.playlists = SearchPlaylists()
|
||||
|
||||
|
||||
class LoginKey(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.deviceCode = None
|
||||
self.userCode = None
|
||||
self.verificationUrl = None
|
||||
self.authCheckTimeout = None
|
||||
self.authCheckInterval = None
|
||||
self.userId = None
|
||||
self.countryCode = None
|
||||
self.accessToken = None
|
||||
self.refreshToken = None
|
||||
self.expiresIn = None
|
||||
|
||||
|
||||
class StreamRespond(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.trackid = None
|
||||
self.videoid = None
|
||||
self.streamType = None
|
||||
self.assetPresentation = None
|
||||
self.audioMode = None
|
||||
self.audioQuality = None
|
||||
self.videoQuality = None
|
||||
self.manifestMimeType = None
|
||||
self.manifest = None
|
||||
|
||||
192
TIDALDL-PY/tidal_dl/paths.py
Normal file
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : paths.py
|
||||
@Date : 2022/06/10
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
import os
|
||||
import aigpy
|
||||
import datetime
|
||||
|
||||
from tidal_dl.tidal import *
|
||||
from tidal_dl.settings import *
|
||||
|
||||
|
||||
def __fixPath__(name: str):
|
||||
return aigpy.path.replaceLimitChar(name, '-').strip()
|
||||
|
||||
|
||||
def __getYear__(releaseDate: str):
|
||||
if releaseDate is None or releaseDate == '':
|
||||
return ''
|
||||
return aigpy.string.getSubOnlyEnd(releaseDate, '-')
|
||||
|
||||
|
||||
def __getDurationStr__(seconds):
|
||||
time_string = str(datetime.timedelta(seconds=seconds))
|
||||
if time_string.startswith('0:'):
|
||||
time_string = time_string[2:]
|
||||
return time_string
|
||||
|
||||
|
||||
def __getExtension__(stream: StreamUrl):
|
||||
if '.flac' in stream.url:
|
||||
return '.flac'
|
||||
if '.mp4' in stream.url:
|
||||
if 'ac4' in stream.codec or 'mha1' in stream.codec:
|
||||
return '.mp4'
|
||||
return '.m4a'
|
||||
return '.m4a'
|
||||
|
||||
|
||||
def getAlbumPath(album):
|
||||
artistName = __fixPath__(TIDAL_API.getArtistsName(album.artists))
|
||||
albumArtistName = __fixPath__(album.artist.name) if album.artist is not None else ""
|
||||
|
||||
# album folder pre: [ME]
|
||||
flag = TIDAL_API.getFlag(album, Type.Album, True, "")
|
||||
if SETTINGS.audioQuality != AudioQuality.Master:
|
||||
flag = flag.replace("M", "")
|
||||
if flag != "":
|
||||
flag = "[" + flag + "] "
|
||||
|
||||
# album and addyear
|
||||
albumName = __fixPath__(album.title)
|
||||
year = __getYear__(album.releaseDate)
|
||||
|
||||
# retpath
|
||||
retpath = SETTINGS.albumFolderFormat
|
||||
if retpath is None or len(retpath) <= 0:
|
||||
retpath = SETTINGS.getDefaultAlbumFolderFormat()
|
||||
retpath = retpath.replace(R"{ArtistName}", artistName)
|
||||
retpath = retpath.replace(R"{AlbumArtistName}", albumArtistName)
|
||||
retpath = retpath.replace(R"{Flag}", flag)
|
||||
retpath = retpath.replace(R"{AlbumID}", str(album.id))
|
||||
retpath = retpath.replace(R"{AlbumYear}", year)
|
||||
retpath = retpath.replace(R"{AlbumTitle}", albumName)
|
||||
retpath = retpath.replace(R"{AudioQuality}", album.audioQuality)
|
||||
retpath = retpath.replace(R"{DurationSeconds}", str(album.duration))
|
||||
retpath = retpath.replace(R"{Duration}", __getDurationStr__(album.duration))
|
||||
retpath = retpath.replace(R"{NumberOfTracks}", str(album.numberOfTracks))
|
||||
retpath = retpath.replace(R"{NumberOfVideos}", str(album.numberOfVideos))
|
||||
retpath = retpath.replace(R"{NumberOfVolumes}", str(album.numberOfVolumes))
|
||||
retpath = retpath.replace(R"{ReleaseDate}", str(album.releaseDate))
|
||||
retpath = retpath.replace(R"{RecordType}", album.type)
|
||||
retpath = retpath.replace(R"{None}", "")
|
||||
retpath = retpath.strip()
|
||||
return f"{SETTINGS.downloadPath}/{retpath}"
|
||||
|
||||
|
||||
def getPlaylistPath(playlist):
|
||||
# name
|
||||
name = __fixPath__(playlist.title)
|
||||
return f"{SETTINGS.downloadPath}/Playlist/{name}"
|
||||
|
||||
|
||||
def getTrackPath(track, stream, album=None, playlist=None):
|
||||
base = './'
|
||||
number = str(track.trackNumber).rjust(2, '0')
|
||||
if album is not None:
|
||||
base = getAlbumPath(album)
|
||||
if album.numberOfVolumes > 1:
|
||||
base += f'/CD{str(track.volumeNumber)}'
|
||||
|
||||
if playlist is not None and SETTINGS.usePlaylistFolder:
|
||||
base = getPlaylistPath(playlist)
|
||||
number = str(track.trackNumberOnPlaylist).rjust(2, '0')
|
||||
|
||||
# artist
|
||||
artists = __fixPath__(TIDAL_API.getArtistsName(track.artists))
|
||||
artist = __fixPath__(track.artist.name) if track.artist is not None else ""
|
||||
|
||||
# title
|
||||
title = __fixPath__(track.title)
|
||||
if not aigpy.string.isNull(track.version):
|
||||
title += f' ({__fixPath__(track.version)})'
|
||||
|
||||
# explicit
|
||||
explicit = "(Explicit)" if track.explicit else ''
|
||||
|
||||
# album and addyear
|
||||
albumName = __fixPath__(album.title)
|
||||
year = __getYear__(album.releaseDate)
|
||||
|
||||
# extension
|
||||
extension = __getExtension__(stream)
|
||||
|
||||
retpath = SETTINGS.trackFileFormat
|
||||
if retpath is None or len(retpath) <= 0:
|
||||
retpath = SETTINGS.getDefaultTrackFileFormat()
|
||||
retpath = retpath.replace(R"{TrackNumber}", number)
|
||||
retpath = retpath.replace(R"{ArtistName}", artist)
|
||||
retpath = retpath.replace(R"{ArtistsName}", artists)
|
||||
retpath = retpath.replace(R"{TrackTitle}", title)
|
||||
retpath = retpath.replace(R"{ExplicitFlag}", explicit)
|
||||
retpath = retpath.replace(R"{AlbumYear}", year)
|
||||
retpath = retpath.replace(R"{AlbumTitle}", albumName)
|
||||
retpath = retpath.replace(R"{AudioQuality}", track.audioQuality)
|
||||
retpath = retpath.replace(R"{DurationSeconds}", str(track.duration))
|
||||
retpath = retpath.replace(R"{Duration}", __getDurationStr__(track.duration))
|
||||
retpath = retpath.replace(R"{TrackID}", str(track.id))
|
||||
retpath = retpath.strip()
|
||||
return f"{base}/{retpath}{extension}"
|
||||
|
||||
|
||||
def getVideoPath(video, album=None, playlist=None):
|
||||
base = SETTINGS.downloadPath + '/Video/'
|
||||
if album is not None and album.title is not None:
|
||||
base = getAlbumPath(album)
|
||||
elif playlist is not None:
|
||||
base = getPlaylistPath(playlist)
|
||||
|
||||
# get number
|
||||
number = str(video.trackNumber).rjust(2, '0')
|
||||
|
||||
# get artist
|
||||
artists = __fixPath__(TIDAL_API.getArtistsName(video.artists))
|
||||
artist = __fixPath__(video.artist.name) if video.artist is not None else ""
|
||||
|
||||
# explicit
|
||||
explicit = "(Explicit)" if video.explicit else ''
|
||||
|
||||
# title and year and extension
|
||||
title = __fixPath__(video.title)
|
||||
year = __getYear__(video.releaseDate)
|
||||
extension = ".mp4"
|
||||
|
||||
retpath = SETTINGS.videoFileFormat
|
||||
if retpath is None or len(retpath) <= 0:
|
||||
retpath = SETTINGS.getDefaultVideoFileFormat()
|
||||
retpath = retpath.replace(R"{VideoNumber}", number)
|
||||
retpath = retpath.replace(R"{ArtistName}", artist)
|
||||
retpath = retpath.replace(R"{ArtistsName}", artists)
|
||||
retpath = retpath.replace(R"{VideoTitle}", title)
|
||||
retpath = retpath.replace(R"{ExplicitFlag}", explicit)
|
||||
retpath = retpath.replace(R"{VideoYear}", year)
|
||||
retpath = retpath.replace(R"{VideoID}", str(video.id))
|
||||
retpath = retpath.strip()
|
||||
return f"{base}/{retpath}{extension}"
|
||||
|
||||
|
||||
def __getHomePath__():
|
||||
if "XDG_CONFIG_HOME" in os.environ:
|
||||
return os.environ['XDG_CONFIG_HOME']
|
||||
elif "HOME" in os.environ:
|
||||
return os.environ['HOME']
|
||||
elif "HOMEDRIVE" in os.environ and "HOMEPATH" in os.environ:
|
||||
return os.environ['HOMEDRIVE'] + os.environ['HOMEPATH']
|
||||
else:
|
||||
return os.path.abspath("./")
|
||||
|
||||
def getLogPath():
|
||||
return __getHomePath__() + '/.tidal-dl.log'
|
||||
|
||||
def getTokenPath():
|
||||
return __getHomePath__() + '/.tidal-dl.token.json'
|
||||
|
||||
def getProfilePath():
|
||||
return __getHomePath__() + '/.tidal-dl.json'
|
||||
@@ -4,21 +4,24 @@
|
||||
@File : printf.py
|
||||
@Time : 2020/08/16
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Version : 3.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
'''
|
||||
import logging
|
||||
|
||||
import aigpy
|
||||
import logging
|
||||
import prettytable
|
||||
from tidal_dl.lang.language import getLangName, getLang
|
||||
from tidal_dl.model import Album, Track, Video, Artist, StreamUrl, VideoStreamUrl
|
||||
from tidal_dl.settings import Settings, getSettingsPath
|
||||
|
||||
import tidal_dl.apiKey as apiKey
|
||||
|
||||
from tidal_dl.model import *
|
||||
from tidal_dl.paths import *
|
||||
from tidal_dl.settings import *
|
||||
from tidal_dl.lang.language import *
|
||||
|
||||
__LOGO__ = '''
|
||||
|
||||
VERSION = '2022.06.20.1'
|
||||
__LOGO__ = f'''
|
||||
/$$$$$$$$ /$$ /$$ /$$ /$$ /$$
|
||||
|__ $$__/|__/ | $$ | $$ | $$| $$
|
||||
| $$ /$$ /$$$$$$$ /$$$$$$ | $$ /$$$$$$$| $$
|
||||
@@ -29,87 +32,94 @@ __LOGO__ = '''
|
||||
|__/ |__/ \_______/ \_______/|__/ \_______/|__/
|
||||
|
||||
https://github.com/yaronzz/Tidal-Media-Downloader
|
||||
|
||||
{VERSION}
|
||||
'''
|
||||
VERSION = '2022.06.17.2'
|
||||
|
||||
|
||||
|
||||
|
||||
class Printf(object):
|
||||
|
||||
@staticmethod
|
||||
def logo():
|
||||
string = __LOGO__ + '\n v' + VERSION
|
||||
print(string)
|
||||
logging.info(string)
|
||||
print(__LOGO__)
|
||||
logging.info(__LOGO__)
|
||||
|
||||
@staticmethod
|
||||
def __gettable__(columns, rows):
|
||||
tb = prettytable.PrettyTable()
|
||||
tb.field_names = list(aigpy.cmd.green(item) for item in columns)
|
||||
tb.align = 'l'
|
||||
for item in rows:
|
||||
tb.add_row(item)
|
||||
return tb
|
||||
|
||||
@staticmethod
|
||||
def usage():
|
||||
print("=============TIDAL-DL HELP==============")
|
||||
tb = prettytable.PrettyTable()
|
||||
tb.field_names = [aigpy.cmd.green("OPTION"), aigpy.cmd.green("DESC")]
|
||||
tb.align = 'l'
|
||||
tb.add_row(["-h or --help", "show help-message"])
|
||||
tb.add_row(["-v or --version", "show version"])
|
||||
tb.add_row(["-o or --output", "download path"])
|
||||
tb.add_row(["-l or --link", "url/id/filePath"])
|
||||
tb.add_row(["-q or --quality", "track quality('Normal','High,'HiFi','Master')"])
|
||||
tb.add_row(["-r or --resolution", "video resolution('P1080', 'P720', 'P480', 'P360')"])
|
||||
# tb.add_row(["-u or --username", "account-email"])
|
||||
# tb.add_row(["-p or --password", "account-password"])
|
||||
# tb.add_row(["-a or --accessToken", "account-accessToken"])
|
||||
tb = Printf.__gettable__(["OPTION", "DESC"], [
|
||||
["-h or --help", "show help-message"],
|
||||
["-v or --version", "show version"],
|
||||
["-o or --output", "download path"],
|
||||
["-l or --link", "url/id/filePath"],
|
||||
["-q or --quality", "track quality('Normal','High,'HiFi','Master')"],
|
||||
["-r or --resolution", "video resolution('P1080', 'P720', 'P480', 'P360')"]
|
||||
])
|
||||
print(tb)
|
||||
|
||||
@staticmethod
|
||||
def checkVersion():
|
||||
onlineVer = aigpy.pip.getLastVersion('tidal-dl')
|
||||
if onlineVer is None:
|
||||
icmp = aigpy.system.cmpVersion(onlineVer, VERSION)
|
||||
if icmp > 0:
|
||||
Printf.info(LANG.PRINT_LATEST_VERSION + ' ' + onlineVer)
|
||||
|
||||
@staticmethod
|
||||
def settings(data: Settings):
|
||||
LANG = getLang()
|
||||
tb = prettytable.PrettyTable()
|
||||
tb.field_names = [aigpy.cmd.green(LANG.SETTING), aigpy.cmd.green(LANG.VALUE)]
|
||||
tb.align = 'l'
|
||||
# tb.add_row(["Settings path", getSettingsPath()])
|
||||
tb.add_row([LANG.SETTING_PATH, getSettingsPath()])
|
||||
tb.add_row([LANG.SETTING_DOWNLOAD_PATH, data.downloadPath])
|
||||
tb.add_row([LANG.SETTING_ONLY_M4A, data.onlyM4a])
|
||||
# tb.add_row([LANG.SETTING_ADD_EXPLICIT_TAG, data.addExplicitTag])
|
||||
# tb.add_row([LANG.SETTING_ADD_HYPHEN, data.addHyphen])
|
||||
# tb.add_row([LANG.SETTING_ADD_YEAR, data.addYear])
|
||||
# tb.add_row([LANG.SETTING_USE_TRACK_NUM, data.useTrackNumber])
|
||||
tb.add_row([LANG.SETTING_AUDIO_QUALITY, data.audioQuality])
|
||||
tb.add_row([LANG.SETTING_VIDEO_QUALITY, data.videoQuality])
|
||||
tb.add_row([LANG.SETTING_CHECK_EXIST, data.checkExist])
|
||||
tb.add_row([LANG.SETTING_SHOW_PROGRESS, data.showProgress])
|
||||
tb.add_row([LANG.SETTING_SAVE_ALBUMINFO, data.saveAlbumInfo])
|
||||
tb.add_row([LANG.SETTING_SHOW_TRACKINFO, data.showTrackInfo])
|
||||
# tb.add_row([LANG.SETTING_ARTIST_BEFORE_TITLE, data.artistBeforeTitle])
|
||||
# tb.add_row([LANG.SETTING_ALBUMID_BEFORE_FOLDER, data.addAlbumIDBeforeFolder])
|
||||
tb.add_row([LANG.SETTING_INCLUDE_EP, data.includeEP])
|
||||
tb.add_row([LANG.SETTING_SAVE_COVERS, data.saveCovers])
|
||||
tb.add_row([LANG.SETTING_LANGUAGE, getLangName(data.language)])
|
||||
tb.add_row([LANG.SETTING_USE_PLAYLIST_FOLDER, data.usePlaylistFolder])
|
||||
tb.add_row([LANG.SETTING_MULITHREAD_DOWNLOAD, data.multiThreadDownload])
|
||||
tb.add_row([LANG.SETTING_ALBUM_FOLDER_FORMAT, data.albumFolderFormat])
|
||||
tb.add_row([LANG.SETTING_TRACK_FILE_FORMAT, data.trackFileFormat])
|
||||
tb.add_row([LANG.SETTING_VIDEO_FILE_FORMAT, data.videoFileFormat])
|
||||
tb.add_row([LANG.SETTING_ADD_LYRICS, data.addLyrics])
|
||||
tb.add_row([LANG.SETTING_LYRICS_SERVER_PROXY, data.lyricsServerProxy])
|
||||
tb.add_row([LANG.SETTINGS_ADD_LRC_FILE, data.lyricFile])
|
||||
tb.add_row([LANG.SETTING_ADD_TYPE_FOLDER, data.addTypeFolder])
|
||||
tb.add_row([LANG.SETTING_APIKEY, apiKey.getItem(data.apiKeyIndex)['formats']])
|
||||
def settings():
|
||||
data = SETTINGS
|
||||
tb = Printf.__gettable__([LANG.SETTING, LANG.VALUE], [
|
||||
#settings - path and format
|
||||
[LANG.SETTING_PATH, getProfilePath()],
|
||||
[LANG.SETTING_DOWNLOAD_PATH, data.downloadPath],
|
||||
[LANG.SETTING_ALBUM_FOLDER_FORMAT, data.albumFolderFormat],
|
||||
[LANG.SETTING_TRACK_FILE_FORMAT, data.trackFileFormat],
|
||||
[LANG.SETTING_VIDEO_FILE_FORMAT, data.videoFileFormat],
|
||||
|
||||
#settings - quality
|
||||
[LANG.SETTING_AUDIO_QUALITY, data.audioQuality],
|
||||
[LANG.SETTING_VIDEO_QUALITY, data.videoQuality],
|
||||
|
||||
#settings - else
|
||||
[LANG.SETTING_USE_PLAYLIST_FOLDER, data.usePlaylistFolder],
|
||||
[LANG.SETTING_CHECK_EXIST, data.checkExist],
|
||||
[LANG.SETTING_SHOW_PROGRESS, data.showProgress],
|
||||
[LANG.SETTING_SHOW_TRACKINFO, data.showTrackInfo],
|
||||
[LANG.SETTING_SAVE_ALBUMINFO, data.saveAlbumInfo],
|
||||
[LANG.SETTING_SAVE_COVERS, data.saveCovers],
|
||||
[LANG.SETTING_INCLUDE_EP, data.includeEP],
|
||||
[LANG.SETTING_LANGUAGE, getLangName(data.language)],
|
||||
[LANG.SETTINGS_ADD_LRC_FILE, data.lyricFile],
|
||||
[LANG.SETTING_APIKEY, f"[{data.apiKeyIndex}]" + apiKey.getItem(data.apiKeyIndex)['formats']]
|
||||
])
|
||||
print(tb)
|
||||
|
||||
@staticmethod
|
||||
def choices():
|
||||
LANG = getLang()
|
||||
print("====================================================")
|
||||
tb = prettytable.PrettyTable()
|
||||
tb.field_names = [LANG.CHOICE, LANG.FUNCTION]
|
||||
tb.align = 'l'
|
||||
tb = Printf.__gettable__([LANG.CHOICE, LANG.FUNCTION], [
|
||||
[aigpy.cmd.green(LANG.CHOICE_ENTER + " '0':"), LANG.CHOICE_EXIT],
|
||||
[aigpy.cmd.green(LANG.CHOICE_ENTER + " '1':"), LANG.CHOICE_LOGIN],
|
||||
[aigpy.cmd.green(LANG.CHOICE_ENTER + " '2':"), LANG.CHOICE_LOGOUT],
|
||||
[aigpy.cmd.green(LANG.CHOICE_ENTER + " '3':"), LANG.CHOICE_SET_ACCESS_TOKEN],
|
||||
[aigpy.cmd.green(LANG.CHOICE_ENTER + " '4':"), LANG.CHOICE_SETTINGS + '-Path'],
|
||||
[aigpy.cmd.green(LANG.CHOICE_ENTER + " '5':"), LANG.CHOICE_SETTINGS + '-Quality'],
|
||||
[aigpy.cmd.green(LANG.CHOICE_ENTER + " '6':"), LANG.CHOICE_SETTINGS + '-Else'],
|
||||
[aigpy.cmd.green(LANG.CHOICE_ENTER + " '7':"), LANG.CHOICE_APIKEY],
|
||||
[aigpy.cmd.green(LANG.CHOICE_ENTER_URLID), LANG.CHOICE_DOWNLOAD_BY_URL],
|
||||
])
|
||||
tb.set_style(prettytable.PLAIN_COLUMNS)
|
||||
tb.add_row([aigpy.cmd.green(LANG.CHOICE_ENTER + " '0':"), LANG.CHOICE_EXIT])
|
||||
tb.add_row([aigpy.cmd.green(LANG.CHOICE_ENTER + " '1':"), LANG.CHOICE_LOGIN])
|
||||
tb.add_row([aigpy.cmd.green(LANG.CHOICE_ENTER + " '2':"), LANG.CHOICE_SETTINGS])
|
||||
tb.add_row([aigpy.cmd.green(LANG.CHOICE_ENTER + " '3':"), LANG.CHOICE_LOGOUT])
|
||||
tb.add_row([aigpy.cmd.green(LANG.CHOICE_ENTER + " '4':"), LANG.CHOICE_SET_ACCESS_TOKEN])
|
||||
tb.add_row([aigpy.cmd.green(LANG.CHOICE_ENTER + " '5':"), LANG.CHOICE_APIKEY])
|
||||
tb.add_row([aigpy.cmd.green(LANG.CHOICE_ENTER_URLID), LANG.CHOICE_DOWNLOAD_BY_URL])
|
||||
print(tb)
|
||||
print("====================================================")
|
||||
|
||||
@@ -118,10 +128,15 @@ 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)
|
||||
ret = input("")
|
||||
return ret == '1'
|
||||
|
||||
@staticmethod
|
||||
def enterPath(string, errmsg, retWord='0', default=""):
|
||||
LANG = getLang()
|
||||
while True:
|
||||
ret = aigpy.cmd.inputPath(aigpy.cmd.yellow(string), retWord)
|
||||
if ret == retWord:
|
||||
@@ -134,7 +149,6 @@ class Printf(object):
|
||||
|
||||
@staticmethod
|
||||
def enterLimit(string, errmsg, limit=[]):
|
||||
LANG = getLang()
|
||||
while True:
|
||||
ret = aigpy.cmd.inputLimit(aigpy.cmd.yellow(string), limit)
|
||||
if ret is None:
|
||||
@@ -154,33 +168,28 @@ class Printf(object):
|
||||
|
||||
@staticmethod
|
||||
def err(string):
|
||||
LANG = getLang()
|
||||
print(aigpy.cmd.red(LANG.PRINT_ERR + " ") + string)
|
||||
logging.error(string)
|
||||
# logging.error(string)
|
||||
|
||||
@staticmethod
|
||||
def info(string):
|
||||
LANG = getLang()
|
||||
print(aigpy.cmd.blue(LANG.PRINT_INFO + " ") + string)
|
||||
|
||||
@staticmethod
|
||||
def success(string):
|
||||
LANG = getLang()
|
||||
print(aigpy.cmd.green(LANG.PRINT_SUCCESS + " ") + string)
|
||||
|
||||
@staticmethod
|
||||
def album(data: Album):
|
||||
LANG = getLang()
|
||||
tb = prettytable.PrettyTable()
|
||||
tb.field_names = [aigpy.cmd.green(LANG.MODEL_ALBUM_PROPERTY), aigpy.cmd.green(LANG.VALUE)]
|
||||
tb.align = 'l'
|
||||
tb.add_row([LANG.MODEL_TITLE, data.title])
|
||||
tb.add_row(["ID", data.id])
|
||||
tb.add_row([LANG.MODEL_TRACK_NUMBER, data.numberOfTracks])
|
||||
tb.add_row([LANG.MODEL_VIDEO_NUMBER, data.numberOfVideos])
|
||||
tb.add_row([LANG.MODEL_RELEASE_DATE, data.releaseDate])
|
||||
tb.add_row([LANG.MODEL_VERSION, data.version])
|
||||
tb.add_row([LANG.MODEL_EXPLICIT, data.explicit])
|
||||
tb = Printf.__gettable__([LANG.MODEL_ALBUM_PROPERTY, LANG.VALUE], [
|
||||
[LANG.MODEL_TITLE, data.title],
|
||||
["ID", data.id],
|
||||
[LANG.MODEL_TRACK_NUMBER, data.numberOfTracks],
|
||||
[LANG.MODEL_VIDEO_NUMBER, data.numberOfVideos],
|
||||
[LANG.MODEL_RELEASE_DATE, data.releaseDate],
|
||||
[LANG.MODEL_VERSION, data.version],
|
||||
[LANG.MODEL_EXPLICIT, data.explicit],
|
||||
])
|
||||
print(tb)
|
||||
logging.info("====album " + str(data.id) + "====\n" +
|
||||
"title:" + data.title + "\n" +
|
||||
@@ -190,16 +199,14 @@ class Printf(object):
|
||||
|
||||
@staticmethod
|
||||
def track(data: Track, stream: StreamUrl = None):
|
||||
LANG = getLang()
|
||||
tb = prettytable.PrettyTable()
|
||||
tb.field_names = [aigpy.cmd.green(LANG.MODEL_TRACK_PROPERTY), aigpy.cmd.green(LANG.VALUE)]
|
||||
tb.align = 'l'
|
||||
tb.add_row([LANG.MODEL_TITLE, data.title])
|
||||
tb.add_row(["ID", data.id])
|
||||
tb.add_row([LANG.MODEL_ALBUM, data.album.title])
|
||||
tb.add_row([LANG.MODEL_VERSION, data.version])
|
||||
tb.add_row([LANG.MODEL_EXPLICIT, data.explicit])
|
||||
tb.add_row(["Max-Q", data.audioQuality])
|
||||
tb = Printf.__gettable__([LANG.MODEL_TRACK_PROPERTY, LANG.VALUE], [
|
||||
[LANG.MODEL_TITLE, data.title],
|
||||
["ID", data.id],
|
||||
[LANG.MODEL_ALBUM, data.album.title],
|
||||
[LANG.MODEL_VERSION, data.version],
|
||||
[LANG.MODEL_EXPLICIT, data.explicit],
|
||||
["Max-Q", data.audioQuality],
|
||||
])
|
||||
if stream is not None:
|
||||
tb.add_row(["Get-Q", str(stream.soundQuality)])
|
||||
tb.add_row(["Get-Codec", str(stream.codec)])
|
||||
@@ -211,19 +218,16 @@ class Printf(object):
|
||||
|
||||
@staticmethod
|
||||
def video(data: Video, stream: VideoStreamUrl = None):
|
||||
LANG = getLang()
|
||||
tb = prettytable.PrettyTable()
|
||||
tb.field_names = [aigpy.cmd.green(LANG.MODEL_VIDEO_PROPERTY), aigpy.cmd.green(LANG.VALUE)]
|
||||
tb.align = 'l'
|
||||
tb.add_row([LANG.MODEL_TITLE, data.title])
|
||||
tb.add_row([LANG.MODEL_ALBUM, data.album.title if data.album != None else None])
|
||||
tb.add_row([LANG.MODEL_VERSION, data.version])
|
||||
tb.add_row([LANG.MODEL_EXPLICIT, data.explicit])
|
||||
tb.add_row(["Max-Q", data.quality])
|
||||
tb = Printf.__gettable__([LANG.MODEL_VIDEO_PROPERTY, LANG.VALUE], [
|
||||
[LANG.MODEL_TITLE, data.title],
|
||||
[LANG.MODEL_ALBUM, data.album.title if data.album != None else None],
|
||||
[LANG.MODEL_VERSION, data.version],
|
||||
[LANG.MODEL_EXPLICIT, data.explicit],
|
||||
["Max-Q", data.quality],
|
||||
])
|
||||
if stream is not None:
|
||||
tb.add_row(["Get-Q", str(stream.resolution)])
|
||||
tb.add_row(["Get-Codec", str(stream.codec)])
|
||||
|
||||
print(tb)
|
||||
logging.info("====video " + str(data.id) + "====\n" +
|
||||
"title:" + data.title + "\n" +
|
||||
@@ -232,14 +236,12 @@ class Printf(object):
|
||||
|
||||
@staticmethod
|
||||
def artist(data: Artist, num):
|
||||
LANG = getLang()
|
||||
tb = prettytable.PrettyTable()
|
||||
tb.field_names = [aigpy.cmd.green(LANG.MODEL_ARTIST_PROPERTY), aigpy.cmd.green(LANG.VALUE)]
|
||||
tb.align = 'l'
|
||||
tb.add_row([LANG.MODEL_ID, data.id])
|
||||
tb.add_row([LANG.MODEL_NAME, data.name])
|
||||
tb.add_row(["Number of albums", num])
|
||||
tb.add_row([LANG.MODEL_TYPE, str(data.type)])
|
||||
tb = Printf.__gettable__([LANG.MODEL_ARTIST_PROPERTY, LANG.VALUE], [
|
||||
[LANG.MODEL_ID, data.id],
|
||||
[LANG.MODEL_NAME, data.name],
|
||||
["Number of albums", num],
|
||||
[LANG.MODEL_TYPE, str(data.type)],
|
||||
])
|
||||
print(tb)
|
||||
logging.info("====artist " + str(data.id) + "====\n" +
|
||||
"name:" + data.name + "\n" +
|
||||
@@ -248,13 +250,11 @@ class Printf(object):
|
||||
|
||||
@staticmethod
|
||||
def playlist(data):
|
||||
LANG = getLang()
|
||||
tb = prettytable.PrettyTable()
|
||||
tb.field_names = [aigpy.cmd.green(LANG.MODEL_PLAYLIST_PROPERTY), aigpy.cmd.green(LANG.VALUE)]
|
||||
tb.align = 'l'
|
||||
tb.add_row([LANG.MODEL_TITLE, data.title])
|
||||
tb.add_row([LANG.MODEL_TRACK_NUMBER, data.numberOfTracks])
|
||||
tb.add_row([LANG.MODEL_VIDEO_NUMBER, data.numberOfVideos])
|
||||
tb = Printf.__gettable__([LANG.MODEL_PLAYLIST_PROPERTY, LANG.VALUE], [
|
||||
[LANG.MODEL_TITLE, data.title],
|
||||
[LANG.MODEL_TRACK_NUMBER, data.numberOfTracks],
|
||||
[LANG.MODEL_VIDEO_NUMBER, data.numberOfVideos],
|
||||
])
|
||||
print(tb)
|
||||
logging.info("====playlist " + str(data.uuid) + "====\n" +
|
||||
"title:" + data.title + "\n" +
|
||||
@@ -264,13 +264,11 @@ class Printf(object):
|
||||
|
||||
@staticmethod
|
||||
def mix(data):
|
||||
LANG = getLang()
|
||||
tb = prettytable.PrettyTable()
|
||||
tb.field_names = [aigpy.cmd.green(LANG.MODEL_PLAYLIST_PROPERTY), aigpy.cmd.green(LANG.VALUE)]
|
||||
tb.align = 'l'
|
||||
tb.add_row([LANG.MODEL_ID, data.id])
|
||||
tb.add_row([LANG.MODEL_TRACK_NUMBER, len(data.tracks)])
|
||||
tb.add_row([LANG.MODEL_VIDEO_NUMBER, len(data.videos)])
|
||||
tb = Printf.__gettable__([LANG.MODEL_PLAYLIST_PROPERTY, LANG.VALUE], [
|
||||
[LANG.MODEL_ID, data.id],
|
||||
[LANG.MODEL_TRACK_NUMBER, len(data.tracks)],
|
||||
[LANG.MODEL_VIDEO_NUMBER, len(data.videos)],
|
||||
])
|
||||
print(tb)
|
||||
logging.info("====Mix " + str(data.id) + "====\n" +
|
||||
"track num:" + str(len(data.tracks)) + "\n" +
|
||||
@@ -280,7 +278,6 @@ class Printf(object):
|
||||
@staticmethod
|
||||
def apikeys(items):
|
||||
print("-------------API-KEYS---------------")
|
||||
LANG = getLang()
|
||||
tb = prettytable.PrettyTable()
|
||||
tb.field_names = [aigpy.cmd.green('Index'),
|
||||
aigpy.cmd.green('Valid'),
|
||||
|
||||
@@ -4,169 +4,119 @@
|
||||
@File : settings.py
|
||||
@Time : 2020/11/08
|
||||
@Author : Yaronzz
|
||||
@Version : 2.0
|
||||
@Version : 3.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
'''
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import aigpy
|
||||
import base64
|
||||
|
||||
from aigpy.fileHelper import getContent, write
|
||||
from aigpy.modelHelper import dictToModel, modelToDict, ModelBase
|
||||
from tidal_dl.enums import AudioQuality, VideoQuality
|
||||
from tidal_dl.enums import *
|
||||
|
||||
|
||||
def __encode__(string):
|
||||
sw = bytes(string, 'utf-8')
|
||||
st = base64.b64encode(sw)
|
||||
return st
|
||||
class Settings(aigpy.model.ModelBase):
|
||||
checkExist = True
|
||||
includeEP = True
|
||||
saveCovers = True
|
||||
language = 0
|
||||
lyricFile = False
|
||||
apiKeyIndex = 0
|
||||
showProgress = True
|
||||
showTrackInfo = True
|
||||
saveAlbumInfo = False
|
||||
|
||||
downloadPath = "./download/"
|
||||
audioQuality = AudioQuality.Normal
|
||||
videoQuality = VideoQuality.P360
|
||||
usePlaylistFolder = True
|
||||
albumFolderFormat = R"{ArtistName}/{Flag} {AlbumTitle} [{AlbumID}] [{AlbumYear}]"
|
||||
trackFileFormat = R"{TrackNumber} - {ArtistName} - {TrackTitle}{ExplicitFlag}"
|
||||
videoFileFormat = R"{VideoNumber} - {ArtistName} - {VideoTitle}{ExplicitFlag}"
|
||||
|
||||
def getDefaultPathFormat(self, type: Type):
|
||||
if type == Type.Album:
|
||||
return R"{ArtistName}/{Flag} {AlbumTitle} [{AlbumID}] [{AlbumYear}]"
|
||||
elif type == Type.Track:
|
||||
return R"{TrackNumber} - {ArtistName} - {TrackTitle}{ExplicitFlag}"
|
||||
elif type == Type.Video:
|
||||
return R"{VideoNumber} - {ArtistName} - {VideoTitle}{ExplicitFlag}"
|
||||
return ""
|
||||
|
||||
def getAudioQuality(self, value):
|
||||
for item in AudioQuality:
|
||||
if item.name == value:
|
||||
return item
|
||||
return AudioQuality.Normal
|
||||
|
||||
def getVideoQuality(self, value):
|
||||
for item in VideoQuality:
|
||||
if item.name == value:
|
||||
return item
|
||||
return VideoQuality.P360
|
||||
|
||||
def read(self, path):
|
||||
self._path_ = path
|
||||
txt = aigpy.file.getContent(self._path_)
|
||||
if len(txt) > 0:
|
||||
data = json.loads(txt)
|
||||
if aigpy.model.dictToModel(data, self) is None:
|
||||
return
|
||||
|
||||
self.audioQuality = self.getAudioQuality(self.audioQuality)
|
||||
self.videoQuality = self.getVideoQuality(self.videoQuality)
|
||||
|
||||
if self.albumFolderFormat is None:
|
||||
self.albumFolderFormat = self.getDefaultPathFormat(Type.Album)
|
||||
if self.trackFileFormat is None:
|
||||
self.trackFileFormat = self.getDefaultPathFormat(Type.Track)
|
||||
if self.videoFileFormat is None:
|
||||
self.videoFileFormat = self.getDefaultPathFormat(Type.Video)
|
||||
if self.apiKeyIndex is None:
|
||||
self.apiKeyIndex = 0
|
||||
|
||||
def save(self):
|
||||
data = aigpy.model.modelToDict(self)
|
||||
data['audioQuality'] = self.audioQuality.name
|
||||
data['videoQuality'] = self.videoQuality.name
|
||||
txt = json.dumps(data)
|
||||
aigpy.file.write(self._path_, txt, 'w+')
|
||||
|
||||
|
||||
def __decode__(string):
|
||||
try:
|
||||
sr = base64.b64decode(string)
|
||||
st = sr.decode()
|
||||
return st
|
||||
except:
|
||||
return string
|
||||
|
||||
|
||||
def getSettingsPath():
|
||||
if "XDG_CONFIG_HOME" in os.environ:
|
||||
return os.environ['XDG_CONFIG_HOME']
|
||||
elif "HOME" in os.environ:
|
||||
return os.environ['HOME']
|
||||
elif "HOMEDRIVE" in os.environ and "HOMEPATH" in os.environ:
|
||||
return os.environ['HOMEDRIVE'] + os.environ['HOMEPATH']
|
||||
else:
|
||||
return os.path.abspath("./")
|
||||
|
||||
|
||||
def getLogPath():
|
||||
return getSettingsPath() + '/.tidal-dl.log'
|
||||
|
||||
|
||||
class TokenSettings(ModelBase):
|
||||
class TokenSettings(aigpy.model.ModelBase):
|
||||
userid = None
|
||||
countryCode = None
|
||||
accessToken = None
|
||||
refreshToken = None
|
||||
expiresAfter = 0
|
||||
|
||||
@staticmethod
|
||||
def read():
|
||||
path = TokenSettings.__getFilePath__()
|
||||
txt = getContent(path)
|
||||
if txt == "":
|
||||
return TokenSettings()
|
||||
txt = __decode__(txt)
|
||||
data = json.loads(txt)
|
||||
ret = dictToModel(data, TokenSettings())
|
||||
if ret is None:
|
||||
return TokenSettings()
|
||||
return ret
|
||||
def __encode__(self, string):
|
||||
sw = bytes(string, 'utf-8')
|
||||
st = base64.b64encode(sw)
|
||||
return st
|
||||
|
||||
@staticmethod
|
||||
def save(model):
|
||||
data = modelToDict(model)
|
||||
def __decode__(self, string):
|
||||
try:
|
||||
sr = base64.b64decode(string)
|
||||
st = sr.decode()
|
||||
return st
|
||||
except:
|
||||
return string
|
||||
|
||||
def read(self, path):
|
||||
self._path_ = path
|
||||
txt = aigpy.file.getContent(self._path_)
|
||||
if len(txt) > 0:
|
||||
data = json.loads(self.__decode__(txt))
|
||||
aigpy.model.dictToModel(data, self)
|
||||
|
||||
def save(self):
|
||||
data = aigpy.model.modelToDict(self)
|
||||
txt = json.dumps(data)
|
||||
txt = __encode__(txt)
|
||||
path = TokenSettings.__getFilePath__()
|
||||
write(path, txt, 'wb')
|
||||
|
||||
@staticmethod
|
||||
def __getFilePath__():
|
||||
return getSettingsPath() + '/.tidal-dl.token.json'
|
||||
aigpy.file.write(self._path_, self.__encode__(txt), 'wb')
|
||||
|
||||
|
||||
class Settings(ModelBase):
|
||||
addLyrics = False
|
||||
lyricsServerProxy = ''
|
||||
downloadPath = "./download/"
|
||||
onlyM4a = False
|
||||
addExplicitTag = True
|
||||
addHyphen = True
|
||||
addYear = False
|
||||
useTrackNumber = True
|
||||
audioQuality = AudioQuality.Normal
|
||||
videoQuality = VideoQuality.P360
|
||||
checkExist = True
|
||||
artistBeforeTitle = False
|
||||
includeEP = True
|
||||
addAlbumIDBeforeFolder = False
|
||||
saveCovers = True
|
||||
language = 0
|
||||
usePlaylistFolder = True
|
||||
multiThreadDownload = True
|
||||
albumFolderFormat = R"{ArtistName}/{Flag} {AlbumTitle} [{AlbumID}] [{AlbumYear}]"
|
||||
trackFileFormat = R"{TrackNumber} - {ArtistName} - {TrackTitle}{ExplicitFlag}"
|
||||
videoFileFormat = R"{VideoNumber} - {ArtistName} - {VideoTitle}{ExplicitFlag}"
|
||||
showProgress = True
|
||||
showTrackInfo = True
|
||||
saveAlbumInfo = False
|
||||
lyricFile = False
|
||||
apiKeyIndex = 0
|
||||
addTypeFolder = True
|
||||
|
||||
@staticmethod
|
||||
def getDefaultAlbumFolderFormat():
|
||||
return R"{ArtistName}/{Flag} {AlbumTitle} [{AlbumID}] [{AlbumYear}]"
|
||||
|
||||
@staticmethod
|
||||
def getDefaultTrackFileFormat():
|
||||
return R"{TrackNumber} - {ArtistName} - {TrackTitle}{ExplicitFlag}"
|
||||
|
||||
@staticmethod
|
||||
def getDefaultVideoFileFormat():
|
||||
return R"{VideoNumber} - {ArtistName} - {VideoTitle}{ExplicitFlag}"
|
||||
|
||||
@staticmethod
|
||||
def read():
|
||||
path = Settings.__getFilePath__()
|
||||
txt = getContent(path)
|
||||
if txt == "":
|
||||
return Settings()
|
||||
data = json.loads(txt)
|
||||
ret = dictToModel(data, Settings())
|
||||
if ret is None:
|
||||
return Settings()
|
||||
ret.audioQuality = Settings.getAudioQuality(ret.audioQuality)
|
||||
ret.videoQuality = Settings.getVideoQuality(ret.videoQuality)
|
||||
ret.usePlaylistFolder = ret.usePlaylistFolder == True or ret.usePlaylistFolder is None
|
||||
ret.multiThreadDownload = ret.multiThreadDownload == True or ret.multiThreadDownload is None
|
||||
ret.addTypeFolder = ret.addTypeFolder == True or ret.addTypeFolder is None
|
||||
if ret.albumFolderFormat is None:
|
||||
ret.albumFolderFormat = Settings.getDefaultAlbumFolderFormat()
|
||||
if ret.trackFileFormat is None:
|
||||
ret.trackFileFormat = Settings.getDefaultTrackFileFormat()
|
||||
if ret.apiKeyIndex is None:
|
||||
ret.apiKeyIndex = 0
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def save(model):
|
||||
data = modelToDict(model)
|
||||
data['audioQuality'] = model.audioQuality.name
|
||||
data['videoQuality'] = model.videoQuality.name
|
||||
txt = json.dumps(data)
|
||||
path = Settings.__getFilePath__()
|
||||
write(path, txt, 'w+')
|
||||
|
||||
@staticmethod
|
||||
def getAudioQuality(value):
|
||||
for item in AudioQuality:
|
||||
if item.name == value:
|
||||
return item
|
||||
return AudioQuality.Normal
|
||||
|
||||
@staticmethod
|
||||
def getVideoQuality(value):
|
||||
for item in VideoQuality:
|
||||
if item.name == value:
|
||||
return item
|
||||
return VideoQuality.P360
|
||||
|
||||
@staticmethod
|
||||
def __getFilePath__():
|
||||
return getSettingsPath() + '/.tidal-dl.json'
|
||||
# Singleton
|
||||
SETTINGS = Settings()
|
||||
TOKEN = TokenSettings()
|
||||
|
||||
@@ -4,134 +4,68 @@
|
||||
@File : tidal.py
|
||||
@Time : 2019/02/27
|
||||
@Author : Yaronzz
|
||||
@VERSION : 2.0
|
||||
@VERSION : 3.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc : tidal api
|
||||
'''
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
|
||||
import aigpy.stringHelper as stringHelper
|
||||
import aigpy
|
||||
import base64
|
||||
import requests
|
||||
from aigpy.modelHelper import dictToModel
|
||||
from aigpy.stringHelper import isNull
|
||||
from requests.packages import urllib3
|
||||
from tidal_dl.enums import Type, AudioQuality, VideoQuality
|
||||
from tidal_dl.model import Album, Track, Video, Artist, Playlist, StreamUrl, VideoStreamUrl, SearchResult, Lyrics, Mix
|
||||
import tidal_dl.apiKey as apiKey
|
||||
|
||||
__VERSION__ = '1.9.1'
|
||||
__URL_PRE__ = 'https://api.tidalhifi.com/v1/'
|
||||
__AUTH_URL__ = 'https://auth.tidal.com/v1/oauth2'
|
||||
__API_KEY__ = {'clientId': '7m7Ap0JC9j1cOM3n',
|
||||
'clientSecret': 'vRAdA108tlvkJpTsGZS8rGZ7xTlbJ0qaZ2K9saEzsgY='}
|
||||
from tidal_dl.model import *
|
||||
from tidal_dl.enums import *
|
||||
|
||||
# SSL Warnings
|
||||
urllib3.disable_warnings()
|
||||
# add retry number
|
||||
# SSL Warnings | retry number
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
requests.adapters.DEFAULT_RETRIES = 5
|
||||
|
||||
|
||||
class LoginKey(object):
|
||||
def __init__(self):
|
||||
self.deviceCode = None
|
||||
self.userCode = None
|
||||
self.verificationUrl = None
|
||||
self.authCheckTimeout = None
|
||||
self.authCheckInterval = None
|
||||
self.userId = None
|
||||
self.countryCode = None
|
||||
self.accessToken = None
|
||||
self.refreshToken = None
|
||||
self.expiresIn = None
|
||||
|
||||
|
||||
class __StreamRespond__(object):
|
||||
trackid = None
|
||||
videoid = None
|
||||
streamType = None
|
||||
assetPresentation = None
|
||||
audioMode = None
|
||||
audioQuality = None
|
||||
videoQuality = None
|
||||
manifestMimeType = None
|
||||
manifest = None
|
||||
|
||||
|
||||
class TidalAPI(object):
|
||||
def __init__(self):
|
||||
self.apiKey = __API_KEY__
|
||||
self.key = LoginKey()
|
||||
self.__debugVar = 0
|
||||
self.apiKey = {'clientId': '7m7Ap0JC9j1cOM3n',
|
||||
'clientSecret': 'vRAdA108tlvkJpTsGZS8rGZ7xTlbJ0qaZ2K9saEzsgY='}
|
||||
|
||||
def __toJson__(self, string: str):
|
||||
try:
|
||||
json_object = json.loads(string)
|
||||
except:
|
||||
return None
|
||||
return json_object
|
||||
|
||||
def __get__(self, path, params={}, retry=3, urlpre=__URL_PRE__):
|
||||
# deprecate the sessionId
|
||||
# header = {'X-Tidal-SessionId': self.key.sessionId}T
|
||||
def __get__(self, path, params={}, urlpre='https://api.tidalhifi.com/v1/'):
|
||||
header = {}
|
||||
if not isNull(self.key.accessToken):
|
||||
header = {'authorization': 'Bearer {}'.format(self.key.accessToken)}
|
||||
header = {'authorization': f'Bearer {self.key.accessToken}'}
|
||||
params['countryCode'] = self.key.countryCode
|
||||
|
||||
result = None
|
||||
respond = None
|
||||
for index in range(0, retry):
|
||||
errmsg = "Get operation err!"
|
||||
for index in range(0, 3):
|
||||
try:
|
||||
respond = requests.get(urlpre + path, headers=header, params=params)
|
||||
result = self.__toJson__(respond.text)
|
||||
result = json.loads(respond.text)
|
||||
if 'status' not in result:
|
||||
return result
|
||||
|
||||
if 'userMessage' in result and result['userMessage'] is not None:
|
||||
errmsg += result['userMessage']
|
||||
break
|
||||
except:
|
||||
continue
|
||||
except Exception as e:
|
||||
if index >= 3:
|
||||
errmsg += respond.text
|
||||
|
||||
if result is None:
|
||||
return "Get operation err!" + respond.text, None
|
||||
if 'status' in result:
|
||||
if 'userMessage' in result and result['userMessage'] is not None:
|
||||
return result['userMessage'], None
|
||||
else:
|
||||
logging.error("[Get operation err] path=" + path + ". respon=" + respond.text)
|
||||
return "Get operation err!", None
|
||||
return None, result
|
||||
raise Exception(errmsg)
|
||||
|
||||
def __getItems__(self, path, params={}, retry=3):
|
||||
def __getItems__(self, path, params={}):
|
||||
params['limit'] = 50
|
||||
params['offset'] = 0
|
||||
total = 0
|
||||
ret = []
|
||||
while True:
|
||||
msg, data = self.__get__(path, params, retry)
|
||||
if msg is not None:
|
||||
return msg, None
|
||||
|
||||
data = self.__get__(path, params)
|
||||
if 'totalNumberOfItems' in data:
|
||||
total = data['totalNumberOfItems']
|
||||
if total > 0 and total <= len(ret):
|
||||
return None, ret
|
||||
return ret
|
||||
|
||||
num = 0
|
||||
for item in data["items"]:
|
||||
num += 1
|
||||
ret.append(item)
|
||||
ret += data["items"]
|
||||
num = len(data["items"])
|
||||
if num < 50:
|
||||
break
|
||||
params['offset'] += num
|
||||
return None, ret
|
||||
|
||||
def __getQualityString__(self, quality: AudioQuality):
|
||||
if quality == AudioQuality.Normal:
|
||||
return "LOW"
|
||||
if quality == AudioQuality.High:
|
||||
return "HIGH"
|
||||
if quality == AudioQuality.HiFi:
|
||||
return "LOSSLESS"
|
||||
return "HI_RES"
|
||||
return ret
|
||||
|
||||
def __getResolutionList__(self, url):
|
||||
ret = []
|
||||
@@ -144,66 +78,53 @@ class TidalAPI(object):
|
||||
if "EXT-X-STREAM-INF:" not in item:
|
||||
continue
|
||||
stream = VideoStreamUrl()
|
||||
stream.codec = stringHelper.getSub(item, "CODECS=\"", "\"")
|
||||
stream.m3u8Url = "http" + stringHelper.getSubOnlyStart(item, "http").strip()
|
||||
stream.resolution = stringHelper.getSub(item, "RESOLUTION=", "http").strip()
|
||||
stream.codec = aigpy.string.getSub(item, "CODECS=\"", "\"")
|
||||
stream.m3u8Url = "http" + aigpy.string.getSubOnlyStart(item, "http").strip()
|
||||
stream.resolution = aigpy.string.getSub(item, "RESOLUTION=", "http").strip()
|
||||
stream.resolution = stream.resolution.split(',')[0]
|
||||
stream.resolutions = stream.resolution.split("x")
|
||||
ret.append(stream)
|
||||
return ret
|
||||
|
||||
def __post__(self, url, data, auth=None):
|
||||
retry = 3
|
||||
while retry > 0:
|
||||
def __post__(self, path, data, auth=None, urlpre='https://auth.tidal.com/v1/oauth2'):
|
||||
for index in range(0, 3):
|
||||
try:
|
||||
result = requests.post(url, data=data, auth=auth, verify=False).json()
|
||||
except (
|
||||
requests.ConnectionError,
|
||||
requests.exceptions.ReadTimeout,
|
||||
requests.exceptions.Timeout,
|
||||
requests.exceptions.ConnectTimeout,
|
||||
) as e:
|
||||
retry -= 1
|
||||
if retry <= 0:
|
||||
return e, None
|
||||
continue
|
||||
return None, result
|
||||
result = requests.post(urlpre+path, data=data, auth=auth, verify=False).json()
|
||||
return result
|
||||
except Exception as e:
|
||||
if index >= 3:
|
||||
raise e
|
||||
|
||||
def getDeviceCode(self):
|
||||
def getDeviceCode(self) -> str:
|
||||
data = {
|
||||
'client_id': self.apiKey['clientId'],
|
||||
'scope': 'r_usr+w_usr+w_sub'
|
||||
}
|
||||
e, result = self.__post__(__AUTH_URL__ + '/device_authorization', data)
|
||||
if e is not None:
|
||||
return str(e), False
|
||||
|
||||
result = self.__post__('/device_authorization', data)
|
||||
if 'status' in result and result['status'] != 200:
|
||||
return "Device authorization failed. Please try again.", False
|
||||
raise Exception("Device authorization failed. Please choose another apikey.")
|
||||
|
||||
self.key.deviceCode = result['deviceCode']
|
||||
self.key.userCode = result['userCode']
|
||||
self.key.verificationUrl = result['verificationUri']
|
||||
self.key.authCheckTimeout = result['expiresIn']
|
||||
self.key.authCheckInterval = result['interval']
|
||||
return None, True
|
||||
return "http://" + self.key.verificationUrl + "/" + self.key.userCode
|
||||
|
||||
def checkAuthStatus(self):
|
||||
def checkAuthStatus(self) -> bool:
|
||||
data = {
|
||||
'client_id': self.apiKey['clientId'],
|
||||
'device_code': self.key.deviceCode,
|
||||
'grant_type': 'urn:ietf:params:oauth:grant-type:device_code',
|
||||
'scope': 'r_usr+w_usr+w_sub'
|
||||
}
|
||||
e, result = self.__post__(__AUTH_URL__ + '/token', data, (self.apiKey['clientId'], self.apiKey['clientSecret']))
|
||||
if e is not None:
|
||||
return str(e), False
|
||||
|
||||
auth = (self.apiKey['clientId'], self.apiKey['clientSecret'])
|
||||
result = self.__post__('/token', data, auth)
|
||||
if 'status' in result and result['status'] != 200:
|
||||
if result['status'] == 400 and result['sub_status'] == 1002:
|
||||
return "pending", False
|
||||
return False
|
||||
else:
|
||||
return "Error while checking for authorization. Trying again...", False
|
||||
raise Exception("Error while checking for authorization. Trying again...")
|
||||
|
||||
# if auth is successful:
|
||||
self.key.userId = result['user']['userId']
|
||||
@@ -211,151 +132,154 @@ class TidalAPI(object):
|
||||
self.key.accessToken = result['access_token']
|
||||
self.key.refreshToken = result['refresh_token']
|
||||
self.key.expiresIn = result['expires_in']
|
||||
return None, True
|
||||
return True
|
||||
|
||||
def verifyAccessToken(self, accessToken):
|
||||
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 "Login failed!", False
|
||||
return None, True
|
||||
return False
|
||||
return True
|
||||
|
||||
def refreshAccessToken(self, refreshToken):
|
||||
def refreshAccessToken(self, refreshToken) -> bool:
|
||||
data = {
|
||||
'client_id': self.apiKey['clientId'],
|
||||
'refresh_token': refreshToken,
|
||||
'grant_type': 'refresh_token',
|
||||
'scope': 'r_usr+w_usr+w_sub'
|
||||
}
|
||||
|
||||
e, result = self.__post__(__AUTH_URL__ + '/token', data, (self.apiKey['clientId'], self.apiKey['clientSecret']))
|
||||
if e is not None:
|
||||
return str(e), False
|
||||
|
||||
# result = requests.post(__AUTH_URL__ + '/token', data=data, auth=(self.apiKey['clientId'], self.apiKey['clientSecret'])).json()
|
||||
auth = (self.apiKey['clientId'], self.apiKey['clientSecret'])
|
||||
result = self.__post__('/token', data, auth)
|
||||
if 'status' in result and result['status'] != 200:
|
||||
return "Refresh failed. Please log in again.", False
|
||||
return False
|
||||
|
||||
# if auth is successful:
|
||||
self.key.userId = result['user']['userId']
|
||||
self.key.countryCode = result['user']['countryCode']
|
||||
self.key.accessToken = result['access_token']
|
||||
self.key.expiresIn = result['expires_in']
|
||||
return None, True
|
||||
return True
|
||||
|
||||
def loginByAccessToken(self, accessToken, userid=None):
|
||||
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 "Login failed!", False
|
||||
raise Exception("Login failed!")
|
||||
|
||||
if not isNull(userid):
|
||||
if not aigpy.string.isNull(userid):
|
||||
if str(result['userId']) != str(userid):
|
||||
return "User mismatch! Please use your own accesstoken.", False
|
||||
raise Exception("User mismatch! Please use your own accesstoken.",)
|
||||
|
||||
self.key.userId = result['userId']
|
||||
self.key.countryCode = result['countryCode']
|
||||
self.key.accessToken = accessToken
|
||||
return None, True
|
||||
return
|
||||
|
||||
def getAlbum(self, id):
|
||||
msg, data = self.__get__('albums/' + str(id))
|
||||
return msg, dictToModel(data, Album())
|
||||
def getAlbum(self, id) -> Album:
|
||||
return aigpy.model.dictToModel(self.__get__('albums/' + str(id)), Album())
|
||||
|
||||
def getPlaylist(self, id):
|
||||
msg, data = self.__get__('playlists/' + str(id))
|
||||
return msg, dictToModel(data, Playlist())
|
||||
def getPlaylist(self, id) -> Playlist:
|
||||
return aigpy.model.dictToModel(self.__get__('playlists/' + str(id)), Playlist())
|
||||
|
||||
def getArtist(self, id):
|
||||
msg, data = self.__get__('artists/' + str(id))
|
||||
return msg, dictToModel(data, Artist())
|
||||
def getArtist(self, id) -> Artist:
|
||||
return aigpy.model.dictToModel(self.__get__('artists/' + str(id)), Artist())
|
||||
|
||||
def getTrack(self, id):
|
||||
msg, data = self.__get__('tracks/' + str(id))
|
||||
return msg, dictToModel(data, Track())
|
||||
def getTrack(self, id) -> Track:
|
||||
return aigpy.model.dictToModel(self.__get__('tracks/' + str(id)), Track())
|
||||
|
||||
def getVideo(self, id):
|
||||
msg, data = self.__get__('videos/' + str(id))
|
||||
return msg, dictToModel(data, Video())
|
||||
def getVideo(self, id) -> Video:
|
||||
return aigpy.model.dictToModel(self.__get__('videos/' + str(id)), Video())
|
||||
|
||||
def getMix(self, id):
|
||||
msg, tracks, videos = self.getItems(id, Type.Mix)
|
||||
if msg is not None:
|
||||
return msg, None
|
||||
def getMix(self, id) -> Mix:
|
||||
mix = Mix()
|
||||
mix.id = id
|
||||
mix.tracks = tracks
|
||||
mix.videos = videos
|
||||
mix.tracks, mix.videos = self.getItems(id, Type.Mix)
|
||||
return None, mix
|
||||
|
||||
def search(self, text: str, type: Type, offset: int, limit: int):
|
||||
typeStr = "ARTISTS,ALBUMS,TRACKS,VIDEOS,PLAYLISTS"
|
||||
def getTypeData(self, id, type: Type):
|
||||
if type == Type.Album:
|
||||
typeStr = "ALBUMS"
|
||||
return self.getAlbum(id)
|
||||
if type == Type.Artist:
|
||||
typeStr = "ARTISTS"
|
||||
return self.getArtist(id)
|
||||
if type == Type.Track:
|
||||
typeStr = "TRACKS"
|
||||
return self.getTrack(id)
|
||||
if type == Type.Video:
|
||||
typeStr = "VIDEOS"
|
||||
return self.getVideo(id)
|
||||
if type == Type.Playlist:
|
||||
typeStr = "PLAYLISTS"
|
||||
return self.getPlaylist(id)
|
||||
if type == Type.Mix:
|
||||
return self.getMix(id)
|
||||
return None
|
||||
|
||||
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"
|
||||
|
||||
params = {"query": text,
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
"types": typeStr}
|
||||
return aigpy.model.dictToModel(self.__get__('search', params=params), SearchResult())
|
||||
|
||||
msg, data = self.__get__('search', params=params)
|
||||
return msg, dictToModel(data, SearchResult())
|
||||
def getSearchResultItems(self, result: SearchResult, type: Type):
|
||||
if type == Type.Track:
|
||||
return result.tracks.items
|
||||
if type == Type.Video:
|
||||
return result.videos.items
|
||||
if type == Type.Album:
|
||||
return result.albums.items
|
||||
if type == Type.Artist:
|
||||
return result.artists.items
|
||||
if type == Type.Playlist:
|
||||
return result.playlists.items
|
||||
return []
|
||||
|
||||
def getLyrics(self, id):
|
||||
msg, data = self.__get__('tracks/' + str(id) + "/lyrics", urlpre='https://listen.tidal.com/v1/')
|
||||
return msg, dictToModel(data, Lyrics())
|
||||
def getLyrics(self, id) -> Lyrics:
|
||||
data = self.__get__(f'tracks/{str(id)}/lyrics', urlpre='https://listen.tidal.com/v1/')
|
||||
return aigpy.model.dictToModel(data, Lyrics())
|
||||
|
||||
def getItems(self, id, type: Type):
|
||||
if type == Type.Playlist:
|
||||
msg, data = self.__getItems__('playlists/' + str(id) + "/items")
|
||||
data = self.__getItems__('playlists/' + str(id) + "/items")
|
||||
elif type == Type.Album:
|
||||
msg, data = self.__getItems__('albums/' + str(id) + "/items")
|
||||
data = self.__getItems__('albums/' + str(id) + "/items")
|
||||
elif type == Type.Mix:
|
||||
msg, data = self.__getItems__('mixes/' + str(id) + '/items')
|
||||
data = self.__getItems__('mixes/' + str(id) + '/items')
|
||||
else:
|
||||
return "invalid Type!", None, None
|
||||
if msg is not None:
|
||||
return msg, None, None
|
||||
raise Exception("invalid Type!")
|
||||
|
||||
tracks = []
|
||||
videos = []
|
||||
for item in data:
|
||||
if item['type'] == 'track':
|
||||
tracks.append(dictToModel(item['item'], Track()))
|
||||
tracks.append(aigpy.model.dictToModel(item['item'], Track()))
|
||||
else:
|
||||
videos.append(dictToModel(item['item'], Video()))
|
||||
return msg, tracks, videos
|
||||
videos.append(aigpy.model.dictToModel(item['item'], Video()))
|
||||
return tracks, videos
|
||||
|
||||
def getArtistAlbums(self, id, includeEP=False):
|
||||
albums = []
|
||||
msg, data = self.__getItems__('artists/' + str(id) + "/albums")
|
||||
if msg is not None:
|
||||
return msg, None
|
||||
for item in data:
|
||||
albums.append(dictToModel(item, Album()))
|
||||
if includeEP == False:
|
||||
return None, albums
|
||||
msg, data = self.__getItems__('artists/' + str(id) + "/albums", {"filter": "EPSANDSINGLES"})
|
||||
if msg is not None:
|
||||
return msg, None
|
||||
for item in data:
|
||||
albums.append(dictToModel(item, Album()))
|
||||
return None, albums
|
||||
data = self.__getItems__(f'artists/{str(id)}/albums')
|
||||
albums = list(aigpy.model.dictToModel(item, Album()) for item in data)
|
||||
if not includeEP:
|
||||
return albums
|
||||
|
||||
data = self.__getItems__(f'artists/{str(id)}/albums', {"filter": "EPSANDSINGLES"})
|
||||
albums += list(aigpy.model.dictToModel(item, Album()) for item in data)
|
||||
return albums
|
||||
|
||||
def getStreamUrl(self, id, quality: AudioQuality):
|
||||
squality = self.__getQualityString__(quality)
|
||||
squality = "HI_RES"
|
||||
if quality == AudioQuality.Normal:
|
||||
squality = "LOW"
|
||||
elif quality == AudioQuality.High:
|
||||
squality = "HIGH"
|
||||
elif quality == AudioQuality.HiFi:
|
||||
squality = "LOSSLESS"
|
||||
|
||||
paras = {"audioquality": squality, "playbackmode": "STREAM", "assetpresentation": "FULL"}
|
||||
msg, data = self.__get__('tracks/' + str(id) + "/playbackinfopostpaywall", paras)
|
||||
if msg is not None:
|
||||
return msg, None
|
||||
resp = dictToModel(data, __StreamRespond__())
|
||||
data = self.__get__(f'tracks/{str(id)}/playbackinfopostpaywall', paras)
|
||||
resp = aigpy.model.dictToModel(data, StreamRespond())
|
||||
|
||||
if "vnd.tidal.bt" in resp.manifestMimeType:
|
||||
manifest = json.loads(base64.b64decode(resp.manifest).decode('utf-8'))
|
||||
@@ -365,15 +289,13 @@ class TidalAPI(object):
|
||||
ret.codec = manifest['codecs']
|
||||
ret.encryptionKey = manifest['keyId'] if 'keyId' in manifest else ""
|
||||
ret.url = manifest['urls'][0]
|
||||
return "", ret
|
||||
return "Can't get the streamUrl, type is " + resp.manifestMimeType, None
|
||||
return ret
|
||||
raise Exception("Can't get the streamUrl, type is " + resp.manifestMimeType)
|
||||
|
||||
def getVideoStreamUrl(self, id, quality: VideoQuality):
|
||||
paras = {"videoquality": "HIGH", "playbackmode": "STREAM", "assetpresentation": "FULL"}
|
||||
msg, data = self.__get__('videos/' + str(id) + "/playbackinfopostpaywall", paras)
|
||||
if msg is not None:
|
||||
return msg, None
|
||||
resp = dictToModel(data, __StreamRespond__())
|
||||
data = self.__get__(f'videos/{str(id)}/playbackinfopostpaywall', paras)
|
||||
resp = aigpy.model.dictToModel(data, StreamRespond())
|
||||
|
||||
if "vnd.tidal.emu" in resp.manifestMimeType:
|
||||
manifest = json.loads(base64.b64decode(resp.manifest).decode('utf-8'))
|
||||
@@ -386,31 +308,25 @@ class TidalAPI(object):
|
||||
index += 1
|
||||
if index >= len(array):
|
||||
index = len(array) - 1
|
||||
return "", array[index]
|
||||
return "Can't get the streamUrl, type is " + resp.manifestMimeType, None
|
||||
return array[index]
|
||||
raise Exception("Can't get the streamUrl, type is " + resp.manifestMimeType)
|
||||
|
||||
def getTrackContributors(self, id):
|
||||
msg, data = self.__get__('tracks/' + str(id) + "/contributors")
|
||||
return msg, data
|
||||
return self.__get__(f'tracks/{str(id)}/contributors')
|
||||
|
||||
def getCoverUrl(self, sid, width="320", height="320"):
|
||||
if sid is None or sid == "":
|
||||
return None
|
||||
return "https://resources.tidal.com/images/" + sid.replace("-", "/") + "/" + width + "x" + height + ".jpg"
|
||||
return f"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
|
||||
return requests.get(url).content
|
||||
except:
|
||||
return ''
|
||||
|
||||
def getArtistsName(self, artists=[]):
|
||||
array = []
|
||||
for item in artists:
|
||||
array.append(item.name)
|
||||
return " / ".join(array)
|
||||
array = list(item.name for item in artists)
|
||||
return ", ".join(array)
|
||||
|
||||
def getFlag(self, data, type: Type, short=True, separator=" / "):
|
||||
master = False
|
||||
@@ -438,80 +354,35 @@ class TidalAPI(object):
|
||||
return separator.join(array)
|
||||
|
||||
def parseUrl(self, url):
|
||||
etype = Type.Null
|
||||
sid = ""
|
||||
if "tidal.com" not in url:
|
||||
return etype, sid
|
||||
return Type.Null, url
|
||||
|
||||
url = url.lower()
|
||||
if 'artist' in url:
|
||||
etype = Type.Artist
|
||||
if 'album' in url:
|
||||
etype = Type.Album
|
||||
if 'track' in url:
|
||||
etype = Type.Track
|
||||
if 'video' in url:
|
||||
etype = Type.Video
|
||||
if 'playlist' in url:
|
||||
etype = Type.Playlist
|
||||
if 'mix' in url:
|
||||
etype = Type.Mix
|
||||
|
||||
if etype == Type.Null:
|
||||
return etype, sid
|
||||
|
||||
sid = stringHelper.getSub(url, etype.name.lower() + '/', '/')
|
||||
return etype, sid
|
||||
for index, item in enumerate(Type):
|
||||
if item.name.lower() in url:
|
||||
etype = item
|
||||
return etype, aigpy.string.getSub(url, etype.name.lower() + '/', '/')
|
||||
return Type.Null, url
|
||||
|
||||
def getByString(self, string):
|
||||
etype = Type.Null
|
||||
if aigpy.string.isNull(string):
|
||||
raise Exception("Please enter something.")
|
||||
|
||||
obj = None
|
||||
|
||||
if isNull(string):
|
||||
return "Please enter something.", etype, obj
|
||||
etype, sid = self.parseUrl(string)
|
||||
if isNull(sid):
|
||||
sid = string
|
||||
for index, item in enumerate(Type):
|
||||
if etype != Type.Null and etype != item:
|
||||
continue
|
||||
if item == Type.Null:
|
||||
continue
|
||||
try:
|
||||
obj = self.getTypeData(sid, item)
|
||||
return item, obj
|
||||
except:
|
||||
continue
|
||||
|
||||
if obj is None and (etype == Type.Null or etype == Type.Album):
|
||||
msg, obj = self.getAlbum(sid)
|
||||
if obj is None and (etype == Type.Null or etype == Type.Artist):
|
||||
msg, obj = self.getArtist(sid)
|
||||
if obj is None and (etype == Type.Null or etype == Type.Track):
|
||||
msg, obj = self.getTrack(sid)
|
||||
if obj is None and (etype == Type.Null or etype == Type.Video):
|
||||
msg, obj = self.getVideo(sid)
|
||||
if obj is None and (etype == Type.Null or etype == Type.Playlist):
|
||||
msg, obj = self.getPlaylist(sid)
|
||||
if obj is None and (etype == Type.Null or etype == Type.Mix):
|
||||
msg, obj = self.getMix(sid)
|
||||
raise Exception("No result.")
|
||||
|
||||
if obj is None or etype != Type.Null:
|
||||
return msg, etype, obj
|
||||
if obj.__class__ == Album:
|
||||
etype = Type.Album
|
||||
if obj.__class__ == Artist:
|
||||
etype = Type.Artist
|
||||
if obj.__class__ == Track:
|
||||
etype = Type.Track
|
||||
if obj.__class__ == Video:
|
||||
etype = Type.Video
|
||||
if obj.__class__ == Playlist:
|
||||
etype = Type.Playlist
|
||||
if obj.__class__ == Mix:
|
||||
etype = Type.Mix
|
||||
return msg, etype, obj
|
||||
|
||||
"""
|
||||
def getToken(self):
|
||||
token1 = "MbjR4DLXz1ghC4rV"
|
||||
token2 = "pl4Vc0hemlAXD0mN" # only lossless
|
||||
try:
|
||||
msg = requests.get( "https://cdn.jsdelivr.net/gh/yaronzz/CDN@latest/app/tidal/tokens.json", timeout=(20.05, 27.05))
|
||||
tokens = json.loads(msg.text)
|
||||
token1 = tokens['token']
|
||||
token2 = tokens['token2']
|
||||
except Exception as e:
|
||||
pass
|
||||
return token1,token2
|
||||
"""
|
||||
# Singleton
|
||||
TIDAL_API = TidalAPI()
|
||||
|
||||
@@ -1,553 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : util.py
|
||||
@Date : 2021/10/09
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
import requests
|
||||
import aigpy
|
||||
import lyricsgenius
|
||||
import datetime
|
||||
import tidal_dl.m3u8dl as m3u8dl
|
||||
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, 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):
|
||||
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 __secondsToTimeStr__(seconds):
|
||||
time_string = str(datetime.timedelta(seconds=seconds))
|
||||
if time_string.startswith('0:'):
|
||||
time_string = time_string[2:]
|
||||
return time_string
|
||||
|
||||
|
||||
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 getLyricsFromGemius(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 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()
|
||||
|
||||
|
||||
def getArtistsName(artists):
|
||||
return ", ".join(map(lambda artist: artist.name, artists))
|
||||
|
||||
|
||||
# "{ArtistName}/{Flag} [{AlbumID}] [{AlbumYear}] {AlbumTitle}"
|
||||
def getAlbumPath(conf: Settings, album):
|
||||
base = conf.downloadPath + '/'
|
||||
if conf.addTypeFolder:
|
||||
base = base + 'Album/'
|
||||
artist = aigpy.path.replaceLimitChar(getArtistsName(album.artists), '-')
|
||||
albumArtistName = aigpy.path.replaceLimitChar(album.artist.name, '-') if album.artist is not None else ""
|
||||
# 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"{AlbumArtistName}", albumArtistName.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 = retpath.replace(R"{AudioQuality}", album.audioQuality)
|
||||
retpath = retpath.replace(R"{DurationSeconds}", str(album.duration))
|
||||
retpath = retpath.replace(R"{Duration}", __secondsToTimeStr__(album.duration))
|
||||
retpath = retpath.replace(R"{NumberOfTracks}", str(album.numberOfTracks))
|
||||
retpath = retpath.replace(R"{NumberOfVideos}", str(album.numberOfVideos))
|
||||
retpath = retpath.replace(R"{NumberOfVolumes}", str(album.numberOfVolumes))
|
||||
retpath = retpath.replace(R"{ReleaseDate}", str(album.releaseDate))
|
||||
retpath = retpath.replace(R"{RecordType}", album.type)
|
||||
retpath = retpath.replace(R"{None}", "")
|
||||
retpath = stripPath(retpath.strip())
|
||||
return base + retpath
|
||||
|
||||
|
||||
def getPlaylistPath(conf: Settings, playlist):
|
||||
# outputdir/Playlist/
|
||||
base = conf.downloadPath + '/'
|
||||
if conf.addTypeFolder:
|
||||
base = base + 'Playlist/'
|
||||
# name
|
||||
name = aigpy.path.replaceLimitChar(playlist.title, '-')
|
||||
return base + name + '/'
|
||||
|
||||
|
||||
def getTrackPath(conf: Settings, track, stream, album=None, playlist=None):
|
||||
base = './'
|
||||
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
|
||||
artists = aigpy.path.replaceLimitChar(getArtistsName(track.artists), '-')
|
||||
artist = aigpy.path.replaceLimitChar(track.artist.name, '-') if track.artist is not None else ""
|
||||
# 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"{ArtistsName}", artists.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.replace(R"{AudioQuality}", track.audioQuality)
|
||||
retpath = retpath.replace(R"{DurationSeconds}", str(track.duration))
|
||||
retpath = retpath.replace(R"{Duration}", __secondsToTimeStr__(track.duration))
|
||||
retpath = retpath.replace(R"{TrackID}", str(track.id))
|
||||
retpath = retpath.strip()
|
||||
return base + retpath + 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 + '/'
|
||||
if conf.addTypeFolder:
|
||||
base = base + 'Video/'
|
||||
|
||||
# get number
|
||||
number = __getIndexStr__(video.trackNumber)
|
||||
# get artist
|
||||
artists = aigpy.path.replaceLimitChar(getArtistsName(video.artists), '-')
|
||||
artist = aigpy.path.replaceLimitChar(video.artist.name, '-') if video.artist is not None else ""
|
||||
# get explicit
|
||||
explicit = "(Explicit)" if conf.addExplicitTag and video.explicit else ''
|
||||
# title
|
||||
title = aigpy.path.replaceLimitChar(video.title, '-')
|
||||
# year
|
||||
year = ""
|
||||
if video.releaseDate is not None:
|
||||
year = aigpy.string.getSubOnlyEnd(video.releaseDate, '-')
|
||||
# extension
|
||||
extension = ".mp4"
|
||||
|
||||
retpath = conf.videoFileFormat # R"{VideoNumber} - {ArtistName} - [{ArtistsName}] - {VideoYear} - {VideoID} - {VideoTitle}{ExplicitFlag}"
|
||||
if retpath is None or len(retpath) <= 0:
|
||||
retpath = Settings.getDefaultVideoFileFormat()
|
||||
retpath = retpath.replace(R"{VideoNumber}", number)
|
||||
retpath = retpath.replace(R"{ArtistName}", artist.strip())
|
||||
retpath = retpath.replace(R"{ArtistsName}", artists.strip())
|
||||
retpath = retpath.replace(R"{VideoTitle}", title)
|
||||
retpath = retpath.replace(R"{ExplicitFlag}", explicit)
|
||||
retpath = retpath.replace(R"{VideoYear}", year)
|
||||
retpath = retpath.replace(R"{VideoID}", str(video.id))
|
||||
retpath = retpath.strip()
|
||||
|
||||
return base + retpath + extension
|
||||
|
||||
|
||||
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 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 = list(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 = list(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 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 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 getCurAudioQuality():
|
||||
return CONF.audioQuality.name
|
||||
|
||||
def setCurAudioQuality(text):
|
||||
if CONF.audioQuality.name == text:
|
||||
return
|
||||
for item in tidal_dl.enums.AudioQuality:
|
||||
if item.name == text:
|
||||
CONF.audioQuality = item
|
||||
break
|
||||
Settings.save(CONF)
|
||||
|
||||
def getVideoQualityList():
|
||||
return map(lambda quality: quality.name, tidal_dl.enums.VideoQuality)
|
||||
|
||||
def getCurVideoQuality():
|
||||
return CONF.videoQuality.name
|
||||
|
||||
def setCurVideoQuality(text):
|
||||
if CONF.videoQuality.name == text:
|
||||
return
|
||||
for item in tidal_dl.enums.VideoQuality:
|
||||
if item.name == text:
|
||||
CONF.videoQuality = item
|
||||
break
|
||||
Settings.save(CONF)
|
||||
|
||||
def skip(finalpath, url):
|
||||
if CONF.checkExist and isNeedDownload(finalpath, 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, partSize=1048576, dictNewFiles=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)
|
||||
if userProgress is not None:
|
||||
userProgress.updateStream(stream)
|
||||
path = getTrackPath(CONF, track, stream, album, playlist)
|
||||
|
||||
if CONF.onlyM4a:
|
||||
finalpath = path.replace(".mp4", ".m4a")
|
||||
else:
|
||||
finalpath = path
|
||||
|
||||
if not dictNewFiles is None:
|
||||
dictNewFiles[os.path.basename(finalpath)] = finalpath # preserve full path to be used by caller
|
||||
|
||||
# check exist
|
||||
if skip(finalpath, 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)
|
||||
tool.setPartSize(partSize)
|
||||
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 to M4a if configured
|
||||
# note from here to end, path will be = finalpath
|
||||
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)
|
||||
# dl = m3u8dl.M3u8Download(stream.m3u8Url, video.title, path)
|
||||
# check = dl.start()
|
||||
# msg = ''
|
||||
|
||||
m3u8content = requests.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_INVALID_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.album)
|
||||
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 './'
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : __init__.py
|
||||
@Date : 2021/05/08
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
import sys
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QApplication
|
||||
|
||||
from tidal_gui import theme
|
||||
from tidal_gui.viewModel.mainModel import MainModel
|
||||
|
||||
|
||||
def main():
|
||||
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
|
||||
qss = theme.getThemeQssContent()
|
||||
app = QApplication(sys.argv)
|
||||
app.setStyleSheet(qss)
|
||||
|
||||
mainView = MainModel()
|
||||
mainView.show()
|
||||
|
||||
app.exec_()
|
||||
mainView.uninit()
|
||||
|
||||
sys.exit()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : __init__.py
|
||||
@Date : 2021/05/08
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
@@ -1,19 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : checkBox.py
|
||||
@Date : 2021/05/08
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
|
||||
from PyQt5.QtWidgets import QCheckBox
|
||||
|
||||
|
||||
class CheckBox(QCheckBox):
|
||||
def __init__(self, text: str = "", checked: bool = False):
|
||||
super(CheckBox, self).__init__()
|
||||
self.setChecked(checked)
|
||||
self.setText(text)
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : comboBox.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QComboBox, QListView
|
||||
|
||||
|
||||
class ComboBox(QComboBox):
|
||||
def __init__(self, items: list, width: int = 200):
|
||||
super(ComboBox, self).__init__()
|
||||
self.setItems(items)
|
||||
self.setFixedWidth(width)
|
||||
self.setView(QListView())
|
||||
# remove shadow
|
||||
self.view().window().setWindowFlags(Qt.Popup | Qt.FramelessWindowHint | Qt.NoDropShadowWindowHint)
|
||||
self.view().window().setAttribute(Qt.WA_TranslucentBackground)
|
||||
|
||||
def setItems(self, items):
|
||||
for item in items:
|
||||
self.addItem(str(item))
|
||||
@@ -1,156 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : framelessWidget.py
|
||||
@Date : 2021/05/08
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
|
||||
from PyQt5.QtCore import Qt, QPoint
|
||||
from PyQt5.QtGui import QMouseEvent
|
||||
from PyQt5.QtWidgets import QWidget, QGridLayout, QHBoxLayout
|
||||
|
||||
from tidal_gui.control.pushButton import PushButton
|
||||
from tidal_gui.style import ButtonStyle
|
||||
|
||||
|
||||
class FramelessWidget(QWidget):
|
||||
def __init__(self):
|
||||
super(FramelessWidget, self).__init__()
|
||||
self.setWindowFlags(Qt.FramelessWindowHint)
|
||||
self.BorderWidth = 5
|
||||
self.borderWidget = QWidget()
|
||||
self.borderWidget.setObjectName("widgetMain")
|
||||
self.borderWidget.setStyleSheet("QWidget#widgetMain{border: 1px solid #000000;};")
|
||||
|
||||
self.contentGrid = QGridLayout()
|
||||
self.contentGrid.setContentsMargins(1, 1, 1, 1)
|
||||
|
||||
self.windowBtnGrid = self.__createWindowsButtonLayout__()
|
||||
self.enableMove = True
|
||||
self.validMoveWidget = None
|
||||
self.clickPos = None
|
||||
|
||||
self.grid = QGridLayout()
|
||||
self.grid.setSpacing(0)
|
||||
self.grid.setContentsMargins(0, 0, 0, 0)
|
||||
self.grid.addWidget(self.borderWidget, 0, 0)
|
||||
self.grid.addLayout(self.contentGrid, 0, 0)
|
||||
self.setLayout(self.grid)
|
||||
|
||||
def __showMaxWindows__(self):
|
||||
if self.windowState() == Qt.WindowMaximized:
|
||||
self.showNormal()
|
||||
else:
|
||||
self.showMaximized()
|
||||
|
||||
def __createWindowsButtonLayout__(self):
|
||||
self.closeBtn = PushButton('', ButtonStyle.CloseWindow)
|
||||
self.maxBtn = PushButton('', ButtonStyle.MaxWindow)
|
||||
self.minBtn = PushButton('', ButtonStyle.MinWindow)
|
||||
|
||||
self.closeBtn.clicked.connect(self.close)
|
||||
self.minBtn.clicked.connect(self.showMinimized)
|
||||
self.maxBtn.clicked.connect(self.__showMaxWindows__)
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.setSpacing(0)
|
||||
layout.setContentsMargins(1, 1, 1, 1)
|
||||
layout.addWidget(self.minBtn)
|
||||
layout.addWidget(self.maxBtn)
|
||||
layout.addWidget(self.closeBtn)
|
||||
return layout
|
||||
|
||||
def __clickInValidMoveWidget__(self, x=-1, y=-1) -> bool:
|
||||
if self.validMoveWidget is None:
|
||||
return False
|
||||
if self.clickPos is None:
|
||||
return False
|
||||
|
||||
if x == -1 and y == -1:
|
||||
x = self.clickPos.x()
|
||||
y = self.clickPos.y()
|
||||
|
||||
pos = self.validMoveWidget.pos()
|
||||
if x < pos.x() or x > pos.x() + self.validMoveWidget.width():
|
||||
return False
|
||||
if y < pos.y() or y > pos.y() + self.validMoveWidget.height():
|
||||
return False
|
||||
return True
|
||||
|
||||
def nativeEvent(self, eventType, message):
|
||||
retVal, result = super(FramelessWidget, self).nativeEvent(eventType, message)
|
||||
# if eventType == "windows_generic_MSG":
|
||||
# msg = ctypes.wintypes.MSG.from_address(message.__int__())
|
||||
# if msg.message != win32con.WM_NCHITTEST:
|
||||
# return retVal, result
|
||||
|
||||
# # 获取鼠标移动经过时的坐标
|
||||
# x = win32api.LOWORD(msg.lParam) - self.frameGeometry().x()
|
||||
# y = win32api.HIWORD(msg.lParam) - self.frameGeometry().y()
|
||||
|
||||
# w, h = self.width(), self.height()
|
||||
# lx = x < self.BorderWidth
|
||||
# rx = x > w - self.BorderWidth
|
||||
# ty = y < self.BorderWidth
|
||||
# by = y > h - self.BorderWidth
|
||||
# if (lx and ty):# 左上角
|
||||
# return True, win32con.HTTOPLEFT
|
||||
# elif (rx and by):# 右下角
|
||||
# return True, win32con.HTBOTTOMRIGHT
|
||||
# elif (rx and ty):# 右上角
|
||||
# return True, win32con.HTTOPRIGHT
|
||||
# elif (lx and by):# 左下角
|
||||
# return True, win32con.HTBOTTOMLEFT
|
||||
# elif ty:# 上
|
||||
# return True, win32con.HTTOP
|
||||
# elif by:# 下
|
||||
# return True, win32con.HTBOTTOM
|
||||
# elif lx:# 左
|
||||
# return True, win32con.HTLEFT
|
||||
# elif rx:# 右
|
||||
# return True, win32con.HTRIGHT
|
||||
return retVal, result
|
||||
|
||||
def mousePressEvent(self, e: QMouseEvent):
|
||||
if e.button() == Qt.LeftButton:
|
||||
self.clickPos = e.pos()
|
||||
|
||||
def mouseReleaseEvent(self, e: QMouseEvent):
|
||||
if e.button() == Qt.LeftButton:
|
||||
self.clickPos = QPoint(-1, -1)
|
||||
|
||||
def mouseMoveEvent(self, e: QMouseEvent):
|
||||
if not self.enableMove:
|
||||
return
|
||||
if Qt.LeftButton & e.buttons():
|
||||
if self.__clickInValidMoveWidget__() and self.clickPos:
|
||||
self.move(e.pos() + self.pos() - self.clickPos)
|
||||
|
||||
def mouseDoubleClickEvent(self, e: QMouseEvent):
|
||||
if self.maxBtn.isHidden():
|
||||
return
|
||||
if Qt.LeftButton & e.buttons():
|
||||
if self.__clickInValidMoveWidget__(e.x(), e.y()):
|
||||
self.__showMaxWindows__()
|
||||
|
||||
def getGrid(self):
|
||||
return self.contentGrid
|
||||
|
||||
def disableMove(self):
|
||||
self.enableMove = False
|
||||
|
||||
def setValidMoveWidget(self, widget):
|
||||
self.validMoveWidget = widget
|
||||
|
||||
def setWindowButton(self, showClose=True, showMin=True, showMax=True):
|
||||
if not showMax:
|
||||
self.maxBtn.hide()
|
||||
if not showMin:
|
||||
self.minBtn.hide()
|
||||
if not showClose:
|
||||
self.closeBtn.hide()
|
||||
self.grid.addLayout(self.windowBtnGrid, 0, 0, Qt.AlignTop | Qt.AlignRight)
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : label.py
|
||||
@Date : 2021/05/08
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtWidgets import QLabel
|
||||
|
||||
from tidal_gui.style import LabelStyle
|
||||
|
||||
|
||||
class Label(QLabel):
|
||||
def __init__(self, text: str = "", style: LabelStyle = LabelStyle.Default):
|
||||
super(Label, self).__init__()
|
||||
self.setText(text)
|
||||
self.setObjectName(style.name + "Label")
|
||||
@@ -1,25 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : layout.py
|
||||
@Date : 2021/8/13
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtWidgets import QHBoxLayout, QVBoxLayout
|
||||
|
||||
|
||||
def createHBoxLayout(widgets):
|
||||
layout = QHBoxLayout()
|
||||
for item in widgets:
|
||||
layout.addWidget(item)
|
||||
return layout
|
||||
|
||||
|
||||
def createVBoxLayout(widgets):
|
||||
layout = QVBoxLayout()
|
||||
for item in widgets:
|
||||
layout.addWidget(item)
|
||||
return layout
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : line.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtWidgets import QFrame
|
||||
|
||||
|
||||
class Line(QFrame):
|
||||
def __init__(self, shape: str = 'V'):
|
||||
super(Line, self).__init__()
|
||||
self.setFrameShape(QFrame.VLine if shape == 'V' else QFrame.HLine)
|
||||
self.setFrameShadow(QFrame.Sunken)
|
||||
|
||||
self.setObjectName('VLineQFrame' if shape == 'V' else 'HLineQFrame')
|
||||
@@ -1,23 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : lineEdit.py
|
||||
@Date : 2021/05/08
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QLineEdit, QAction
|
||||
|
||||
|
||||
class LineEdit(QLineEdit):
|
||||
def __init__(self, placeholderText: str = "", iconUrl: str = ''):
|
||||
super(LineEdit, self).__init__()
|
||||
self.setPlaceholderText(placeholderText)
|
||||
|
||||
if iconUrl != '':
|
||||
action = QAction(self)
|
||||
action.setIcon(QIcon(iconUrl))
|
||||
self.addAction(action, QLineEdit.LeadingPosition)
|
||||
@@ -1,32 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : listWidget.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QListWidget, QListWidgetItem, QWidget
|
||||
|
||||
from tidal_gui.style import ListWidgetStyle
|
||||
|
||||
|
||||
class ListWidget(QListWidget):
|
||||
def __init__(self, style: ListWidgetStyle = ListWidgetStyle.Default):
|
||||
super(ListWidget, self).__init__()
|
||||
self.setObjectName(style.name + "ListWidget")
|
||||
|
||||
def addIConTextItem(self, iconUrl: str, text: str):
|
||||
self.addItem(QListWidgetItem(QIcon(iconUrl), text))
|
||||
|
||||
def addWidgetItem(self, widget: QWidget):
|
||||
item = QListWidgetItem(self)
|
||||
# item.setSizeHint(QSize(widget.width(), widget.height()))
|
||||
self.addItem(item)
|
||||
self.setItemWidget(item, widget)
|
||||
|
||||
def setAdjustMode(self):
|
||||
self.setResizeMode(QListWidget.Adjust)
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : pushButton.py
|
||||
@Date : 2021/05/08
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtGui import QIcon
|
||||
from PyQt5.QtWidgets import QPushButton
|
||||
|
||||
from tidal_gui.style import ButtonStyle
|
||||
|
||||
|
||||
class PushButton(QPushButton):
|
||||
def __init__(self,
|
||||
text: str = '',
|
||||
style: ButtonStyle = ButtonStyle.Default,
|
||||
width=0,
|
||||
iconUrl=''):
|
||||
super(PushButton, self).__init__()
|
||||
self.setText(text)
|
||||
self.setObjectName(style.name + "PushButton")
|
||||
|
||||
if width > 0:
|
||||
self.setFixedWidth(width)
|
||||
|
||||
if iconUrl != '':
|
||||
self.setIcon(QIcon(iconUrl))
|
||||
@@ -1,45 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : scrollWidget.py
|
||||
@Date : 2021/10/08
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QWidget, QScrollArea, QVBoxLayout
|
||||
from PyQt5.QtGui import QResizeEvent
|
||||
|
||||
|
||||
class ScrollWidget(QScrollArea):
|
||||
def __init__(self):
|
||||
super(ScrollWidget, self).__init__()
|
||||
self._numWidget = 0
|
||||
self._layout = QVBoxLayout()
|
||||
self._layout.addStretch(1)
|
||||
|
||||
self._mainW = QWidget()
|
||||
self._mainW.setLayout(self._layout)
|
||||
|
||||
self.setWidget(self._mainW)
|
||||
self.setWidgetResizable(True)
|
||||
|
||||
def addWidgetItem(self, widget: QWidget, isTail=False):
|
||||
if isTail:
|
||||
self._layout.insertWidget(self._numWidget, widget)
|
||||
else:
|
||||
self._layout.insertWidget(0, widget)
|
||||
self._numWidget += 1
|
||||
|
||||
def delWidgetItem(self, widget: QWidget):
|
||||
self._layout.removeWidget(widget)
|
||||
self._numWidget -= 1
|
||||
|
||||
def resizeEvent(self, e: QResizeEvent):
|
||||
super().resizeEvent(e)
|
||||
width = e.size().width()
|
||||
if width > 0:
|
||||
self._mainW.setMaximumWidth(width)
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : tableView.py
|
||||
@Date : 2021/9/10
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QStandardItemModel, QStandardItem
|
||||
from PyQt5.QtWidgets import QTableView, QAbstractItemView
|
||||
|
||||
|
||||
class TableView(QTableView):
|
||||
def __init__(self, columnNames: list, rowCount: int = 20):
|
||||
super(TableView, self).__init__()
|
||||
|
||||
self._model = QStandardItemModel()
|
||||
self._model.setColumnCount(len(columnNames))
|
||||
self._model.setRowCount(rowCount)
|
||||
|
||||
for index, name in enumerate(columnNames):
|
||||
self._model.setHeaderData(index, Qt.Horizontal, name)
|
||||
|
||||
self.setModel(self._model)
|
||||
# self.setHorizontalHeaderItem(index, QTableWidgetItem(name))
|
||||
# for index in range(0, rowCount):
|
||||
# self.setRowHeight(index, 50)
|
||||
|
||||
self.setShowGrid(False)
|
||||
self.verticalHeader().setVisible(False)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
|
||||
self.horizontalHeader().setStretchLastSection(True)
|
||||
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
def addItem(self, rowIdx: int, colIdx: int, text: str):
|
||||
item = QStandardItem(text)
|
||||
item.setTextAlignment(Qt.AlignCenter)
|
||||
self._model.setItem(rowIdx, colIdx, item)
|
||||
#
|
||||
# def addWidgetItem(self, rowIdx: int, colIdx: int, widget):
|
||||
# self.setCellWidget(rowIdx, colIdx, widget)
|
||||
@@ -1,95 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : tableWidget.py
|
||||
@Date : 2021/8/18
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
import threading
|
||||
|
||||
from PyQt5.QtCore import Qt, QUrl
|
||||
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
|
||||
|
||||
|
||||
class TableWidget(QTableWidget):
|
||||
def __init__(self, columnNames: list, rowCount: int = 20):
|
||||
super(TableWidget, self).__init__()
|
||||
self.setColumnCount(len(columnNames))
|
||||
self.setRowCount(rowCount)
|
||||
|
||||
self._lock = threading.Lock()
|
||||
self._netManager = QNetworkAccessManager()
|
||||
|
||||
self.columnAligns = []
|
||||
|
||||
for index, name in enumerate(columnNames):
|
||||
item = QTableWidgetItem(name)
|
||||
|
||||
align = Qt.AlignLeft | Qt.AlignVCenter
|
||||
if name == '#' or name == ' ':
|
||||
align = Qt.AlignCenter
|
||||
|
||||
self.columnAligns.append(align)
|
||||
item.setTextAlignment(align)
|
||||
self.setHorizontalHeaderItem(index, item)
|
||||
|
||||
for index in range(0, rowCount):
|
||||
self.setRowHeight(index, 50)
|
||||
|
||||
self.setShowGrid(False)
|
||||
self.verticalHeader().setVisible(False)
|
||||
self.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
|
||||
self.horizontalHeader().setStretchLastSection(True)
|
||||
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
|
||||
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
def changeRowCount(self, rows: int):
|
||||
if rows != self.rowCount():
|
||||
self.setRowCount(rows)
|
||||
for index in range(0, rows):
|
||||
self.setRowHeight(index, 50)
|
||||
|
||||
def addItem(self, rowIdx: int, colIdx: int, text):
|
||||
if isinstance(text, str):
|
||||
item = QTableWidgetItem(text)
|
||||
item.setTextAlignment(self.columnAligns[colIdx])
|
||||
self.setItem(rowIdx, colIdx, item)
|
||||
elif isinstance(text, QUrl):
|
||||
self.__addPicItem__(rowIdx, colIdx, text)
|
||||
|
||||
def __picDownload__(self, rowIdx, colIdx):
|
||||
reply = self.sender()
|
||||
data = reply.readAll()
|
||||
if data.size() <= 0:
|
||||
return
|
||||
|
||||
pic = QPixmap()
|
||||
pic.loadFromData(data)
|
||||
|
||||
self._lock.acquire()
|
||||
self.cellWidget(rowIdx, colIdx).setPixmap(pic.scaled(32, 32))
|
||||
self._lock.release()
|
||||
|
||||
reply.deleteLater()
|
||||
|
||||
def __addPicItem__(self, rowIdx: int, colIdx: int, url: QUrl):
|
||||
icon = Label('', LabelStyle.Icon)
|
||||
icon.setAlignment(Qt.AlignCenter)
|
||||
|
||||
self.setCellWidget(rowIdx, colIdx, icon)
|
||||
|
||||
reply = self._netManager.get(QNetworkRequest(url))
|
||||
reply.finished.connect(lambda: self.__picDownload__(rowIdx, colIdx))
|
||||
|
||||
def addWidgetItem(self, rowIdx: int, colIdx: int, widget):
|
||||
self.setCellWidget(rowIdx, colIdx, widget)
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : downloader.py
|
||||
@Date : 2021/09/15
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
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():
|
||||
if self._taskModel is not None:
|
||||
item = self._taskModel.getWaitDownloadItem()
|
||||
if item is not None:
|
||||
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()
|
||||
@@ -1,4 +0,0 @@
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>svg/buymeacoffee.svg</file>
|
||||
<file>svg/check.svg</file>
|
||||
<file>svg/down.svg</file>
|
||||
<file>svg/downHover.svg</file>
|
||||
<file>svg/github.svg</file>
|
||||
<file>svg/left.svg</file>
|
||||
<file>svg/paypal.svg</file>
|
||||
<file>svg/right.svg</file>
|
||||
<file>svg/search.svg</file>
|
||||
<file>svg/search2.svg</file>
|
||||
<file>svg/upHover.svg</file>
|
||||
<file>svg/V.svg</file>
|
||||
|
||||
<file>svg/leftTab/download.svg</file>
|
||||
<file>svg/leftTab/downloadHover.svg</file>
|
||||
<file>svg/leftTab/info.svg</file>
|
||||
<file>svg/leftTab/search.svg</file>
|
||||
<file>svg/leftTab/settings.svg</file>
|
||||
|
||||
<file>svg/taskItem/cancel.svg</file>
|
||||
<file>svg/taskItem/delete.svg</file>
|
||||
<file>svg/taskItem/expand.svg</file>
|
||||
<file>svg/taskItem/open.svg</file>
|
||||
<file>svg/taskItem/retry.svg</file>
|
||||
|
||||
<file>svg/taskTab/complete.svg</file>
|
||||
<file>svg/taskTab/download.svg</file>
|
||||
<file>svg/taskTab/error.svg</file>
|
||||
|
||||
<file>svg/windows/close.svg</file>
|
||||
<file>svg/windows/closeHover.svg</file>
|
||||
<file>svg/windows/max.svg</file>
|
||||
<file>svg/windows/min.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -1,12 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1629184159878" class="icon" viewBox="0 0 1092 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="23123" width="136.5" height="128">
|
||||
<defs>
|
||||
<style type="text/css"></style>
|
||||
</defs>
|
||||
<path d="M538.624 1017.344L0.580267 405.162667 212.445867 0h652.356266L1076.565333 405.162667z" fill="#46F256"
|
||||
p-id="23124"></path>
|
||||
<path d="M538.624 748.6464l-317.44-351.607467 67.618133-61.098666 249.787734 276.718933 249.821866-276.6848 67.618134 61.064533z"
|
||||
fill="#000000" p-id="23125"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 694 B |
@@ -1,4 +0,0 @@
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg t="1629268466285" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="18338" width="128" height="128">
|
||||
<path d="M294.314667 0L242.432 119.424H165.418667v107.648h29.653333L225.152 418.133333H178.005333l62.293334 351.146667 40.021333-0.426667 40.192 255.146667h380.501333l2.645334-17.066667 37.546666-238.08 37.888 0.426667 62.293334-351.189333h-45.056l30.08-191.018667h32.256V119.466667h-81.834667L724.906667 0H294.314667z m22.528 34.346667h385.834666l32.896 75.946666H283.818667l33.024-75.946666z m-117.333334 119.338666H824.32v39.253334H199.509333v-39.253334z m19.328 298.581334h581.76l-50.176 282.453333-241.024-2.56-240.469333 2.56-50.090667-282.453333z"
|
||||
p-id="18339" fill="#ffffff"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 764 B |
@@ -1,5 +0,0 @@
|
||||
<svg fill="#ffffff" t="1606188206696" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="2750" width="16" height="16">
|
||||
<path d="M376.123077 836.923077L51.2 510.030769c-11.815385-11.815385-11.815385-31.507692 0-43.323077l43.323077-43.323077c11.815385-11.815385 31.507692-11.815385 43.323077 0L382.030769 669.538462c7.876923 7.876923 21.661538 7.876923 29.538462 0L890.092308 187.076923c11.815385-11.815385 31.507692-11.815385 43.323077 0l43.323077 43.323077c11.815385 11.815385 11.815385 31.507692 0 43.323077L419.446154 836.923077c-11.815385 13.784615-31.507692 13.784615-43.323077 0z"
|
||||
p-id="2751"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 672 B |
@@ -1,10 +0,0 @@
|
||||
<!-- <svg t="1605685211717" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1511"
|
||||
width="150" height="150">
|
||||
<path d="M316.16 366.506667L512 561.92l195.84-195.413333L768 426.666667l-256 256-256-256z" fill="#cbcbcb"
|
||||
p-id="1512"></path>
|
||||
</svg> -->
|
||||
<svg t="1629251448482" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1609"
|
||||
width="128" height="128">
|
||||
<path d="M179.758545 374.008242a31.030303 31.030303 0 0 1 43.876849 0L512 662.372848l288.364606-288.364606a31.030303 31.030303 0 0 1 43.876849 43.876849l-310.303031 310.30303a31.030303 31.030303 0 0 1-43.876848 0l-310.303031-310.30303a31.030303 31.030303 0 0 1 0-43.876849z"
|
||||
p-id="1610" fill="#cbcbcb"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 790 B |
@@ -1,10 +0,0 @@
|
||||
<!-- <svg t="1605685211717" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1511"
|
||||
width="150" height="150">
|
||||
<path d="M316.16 366.506667L512 561.92l195.84-195.413333L768 426.666667l-256 256-256-256z" fill="#326cf3"
|
||||
p-id="1512"></path>
|
||||
</svg> -->
|
||||
<svg t="1629251448482" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1609"
|
||||
width="128" height="128">
|
||||
<path d="M179.758545 374.008242a31.030303 31.030303 0 0 1 43.876849 0L512 662.372848l288.364606-288.364606a31.030303 31.030303 0 0 1 43.876849 43.876849l-310.303031 310.30303a31.030303 31.030303 0 0 1-43.876848 0l-310.303031-310.30303a31.030303 31.030303 0 0 1 0-43.876849z"
|
||||
p-id="1610" fill="#326cf3"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 790 B |
@@ -1,5 +0,0 @@
|
||||
<svg t="1629268481541" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="19187" width="128" height="128">
|
||||
<path d="M512.032 831.904c-19.168 0-38.304-9.92-58.144-29.76-7.808-7.808-7.808-20.48 0-28.288s20.48-7.808 28.288 0C494.368 786.08 504.16 792 512.032 792s17.664-5.92 29.856-18.144c7.808-7.808 20.48-7.808 28.288 0s7.808 20.48 0 28.288c-19.84 19.84-38.976 29.76-58.144 29.76z m-512-306.4c0 49.888 4.256 95.136 12.8 135.68s20.544 75.744 36 105.536 35.008 55.904 58.656 78.336 49.344 40.928 77.056 55.456c27.744 14.528 59.456 26.304 95.2 35.264S351.84 951.04 388.8 954.624 466.496 960 510.944 960c44.448 0 85.248-1.792 122.4-5.376s73.6-9.856 109.344-18.848c35.744-8.96 67.552-20.736 95.456-35.264s53.792-33.024 77.6-55.456c23.808-22.432 43.456-48.544 58.944-78.336s27.552-64.96 36.256-105.536c8.704-40.576 13.056-85.792 13.056-135.68 0-89.376-27.744-166.368-83.2-230.976 3.2-8.608 5.952-18.496 8.256-29.6s4.544-26.816 6.656-47.104c2.144-20.288 1.344-43.712-2.4-70.272S942.56 93.888 932.256 66.24l-8-1.632c-5.344-1.088-14.048-0.704-26.144 1.088s-26.208 5.024-42.4 9.696-37.056 13.92-62.656 27.744-52.608 31.328-81.056 52.512c-48.352-14.72-115.008-30.112-200-30.112s-151.808 15.392-200.544 30.112c-28.448-21.184-55.552-38.592-81.344-52.224s-46.4-22.976-61.856-28c-15.456-5.024-29.792-8.256-42.944-9.696s-21.6-1.888-25.344-1.344c-3.744 0.544-6.496 1.152-8.256 1.888-10.304 27.648-17.408 54.752-21.344 81.312s-4.8 49.888-2.656 69.984c2.144 20.096 4.448 35.904 6.944 47.392S80 286.304 83.2 294.56C27.744 358.816 0 435.808 0 525.536z m136.544 113.888c0-58.016 21.344-110.624 64-157.856 12.8-14.4 27.648-25.312 44.544-32.704s36.096-11.616 57.6-12.608 42.048-0.8 61.6 0.608 43.744 3.296 72.544 5.696 53.696 3.616 74.656 3.616c20.96 0 45.856-1.184 74.656-3.616s52.992-4.288 72.544-5.696c19.552-1.408 40.096-1.6 61.6-0.608s40.8 5.216 57.856 12.608c17.056 7.392 32 18.304 44.8 32.704 42.656 47.232 64 99.84 64 157.856 0 34.016-3.552 64.32-10.656 90.944s-16.096 48.928-26.944 66.912c-10.848 18.016-26.048 33.216-45.6 45.632s-38.496 22.016-56.8 28.8c-18.304 6.784-41.952 12.096-70.944 15.904s-54.944 6.112-77.856 6.912c-22.944 0.8-51.808 1.216-86.656 1.216s-63.648-0.416-86.4-1.216c-22.752-0.8-48.608-3.104-77.6-6.912s-52.608-9.12-70.944-15.904c-18.304-6.816-37.248-16.416-56.8-28.8s-34.752-27.616-45.6-45.632c-10.848-18.016-19.84-40.32-26.944-66.912s-10.656-56.928-10.656-90.944zM256.032 608c0-53.024 28.64-96 64-96s64 42.976 64 96-28.64 96-64 96-64-42.976-64-96z m384 0c0-53.024 28.64-96 64-96s64 42.976 64 96-28.64 96-64 96-64-42.976-64-96z"
|
||||
p-id="19188"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg t="1629270772080" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1817"
|
||||
width="128" height="128">
|
||||
<path d="M658.059636 187.826424a31.030303 31.030303 0 0 1 0 43.876849l-288.364606 288.364606 288.364606 288.364606a31.030303 31.030303 0 0 1-43.876848 43.876848l-310.30303-310.30303a31.030303 31.030303 0 0 1 0-43.876848l310.30303-310.303031a31.030303 31.030303 0 0 1 43.876848 0z"
|
||||
p-id="1818"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 473 B |
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1629182866847" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="22424" width="128" height="128">
|
||||
<defs>
|
||||
<style type="text/css"></style>
|
||||
</defs>
|
||||
<path d="M938.666667 682.666667v170.666666a85.333333 85.333333 0 0 1-85.333334 85.333334H170.666667a85.333333 85.333333 0 0 1-85.333334-85.333334v-170.666666h85.333334v170.666666h682.666666v-170.666666h85.333334z m-384-145.664l140.501333-140.501334 60.330667 60.330667L512 700.330667l-243.498667-243.498667 60.330667-60.330667L469.333333 537.002667V85.333333h85.333334v451.669334z"
|
||||
p-id="22425" fill="#ffffff"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 798 B |
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1629183863957" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="22616" width="128" height="128">
|
||||
<defs>
|
||||
<style type="text/css"></style>
|
||||
</defs>
|
||||
<path d="M938.666667 682.666667v170.666666a85.333333 85.333333 0 0 1-85.333334 85.333334H170.666667a85.333333 85.333333 0 0 1-85.333334-85.333334v-170.666666h85.333334v170.666666h682.666666v-170.666666h85.333334z m-384-145.664l140.501333-140.501334 60.330667 60.330667L512 700.330667l-243.498667-243.498667 60.330667-60.330667L469.333333 537.002667V85.333333h85.333334v451.669334z"
|
||||
p-id="22617" fill="#e6e6e6"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 798 B |
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1629182847499" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="22040" width="128" height="128">
|
||||
<defs>
|
||||
<style type="text/css"></style>
|
||||
</defs>
|
||||
<path d="M512 981.333333C252.8 981.333333 42.666667 771.2 42.666667 512S252.8 42.666667 512 42.666667s469.333333 210.133333 469.333333 469.333333-210.133333 469.333333-469.333333 469.333333z m0-85.333333a384 384 0 1 0 0-768 384 384 0 0 0 0 768z m42.837333-298.752h42.624v85.333333h-170.666666v-85.333333h42.666666v-85.333333h-42.666666v-85.333334h128v170.666667z m-42.837333-213.333333a42.666667 42.666667 0 1 1 0-85.333334 42.666667 42.666667 0 0 1 0 85.333334z"
|
||||
p-id="22041" fill="#ffffff"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 880 B |
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1629181942259" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="21410" width="128" height="128">
|
||||
<defs>
|
||||
<style type="text/css"></style>
|
||||
</defs>
|
||||
<path d="M696.32 635.989333l229.845333 229.845334-60.330666 60.330666-229.845334-229.845333a341.333333 341.333333 0 1 1 60.330667-60.330667zM426.666667 682.666667a256 256 0 1 0 0-512 256 256 0 0 0 0 512z"
|
||||
p-id="21411" fill="#ffffff"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 621 B |
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
|
||||
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg t="1629182857650" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="22232" width="128" height="128">
|
||||
<defs>
|
||||
<style type="text/css"></style>
|
||||
</defs>
|
||||
<path d="M890.581333 797.013333l-94.592 94.592-121.088-33.706666-34.602666 14.250666L578.133333 981.333333h-133.76l-61.824-109.525333-34.56-14.506667-121.088 33.322667-94.549333-94.549333 33.706667-121.088-14.250667-34.602667L42.666667 578.133333v-133.76l109.568-61.824 14.506666-34.56-33.322666-121.088 94.506666-94.506666 121.088 33.749333 34.56-14.250667L445.696 42.666667h133.802667l61.824 109.568 34.56 14.506666 121.045333-33.322666 94.72 94.506666-33.792 121.130667 14.293333 34.56L981.333333 445.738667v133.802666l-109.525333 61.781334-14.506667 34.688 33.28 121.045333z m-123.392-126.805333l37.205334-88.832L896 529.664v-34.304l-91.605333-52.053333-36.650667-88.874667 28.245333-101.333333-24.277333-24.234667-101.674667 27.946667-88.746666-37.205334L529.621333 128h-34.304l-52.053333 91.605333-88.874667 36.650667-101.376-28.288-24.149333 24.149333 27.946667 101.674667-37.205334 88.746667L128 494.208v34.346667l91.52 52.138666 36.650667 88.874667-28.245334 101.376 24.192 24.192 101.674667-27.946667 88.746667 37.205334 51.626666 91.562666h34.346667l52.138667-91.52 88.874666-36.650666 101.376 28.245333 24.234667-24.234667-27.946667-101.589333zM512 682.666667a170.666667 170.666667 0 1 1 0-341.333334 170.666667 170.666667 0 0 1 0 341.333334z m0-85.333334a85.333333 85.333333 0 1 0 0-170.666666 85.333333 85.333333 0 0 0 0 170.666666z"
|
||||
p-id="22233" fill="#ffffff"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg t="1629268420515" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="16731" width="128" height="128">
|
||||
<path d="M350.8 591.8c-7 38.4-34.8 217.4-43 268-0.6 3.6-2 5-6 5H152.6c-15.2 0-26.2-13.2-24.2-27.8L245.6 93.2c3-19.2 20.2-33.8 40-33.8 304.6 0 330.2-7.4 408 22.8 120.2 46.6 131.2 159 88 280.6-43 125.2-145 179-280.2 180.6-86.8 1.4-139-14-150.6 48.4zM842.2 304c-3.6-2.6-5-3.6-6 2.6-4 22.8-10.2 45-17.6 67.2-79.8 227.6-301 207.8-409 207.8-12.2 0-20.2 6.6-21.8 18.8-45.2 280.8-54.2 339.4-54.2 339.4-2 14.2 7 25.8 21.2 25.8h127c17.2 0 31.4-12.6 34.8-29.8 1.4-10.8-2.2 12.2 28.8-182.6 9.2-44 28.6-39.4 58.6-39.4 142 0 252.8-57.6 285.8-224.6 13-69.6 9.2-142.8-47.6-185.2z"
|
||||
p-id="16732" fill="#ffffff"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 774 B |
@@ -1,5 +0,0 @@
|
||||
<svg t="1629270798317" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1968"
|
||||
width="128" height="128">
|
||||
<path d="M365.940364 187.826424a31.030303 31.030303 0 0 1 43.876848 0l310.30303 310.303031a31.030303 31.030303 0 0 1 0 43.876848l-310.30303 310.30303a31.030303 31.030303 0 0 1-43.876848-43.876848l288.364606-288.364606-288.364606-288.364606a31.030303 31.030303 0 0 1 0-43.876849z"
|
||||
p-id="1969"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 472 B |
@@ -1,5 +0,0 @@
|
||||
<svg t="1629269306451" class="icon" viewBox="0 0 1057 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="15526" width="128" height="128">
|
||||
<path d="M409.012356 811.965935c-108.676129 0.660645-213.058065-41.851871-289.924129-117.991225A401.242839 401.242839 0 0 1 0.006937 406.594065a401.242839 401.242839 0 0 1 119.015226-287.512775A408.344774 408.344774 0 0 1 409.012356 0.990968a408.344774 408.344774 0 0 1 289.924129 118.05729 401.275871 401.275871 0 0 1 119.08129 287.44671 401.275871 401.275871 0 0 1-119.048258 287.380645 408.344774 408.344774 0 0 1-289.891096 118.090322h-0.066065z m0-695.130838A290.617806 290.617806 0 0 0 201.63584 200.836129a285.563871 285.563871 0 0 0-84.69471 205.625806c0 162.221419 128.495484 289.626839 292.071226 289.626839 163.641806 0 292.13729-127.405419 292.13729-289.560774 0-162.221419-128.495484-289.725935-292.071225-289.725935h-0.066065zM957.942421 1005.799226l-173.980904-171.866839a55.130839 55.130839 0 0 1 0-80.235355c23.221677-22.990452 58.004645-22.990452 81.193291 0l173.947871 171.965936a55.130839 55.130839 0 0 1 0 80.235355 56.419097 56.419097 0 0 1-81.160258 0v-0.099097z"
|
||||
p-id="15527" fill="#ffffff"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg t="1629269306451" class="icon" viewBox="0 0 1057 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
p-id="15526" width="128" height="128">
|
||||
<path d="M409.012356 811.965935c-108.676129 0.660645-213.058065-41.851871-289.924129-117.991225A401.242839 401.242839 0 0 1 0.006937 406.594065a401.242839 401.242839 0 0 1 119.015226-287.512775A408.344774 408.344774 0 0 1 409.012356 0.990968a408.344774 408.344774 0 0 1 289.924129 118.05729 401.275871 401.275871 0 0 1 119.08129 287.44671 401.275871 401.275871 0 0 1-119.048258 287.380645 408.344774 408.344774 0 0 1-289.891096 118.090322h-0.066065z m0-695.130838A290.617806 290.617806 0 0 0 201.63584 200.836129a285.563871 285.563871 0 0 0-84.69471 205.625806c0 162.221419 128.495484 289.626839 292.071226 289.626839 163.641806 0 292.13729-127.405419 292.13729-289.560774 0-162.221419-128.495484-289.725935-292.071225-289.725935h-0.066065zM957.942421 1005.799226l-173.980904-171.866839a55.130839 55.130839 0 0 1 0-80.235355c23.221677-22.990452 58.004645-22.990452 81.193291 0l173.947871 171.965936a55.130839 55.130839 0 0 1 0 80.235355 56.419097 56.419097 0 0 1-81.160258 0v-0.099097z"
|
||||
p-id="15527" fill="#0f0f0f"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48">
|
||||
<path fill-opacity=".01" fill="#fff" d="M0 0h48v48H0z" data-follow-fill="#fff"/>
|
||||
<path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="#333" d="m14 14 20 20M14 34l20-20"
|
||||
data-follow-stroke="#333"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 319 B |
@@ -1,9 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48">
|
||||
<path fill-opacity=".01" fill="#fff" d="M0 0h48v48H0z" data-follow-fill="#fff"/>
|
||||
<path stroke-linejoin="round" stroke-width="4" stroke="#333" d="M8 15h32l-3 29H11L8 15z" clip-rule="evenodd"
|
||||
data-follow-stroke="#333"/>
|
||||
<path stroke-linecap="round" stroke-width="4" stroke="#333" d="M20.002 25.002v10M28.002 25v9.997"
|
||||
data-follow-stroke="#333"/>
|
||||
<path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="#333" d="M12 15 28.324 3 36 15"
|
||||
data-follow-stroke="#333"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 607 B |
@@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48">
|
||||
<path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="#333" d="M22 42H6V26M26 6h16v16"
|
||||
data-follow-stroke="#333"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 232 B |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48">
|
||||
<path stroke-linejoin="round" stroke-width="4" stroke="#333"
|
||||
d="M5 8a2 2 0 0 1 2-2h12l5 6h17a2 2 0 0 1 2 2v26a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8z" data-follow-stroke="#333"/>
|
||||
<path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="#333"
|
||||
d="m30 28-6.007 6L18 28.013M24 20v14" data-follow-stroke="#333"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 429 B |
@@ -1,8 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 48 48">
|
||||
<path fill-opacity=".01" fill="#fff" d="M0 0h48v48H0z" data-follow-fill="#fff"/>
|
||||
<path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="#333"
|
||||
d="M36.728 36.728A17.943 17.943 0 0 1 24 42c-9.941 0-18-8.059-18-18S14.059 6 24 6c4.97 0 9.47 2.015 12.728 5.272C38.386 12.93 42 17 42 17"
|
||||
data-follow-stroke="#333"/>
|
||||
<path stroke-linejoin="round" stroke-linecap="round" stroke-width="4" stroke="#333" d="M42 8v9h-9"
|
||||
data-follow-stroke="#333"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 580 B |
@@ -1,5 +0,0 @@
|
||||
<svg t="1629255942168" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8597"
|
||||
width="128" height="128">
|
||||
<path d="M512 0C229.248 0 0 229.248 0 512s229.248 512 512 512 512-229.248 512-512S794.752 0 512 0zM432.64 748.256 197.056 512.64l90.496-90.496 145.056 145.12 307.744-307.744 90.496 90.496L432.64 748.256z"
|
||||
p-id="8598" fill="#2DB84D"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 412 B |
@@ -1,5 +0,0 @@
|
||||
<svg t="1629255998157" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8792"
|
||||
width="128" height="128">
|
||||
<path d="M512 0C229.248 0 0 229.248 0 512s229.248 512 512 512 512-229.248 512-512S794.752 0 512 0zM320 768 320 256l512.256 256L320 768z"
|
||||
p-id="8793" fill="#00BCD4"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 344 B |
@@ -1,5 +0,0 @@
|
||||
<svg t="1629256110134" class="icon" viewBox="0 0 1185 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3025"
|
||||
width="128" height="128">
|
||||
<path d="M700.365715 60.074208l468.319709 783.01037a118.307323 118.307323 0 0 1-45.345458 164.539234 127.444603 127.444603 0 0 1-62.324434 16.297089H124.376114A122.739586 122.739586 0 0 1 0 903.567918a117.557248 117.557248 0 0 1 16.706221-60.346963L485.02593 60.210586A126.694528 126.694528 0 0 1 655.020257 16.365278a122.739586 122.739586 0 0 1 45.345458 43.845308zM592.764011 916.046443a80.803561 80.803561 0 1 0-80.80356-80.803561 80.803561 80.803561 0 0 0 80.735372 80.803561z m0-673.567572a95.464122 95.464122 0 0 0-94.373104 106.988006l34.09433 270.640786a61.369793 61.369793 0 0 0 121.103058 0l34.094329-270.640786a95.464122 95.464122 0 0 0-94.373103-106.919818z"
|
||||
p-id="3026" fill="#DB3340"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 878 B |
@@ -1,5 +0,0 @@
|
||||
<svg t="1629251564375" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1864"
|
||||
width="128" height="128">
|
||||
<path d="M490.061576 311.947636a31.030303 31.030303 0 0 1 43.876848 0l310.303031 310.303031a31.030303 31.030303 0 0 1-43.876849 43.876848L512 377.762909l-288.364606 288.364606a31.030303 31.030303 0 0 1-43.876849-43.876848l310.303031-310.303031z"
|
||||
p-id="1865" fill="#326cf3"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 453 B |
@@ -1,5 +0,0 @@
|
||||
<svg fill="#cbcbcb" t="1605778065489" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="6682" width="16" height="16">
|
||||
<path d="M622.276923 508.061538l257.969231-257.96923c11.815385-11.815385 11.815385-29.538462 0-41.353846l-41.353846-41.353847c-11.815385-11.815385-29.538462-11.815385-41.353846 0L539.569231 425.353846c-7.876923 7.876923-19.692308 7.876923-27.569231 0L254.030769 165.415385c-11.815385-11.815385-29.538462-11.815385-41.353846 0l-41.353846 41.353846c-11.815385 11.815385-11.815385 29.538462 0 41.353846l257.969231 257.969231c7.876923 7.876923 7.876923 19.692308 0 27.56923L169.353846 793.6c-11.815385 11.815385-11.815385 29.538462 0 41.353846l41.353846 41.353846c11.815385 11.815385 29.538462 11.815385 41.353846 0L512 618.338462c7.876923-7.876923 19.692308-7.876923 27.569231 0l257.969231 257.96923c11.815385 11.815385 29.538462 11.815385 41.353846 0l41.353846-41.353846c11.815385-11.815385 11.815385-29.538462 0-41.353846L622.276923 535.630769c-5.907692-7.876923-5.907692-19.692308 0-27.569231z"
|
||||
p-id="6683"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg fill="#ffffff" t="1605778065489" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="6682" width="16" height="16">
|
||||
<path d="M622.276923 508.061538l257.969231-257.96923c11.815385-11.815385 11.815385-29.538462 0-41.353846l-41.353846-41.353847c-11.815385-11.815385-29.538462-11.815385-41.353846 0L539.569231 425.353846c-7.876923 7.876923-19.692308 7.876923-27.569231 0L254.030769 165.415385c-11.815385-11.815385-29.538462-11.815385-41.353846 0l-41.353846 41.353846c-11.815385 11.815385-11.815385 29.538462 0 41.353846l257.969231 257.969231c7.876923 7.876923 7.876923 19.692308 0 27.56923L169.353846 793.6c-11.815385 11.815385-11.815385 29.538462 0 41.353846l41.353846 41.353846c11.815385 11.815385 29.538462 11.815385 41.353846 0L512 618.338462c7.876923-7.876923 19.692308-7.876923 27.569231 0l257.969231 257.96923c11.815385 11.815385 29.538462 11.815385 41.353846 0l41.353846-41.353846c11.815385-11.815385 11.815385-29.538462 0-41.353846L622.276923 535.630769c-5.907692-7.876923-5.907692-19.692308 0-27.569231z"
|
||||
p-id="6683"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,5 +0,0 @@
|
||||
<svg fill="#cbcbcb" t="1605778299751" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="12048" width="16" height="16">
|
||||
<path d="M170.666667 85.333333h682.666666a85.333333 85.333333 0 0 1 85.333334 85.333334v682.666666a85.333333 85.333333 0 0 1-85.333334 85.333334H170.666667a85.333333 85.333333 0 0 1-85.333334-85.333334V170.666667a85.333333 85.333333 0 0 1 85.333334-85.333334z m0 85.333334v682.666666h682.666666V170.666667H170.666667z"
|
||||
p-id="12049"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 526 B |
@@ -1,4 +0,0 @@
|
||||
<svg fill="#cbcbcb" t="1605777927585" class="icon" viewBox="0 0 1024 1024" version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg" p-id="5690" width="16" height="16">
|
||||
<path d="M938.666667 469.333333v85.333334H85.333333v-85.333334z" p-id="5691"></path>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 260 B |
@@ -1,810 +0,0 @@
|
||||
:root {
|
||||
--Font_Family: Microsoft YaHei UI;
|
||||
--Font_Size: 12px;
|
||||
--Font_Size_TabHeader: 12px;
|
||||
--Font_Size_TableHeader: 12px;
|
||||
--Font_Size_PageTitle: 15px;
|
||||
--Font_Size_HugeTitle: 30px;
|
||||
--Font_Size_MsgLabel: 15px;
|
||||
|
||||
--Color_Default: #ffffff;
|
||||
--Color_DefaultHover: #f0f0f0;
|
||||
--Color_DefaultPressed: #ececec;
|
||||
--Color_DefaultText: #212121;
|
||||
|
||||
--Color_Primary: #326cf3;
|
||||
--Color_PrimaryHover: #477bf4;
|
||||
--Color_PrimaryPressed: #84a7f8;
|
||||
--Color_PrimaryText: #ffffff;
|
||||
|
||||
--Color_Success: #2db84d;
|
||||
--Color_SuccessHover: #42bf5f;
|
||||
--Color_SuccessPressed: #81d494;
|
||||
|
||||
--Color_Danger: #db3340;
|
||||
--Color_DangerHover: #df4853;
|
||||
--Color_DangerPressed: #e9858c;
|
||||
|
||||
--Color_Warning: #e9af20;
|
||||
--Color_WarningHover: #ebb737;
|
||||
--Color_WarningPressed: #f2cf79;
|
||||
|
||||
--Color_Info: #00bcd4;
|
||||
--Color_InfoHover: #1ac3d8;
|
||||
--Color_InfoPressed: #66d7e5;
|
||||
|
||||
--Color_Border: #cbcbcb;
|
||||
--Color_WindowsIcon: #cbcbcb;
|
||||
--Color_Background: #eeeeee;
|
||||
--Color_Shadow: #AA000000;
|
||||
|
||||
--Color_Transparent: transparent;
|
||||
|
||||
--Size_InputPaddingLR: 8px;
|
||||
--Size_InputPaddingTB: 6px;
|
||||
|
||||
--Size_HeaderHeight: 45px;
|
||||
--Size_ItemHeight: 18px;
|
||||
--Size_ProgressHeight: 26px;
|
||||
--Size_ControlHeight: 18px;
|
||||
|
||||
--Size_ControlPaddingLR: 8px;
|
||||
--Size_ControlPaddingTB: 6px;
|
||||
|
||||
--Size_IconSize: 24px;
|
||||
--Size_CornerRadius: 4px;
|
||||
|
||||
--Size_CalendarWidth: 287px;
|
||||
--Size_CalendarHeight: 300px;
|
||||
}
|
||||
|
||||
/* #QSS_START */
|
||||
|
||||
* {
|
||||
font-family: var(--Font_Family);
|
||||
font-size: var(--Font_Size);
|
||||
|
||||
background: var(--Color_Default);
|
||||
color: var(--Color_DefaultText);
|
||||
}
|
||||
|
||||
/* Widget */
|
||||
|
||||
QWidget#MainViewLeftWidget {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
QWidget#TestWidget {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
QWidget#BaseWidget {
|
||||
background: var(--Color_Default);
|
||||
}
|
||||
|
||||
|
||||
|
||||
QWidget#TaskItemView {
|
||||
background: var(--Color_Default);
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: var(--Color_Border);
|
||||
border-radius: var(--Size_CornerRadius);
|
||||
}
|
||||
|
||||
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 */
|
||||
|
||||
QPushButton {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: var(--Color_DefaultPressed);
|
||||
padding-left: var(--Size_InputPaddingLR);
|
||||
padding-right: var(--Size_InputPaddingLR);
|
||||
padding-top: var(--Size_InputPaddingTB);
|
||||
padding-bottom: var(--Size_InputPaddingTB);
|
||||
border-radius: var(--Size_CornerRadius);
|
||||
color: var(--Color_DefaultText);
|
||||
background: var(--Color_Default);
|
||||
}
|
||||
|
||||
QPushButton::hover {
|
||||
color: var(--Color_DefaultText);
|
||||
background: var(--Color_DefaultHover);
|
||||
}
|
||||
|
||||
QPushButton::pressed {
|
||||
color: var(--Color_DefaultText);
|
||||
background: var(--Color_DefaultPressed);
|
||||
}
|
||||
|
||||
|
||||
QPushButton#PrimaryPushButton {
|
||||
border-color: var(--Color_PrimaryPressed);
|
||||
color: var(--Color_PrimaryText);
|
||||
background: var(--Color_Primary);
|
||||
}
|
||||
|
||||
QPushButton#PrimaryPushButton::hover {
|
||||
background: var(--Color_PrimaryHover);
|
||||
}
|
||||
|
||||
QPushButton#PrimaryPushButton::pressed {
|
||||
background: var(--Color_PrimaryPressed);
|
||||
}
|
||||
|
||||
QPushButton#SuccessPushButton {
|
||||
border-color: var(--Color_SuccessPressed);
|
||||
color: var(--Color_PrimaryText);
|
||||
background: var(--Color_Success);
|
||||
}
|
||||
|
||||
QPushButton#SuccessPushButton::hover {
|
||||
background: var(--Color_SuccessHover);
|
||||
}
|
||||
|
||||
QPushButton#SuccessPushButton::pressed {
|
||||
background: var(--Color_SuccessPressed);
|
||||
}
|
||||
|
||||
QPushButton#DangerPushButton {
|
||||
border-color: var(--Color_DangerPressed);
|
||||
color: var(--Color_PrimaryText);
|
||||
background: var(--Color_Danger);
|
||||
}
|
||||
|
||||
QPushButton#DangerPushButton::hover {
|
||||
background: var(--Color_DangerHover);
|
||||
}
|
||||
|
||||
QPushButton#DangerPushButton::pressed {
|
||||
background: var(--Color_DangerPressed);
|
||||
}
|
||||
|
||||
QPushButton#WarningPushButton {
|
||||
border-color: var(--Color_WarningPressed);
|
||||
color: var(--Color_PrimaryText);
|
||||
background: var(--Color_Warning);
|
||||
}
|
||||
|
||||
QPushButton#WarningPushButton::hover {
|
||||
background: var(--Color_WarningHover);
|
||||
}
|
||||
|
||||
QPushButton#WarningPushButton::pressed {
|
||||
background: var(--Color_WarningPressed);
|
||||
}
|
||||
|
||||
QPushButton#InfoPushButton {
|
||||
border-color: var(--Color_InfoPressed);
|
||||
color: var(--Color_PrimaryText);
|
||||
background: var(--Color_Info);
|
||||
}
|
||||
|
||||
QPushButton#InfoPushButton::hover {
|
||||
background: var(--Color_InfoHover);
|
||||
}
|
||||
|
||||
QPushButton#InfoPushButton::pressed {
|
||||
background: var(--Color_InfoPressed);
|
||||
}
|
||||
|
||||
/* Software Buton */
|
||||
|
||||
QPushButton#SoftwareIconPushButton {
|
||||
border-style: none;
|
||||
border-radius: 0px;
|
||||
height: 26;
|
||||
width: 26;
|
||||
background: var(--Color_Transparent);
|
||||
image: url($RESOURCE_PATH$/svg/V.svg)
|
||||
}
|
||||
QPushButton#SoftwareIconPushButton::hover {
|
||||
image: url($RESOURCE_PATH$/svg/V.svg)
|
||||
}
|
||||
QPushButton#SoftwareIconPushButton::pressed {
|
||||
image: url($RESOURCE_PATH$/svg/V.svg)
|
||||
}
|
||||
|
||||
|
||||
/* Windows Buton */
|
||||
QPushButton#CloseWindowPushButton {
|
||||
border-style: none;
|
||||
border-radius: 0px;
|
||||
background: var(--Color_Transparent);
|
||||
image: url($RESOURCE_PATH$/svg/windows/close.svg)
|
||||
}
|
||||
|
||||
QPushButton#CloseWindowPushButton::hover {
|
||||
background: #ff1a1a;
|
||||
image: url($RESOURCE_PATH$/svg/windows/closeHover.svg)
|
||||
}
|
||||
|
||||
QPushButton#CloseWindowPushButton::pressed {
|
||||
background: #ff6666;
|
||||
image: url($RESOURCE_PATH$/svg/windows/closeHover.svg)
|
||||
}
|
||||
|
||||
QPushButton#MaxWindowPushButton {
|
||||
border-style: none;
|
||||
border-radius: 0px;
|
||||
background: var(--Color_Transparent);
|
||||
image: url($RESOURCE_PATH$/svg/windows/max.svg)
|
||||
}
|
||||
|
||||
QPushButton#MaxWindowPushButton::hover {
|
||||
background: var(--Color_DefaultHover);
|
||||
}
|
||||
|
||||
QPushButton#MaxWindowPushButton::pressed {
|
||||
background: var(--Color_DefaultPressed);
|
||||
}
|
||||
|
||||
QPushButton#MinWindowPushButton {
|
||||
border-style: none;
|
||||
border-radius: 0px;
|
||||
background: var(--Color_Transparent);
|
||||
image: url($RESOURCE_PATH$/svg/windows/min.svg)
|
||||
}
|
||||
|
||||
QPushButton#MinWindowPushButton::hover {
|
||||
background: var(--Color_DefaultHover);
|
||||
}
|
||||
|
||||
QPushButton#MinWindowPushButton::pressed {
|
||||
background: var(--Color_DefaultPressed);
|
||||
}
|
||||
|
||||
/* Task item tool button */
|
||||
QPushButton#TaskRetryPushButton {
|
||||
border-style: none;
|
||||
background: var(--Color_Default);
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
image: url($RESOURCE_PATH$/svg/taskItem/retry.svg)
|
||||
}
|
||||
QPushButton#TaskRetryPushButton::hover {
|
||||
background: var(--Color_DefaultHover);
|
||||
}
|
||||
QPushButton#TaskRetryPushButton::pressed {
|
||||
background: var(--Color_DefaultPressed);
|
||||
}
|
||||
|
||||
QPushButton#TaskCancelPushButton {
|
||||
border-style: none;
|
||||
background: var(--Color_Default);
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
image: url($RESOURCE_PATH$/svg/taskItem/cancel.svg)
|
||||
}
|
||||
QPushButton#TaskCancelPushButton::hover {
|
||||
background: var(--Color_DefaultHover);
|
||||
}
|
||||
QPushButton#TaskCancelPushButton::pressed {
|
||||
background: var(--Color_DefaultPressed);
|
||||
}
|
||||
|
||||
QPushButton#TaskDeletePushButton {
|
||||
border-style: none;
|
||||
background: var(--Color_Default);
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
image: url($RESOURCE_PATH$/svg/taskItem/delete.svg)
|
||||
}
|
||||
QPushButton#TaskDeletePushButton::hover {
|
||||
background: var(--Color_DefaultHover);
|
||||
}
|
||||
QPushButton#TaskDeletePushButton::pressed {
|
||||
background: var(--Color_DefaultPressed);
|
||||
}
|
||||
|
||||
QPushButton#TaskOpenPushButton {
|
||||
border-style: none;
|
||||
background: var(--Color_Default);
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
image: url($RESOURCE_PATH$/svg/taskItem/open.svg)
|
||||
}
|
||||
QPushButton#TaskOpenPushButton::hover {
|
||||
background: var(--Color_DefaultHover);
|
||||
}
|
||||
QPushButton#TaskOpenPushButton::pressed {
|
||||
background: var(--Color_DefaultPressed);
|
||||
}
|
||||
|
||||
QPushButton#TaskExpandPushButton {
|
||||
border-style: none;
|
||||
background: var(--Color_Default);
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
image: url($RESOURCE_PATH$/svg/taskItem/expand.svg)
|
||||
}
|
||||
QPushButton#TaskExpandPushButton::hover {
|
||||
background: var(--Color_DefaultHover);
|
||||
}
|
||||
QPushButton#TaskExpandPushButton::pressed {
|
||||
background: var(--Color_DefaultPressed);
|
||||
}
|
||||
|
||||
/* MainTab Icon Button */
|
||||
QPushButton#SearchIconPushButton {
|
||||
border-style: none;
|
||||
border-radius: 0px;
|
||||
height: 26;
|
||||
width: 26;
|
||||
background: var(--Color_Transparent);
|
||||
image: url($RESOURCE_PATH$/svg/leftTab/search.svg)
|
||||
}
|
||||
QPushButton#SearchIconPushButton::hover {
|
||||
image: url($RESOURCE_PATH$/svg/leftTab/search.svg)
|
||||
}
|
||||
QPushButton#SearchIconPushButton::pressed {
|
||||
image: url($RESOURCE_PATH$/svg/leftTab/search.svg)
|
||||
}
|
||||
|
||||
QPushButton#TaskIconPushButton {
|
||||
border-style: none;
|
||||
border-radius: 0px;
|
||||
height: 26;
|
||||
width: 26;
|
||||
background: var(--Color_Transparent);
|
||||
image: url($RESOURCE_PATH$/svg/leftTab/download.svg)
|
||||
}
|
||||
QPushButton#TaskIconPushButton::hover {
|
||||
image: url($RESOURCE_PATH$/svg/leftTab/downloadHover.svg)
|
||||
}
|
||||
QPushButton#TaskIconPushButton::pressed {
|
||||
image: url($RESOURCE_PATH$/svg/leftTab/downloadHover.svg)
|
||||
}
|
||||
|
||||
QPushButton#SettingsIconPushButton {
|
||||
border-style: none;
|
||||
border-radius: 0px;
|
||||
height: 26;
|
||||
width: 26;
|
||||
background: var(--Color_Transparent);
|
||||
image: url($RESOURCE_PATH$/svg/leftTab/settings.svg)
|
||||
}
|
||||
QPushButton#SettingsIconPushButton::hover {
|
||||
image: url($RESOURCE_PATH$/svg/leftTab/settings.svg)
|
||||
}
|
||||
QPushButton#SettingsIconPushButton::pressed {
|
||||
image: url($RESOURCE_PATH$/svg/leftTab/settings.svg)
|
||||
}
|
||||
|
||||
QPushButton#AboutIconPushButton {
|
||||
border-style: none;
|
||||
border-radius: 0px;
|
||||
height: 26;
|
||||
width: 26;
|
||||
background: var(--Color_Transparent);
|
||||
image: url($RESOURCE_PATH$/svg/leftTab/info.svg)
|
||||
}
|
||||
QPushButton#AboutIconPushButton::hover {
|
||||
image: url($RESOURCE_PATH$/svg/leftTab/info.svg)
|
||||
}
|
||||
QPushButton#AboutIconPushButton::pressed {
|
||||
image: url($RESOURCE_PATH$/svg/leftTab/info.svg)
|
||||
}
|
||||
|
||||
|
||||
QPushButton#PrePagePushButton {
|
||||
border-style: none;
|
||||
border-radius: 0px;
|
||||
border-width: 1px;
|
||||
border-color: var(--Color_Border);
|
||||
height: 18;
|
||||
width: 18;
|
||||
background: var(--Color_DefaultHover);
|
||||
image: url($RESOURCE_PATH$/svg/left.svg)
|
||||
}
|
||||
QPushButton#PrePagePushButton::hover {
|
||||
border-style: solid;
|
||||
image: url($RESOURCE_PATH$/svg/left.svg)
|
||||
}
|
||||
QPushButton#PrePagePushButton::pressed {
|
||||
image: url($RESOURCE_PATH$/svg/left.svg)
|
||||
}
|
||||
|
||||
QPushButton#NextPagePushButton {
|
||||
border-style: none;
|
||||
border-radius: 0px;
|
||||
border-width: 1px;
|
||||
border-color: var(--Color_Border);
|
||||
height: 18;
|
||||
width: 18;
|
||||
background: var(--Color_DefaultHover);
|
||||
image: url($RESOURCE_PATH$/svg/right.svg)
|
||||
}
|
||||
QPushButton#NextPagePushButton::hover {
|
||||
border-style: solid;
|
||||
image: url($RESOURCE_PATH$/svg/right.svg)
|
||||
}
|
||||
QPushButton#NextPagePushButton::pressed {
|
||||
image: url($RESOURCE_PATH$/svg/right.svg)
|
||||
}
|
||||
/* LineEdit Style */
|
||||
|
||||
QLineEdit {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: var(--Color_Border);
|
||||
padding-left: var(--Size_InputPaddingLR);
|
||||
padding-right: var(--Size_InputPaddingLR);
|
||||
padding-top: var(--Size_InputPaddingTB);
|
||||
padding-bottom: var(--Size_InputPaddingTB);
|
||||
border-radius: var(--Size_CornerRadius);
|
||||
min-height: var(--Size_ControlHeight);
|
||||
color: var(--Color_DefaultText);
|
||||
background: var(--Color_Default);
|
||||
}
|
||||
|
||||
QLineEdit::hover {
|
||||
border-color: var(--Color_Primary);
|
||||
}
|
||||
|
||||
QLineEdit::focus {
|
||||
border-color: var(--Color_Primary);
|
||||
}
|
||||
|
||||
QLineEdit::disabled {
|
||||
background: var(--Color_DefaultHover);
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* CheckBox Style */
|
||||
|
||||
QCheckBox::indicator {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
padding: 2px;
|
||||
|
||||
border: 1px solid var(--Color_Border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
QCheckBox::indicator::unchecked:hover {
|
||||
border: 1px solid var(--Color_PrimaryHover);
|
||||
}
|
||||
|
||||
QCheckBox::indicator::checked {
|
||||
background: var(--Color_Primary);
|
||||
image: url($RESOURCE_PATH$/svg/check.svg);
|
||||
border: 1px solid var(--Color_Primary);
|
||||
}
|
||||
|
||||
/* TabWidget */
|
||||
|
||||
QTabWidget::pane {
|
||||
border:none;
|
||||
border-top: 1px solid var(--Color_Border);
|
||||
}
|
||||
QTabBar::tab {
|
||||
border:none;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding-bottom: 3px;
|
||||
font-size: var(--Font_Size_TabHeader);
|
||||
font-weight: bold;
|
||||
min-width: 60;
|
||||
}
|
||||
QTabBar::tab:selected {
|
||||
color: var(--Color_Primary);
|
||||
border-color: var(--Color_Primary);
|
||||
border-style: solid;
|
||||
border-bottom-width: 3px;
|
||||
}
|
||||
|
||||
/* Label */
|
||||
QLabel#PageTitleLabel {
|
||||
font-size: var(--Font_Size_PageTitle);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
QLabel#PageSubTitleLabel {
|
||||
font-size: var(--Font_Size_PageTitle);
|
||||
}
|
||||
|
||||
QLabel#HugeTitleLabel {
|
||||
font-size: var(--Font_Size_HugeTitle);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
QLabel#LogoBottomLabel {
|
||||
font-size: var(--Font_Size_MsgLabel);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
QLabel#SearchErrLabel {
|
||||
font-size: var(--Font_Size);
|
||||
color: var(--Color_Danger);
|
||||
max-height: var(--Font_Size_MsgLabel);
|
||||
}
|
||||
|
||||
QLabel#IconLabel {
|
||||
background: var(--Color_Transparent);
|
||||
}
|
||||
|
||||
QLabel#BoldLabel {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
QLabel#ItalicLabel {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
QLabel#TagLabel {
|
||||
background: var(--Color_DangerPressed);
|
||||
}
|
||||
|
||||
|
||||
/* QFrame */
|
||||
QFrame#VLineQFrame
|
||||
{
|
||||
border-top-style: none;
|
||||
border-bottom-style: none;
|
||||
border-right-style: none;
|
||||
border-left: 1px solid var(--Color_Border);
|
||||
}
|
||||
|
||||
QFrame#HLineQFrame
|
||||
{
|
||||
border-top: 1px solid var(--Color_Border);
|
||||
border-bottom-style: none;
|
||||
border-right-style: none;
|
||||
border-left-style: none;
|
||||
}
|
||||
|
||||
/* QComboBox */
|
||||
QComboBox {
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-color: var(--Color_Border);
|
||||
padding-left: var(--Size_InputPaddingLR);
|
||||
padding-right: var(--Size_InputPaddingLR);
|
||||
padding-top: var(--Size_InputPaddingTB);
|
||||
padding-bottom: var(--Size_InputPaddingTB);
|
||||
border-radius: var(--Size_CornerRadius);
|
||||
min-height: var(--Size_ControlHeight);
|
||||
color: var(--Color_DefaultText);
|
||||
background: var(--Color_Default);
|
||||
}
|
||||
|
||||
QComboBox::hover {
|
||||
border-color: var(--Color_Primary);
|
||||
}
|
||||
|
||||
QComboBox::focus {
|
||||
border-color: var(--Color_Primary);
|
||||
}
|
||||
|
||||
QComboBox::drop-down {
|
||||
subcontrol-position:center right;
|
||||
margin-right: 15px;
|
||||
border-left-style: none;
|
||||
}
|
||||
QComboBox::down-arrow {
|
||||
image: url($RESOURCE_PATH$/svg/down.svg);
|
||||
width : 24px;
|
||||
height : 24px;
|
||||
}
|
||||
QComboBox::down-arrow:hover {
|
||||
image: url($RESOURCE_PATH$/svg/downHover.svg);
|
||||
}
|
||||
|
||||
QComboBox::down-arrow:on {
|
||||
image: url($RESOURCE_PATH$/svg/upHover.svg);
|
||||
}
|
||||
|
||||
QComboBox QAbstractItemView {
|
||||
margin-top:5px;
|
||||
border:1px solid var(--Color_Primary);
|
||||
outline:none;
|
||||
}
|
||||
|
||||
QComboBox QAbstractItemView::item {
|
||||
height: var(--Size_ItemHeight);
|
||||
|
||||
padding-top: var(--Size_InputPaddingTB);
|
||||
padding-bottom: var(--Size_InputPaddingTB);
|
||||
}
|
||||
|
||||
QComboBox QAbstractItemView::item:selected{
|
||||
color: var(--Color_PrimaryText);
|
||||
background: var(--Color_Primary);
|
||||
}
|
||||
|
||||
|
||||
/* QListWidget */
|
||||
QListWidget#TaskTabListWidget {
|
||||
border-style: none;
|
||||
background: var(--Color_Default);
|
||||
max-width: 150px;
|
||||
outline:none;
|
||||
}
|
||||
|
||||
QListWidget#TaskTabListWidget::item
|
||||
{
|
||||
height:32px;
|
||||
padding-left: var(--Size_ControlPaddingLR);
|
||||
border: none;
|
||||
border-radius: var(--Size_CornerRadius);
|
||||
margin-top:5px;
|
||||
}
|
||||
|
||||
QListWidget#TaskTabListWidget::item:hover
|
||||
{
|
||||
background: var(--Color_DefaultHover);
|
||||
color: var(--Color_DefaultText);
|
||||
}
|
||||
|
||||
QListWidget#TaskTabListWidget::item:selected
|
||||
{
|
||||
background: var(--Color_Primary);
|
||||
color: var(--Color_PrimaryText);
|
||||
}
|
||||
|
||||
|
||||
QListWidget#TaskContentListWidget {
|
||||
border-style: none;
|
||||
background: var(--Color_Default);
|
||||
outline:none;
|
||||
}
|
||||
|
||||
QListWidget#TaskContentListWidget::item
|
||||
{
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: var(--Size_CornerRadius);
|
||||
border-color: var(--Color_Border);
|
||||
|
||||
min-height:80px;
|
||||
margin-top:5px;
|
||||
background: var(--Color_Default);
|
||||
}
|
||||
|
||||
QListWidget#TaskContentListWidget::item:hover
|
||||
{
|
||||
background: var(--Color_Default);
|
||||
}
|
||||
|
||||
QListWidget#TaskContentListWidget::item:selected
|
||||
{
|
||||
background: var(--Color_Default);
|
||||
}
|
||||
|
||||
QWidget#DownloadItemsWidget {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-top: 1px solid var(--Color_Border);
|
||||
border-bottom: none;
|
||||
background: var(--Color_Default);
|
||||
outline:none;
|
||||
}
|
||||
|
||||
QWidget#DownloadItemView {
|
||||
border-style: none;
|
||||
margin-top: 2px;
|
||||
background: var(--Color_Default);
|
||||
}
|
||||
|
||||
|
||||
QListWidget#DownloadItemsListWidget {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-top: 1px solid var(--Color_Border);
|
||||
border-bottom: none;
|
||||
background: var(--Color_Default);
|
||||
outline:none;
|
||||
}
|
||||
|
||||
QListWidget#DownloadItemsListWidget::item
|
||||
{
|
||||
border-style: none;
|
||||
min-height:45px;
|
||||
margin-top:2px;
|
||||
background: var(--Color_Default);
|
||||
}
|
||||
|
||||
QListWidget#DownloadItemsListWidget::item:hover
|
||||
{
|
||||
background: var(--Color_Default);
|
||||
}
|
||||
|
||||
QListWidget#DownloadItemsListWidget::item:selected
|
||||
{
|
||||
background: var(--Color_Default);
|
||||
}
|
||||
|
||||
QListWidget QScrollBar::vertical{
|
||||
background:transparent;
|
||||
width: 4px;
|
||||
border-radius:6px;
|
||||
}
|
||||
QListWidget QScrollBar::handle{
|
||||
background: lightgray;
|
||||
border-radius:6px;
|
||||
}
|
||||
QListWidget QScrollBar::handle:hover{background:gray;}
|
||||
QListWidget QScrollBar::sub-line{background:transparent;}
|
||||
QListWidget QScrollBar::add-line{background:transparent;}
|
||||
|
||||
/* QTableWidget */
|
||||
QTableWidget {
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
border-top: none;
|
||||
border-bottom: 1px solid var(--Color_Border);
|
||||
|
||||
text-align: center;
|
||||
color: var(--Color_DefaultText);
|
||||
background: var(--Color_DefaultHover);
|
||||
padding:5px;
|
||||
|
||||
}
|
||||
|
||||
QTableWidget::item{
|
||||
margin-top: 5px;
|
||||
background: var(--Color_Default);
|
||||
}
|
||||
QTableWidget::item:selected{
|
||||
color: var(--Color_PrimaryText);
|
||||
background: var(--Color_Primary);
|
||||
}
|
||||
|
||||
|
||||
QTableWidget QHeaderView::section {
|
||||
color: var(--Color_DefaultText);
|
||||
background: var(--Color_DefaultHover);
|
||||
text-align: center;
|
||||
height: var(--Size_HeaderHeight);
|
||||
border: none;
|
||||
font-size: var(--Font_Size_TableHeader);
|
||||
}
|
||||
|
||||
QTableWidget QScrollBar::vertical{
|
||||
background:transparent;
|
||||
width: 4px;
|
||||
border-radius:6px;
|
||||
}
|
||||
QTableWidget QScrollBar::handle{
|
||||
background: lightgray;
|
||||
border-radius:6px;
|
||||
}
|
||||
QTableWidget QScrollBar::handle:hover{background:gray;}
|
||||
QTableWidget QScrollBar::sub-line{background:transparent;}
|
||||
QTableWidget QScrollBar::add-line{background:transparent;}
|
||||
|
||||
/* QProgressBar */
|
||||
QProgressBar {
|
||||
border:none;
|
||||
background:rgb(230, 230, 230);
|
||||
border-radius:0px;
|
||||
text-align:center;
|
||||
color:gray;
|
||||
}
|
||||
QProgressBar::chunk {
|
||||
background:rgb(71, 137, 250);
|
||||
border-radius:0px;
|
||||
}
|
||||
|
||||
/* QScrollArea */
|
||||
QScrollArea {
|
||||
border: none;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
'''
|
||||
@File : enum.py
|
||||
@Date : 2021/05/08
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
'''
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ButtonStyle(Enum):
|
||||
Default = 0,
|
||||
Primary = 1,
|
||||
Success = 2,
|
||||
Danger = 3,
|
||||
Warning = 4,
|
||||
Info = 5,
|
||||
|
||||
CloseWindow = 6,
|
||||
MaxWindow = 7,
|
||||
MinWindow = 8,
|
||||
|
||||
SearchIcon = 9,
|
||||
TaskIcon = 10,
|
||||
SettingsIcon = 11,
|
||||
AboutIcon = 12,
|
||||
|
||||
SoftwareIcon = 13
|
||||
|
||||
PrePage = 14
|
||||
NextPage = 15
|
||||
|
||||
TaskRetry = 16,
|
||||
TaskCancel = 17,
|
||||
TaskDelete = 18,
|
||||
TaskOpen = 19,
|
||||
TaskExpand = 20,
|
||||
|
||||
|
||||
class LabelStyle(Enum):
|
||||
Default = 0,
|
||||
PageTitle = 1,
|
||||
PageSubTitle = 2,
|
||||
HugeTitle = 3,
|
||||
LogoBottom = 4,
|
||||
SearchErr = 5,
|
||||
Icon = 6,
|
||||
Bold = 7,
|
||||
Italic = 9,
|
||||
Tag = 10
|
||||
|
||||
|
||||
class ThemeStyle(Enum):
|
||||
Default = 0,
|
||||
Dark = 1,
|
||||
|
||||
|
||||
class ListWidgetStyle(Enum):
|
||||
Default = 0,
|
||||
TaskTab = 1,
|
||||
DownloadItems = 2,
|
||||
@@ -1,66 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : qssParse.py
|
||||
@Date : 2021/05/08
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import aigpy
|
||||
|
||||
from tidal_gui.style import ThemeStyle
|
||||
|
||||
_RESOURCE_PATH = './resource'
|
||||
if os.path.isdir(_RESOURCE_PATH):
|
||||
_RESOURCE_PATH = os.path.abspath(_RESOURCE_PATH).replace('\\', '/')
|
||||
else:
|
||||
_RESOURCE_PATH = aigpy.path.getDirName(__file__).replace('\\', '/') + "resource"
|
||||
|
||||
|
||||
def __getParam__(line: str):
|
||||
key = aigpy.string.getSub(line, "--", ":")
|
||||
value = aigpy.string.getSub(line, ":", ";")
|
||||
return key, value
|
||||
|
||||
|
||||
def __parseParamsList__(content: str) -> dict:
|
||||
globalStr = aigpy.string.getSub(content, ":root", "}")
|
||||
lines = globalStr.split("\n")
|
||||
|
||||
array = {}
|
||||
for line in lines:
|
||||
key, value = __getParam__(line)
|
||||
if key == "" or value == "":
|
||||
continue
|
||||
array[key] = value
|
||||
return array
|
||||
|
||||
|
||||
def __parseQss__(content: str, params: dict) -> str:
|
||||
content = aigpy.string.getSub(content, "/* #QSS_START */")
|
||||
for key in params:
|
||||
content = content.replace("var(--" + key + ")", params[key])
|
||||
|
||||
content = content.replace("$RESOURCE_PATH$", _RESOURCE_PATH)
|
||||
return content
|
||||
|
||||
|
||||
def __getQss__(filePath: str) -> str:
|
||||
content = aigpy.file.getContent(filePath)
|
||||
params = __parseParamsList__(content)
|
||||
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)
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : __init__.py
|
||||
@Date : 2021/05/11
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
@@ -1,74 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : aboutView.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QGridLayout, QHBoxLayout, QLabel
|
||||
|
||||
from tidal_gui.control.label import Label
|
||||
from tidal_gui.control.pushButton import PushButton
|
||||
from tidal_gui.style import LabelStyle, ButtonStyle
|
||||
from tidal_gui.theme import getResourcePath
|
||||
|
||||
|
||||
class AboutView(QWidget):
|
||||
def __init__(self):
|
||||
super(AboutView, self).__init__()
|
||||
self.__initView__()
|
||||
|
||||
def __initView__(self):
|
||||
grid = QGridLayout(self)
|
||||
grid.addWidget(self.__initLogo__(), 0, 0, Qt.AlignLeft)
|
||||
grid.addLayout(self.__initContent__(), 0, 1)
|
||||
|
||||
def __initLogo__(self):
|
||||
path = getResourcePath() + "/svg/V.svg"
|
||||
self._logo = QLabel()
|
||||
self._logo.setPixmap(QPixmap(path))
|
||||
return self._logo
|
||||
|
||||
def __initButton__(self):
|
||||
path = getResourcePath() + "/svg/"
|
||||
|
||||
self._feedbackBtn = PushButton('Feedback', ButtonStyle.Default, iconUrl=path + 'github.svg')
|
||||
self._buymeacoffeeBtn = PushButton('Buymeacoffee', ButtonStyle.Info, iconUrl=path + 'buymeacoffee.svg')
|
||||
self._paypalBtn = PushButton('Paypal', ButtonStyle.Primary, iconUrl=path + 'paypal.svg')
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.addWidget(self._feedbackBtn)
|
||||
layout.addWidget(self._buymeacoffeeBtn)
|
||||
layout.addWidget(self._paypalBtn)
|
||||
return layout
|
||||
|
||||
def __initContent__(self):
|
||||
self._titleLabel = Label('', LabelStyle.HugeTitle)
|
||||
self._authorLabel = Label('')
|
||||
self._versionLabel = Label('')
|
||||
self._lastVersionLabel = Label('')
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self._titleLabel)
|
||||
layout.addWidget(self._authorLabel)
|
||||
layout.addWidget(self._versionLabel)
|
||||
layout.addWidget(self._lastVersionLabel)
|
||||
layout.addLayout(self.__initButton__())
|
||||
return layout
|
||||
|
||||
def setTitle(self, text: str):
|
||||
self._titleLabel.setText(text)
|
||||
|
||||
def setAuthor(self, text: str):
|
||||
self._authorLabel.setText('MADE WITH ♥ BY ' + text)
|
||||
|
||||
def setVersion(self, text: str):
|
||||
self._versionLabel.setText('VERSION ' + text)
|
||||
|
||||
def setLastVersion(self, text: str):
|
||||
self._lastVersionLabel.setText('LAST-VERSION ' + text)
|
||||
@@ -1,92 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : downloadItemView.py
|
||||
@Date : 2021/10/08
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QWidget, QHBoxLayout, QGridLayout, QProgressBar
|
||||
|
||||
from tidal_gui.control.label import Label
|
||||
from tidal_gui.style import LabelStyle
|
||||
|
||||
|
||||
class DownloadItemView(QWidget):
|
||||
def __init__(self):
|
||||
super(DownloadItemView, self).__init__()
|
||||
self.__initView__()
|
||||
self.setObjectName('DownloadItemView')
|
||||
self.setAttribute(Qt.WA_StyledBackground)
|
||||
|
||||
def __initView__(self):
|
||||
self._indexLabel = Label('1')
|
||||
self._codecLabel = Label('', LabelStyle.Tag)
|
||||
self._titleLabel = Label('title', LabelStyle.Bold)
|
||||
self._ownLabel = Label('own', LabelStyle.Italic)
|
||||
self._ownLabel.setMaximumWidth(200)
|
||||
self._actionLabel = Label('', LabelStyle.Italic)
|
||||
self._actionLabel.setFixedWidth(80)
|
||||
|
||||
self._errLabel = Label('')
|
||||
self._errLabel.setVisible(False)
|
||||
|
||||
self._sizeLabel = Label('/')
|
||||
self._speedLabel = Label('')
|
||||
|
||||
self._progress = QProgressBar()
|
||||
self._progress.setTextVisible(False)
|
||||
self._progress.setFixedHeight(3)
|
||||
self._progress.setFixedWidth(300)
|
||||
self._progress.setRange(0, 100)
|
||||
|
||||
titleLayout = QHBoxLayout()
|
||||
titleLayout.setSpacing(3)
|
||||
titleLayout.setContentsMargins(0, 0, 0, 0)
|
||||
titleLayout.addWidget(self._indexLabel, Qt.AlignLeft)
|
||||
titleLayout.addWidget(self._titleLabel, Qt.AlignLeft)
|
||||
titleLayout.addStretch(50)
|
||||
titleLayout.addWidget(self._codecLabel, Qt.AlignRight)
|
||||
titleLayout.addWidget(self._ownLabel, Qt.AlignRight)
|
||||
|
||||
speedLayout = QHBoxLayout()
|
||||
speedLayout.setSpacing(30)
|
||||
speedLayout.addWidget(self._sizeLabel)
|
||||
speedLayout.addWidget(self._speedLabel)
|
||||
|
||||
grid = QGridLayout(self)
|
||||
grid.setContentsMargins(0,0,0,0)
|
||||
grid.setSpacing(2)
|
||||
grid.addLayout(titleLayout, 0, 0, Qt.AlignLeft | Qt.AlignVCenter)
|
||||
grid.addWidget(self._progress, 0, 1, Qt.AlignRight | Qt.AlignVCenter)
|
||||
grid.addWidget(self._actionLabel, 0, 2, Qt.AlignRight | Qt.AlignVCenter)
|
||||
grid.addWidget(self._errLabel, 1, 0, Qt.AlignLeft | Qt.AlignVCenter)
|
||||
grid.addLayout(speedLayout, 1, 1, Qt.AlignLeft | Qt.AlignVCenter)
|
||||
|
||||
def setLabel(self, index, title, own):
|
||||
self._indexLabel.setText(str(index))
|
||||
self._titleLabel.setText(title)
|
||||
self._ownLabel.setText(own)
|
||||
|
||||
def setErrmsg(self, msg):
|
||||
self._errLabel.setText(msg)
|
||||
self._errLabel.setVisible(len(msg) > 0)
|
||||
|
||||
def setAction(self, msg):
|
||||
self._actionLabel.setText(msg)
|
||||
|
||||
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)
|
||||
|
||||
def setCodec(self, codec: str):
|
||||
self._codecLabel.setText(codec)
|
||||
@@ -1,156 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : loginView.py
|
||||
@Date : 2021/04/30
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QPixmap
|
||||
from PyQt5.QtWidgets import *
|
||||
|
||||
from tidal_gui.control.checkBox import CheckBox
|
||||
from tidal_gui.control.framelessWidget import FramelessWidget
|
||||
from tidal_gui.control.label import Label
|
||||
from tidal_gui.control.layout import createHBoxLayout
|
||||
from tidal_gui.control.lineEdit import LineEdit
|
||||
from tidal_gui.control.pushButton import PushButton
|
||||
from tidal_gui.style import ButtonStyle, LabelStyle
|
||||
from tidal_gui.theme import getResourcePath
|
||||
|
||||
|
||||
class LoginView(FramelessWidget):
|
||||
viewWidth = 650
|
||||
viewHeight = 400
|
||||
logoWidth = 300
|
||||
|
||||
def __init__(self):
|
||||
super(LoginView, self).__init__()
|
||||
self.setFixedSize(self.viewWidth, self.viewHeight)
|
||||
self.__initView__()
|
||||
self.setWindowButton(True, False, False)
|
||||
|
||||
def __initAccountTab__(self):
|
||||
self._deviceCodeEdit = LineEdit()
|
||||
self._confirmBtn = PushButton("LOGIN", ButtonStyle.Primary)
|
||||
|
||||
grid = QGridLayout()
|
||||
grid.setSpacing(15)
|
||||
grid.setRowStretch(0, 1)
|
||||
|
||||
grid.addLayout(createHBoxLayout([Label("DeviceCode"), self._deviceCodeEdit]), 1, 0, 1, 2)
|
||||
grid.setRowStretch(3, 1)
|
||||
grid.addWidget(self._confirmBtn, 5, 0, 1, 2)
|
||||
grid.setRowStretch(6, 1)
|
||||
|
||||
widget = QWidget()
|
||||
widget.setLayout(grid)
|
||||
return widget
|
||||
|
||||
def __initProxyTab__(self):
|
||||
self._enableProxyCheck = CheckBox('')
|
||||
self._proxyHostEdit = LineEdit()
|
||||
self._proxyPortEdit = LineEdit()
|
||||
self._proxyUserEdit = LineEdit()
|
||||
self._proxyPwdEdit = LineEdit()
|
||||
|
||||
grid = QGridLayout()
|
||||
grid.setSpacing(8)
|
||||
grid.setRowStretch(0, 1)
|
||||
grid.addWidget(Label("HttpProxy"), 1, 0)
|
||||
grid.addWidget(self._enableProxyCheck, 1, 1)
|
||||
grid.addWidget(Label("Host"), 2, 0)
|
||||
grid.addWidget(self._proxyHostEdit, 2, 1)
|
||||
grid.addWidget(Label("Port"), 3, 0)
|
||||
grid.addWidget(self._proxyPortEdit, 3, 1)
|
||||
grid.addWidget(Label("UserName"), 4, 0)
|
||||
grid.addWidget(self._proxyUserEdit, 4, 1)
|
||||
grid.addWidget(Label("Password"), 5, 0)
|
||||
grid.addWidget(self._proxyPwdEdit, 5, 1)
|
||||
grid.setRowStretch(6, 1)
|
||||
|
||||
widget = QWidget()
|
||||
widget.setLayout(grid)
|
||||
return widget
|
||||
|
||||
def __initIconWidget__(self):
|
||||
self._icon = Label('')
|
||||
self._icon.setStyleSheet("QLabel{background-color:rgb(0,0,0);}")
|
||||
|
||||
self._icon.setPixmap(QPixmap(getResourcePath() + "/svg/V.svg"))
|
||||
self._icon.setAlignment(Qt.AlignCenter)
|
||||
|
||||
self._iconLabel = Label('', LabelStyle.LogoBottom)
|
||||
self._iconLabel.setAlignment(Qt.AlignCenter)
|
||||
self._iconLabel.hide()
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setSpacing(15)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addStretch(1)
|
||||
layout.addWidget(self._icon)
|
||||
layout.addWidget(self._iconLabel)
|
||||
layout.addStretch(1)
|
||||
|
||||
widget = QWidget()
|
||||
widget.setStyleSheet("QWidget{background-color:rgb(0,0,0);}")
|
||||
widget.setLayout(layout)
|
||||
return widget
|
||||
|
||||
def __initView__(self):
|
||||
iconWidget = self.__initIconWidget__()
|
||||
|
||||
self._tab = QTabWidget(self)
|
||||
self._tab.addTab(self.__initAccountTab__(), "LOGIN")
|
||||
self._tab.addTab(self.__initProxyTab__(), "PROXY")
|
||||
self._tab.setFixedWidth(self.viewWidth - self.logoWidth - 60)
|
||||
self._tab.hide()
|
||||
|
||||
grid = self.getGrid()
|
||||
grid.setSpacing(15)
|
||||
grid.setContentsMargins(0, 0, 0, 0)
|
||||
grid.addWidget(iconWidget)
|
||||
grid.addWidget(self._tab, 0, 1, Qt.AlignCenter)
|
||||
|
||||
self.setValidMoveWidget(iconWidget)
|
||||
|
||||
def showEnterView(self):
|
||||
self._tab.show()
|
||||
|
||||
def hideEnterView(self):
|
||||
self._tab.hide()
|
||||
|
||||
def setDeviceCode(self, text):
|
||||
self._deviceCodeEdit.setText(text)
|
||||
|
||||
def enableHttpProxy(self) -> bool:
|
||||
return self._enableProxyCheck.isChecked()
|
||||
|
||||
def getProxyInfo(self) -> dict:
|
||||
infos = {'host': self._proxyHostEdit.text(), 'port': self._proxyPortEdit.text(),
|
||||
'username': self._proxyUserEdit.text(), 'password': self._proxyPwdEdit.text()}
|
||||
return infos
|
||||
|
||||
def connectConfirmButton(self, func):
|
||||
self._confirmBtn.clicked.connect(func)
|
||||
|
||||
def enableConfirmButton(self, enable):
|
||||
self._confirmBtn.setEnabled(enable)
|
||||
|
||||
def setMsg(self, text):
|
||||
if len(text) <= 0:
|
||||
self._iconLabel.hide()
|
||||
else:
|
||||
self._iconLabel.setText(text)
|
||||
self._iconLabel.show()
|
||||
|
||||
def showErrMessage(self, text: str):
|
||||
qmb = QMessageBox(self)
|
||||
qmb.setWindowTitle('Error')
|
||||
qmb.setIcon(QMessageBox.Warning)
|
||||
qmb.setText(text)
|
||||
qmb.addButton(QPushButton('OK', qmb), QMessageBox.YesRole)
|
||||
qmb.open()
|
||||
@@ -1,123 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : main.py
|
||||
@Date : 2021/05/11
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from enum import IntEnum
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QVBoxLayout, QWidget, QGridLayout
|
||||
|
||||
from tidal_gui.control.framelessWidget import FramelessWidget
|
||||
from tidal_gui.control.pushButton import PushButton
|
||||
from tidal_gui.style import ButtonStyle
|
||||
|
||||
|
||||
class PageType(IntEnum):
|
||||
Search = 0,
|
||||
Task = 1,
|
||||
Settings = 2,
|
||||
About = 3,
|
||||
Max = 4,
|
||||
|
||||
|
||||
class MainView(FramelessWidget):
|
||||
viewWidth = 1200
|
||||
viewHeight = 700
|
||||
|
||||
def __init__(self):
|
||||
super(MainView, self).__init__()
|
||||
self.setMinimumHeight(self.viewHeight)
|
||||
self.setMinimumWidth(self.viewWidth)
|
||||
self.__initPages__()
|
||||
self.__initView__()
|
||||
self.setWindowButton(True, True, True)
|
||||
|
||||
def __initPages__(self):
|
||||
self._pages = []
|
||||
for index in range(0, PageType.Max):
|
||||
self._pages.append(None)
|
||||
|
||||
def __initView__(self):
|
||||
leftTab = self.__initLeftTab__()
|
||||
content = self.__initContent__()
|
||||
moveWgt = self.__initMoveHead__()
|
||||
|
||||
grid = self.getGrid()
|
||||
grid.addWidget(leftTab, 0, 0, 2, 1, Qt.AlignLeft)
|
||||
grid.addWidget(moveWgt, 0, 1, Qt.AlignTop)
|
||||
grid.addLayout(content, 1, 1)
|
||||
|
||||
self.setValidMoveWidget(moveWgt)
|
||||
|
||||
def __initLeftTab__(self):
|
||||
self._icon = PushButton("", ButtonStyle.SoftwareIcon)
|
||||
self._searchBtn = PushButton("", ButtonStyle.SearchIcon)
|
||||
self._taskBtn = PushButton("", ButtonStyle.TaskIcon)
|
||||
self._settingsBtn = PushButton("", ButtonStyle.SettingsIcon)
|
||||
self._aboutBtn = PushButton("", ButtonStyle.AboutIcon)
|
||||
|
||||
self._searchBtn.clicked.connect(lambda: self.showPage(PageType.Search))
|
||||
self._taskBtn.clicked.connect(lambda: self.showPage(PageType.Task))
|
||||
self._settingsBtn.clicked.connect(lambda: self.showPage(PageType.Settings))
|
||||
self._aboutBtn.clicked.connect(lambda: self.showPage(PageType.About))
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.setSpacing(15)
|
||||
layout.setContentsMargins(15, 45, 15, 20)
|
||||
layout.addWidget(self._icon)
|
||||
layout.addWidget(self._searchBtn)
|
||||
layout.addWidget(self._taskBtn)
|
||||
layout.addStretch(1)
|
||||
# layout.addWidget(self._settingsBtn)
|
||||
# layout.addWidget(self._aboutBtn)
|
||||
|
||||
widget = QWidget()
|
||||
widget.setLayout(layout)
|
||||
widget.setObjectName("MainViewLeftWidget")
|
||||
return widget
|
||||
|
||||
def __initMoveHead__(self) -> QWidget:
|
||||
self._moveWidget = QWidget()
|
||||
self._moveWidget.setFixedHeight(30)
|
||||
self._moveWidget.setObjectName("BaseWidget")
|
||||
return self._moveWidget
|
||||
|
||||
def __initContent__(self) -> QGridLayout:
|
||||
self._searchView = None
|
||||
self._settingsView = None
|
||||
self._aboutView = None
|
||||
self._taskView = None
|
||||
self._contentLayout = QGridLayout()
|
||||
self._contentLayout.setContentsMargins(10, 0, 10, 10)
|
||||
return self._contentLayout
|
||||
|
||||
def showPage(self, pageType: PageType = PageType.Search):
|
||||
for index in range(0, PageType.Max):
|
||||
if index == pageType:
|
||||
self._pages[index].show()
|
||||
else:
|
||||
self._pages[index].hide()
|
||||
|
||||
def __setContentPage__(self, view, pageType: PageType):
|
||||
self._pages[pageType] = view
|
||||
self._pages[pageType].hide()
|
||||
self._contentLayout.addWidget(view, 0, 0)
|
||||
|
||||
def setSearchView(self, view):
|
||||
self.__setContentPage__(view, PageType.Search)
|
||||
|
||||
def setTaskView(self, view):
|
||||
self.__setContentPage__(view, PageType.Task)
|
||||
|
||||
def setSettingsView(self, view):
|
||||
self.__setContentPage__(view, PageType.Settings)
|
||||
|
||||
def setAboutView(self, view: QWidget):
|
||||
self._pages[PageType.About] = view
|
||||
self._pages[PageType.About].hide()
|
||||
@@ -1,234 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : searchView.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
import threading
|
||||
|
||||
from PyQt5.QtCore import Qt, QUrl
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QTabWidget
|
||||
|
||||
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.lineEdit import LineEdit
|
||||
from tidal_gui.control.pushButton import PushButton
|
||||
from tidal_gui.control.tableWidget import TableWidget
|
||||
from tidal_gui.style import ButtonStyle, LabelStyle
|
||||
from tidal_gui.theme import getResourcePath
|
||||
|
||||
|
||||
class SearchView(QWidget):
|
||||
def __init__(self):
|
||||
super(SearchView, self).__init__()
|
||||
self._rowCount = 20
|
||||
self._table = {}
|
||||
self._lock = threading.Lock()
|
||||
self.__initView__()
|
||||
self._searchEdit.setFocus()
|
||||
|
||||
def __initView__(self):
|
||||
grid = QVBoxLayout(self)
|
||||
grid.addLayout(self.__initHead__(), Qt.AlignTop)
|
||||
grid.addLayout(self.__initContent__())
|
||||
grid.addLayout(self.__initTail__(), Qt.AlignBottom)
|
||||
|
||||
def __initHead__(self):
|
||||
self._searchEdit = LineEdit(iconUrl=getResourcePath() + "/svg/search2.svg")
|
||||
self._searchErrLabel = Label('', LabelStyle.SearchErr)
|
||||
self._searchErrLabel.hide()
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self._searchEdit)
|
||||
layout.addWidget(self._searchErrLabel)
|
||||
layout.setContentsMargins(0, 0, 0, 5)
|
||||
return layout
|
||||
|
||||
def __initTail__(self):
|
||||
self._trackQualityComboBox = ComboBox([], 150)
|
||||
self._videoQualityComboBox = ComboBox([], 150)
|
||||
|
||||
self._prePageBtn = PushButton('', ButtonStyle.PrePage)
|
||||
self._nextPageBtn = PushButton('', ButtonStyle.NextPage)
|
||||
|
||||
self._pageIndexEdit = LineEdit('')
|
||||
self._pageIndexEdit.setAlignment(Qt.AlignCenter)
|
||||
self._pageIndexEdit.setEnabled(False)
|
||||
|
||||
self._downloadBtn = PushButton('Download', ButtonStyle.Primary)
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.addWidget(Label('Track:'))
|
||||
layout.addWidget(self._trackQualityComboBox)
|
||||
layout.addWidget(Label('Video:'))
|
||||
layout.addWidget(self._videoQualityComboBox)
|
||||
layout.addStretch(1)
|
||||
layout.addWidget(self._prePageBtn)
|
||||
layout.addWidget(self._pageIndexEdit)
|
||||
layout.addWidget(self._nextPageBtn)
|
||||
layout.addWidget(self._downloadBtn)
|
||||
return layout
|
||||
|
||||
def __initContent__(self):
|
||||
self._tabWidget = QTabWidget(self)
|
||||
self._tabWidget.addTab(self.__initTable__(Type.Album), "ALBUM")
|
||||
self._tabWidget.addTab(self.__initTable__(Type.Track), "TRACK")
|
||||
self._tabWidget.addTab(self.__initTable__(Type.Video), "VIDEO")
|
||||
self._tabWidget.addTab(self.__initTable__(Type.Playlist), "PLAYLIST")
|
||||
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self._tabWidget)
|
||||
return layout
|
||||
|
||||
def __initTable__(self, stype: Type):
|
||||
columnHeads = []
|
||||
columnWidths = []
|
||||
if stype == Type.Album:
|
||||
columnHeads = ['#', ' ', ' ', 'Title', 'Artists', 'Release', 'Duration']
|
||||
columnWidths = [50, 60, 60, 400, 200, 200]
|
||||
elif stype == Type.Track:
|
||||
columnHeads = ['#', ' ', 'Title', 'Album', 'Artists', 'Duration']
|
||||
columnWidths = [50, 60, 400, 200, 200]
|
||||
elif stype == Type.Video:
|
||||
columnHeads = ['#', ' ', ' ', 'Title', 'Artists', 'Duration']
|
||||
columnWidths = [50, 60, 60, 400, 200, 200]
|
||||
elif stype == Type.Playlist:
|
||||
columnHeads = ['#', ' ', 'Title', 'Artist', 'Duration']
|
||||
columnWidths = [50, 60, 400, 200, 200]
|
||||
|
||||
self._table[stype] = TableWidget(columnHeads, self._rowCount)
|
||||
for index, width in enumerate(columnWidths):
|
||||
self._table[stype].setColumnWidth(index, width)
|
||||
|
||||
return self._table[stype]
|
||||
|
||||
def __clearTableRowItems__(self, stype: Type, fromIndex: int):
|
||||
endIndex = self._rowCount - 1
|
||||
columnNum = self._table[stype].columnCount()
|
||||
while fromIndex <= endIndex:
|
||||
for colIdx in range(0, columnNum):
|
||||
self._table[stype].addItem(fromIndex, colIdx, '')
|
||||
fromIndex += 1
|
||||
|
||||
def setTableItems(self, stype: Type, indexOffset: int, result: tidal_dl.model.SearchResult):
|
||||
if stype == Type.Album:
|
||||
items = result.albums.items
|
||||
datas = []
|
||||
for index, item in enumerate(items):
|
||||
rowData = [str(index + 1 + indexOffset),
|
||||
QUrl(API.getCoverUrl(item.cover)),
|
||||
API.getFlag(item, Type.Album, True),
|
||||
item.title,
|
||||
item.artists[0].name,
|
||||
str(item.releaseDate),
|
||||
getDurationString(item.duration)]
|
||||
datas.append(rowData)
|
||||
|
||||
elif stype == Type.Track:
|
||||
items = result.tracks.items
|
||||
datas = []
|
||||
for index, item in enumerate(items):
|
||||
rowData = [str(index + 1 + indexOffset),
|
||||
API.getFlag(item, Type.Track, True),
|
||||
item.title,
|
||||
item.album.title,
|
||||
item.artists[0].name,
|
||||
getDurationString(item.duration)]
|
||||
datas.append(rowData)
|
||||
|
||||
elif stype == Type.Video:
|
||||
items = result.videos.items
|
||||
datas = []
|
||||
for index, item in enumerate(items):
|
||||
rowData = [str(index + 1 + indexOffset),
|
||||
QUrl(API.getCoverUrl(item.imageID)),
|
||||
API.getFlag(item, Type.Video, True),
|
||||
item.title,
|
||||
item.artists[0].name,
|
||||
getDurationString(item.duration)]
|
||||
datas.append(rowData)
|
||||
|
||||
elif stype == Type.Playlist:
|
||||
items = result.playlists.items
|
||||
datas = []
|
||||
for index, item in enumerate(items):
|
||||
rowData = [str(index + 1 + indexOffset),
|
||||
QUrl(API.getCoverUrl(item.squareImage)),
|
||||
item.title,
|
||||
'',
|
||||
getDurationString(item.duration)]
|
||||
datas.append(rowData)
|
||||
|
||||
for index, rowData in enumerate(datas):
|
||||
for colIdx, obj in enumerate(rowData):
|
||||
self._table[stype].addItem(index, colIdx, obj)
|
||||
|
||||
self.__clearTableRowItems__(stype, len(items))
|
||||
self._table[stype].viewport().update()
|
||||
|
||||
def getSearchText(self):
|
||||
return self._searchEdit.text()
|
||||
|
||||
def setSearchErrmsg(self, text: str):
|
||||
self._searchErrLabel.setText(text)
|
||||
if text != '':
|
||||
self._searchErrLabel.show()
|
||||
else:
|
||||
self._searchErrLabel.hide()
|
||||
|
||||
def setPageIndex(self, index, sum):
|
||||
self._pageIndexEdit.setText(str(index) + '/' + str(sum))
|
||||
|
||||
def getPageIndex(self) -> (int, int):
|
||||
nums = self._pageIndexEdit.text().split('/')
|
||||
return int(nums[0]), int(nums[1])
|
||||
|
||||
def getSelectedTabIndex(self):
|
||||
return self._tabWidget.currentIndex()
|
||||
|
||||
def getSelectedTableIndex(self, stype: Type):
|
||||
array = self._table[stype].selectedIndexes()
|
||||
if len(array) <= 0:
|
||||
return -1
|
||||
return array[0].row()
|
||||
|
||||
def setTrackQualityItems(self, items: list, curItem = None):
|
||||
self._trackQualityComboBox.setItems(items)
|
||||
if curItem is not None:
|
||||
self._trackQualityComboBox.setCurrentText(curItem)
|
||||
|
||||
def setVideoQualityItems(self, items: list, curItem=None):
|
||||
self._videoQualityComboBox.setItems(items)
|
||||
if curItem is not None:
|
||||
self._videoQualityComboBox.setCurrentText(curItem)
|
||||
|
||||
def getTrackQualityText(self):
|
||||
return self._trackQualityComboBox.currentText()
|
||||
|
||||
def getVideoQualityText(self):
|
||||
return self._videoQualityComboBox.currentText()
|
||||
|
||||
def connectButton(self, name: str, func):
|
||||
if name == 'search':
|
||||
self._searchEdit.returnPressed.connect(func)
|
||||
elif name == 'prePage':
|
||||
self._prePageBtn.clicked.connect(func)
|
||||
elif name == 'nextPage':
|
||||
self._nextPageBtn.clicked.connect(func)
|
||||
elif name == 'download':
|
||||
self._downloadBtn.clicked.connect(func)
|
||||
|
||||
def connectTab(self, func):
|
||||
self._tabWidget.currentChanged.connect(func)
|
||||
|
||||
def connectQualityComboBox(self, name: str, func):
|
||||
if name == 'track':
|
||||
self._trackQualityComboBox.currentIndexChanged.connect(func)
|
||||
else:
|
||||
self._videoQualityComboBox.currentIndexChanged.connect(func)
|
||||
@@ -1,104 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : settingsView.py
|
||||
@Date : 2021/05/11
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QHBoxLayout, QGridLayout
|
||||
|
||||
from tidal_gui.control.checkBox import CheckBox
|
||||
from tidal_gui.control.comboBox import ComboBox
|
||||
from tidal_gui.control.label import Label
|
||||
from tidal_gui.control.line import Line
|
||||
from tidal_gui.control.lineEdit import LineEdit
|
||||
from tidal_gui.control.pushButton import PushButton
|
||||
from tidal_gui.style import LabelStyle, ButtonStyle, ThemeStyle
|
||||
|
||||
|
||||
class SettingsView(QWidget):
|
||||
def __init__(self):
|
||||
super(SettingsView, self).__init__()
|
||||
self.__initView__()
|
||||
|
||||
def __initView__(self):
|
||||
grid = QVBoxLayout(self)
|
||||
grid.addLayout(self.__initHeader__())
|
||||
grid.addLayout(self.__initContent__())
|
||||
grid.addLayout(self.__initToolButton__())
|
||||
|
||||
def __initHeader__(self):
|
||||
self._header = Label("SETTINGS", LabelStyle.PageTitle)
|
||||
layout = QVBoxLayout()
|
||||
layout.addWidget(self._header)
|
||||
layout.addWidget(Line('H'))
|
||||
layout.setSpacing(10)
|
||||
layout.setContentsMargins(0, 0, 0, 10)
|
||||
return layout
|
||||
|
||||
def __initToolButton__(self) -> QHBoxLayout:
|
||||
self._logoutBtn = PushButton('Logout', ButtonStyle.Danger, 100)
|
||||
self._cancelBtn = PushButton('Cancel', ButtonStyle.Default, 100)
|
||||
self._confirmBtn = PushButton('OK', ButtonStyle.Primary, 100)
|
||||
|
||||
layout = QHBoxLayout()
|
||||
layout.addWidget(self._logoutBtn)
|
||||
layout.addStretch(1)
|
||||
layout.addWidget(self._cancelBtn)
|
||||
layout.addWidget(self._confirmBtn)
|
||||
return layout
|
||||
|
||||
def __addContent__(self, control, desc=''):
|
||||
rowIdx = self._contentLayout.rowCount()
|
||||
if desc != '':
|
||||
self._contentLayout.addWidget(Label(desc), rowIdx, 0, Qt.AlignRight)
|
||||
self._contentLayout.addWidget(control, rowIdx, 1)
|
||||
|
||||
def __initContent__(self):
|
||||
self._contentLayout = QGridLayout()
|
||||
self._contentLayout.setSpacing(15)
|
||||
|
||||
self._pathEdit = LineEdit()
|
||||
self._threadNumComboBox = ComboBox([1, 5, 10, 20])
|
||||
self._searchNumComboBox = ComboBox([10, 20, 30, 40, 50])
|
||||
self.__addContent__(self._pathEdit, 'Path:')
|
||||
self.__addContent__(self._threadNumComboBox, 'ThreadNum:')
|
||||
self.__addContent__(self._searchNumComboBox, 'SearchNum:')
|
||||
|
||||
self._contentLayout.setRowStretch(self._contentLayout.rowCount(), 1)
|
||||
|
||||
self._albumFolderFormatEdit = LineEdit()
|
||||
self._trackFileFormatEdit = LineEdit()
|
||||
self._videoFileFormatEdit = LineEdit()
|
||||
self.__addContent__(self._albumFolderFormatEdit, 'AlbumFolderFormat:')
|
||||
self.__addContent__(self._trackFileFormatEdit, 'TrackFileFormat:')
|
||||
self.__addContent__(self._videoFileFormatEdit, 'VideoFileFormat:')
|
||||
|
||||
self._saveCoverCheck = CheckBox('Download album cover')
|
||||
self._skipExistCheck = CheckBox('Skip exist file when downloading')
|
||||
self._convertMp4ToM4a = CheckBox('Convert mp4 to m4a')
|
||||
self.__addContent__(self._saveCoverCheck)
|
||||
self.__addContent__(self._skipExistCheck)
|
||||
self.__addContent__(self._convertMp4ToM4a)
|
||||
|
||||
self._contentLayout.setRowStretch(self._contentLayout.rowCount(), 1)
|
||||
|
||||
self._themeComboBox = ComboBox(list(map(lambda x: x.name, ThemeStyle)))
|
||||
self._languageComboBox = ComboBox(['Default'])
|
||||
self.__addContent__(self._themeComboBox, 'Theme:')
|
||||
self.__addContent__(self._languageComboBox, 'Language:')
|
||||
|
||||
self._contentLayout.setRowStretch(self._contentLayout.rowCount(), 10)
|
||||
return self._contentLayout
|
||||
|
||||
def connectButton(self, name: str, func):
|
||||
if name == "Logout":
|
||||
self._logoutBtn.clicked.connect(func)
|
||||
elif name == "Cancel":
|
||||
self._cancelBtn.clicked.connect(func)
|
||||
elif name == "OK":
|
||||
self._confirmBtn.clicked.connect(func)
|
||||
@@ -1,103 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : taskItemView.py
|
||||
@Date : 2021/9/14
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtGui import QPixmap
|
||||
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.listWidget import ListWidget
|
||||
from tidal_gui.control.pushButton import PushButton
|
||||
from tidal_gui.style import LabelStyle, ButtonStyle, ListWidgetStyle
|
||||
|
||||
|
||||
class TaskItemView(QWidget):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
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__())
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
def __initHead__(self):
|
||||
self._btnRetry = PushButton('', ButtonStyle.TaskRetry)
|
||||
self._btnCancel = PushButton('', ButtonStyle.TaskCancel)
|
||||
self._btnDelete = PushButton('', ButtonStyle.TaskDelete)
|
||||
self._btnOpen = PushButton('', ButtonStyle.TaskOpen)
|
||||
self._btnExpand = PushButton('', ButtonStyle.TaskExpand)
|
||||
self._btnExpand.clicked.connect(self.__expandClick__)
|
||||
btnLayout = createHBoxLayout([self._btnRetry,
|
||||
self._btnCancel,
|
||||
#self._btnDelete,
|
||||
self._btnOpen,
|
||||
self._btnExpand])
|
||||
|
||||
self._titleLabel = Label('', LabelStyle.PageTitle)
|
||||
self._descLabel = Label()
|
||||
self._errLabel = Label()
|
||||
self._errLabel.hide()
|
||||
labelLayout = createVBoxLayout([self._titleLabel, self._descLabel, self._errLabel])
|
||||
labelLayout.insertStretch(0, 1)
|
||||
labelLayout.addStretch(1)
|
||||
|
||||
self._picLabel = Label('', LabelStyle.Icon)
|
||||
self._picLabel.setMinimumHeight(64)
|
||||
headLayout = QHBoxLayout()
|
||||
headLayout.addWidget(self._picLabel)
|
||||
headLayout.addLayout(labelLayout)
|
||||
headLayout.addStretch(1)
|
||||
headLayout.addLayout(btnLayout)
|
||||
|
||||
return headLayout
|
||||
|
||||
def __initList__(self):
|
||||
self._list = QWidget()
|
||||
self._list.setObjectName("DownloadItemsWidget")
|
||||
self._listLayout = QVBoxLayout(self._list)
|
||||
self._listLayout.setSpacing(0)
|
||||
return self._list
|
||||
|
||||
def setLabel(self, title, desc):
|
||||
self._titleLabel.setText(title)
|
||||
self._descLabel.setText(desc)
|
||||
|
||||
def setErrmsg(self, msg):
|
||||
self._errLabel.setText(msg)
|
||||
|
||||
def setPic(self, data):
|
||||
pic = QPixmap()
|
||||
pic.loadFromData(data)
|
||||
self._picLabel.setPixmap(pic.scaled(64, 64))
|
||||
|
||||
def addListItem(self, view):
|
||||
self._listLayout.addWidget(view)
|
||||
|
||||
def __expandClick__(self):
|
||||
if self._list.isHidden():
|
||||
self._list.setVisible(True)
|
||||
else:
|
||||
self._list.setVisible(False)
|
||||
|
||||
def connectButton(self, name: str, func):
|
||||
if name == 'retry':
|
||||
self._btnRetry.clicked.connect(func)
|
||||
elif name == 'cancel':
|
||||
self._btnCancel.clicked.connect(func)
|
||||
elif name == 'delete':
|
||||
self._btnDelete.clicked.connect(func)
|
||||
elif name == 'open':
|
||||
self._btnOpen.clicked.connect(func)
|
||||
@@ -1,90 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : taskView.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from enum import Enum
|
||||
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtWidgets import QWidget, QGridLayout, QListWidgetItem
|
||||
|
||||
from tidal_gui.control.label import Label
|
||||
from tidal_gui.control.line import Line
|
||||
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
|
||||
|
||||
|
||||
class TaskStatus(Enum):
|
||||
Download = 0,
|
||||
Complete = 1,
|
||||
Error = 2,
|
||||
|
||||
|
||||
class TaskView(QWidget):
|
||||
def __init__(self):
|
||||
super(TaskView, self).__init__()
|
||||
self._listMap = {}
|
||||
self._pageMap = {}
|
||||
|
||||
for item in map(lambda typeItem: typeItem.name, TaskStatus):
|
||||
self._listMap[item] = ScrollWidget()
|
||||
self._pageMap[item] = QWidget()
|
||||
|
||||
self.__initView__()
|
||||
self._listTab.setCurrentRow(0)
|
||||
self._pageMap[TaskStatus.Download.name].show()
|
||||
|
||||
def __initView__(self):
|
||||
grid = QGridLayout(self)
|
||||
grid.addLayout(self.__initLefTab__(), 0, 0, Qt.AlignLeft)
|
||||
for item in map(lambda typeItem: typeItem.name, TaskStatus):
|
||||
grid.addWidget(self.__createContent__(item), 0, 1)
|
||||
|
||||
def __initLefTab__(self):
|
||||
self._listTab = ListWidget(ListWidgetStyle.TaskTab)
|
||||
self._listTab.setIconSize(QSize(20, 20))
|
||||
|
||||
iconPath = getResourcePath() + "/svg/taskTab/"
|
||||
self._listTab.addIConTextItem(iconPath + 'download.svg', TaskStatus.Download.name)
|
||||
self._listTab.addIConTextItem(iconPath + 'complete.svg', TaskStatus.Complete.name)
|
||||
self._listTab.addIConTextItem(iconPath + 'error.svg', TaskStatus.Error.name)
|
||||
|
||||
self._listTab.itemClicked.connect(self.__tabItemChanged__)
|
||||
|
||||
layout = QGridLayout()
|
||||
layout.addWidget(Label("TASK LIST", LabelStyle.PageTitle), 0, 0, Qt.AlignLeft)
|
||||
layout.addWidget(self._listTab, 1, 0, Qt.AlignLeft)
|
||||
layout.addWidget(Line('V'), 0, 1, 2, 1, Qt.AlignLeft)
|
||||
return layout
|
||||
|
||||
def __createContent__(self, typeStr: str):
|
||||
layout = QGridLayout()
|
||||
layout.setContentsMargins(0, 0, 10, 10)
|
||||
layout.setSpacing(10)
|
||||
layout.addWidget(Label(typeStr, LabelStyle.PageSubTitle), 0, 0, Qt.AlignTop)
|
||||
layout.addWidget(Line('H'), 1, 0, Qt.AlignTop)
|
||||
layout.addWidget(self._listMap[typeStr], 2, 0)
|
||||
|
||||
self._pageMap[typeStr].setLayout(layout)
|
||||
self._pageMap[typeStr].hide()
|
||||
return self._pageMap[typeStr]
|
||||
|
||||
def __tabItemChanged__(self, item: QListWidgetItem):
|
||||
for name in self._listMap:
|
||||
if name == item.text():
|
||||
self._pageMap[name].show()
|
||||
else:
|
||||
self._pageMap[name].hide()
|
||||
|
||||
def addItemView(self, stype: TaskStatus, view):
|
||||
self._listMap[stype.name].addWidgetItem(view)
|
||||
|
||||
def delItemView(self, stype: TaskStatus, view):
|
||||
self._listMap[stype.name].delWidgetItem(view)
|
||||
@@ -1,10 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : __init__.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : aboutModel.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from tidal_gui.view.aboutView import AboutView
|
||||
from tidal_gui.viewModel.viewModel import ViewModel
|
||||
|
||||
|
||||
class AboutModel(ViewModel):
|
||||
def __init__(self):
|
||||
super(AboutModel, self).__init__()
|
||||
self.view = AboutView()
|
||||
self.view.setTitle('TIDAL-GUI')
|
||||
self.view.setAuthor('Yaronzz')
|
||||
self.view.setVersion('1.0.0.1')
|
||||
self.view.setLastVersion('1.0.0.1')
|
||||
@@ -1,142 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : downloadItemModel.py
|
||||
@Date : 2021/10/08
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
|
||||
import os
|
||||
import aigpy
|
||||
from abc import ABC, ABCMeta
|
||||
from enum import Enum
|
||||
from pickle import FALSE
|
||||
from aigpy.downloadHelper import UserProgress
|
||||
import aigpy.stringHelper
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
|
||||
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,
|
||||
SUCCESS = 2,
|
||||
ERROR = 3,
|
||||
CANCEL = 4,
|
||||
|
||||
|
||||
_endStatus_ = [DownloadStatus.SUCCESS, DownloadStatus.ERROR, DownloadStatus.CANCEL]
|
||||
|
||||
|
||||
class Progress(UserProgress):
|
||||
def __init__(self, model):
|
||||
super().__init__()
|
||||
self.model = model
|
||||
self.curStr = ''
|
||||
self.maxStr = ''
|
||||
|
||||
def __toMBStr__(self, num):
|
||||
size = aigpy.memory.convert(num, aigpy.memory.Unit.BYTE, aigpy.memory.Unit.MB)
|
||||
return str(round(size, 2)) + ' MB'
|
||||
|
||||
def updateCurNum(self):
|
||||
per = self.curNum * 100 / self.maxNum
|
||||
self.curStr = self.__toMBStr__(self.curNum)
|
||||
self.model.SIGNAL_REFRESH_VIEW.emit('updateCurNum', {'per': per,
|
||||
'curStr': self.curStr,
|
||||
'maxStr': self.maxStr})
|
||||
|
||||
def updateMaxNum(self):
|
||||
self.maxStr = self.__toMBStr__(self.maxNum)
|
||||
|
||||
def updateStream(self, stream):
|
||||
self.model.SIGNAL_REFRESH_VIEW.emit('updateStream', {'stream': stream})
|
||||
|
||||
|
||||
class DownloadItemModel(ViewModel):
|
||||
SIGNAL_END = pyqtSignal(DownloadStatus)
|
||||
|
||||
def __init__(self, index, data, basePath):
|
||||
super(DownloadItemModel, self).__init__()
|
||||
self.view = DownloadItemView()
|
||||
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:
|
||||
self.__initVideo__(index)
|
||||
|
||||
self.SIGNAL_REFRESH_VIEW.connect(self.__refresh__)
|
||||
|
||||
def __refresh__(self, stype: str, object):
|
||||
if stype == "updateCurNum":
|
||||
per = object['per']
|
||||
curStr = object['curStr']
|
||||
maxStr = object['maxStr']
|
||||
self.view.setSize(curStr, maxStr)
|
||||
self.view.setProgress(per)
|
||||
elif stype == "updateStream":
|
||||
codec = object['stream'].codec
|
||||
self.view.setCodec(codec)
|
||||
|
||||
|
||||
def __setStatus__(self, status: DownloadStatus, desc: str = ''):
|
||||
self.status = status
|
||||
if desc == '':
|
||||
self.view.setAction(status.name)
|
||||
else:
|
||||
self.view.setAction(status.name + '-' + desc)
|
||||
if status in _endStatus_:
|
||||
self.SIGNAL_END.emit(status)
|
||||
|
||||
|
||||
def __setErrStatus__(self, errmsg: str):
|
||||
self.view.setErrmsg(errmsg)
|
||||
self.__setStatus__(DownloadStatus.ERROR)
|
||||
|
||||
def __initTrack__(self, index):
|
||||
title = self.data.title
|
||||
own = self.data.album.title
|
||||
self.view.setLabel(index, title, own)
|
||||
|
||||
def __initVideo__(self, index):
|
||||
title = self.data.title
|
||||
own = getArtistsNames(self.data.artists)
|
||||
self.view.setLabel(index, title, own)
|
||||
|
||||
def isInWait(self):
|
||||
return self.status == DownloadStatus.WAIT
|
||||
|
||||
def stopDownload(self):
|
||||
if self.status not in _endStatus_:
|
||||
self.__setStatus__(DownloadStatus.CANCEL)
|
||||
|
||||
def retry(self):
|
||||
if self.status in [DownloadStatus.ERROR, DownloadStatus.CANCEL]:
|
||||
self.__setStatus__(DownloadStatus.WAIT)
|
||||
|
||||
def download(self):
|
||||
self.__setStatus__(DownloadStatus.RUNNING)
|
||||
|
||||
if self.isTrack:
|
||||
check, msg = downloadTrack(self.data, self.data.album, self.data.playlist, self.progress)
|
||||
else:
|
||||
check, msg = downloadVideo(self.data)
|
||||
|
||||
if check is False:
|
||||
self.__setErrStatus__(msg)
|
||||
return
|
||||
|
||||
self.view.setProgress(100)
|
||||
self.__setStatus__(DownloadStatus.SUCCESS)
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : loginModel.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
import _thread
|
||||
import webbrowser
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from tidal_dl.util import API, loginByConfig, loginByWeb
|
||||
|
||||
from tidal_gui.view.loginView import LoginView
|
||||
from tidal_gui.viewModel.viewModel import ViewModel
|
||||
|
||||
|
||||
class LoginModel(ViewModel):
|
||||
SIGNAL_LOGIN_SUCCESS = pyqtSignal()
|
||||
|
||||
def __init__(self):
|
||||
super(LoginModel, self).__init__()
|
||||
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)
|
||||
self.view.enableConfirmButton(True)
|
||||
self.view.setMsg(' ')
|
||||
self.view.showEnterView()
|
||||
elif stype == "showMsg":
|
||||
self.view.hideEnterView()
|
||||
self.view.setMsg(text)
|
||||
|
||||
def login(self, useConfig=True):
|
||||
self.SIGNAL_REFRESH_VIEW.emit('showMsg', "LOGIN...")
|
||||
|
||||
def __thread_login__(model: LoginModel, useConfig: bool):
|
||||
if useConfig and loginByConfig():
|
||||
model.SIGNAL_LOGIN_SUCCESS.emit()
|
||||
return
|
||||
model.getDeviceCode()
|
||||
|
||||
_thread.start_new_thread(__thread_login__, (self, useConfig))
|
||||
|
||||
def getDeviceCode(self):
|
||||
self.SIGNAL_REFRESH_VIEW.emit('showMsg', "GET DEVICE-CODE...")
|
||||
|
||||
def __thread_getCode__(model: LoginModel):
|
||||
msg, check = API.getDeviceCode()
|
||||
if check:
|
||||
model.SIGNAL_REFRESH_VIEW.emit('userCode', API.key.userCode)
|
||||
else:
|
||||
model.SIGNAL_REFRESH_VIEW.emit('showMsg', msg)
|
||||
|
||||
_thread.start_new_thread(__thread_getCode__, (self,))
|
||||
|
||||
def __openWeb__(self):
|
||||
self.view.enableConfirmButton(False)
|
||||
webbrowser.open('https://link.tidal.com/' + API.key.userCode, new=0, autoraise=True)
|
||||
|
||||
def __thread_waitLogin__(model: LoginModel):
|
||||
if loginByWeb():
|
||||
model.SIGNAL_LOGIN_SUCCESS.emit()
|
||||
else:
|
||||
model.getDeviceCode()
|
||||
|
||||
_thread.start_new_thread(__thread_waitLogin__, (self,))
|
||||
@@ -1,59 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : mainModel.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from tidal_gui.downloader import downloadImp
|
||||
from tidal_gui.view.mainView import MainView, PageType
|
||||
from tidal_gui.viewModel.aboutModel import AboutModel
|
||||
from tidal_gui.viewModel.loginModel import LoginModel
|
||||
from tidal_gui.viewModel.searchModel import SearchModel
|
||||
from tidal_gui.viewModel.settingsModel import SettingsModel
|
||||
from tidal_gui.viewModel.taskModel import TaskModel
|
||||
from tidal_gui.viewModel.viewModel import ViewModel
|
||||
|
||||
|
||||
class MainModel(ViewModel):
|
||||
def __init__(self):
|
||||
super(MainModel, self).__init__()
|
||||
self.loginModel = LoginModel()
|
||||
self.searchModel = SearchModel()
|
||||
self.taskModel = TaskModel()
|
||||
self.settingsModel = SettingsModel(self)
|
||||
self.aboutModel = AboutModel()
|
||||
|
||||
self.loginModel.SIGNAL_LOGIN_SUCCESS.connect(self.__loginSuccess__)
|
||||
self.searchModel.SIGNAL_ADD_TASKITEM.connect(self.taskModel.addTaskItem)
|
||||
self.searchModel.SIGNAL_ADD_TASKITEM.connect(self.__addTaskItem__)
|
||||
|
||||
self.view = MainView()
|
||||
self.view.setSearchView(self.searchModel.view)
|
||||
self.view.setTaskView(self.taskModel.view)
|
||||
self.view.setSettingsView(self.settingsModel.view)
|
||||
self.view.setAboutView(self.aboutModel.view)
|
||||
|
||||
self.view.showPage()
|
||||
|
||||
downloadImp.setTaskModel(self.taskModel)
|
||||
downloadImp.start()
|
||||
|
||||
def __addTaskItem__(self):
|
||||
self.view.showPage(PageType.Task)
|
||||
|
||||
def uninit(self):
|
||||
self.taskModel.uninit()
|
||||
downloadImp.stop()
|
||||
|
||||
def show(self, relogin: bool = False):
|
||||
self.view.hide()
|
||||
self.loginModel.login(bool(1 - relogin))
|
||||
self.loginModel.show()
|
||||
|
||||
def __loginSuccess__(self):
|
||||
self.loginModel.hide()
|
||||
self.view.show()
|
||||
@@ -1,159 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : searchModel.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
import _thread
|
||||
import threading
|
||||
|
||||
import aigpy.stringHelper
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from aigpy.modelHelper import ModelBase
|
||||
|
||||
import tidal_dl
|
||||
from tidal_dl import Type
|
||||
from tidal_dl.util import API, getAudioQualityList, getCurAudioQuality, getCurVideoQuality, getVideoQualityList, setCurAudioQuality, setCurVideoQuality
|
||||
from tidal_gui.view.searchView import SearchView
|
||||
from tidal_gui.viewModel.viewModel import ViewModel
|
||||
|
||||
|
||||
class SearchModel(ViewModel):
|
||||
SIGNAL_ADD_TASKITEM = pyqtSignal(ModelBase)
|
||||
|
||||
def __init__(self):
|
||||
super(SearchModel, self).__init__()
|
||||
self._lock = threading.Lock()
|
||||
self._resultData = None
|
||||
|
||||
self.view = SearchView()
|
||||
self.view.setPageIndex(1, 1)
|
||||
self.view.setTrackQualityItems(getAudioQualityList(), getCurAudioQuality())
|
||||
self.view.setVideoQualityItems(getVideoQualityList(), getCurVideoQuality())
|
||||
self.view.connectButton('search', self.__search__)
|
||||
self.view.connectButton('prePage', self.__searchPre__)
|
||||
self.view.connectButton('nextPage', self.__searchNext__)
|
||||
self.view.connectButton('download', self.__download__)
|
||||
self.view.connectQualityComboBox('track', self.__changeAudioQuality__)
|
||||
self.view.connectQualityComboBox('video', self.__changeVideoQuality__)
|
||||
self.view.connectTab(lambda: self.__search__(0))
|
||||
self.SIGNAL_REFRESH_VIEW.connect(self.__refresh__)
|
||||
|
||||
def __getSumByResult__(self, stype: Type):
|
||||
if stype == Type.Album:
|
||||
return self._resultData.albums.totalNumberOfItems
|
||||
elif stype == Type.Artist:
|
||||
return self._resultData.artists.totalNumberOfItems
|
||||
elif stype == Type.Track:
|
||||
return self._resultData.tracks.totalNumberOfItems
|
||||
elif stype == Type.Video:
|
||||
return self._resultData.videos.totalNumberOfItems
|
||||
elif stype == Type.Playlist:
|
||||
return self._resultData.playlists.totalNumberOfItems
|
||||
return 0
|
||||
|
||||
def __refresh__(self, stype: str, data):
|
||||
if stype == 'setPageIndex':
|
||||
self.view.setPageIndex(data[0], data[1])
|
||||
elif stype == 'setTableItems':
|
||||
self.view.setTableItems(data[0], data[1], data[2])
|
||||
elif stype == 'setSearchErrmsg':
|
||||
self.view.setSearchErrmsg(data)
|
||||
|
||||
def __startThread__(self, index: int):
|
||||
def __thread_search__(model: SearchModel, index: int):
|
||||
typeIndex = model.view.getSelectedTabIndex()
|
||||
searchText = model.view.getSearchText()
|
||||
|
||||
if aigpy.stringHelper.isNull(searchText):
|
||||
model._lock.release()
|
||||
return
|
||||
|
||||
# search
|
||||
limit = 20
|
||||
offset = (index - 1) * limit
|
||||
stype = tidal_dl.Type(typeIndex)
|
||||
msg, model._resultData = API.search(searchText, stype, offset, limit)
|
||||
|
||||
if not aigpy.stringHelper.isNull(msg):
|
||||
model.SIGNAL_REFRESH_VIEW.emit('setSearchErrmsg', msg)
|
||||
model._lock.release()
|
||||
return
|
||||
|
||||
# get page index
|
||||
total = model.__getSumByResult__(stype)
|
||||
if total <= 0:
|
||||
model.SIGNAL_REFRESH_VIEW.emit('setSearchErrmsg', 'Search results are empty...')
|
||||
model._lock.release()
|
||||
return
|
||||
|
||||
maxIdx = total // limit + (1 if total % limit > 0 else 0)
|
||||
if index > maxIdx:
|
||||
model._lock.release()
|
||||
return
|
||||
|
||||
# set view
|
||||
model.SIGNAL_REFRESH_VIEW.emit('setPageIndex', (index, maxIdx))
|
||||
model.SIGNAL_REFRESH_VIEW.emit('setTableItems', (stype, offset, model._resultData))
|
||||
model._lock.release()
|
||||
|
||||
_thread.start_new_thread(__thread_search__, (self, index))
|
||||
|
||||
def __search__(self, num: int = 0):
|
||||
if not self._lock.acquire(False):
|
||||
return
|
||||
|
||||
self.view.setSearchErrmsg('')
|
||||
|
||||
if aigpy.string.isNull(self.view.getSearchText()):
|
||||
self._lock.release()
|
||||
return
|
||||
|
||||
index = 1
|
||||
if num != 0:
|
||||
curIdx, curSum = self.view.getPageIndex()
|
||||
if curIdx + num > 1:
|
||||
index = curIdx + num
|
||||
|
||||
self.__startThread__(index)
|
||||
|
||||
def __searchNext__(self):
|
||||
self.__search__(1)
|
||||
|
||||
def __searchPre__(self):
|
||||
self.__search__(-1)
|
||||
|
||||
def __download__(self):
|
||||
if self._resultData is None:
|
||||
self.view.setSearchErrmsg('Please search first...')
|
||||
return
|
||||
|
||||
typeIndex = self.view.getSelectedTabIndex()
|
||||
stype = tidal_dl.Type(typeIndex)
|
||||
|
||||
items = []
|
||||
if stype == Type.Album:
|
||||
items = self._resultData.albums.items
|
||||
elif stype == Type.Track:
|
||||
items = self._resultData.tracks.items
|
||||
elif stype == Type.Video:
|
||||
items = self._resultData.videos.items
|
||||
elif stype == Type.Playlist:
|
||||
items = self._resultData.playlists.items
|
||||
|
||||
index = self.view.getSelectedTableIndex(stype)
|
||||
if index < 0 or index >= len(items):
|
||||
self.view.setSearchErrmsg('Please select one item...')
|
||||
return
|
||||
|
||||
self.SIGNAL_ADD_TASKITEM.emit(items[index])
|
||||
|
||||
def __changeAudioQuality__(self):
|
||||
setCurAudioQuality(self.view.getTrackQualityText())
|
||||
|
||||
def __changeVideoQuality__(self):
|
||||
setCurVideoQuality(self.view.getVideoQualityText())
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : settingsModel.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from tidal_gui.view.settingsView import SettingsView
|
||||
from tidal_gui.viewModel.viewModel import ViewModel
|
||||
|
||||
|
||||
class SettingsModel(ViewModel):
|
||||
def __init__(self, parent):
|
||||
super(SettingsModel, self).__init__()
|
||||
self._parent = parent
|
||||
self.view = SettingsView()
|
||||
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
|
||||
|
||||
def __ok__(self):
|
||||
pass
|
||||
@@ -1,183 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : taskItemModel.py
|
||||
@Date : 2021/9/14
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
import _thread
|
||||
from asyncio import tasks
|
||||
from enum import Enum
|
||||
import os
|
||||
import time
|
||||
|
||||
import aigpy.stringHelper
|
||||
|
||||
from tidal_dl import Type
|
||||
from tidal_dl.model import Album, Track, Video, Playlist
|
||||
from tidal_dl.util import API, getArtistsNames, getBasePath, getDurationString
|
||||
from tidal_gui.view.taskItemView import TaskItemView
|
||||
from tidal_gui.view.taskView import TaskStatus
|
||||
from tidal_gui.viewModel.downloadItemModel import DownloadItemModel, DownloadStatus
|
||||
from tidal_gui.viewModel.viewModel import ViewModel
|
||||
|
||||
|
||||
class TaskItemModel(ViewModel):
|
||||
def __init__(self, data):
|
||||
super(TaskItemModel, self).__init__()
|
||||
self.view = TaskItemView()
|
||||
self.data = data
|
||||
self.downloadModelList = []
|
||||
self.path = ''
|
||||
|
||||
if isinstance(data, Album):
|
||||
self.__initAlbum__(data)
|
||||
elif isinstance(data, Track):
|
||||
self.__initTrack__(data)
|
||||
elif isinstance(data, Video):
|
||||
self.__initVideo__(data)
|
||||
elif isinstance(data, Playlist):
|
||||
self.__initPlaylist__(data)
|
||||
|
||||
self.view.connectButton('retry', self.__btnFuncRetry__)
|
||||
self.view.connectButton('cancel', self.__btnFuncCancel__)
|
||||
self.view.connectButton('delete', self.__btnFuncDelete__)
|
||||
self.view.connectButton('open', self.__btnFuncOpen__)
|
||||
|
||||
self.SIGNAL_REFRESH_VIEW.connect(self.__refresh__)
|
||||
|
||||
def getTaskStatus(self) -> TaskStatus:
|
||||
if len(self.downloadModelList) <= 0:
|
||||
return TaskStatus.Download
|
||||
|
||||
errorNum = 0
|
||||
for item in self.downloadModelList:
|
||||
if item.status in [DownloadStatus.WAIT, DownloadStatus.RUNNING, DownloadStatus.CANCEL]:
|
||||
return TaskStatus.Download
|
||||
elif item.status == DownloadStatus.ERROR:
|
||||
errorNum += 1
|
||||
|
||||
if errorNum > 0:
|
||||
return TaskStatus.Error
|
||||
return TaskStatus.Complete
|
||||
|
||||
def __refresh__(self, stype: str, obj):
|
||||
if stype == "setPic":
|
||||
self.view.setPic(obj)
|
||||
elif stype == "addListItems":
|
||||
for index, item in enumerate(obj):
|
||||
downItem = DownloadItemModel(index + 1, item, self.path)
|
||||
self.view.addListItem(downItem.view)
|
||||
self.downloadModelList.append(downItem)
|
||||
|
||||
def __btnFuncRetry__(self):
|
||||
for item in self.downloadModelList:
|
||||
item.retry()
|
||||
|
||||
def __btnFuncCancel__(self):
|
||||
for item in self.downloadModelList:
|
||||
item.stopDownload()
|
||||
|
||||
def __btnFuncDelete__(self):
|
||||
for item in self.downloadModelList:
|
||||
item.stopDownload()
|
||||
|
||||
def __btnFuncOpen__(self):
|
||||
if self.path == '':
|
||||
return
|
||||
if os.path.exists(self.path):
|
||||
os.startfile(self.path)
|
||||
|
||||
def __initAlbum__(self, data: Album):
|
||||
self.path = getBasePath(data)
|
||||
|
||||
title = data.title
|
||||
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 = API.getCoverData(album.cover)
|
||||
model.SIGNAL_REFRESH_VIEW.emit('setPic', cover)
|
||||
|
||||
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('addListItems', tracks + videos)
|
||||
time.sleep(1)
|
||||
|
||||
_thread.start_new_thread(__thread_func__, (self, data))
|
||||
|
||||
def __initTrack__(self, data: Track):
|
||||
title = data.title
|
||||
desc = f"by {getArtistsNames(data.artists)} " \
|
||||
f"{getDurationString(data.duration)} "
|
||||
self.view.setLabel(title, desc)
|
||||
|
||||
def __thread_func__(model: TaskItemModel, track: Track):
|
||||
mag, track.album = API.getAlbum(track.album.id)
|
||||
model.path = getBasePath(track)
|
||||
cover = API.getCoverData(track.album.cover)
|
||||
model.SIGNAL_REFRESH_VIEW.emit('setPic', cover)
|
||||
model.SIGNAL_REFRESH_VIEW.emit('addListItems', [track])
|
||||
time.sleep(1)
|
||||
|
||||
_thread.start_new_thread(__thread_func__, (self, data))
|
||||
|
||||
def __initVideo__(self, data: Video):
|
||||
self.path = getBasePath(data)
|
||||
|
||||
title = data.title
|
||||
desc = f"by {getArtistsNames(data.artists)} " \
|
||||
f"{getDurationString(data.duration)} "
|
||||
self.view.setLabel(title, desc)
|
||||
|
||||
def __thread_func__(model: TaskItemModel, video: Video):
|
||||
cover = API.getCoverData(video.imageID)
|
||||
model.SIGNAL_REFRESH_VIEW.emit('setPic', cover)
|
||||
model.SIGNAL_REFRESH_VIEW.emit('addListItems', [video])
|
||||
time.sleep(1)
|
||||
|
||||
_thread.start_new_thread(__thread_func__, (self, data))
|
||||
|
||||
def __initPlaylist__(self, data: Playlist):
|
||||
self.path = getBasePath(data)
|
||||
|
||||
title = data.title
|
||||
desc = f"{getDurationString(data.duration)} " \
|
||||
f"Track-{data.numberOfTracks} " \
|
||||
f"Video-{data.numberOfVideos}"
|
||||
self.view.setLabel(title, desc)
|
||||
|
||||
def __thread_func__(model: TaskItemModel, playlist: Playlist):
|
||||
cover = API.getCoverData(playlist.squareImage)
|
||||
model.SIGNAL_REFRESH_VIEW.emit('setPic', cover)
|
||||
|
||||
msg, tracks, videos = API.getItems(playlist.uuid, Type.Playlist)
|
||||
if not aigpy.stringHelper.isNull(msg):
|
||||
model.view.setErrmsg(msg)
|
||||
return
|
||||
|
||||
for item in tracks:
|
||||
mag, album = API.getAlbum(item.album.id)
|
||||
item.playlist = playlist
|
||||
item.album = album
|
||||
for item in videos:
|
||||
item.playlist = playlist
|
||||
|
||||
model.SIGNAL_REFRESH_VIEW.emit('addListItems', tracks + videos)
|
||||
time.sleep(1)
|
||||
|
||||
_thread.start_new_thread(__thread_func__, (self, data))
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : taskModel.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
import threading
|
||||
from PyQt5.QtCore import QTimer
|
||||
from tidal_dl.model import Album, Artist
|
||||
from tidal_gui.view.taskView import TaskView, TaskStatus
|
||||
from tidal_gui.viewModel.downloadItemModel import DownloadItemModel
|
||||
from tidal_gui.viewModel.taskItemModel import TaskItemModel, TaskStatus
|
||||
from tidal_gui.viewModel.viewModel import ViewModel
|
||||
|
||||
|
||||
class TaskModel(ViewModel):
|
||||
def __init__(self):
|
||||
super(TaskModel, self).__init__()
|
||||
self.view = TaskView()
|
||||
|
||||
self._listMap = {}
|
||||
for item in map(lambda typeItem: typeItem.name, TaskStatus):
|
||||
self._listMap[item] = []
|
||||
|
||||
self._timer = QTimer(self)
|
||||
self._timer.timeout.connect(self.__checkTaskStatus__)
|
||||
self._timer.start(3000)
|
||||
|
||||
# self.test()
|
||||
|
||||
def __checkTaskStatus__(self):
|
||||
for item in self._listMap[TaskStatus.Download.name][:]:
|
||||
status = item.getTaskStatus()
|
||||
if status == TaskStatus.Download:
|
||||
continue
|
||||
|
||||
self._listMap[TaskStatus.Download.name].remove(item)
|
||||
self.view.delItemView(TaskStatus.Download, item.view)
|
||||
|
||||
self._listMap[status.name].append(item)
|
||||
self.view.addItemView(status, item.view)
|
||||
|
||||
def uninit(self):
|
||||
self._timer.stop()
|
||||
for item in self._listMap[TaskStatus.Download.name]:
|
||||
for downItem in item.downloadModelList:
|
||||
downItem.stopDownload()
|
||||
|
||||
def addTaskItem(self, data):
|
||||
item = TaskItemModel(data)
|
||||
self._listMap[TaskStatus.Download.name].append(item)
|
||||
self.view.addItemView(TaskStatus.Download, item.view)
|
||||
|
||||
def getWaitDownloadItem(self) -> DownloadItemModel:
|
||||
for item in self._listMap[TaskStatus.Download.name]:
|
||||
for downItem in item.downloadModelList:
|
||||
if downItem.isInWait():
|
||||
return downItem
|
||||
return None
|
||||
|
||||
def test(self):
|
||||
ar = Artist()
|
||||
ar.name = 'yaron'
|
||||
ar.id = 110
|
||||
album = Album()
|
||||
album.artist = None
|
||||
album.artists = [ar]
|
||||
album.audioModes = ['STEREO']
|
||||
album.audioQuality = 'LOSSLESS'
|
||||
album.cover = '8203ff9a-47e3-49a0-9b44-a6dbbe9c9469'
|
||||
album.duration = 1140
|
||||
album.explicit = False
|
||||
album.id = 177748204
|
||||
album.numberOfTracks = 10
|
||||
album.numberOfVideos = 0
|
||||
album.numberOfVolumes = 1
|
||||
album.releaseDate = '2021-03-16'
|
||||
album.title = 'Love'
|
||||
album.type = 'ALBUM'
|
||||
album.version = None
|
||||
|
||||
self.addTaskItem(album)
|
||||
@@ -1,26 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : viewModel.py
|
||||
@Date : 2021/8/17
|
||||
@Author : Yaronzz
|
||||
@Version : 1.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
"""
|
||||
from PyQt5.QtCore import QObject
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
|
||||
|
||||
class ViewModel(QObject):
|
||||
SIGNAL_REFRESH_VIEW = pyqtSignal(str, object)
|
||||
|
||||
def __init__(self):
|
||||
super(ViewModel, self).__init__()
|
||||
self.view = None
|
||||
|
||||
def show(self):
|
||||
self.view.show()
|
||||
|
||||
def hide(self):
|
||||
self.view.hide()
|
||||