Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be84a3631a | |||
| 9ce35af72f | |||
| 9d904a0fea | |||
| 5e05afd775 | |||
| dece506d58 | |||
| c2cc170988 | |||
| 3cab9ea295 | |||
| 5cf094d227 | |||
| 2e1e15ed3f | |||
| 7434470216 | |||
| 2ce2a310ae | |||
| dfc7af7796 | |||
| 63bf063dd3 | |||
| 3e8a3436b4 | |||
| e08e5efc88 | |||
| 7f3758e18c | |||
| 3e2d74a37f | |||
| 69ccd65025 | |||
| 81b82e6c29 | |||
| 346befda6d | |||
| 450102bfea | |||
| a966b3d93d | |||
| 55df515256 | |||
| 242747719a | |||
| e3c5886a02 | |||
| 33c49b83db | |||
| ed13b9b92d | |||
| e45f649e46 | |||
| 0172301769 | |||
| a7aa38e563 | |||
| fd97a2b966 | |||
| e1ee47b93c | |||
| 61b9dd3af8 | |||
| 628a42b058 | |||
| 2c392f0e22 | |||
| dc57d4d01d | |||
| 8ed83b1672 | |||
| 3099820956 | |||
| d643de43e9 | |||
| 39db4730c5 | |||
| 937aa7ca8f | |||
| 84b9f6f6bc | |||
| b1ebd6f0b2 | |||
| e1562ba30e | |||
| 8876be13b6 | |||
| 30a6c8c55a | |||
| eb01de7f3a | |||
| e652556de2 | |||
| 00ef2f906f | |||
| aba9493201 | |||
| c78a56fff0 | |||
| 56bf0f9510 | |||
| 2ebf80869c | |||
| bb5be5e5fb | |||
| 5c2a13d4d1 | |||
| c61be4bbf3 | |||
| a969583d79 | |||
| 9f9a4f9aef | |||
| 3b3af6a174 | |||
| 166961297e | |||
| 1f5343569f | |||
| 9e9d47f470 | |||
| 6870a8c37f | |||
| 4d368dd04d | |||
| 64b98ee117 | |||
| 5ec77efb19 | |||
| 65da2f7272 | |||
| a5fb92db92 | |||
| 81e244f230 | |||
| 6a1b931340 | |||
| 8a21b01c1d | |||
| 96f46c2b59 | |||
| ffe335c17e | |||
| 79b537c186 | |||
| a806785609 | |||
| 06ff60ca74 | |||
| b34013129e | |||
| f1a5b1d764 | |||
| d52980506c | |||
| 34bab40d3d | |||
| 747d06c7b4 | |||
| 41573b7b39 | |||
| d7e078db2e | |||
| 363a854428 | |||
| ad00099c9d | |||
| 6676a380e1 | |||
| bab13ff718 | |||
| 9707c5c276 | |||
| 48206d675a | |||
| f877c184bb | |||
| 59b360b285 | |||
| 1a09571788 |
@@ -0,0 +1,28 @@
|
||||
# # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time.
|
||||
# #
|
||||
# # You can adjust the behavior by modifying this file.
|
||||
# # For more information, see:
|
||||
# # https://github.com/actions/stale
|
||||
# name: Mark stale issues and pull requests
|
||||
|
||||
# on:
|
||||
# schedule:
|
||||
# - cron: '45 15 * * *'
|
||||
|
||||
# jobs:
|
||||
# stale:
|
||||
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# issues: write
|
||||
# pull-requests: write
|
||||
|
||||
# steps:
|
||||
# - uses: actions/stale@v5
|
||||
# with:
|
||||
# repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity.'
|
||||
# stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity.'
|
||||
# stale-issue-label: 'no-issue-activity'
|
||||
# stale-pr-label: 'no-pr-activity'
|
||||
# operations-per-run: 60
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Build exe
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
pip3 install wheel
|
||||
pip3 install pyinstaller
|
||||
pip3 install PyQt5
|
||||
pip3 install -r requirements.txt --upgrade
|
||||
working-directory: TIDALDL-PY
|
||||
|
||||
- name: Clean directory
|
||||
shell: bash
|
||||
run: |
|
||||
rm -rf dist
|
||||
rm -rf build
|
||||
rm -rf tidal_dl.egg-info
|
||||
rm -rf tidal_gui.egg-info
|
||||
rm -rf MANIFEST.in
|
||||
working-directory: TIDALDL-PY
|
||||
|
||||
- name: Build tidal-dl
|
||||
run: |
|
||||
pyinstaller -F tidal_dl/__init__.py -n tidal-dl
|
||||
working-directory: TIDALDL-PY
|
||||
|
||||
# - name: Build tidal-gui-macOs
|
||||
# shell: bash
|
||||
# if: ${{ matrix.os == 'macos-latest' }}
|
||||
# run: |
|
||||
# cp -rf guiStatic.in MANIFEST.in
|
||||
# pyinstaller -F tidal_gui/__init__.py -w -n tidal-gui
|
||||
# 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: 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
|
||||
with:
|
||||
name: tidal-dl-${{ runner.os }}
|
||||
path: |
|
||||
TIDALDL-PY/dist/*
|
||||
|
||||
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
name: Tidal Media Downloader
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
Build:
|
||||
name: Build tidal-dl
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Tidal-Media-Downloader repo
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
pip3 install wheel
|
||||
pip3 install pyinstaller
|
||||
pip3 install PyQt5
|
||||
pip3 install -r requirements.txt --upgrade
|
||||
working-directory: TIDALDL-PY
|
||||
|
||||
- name: Clean exe directory
|
||||
run: |
|
||||
rm -force exe/tidal-dl.exe
|
||||
working-directory: TIDALDL-PY
|
||||
|
||||
- name: Build tidal-dl artifact
|
||||
run: |
|
||||
python setup.py sdist bdist_wheel
|
||||
pyinstaller -F tidal_dl/__init__.py
|
||||
mv dist/__init__.exe exe/tidal-dl.exe
|
||||
working-directory: TIDALDL-PY
|
||||
|
||||
- name: Upload tidal-dl artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: tidal-dl
|
||||
path: TIDALDL-PY\exe\tidal-dl.exe
|
||||
@@ -167,3 +167,14 @@ tidal_dl-BAK
|
||||
/TIDALDL-GUI-CROSS/tidal_gui/viewModel/__pycache__
|
||||
/TIDALDL-GUI-CROSS/tidal_gui/control/__pycache__
|
||||
__pycache__
|
||||
clean.sh
|
||||
TIDALDL-PY/MANIFEST.in
|
||||
TIDALDL-PY/exe/resource
|
||||
TIDALDL-PY/exe/tidal_dl_win.tar.gz
|
||||
.github/workflows/continuous-integration-workflow copy.yml
|
||||
TIDALDL-PY/tidal-gui.spec
|
||||
.history/
|
||||
.gitignore
|
||||
/TIDALDL-PY/tidal_dl_old
|
||||
/TIDALDL-PY/tidal_gui_old
|
||||
/TIDALDL-PY/tidal_gui
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"env": {
|
||||
"PYTHONPATH": "${workspaceRoot}/TIDALDL-PY/"
|
||||
},
|
||||
"justMyCode": false
|
||||
},
|
||||
{
|
||||
"name": "Python: common line",
|
||||
@@ -33,7 +34,8 @@
|
||||
"-o",
|
||||
"e:\\test",
|
||||
"-q",
|
||||
"0"
|
||||
"0",
|
||||
"-g"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<br>
|
||||
<a href="https://github.com/yaronzz/Tidal-Media-Downloader-PRO">[GUI-REPOSITORY]</a>
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
<h1>Tidal-Media-Downloader</h1>
|
||||
<a href="https://github.com/yaronzz/Tidal-Media-Downloader/blob/master/LICENSE">
|
||||
@@ -18,6 +19,9 @@
|
||||
<a href="https://pypi.org/project/tidal-dl/">
|
||||
<img src="https://img.shields.io/pypi/dm/tidal-dl?label=tidal-dl%20download" alt="">
|
||||
</a>
|
||||
<a href="https://github.com/yaronzz/Tidal-Media-Downloader/actions/workflows/build.yml">
|
||||
<img src="https://github.com/yaronzz/Tidal-Media-Downloader/actions/workflows/build.yml/badge.svg" alt="">
|
||||
</a>
|
||||
</div>
|
||||
<p align="center">
|
||||
«Tidal-Media-Downloader» is an application that lets you download videos and tracks from Tidal. It supports two version: tidal-dl and tidal-gui. (This repository only contains tidal-dl, and the release isn't the newest gui version.)
|
||||
@@ -25,15 +29,23 @@
|
||||
<a href="https://github.com/yaronzz/Tidal-Media-Downloader-PRO/releases">Download</a> |
|
||||
<a href="https://yaronzz.com/post/tidal_dl_installation/">Documentation</a> |
|
||||
<a href="https://yaronzz.com/post/tidal_dl_installation_chn/">中文文档</a> |
|
||||
<a href="https://t.me/Tidal_Media_Downloader">Channel</a>
|
||||
<br>
|
||||
</p>
|
||||
|
||||
## 📺 Installation
|
||||
| Name | platform | Install |
|
||||
| -------------- | --------------------------------- | ------------------------------------------------------------ |
|
||||
| tidal-gui | Windows | [GUI Repository](https://github.com/yaronzz/Tidal-Media-Downloader-PRO) |
|
||||
| tidal-dl (cli) | Windows \ Linux \ Macos \ Android | ```pip3 install tidal-dl --upgrade```<br />[Detailed Description](https://yaronzz.com/post/tidal_dl_installation/#Install) |
|
||||
|
||||
```shell
|
||||
pip3 install tidal-dl --upgrade
|
||||
```
|
||||
|
||||
| USE | FUNCTION |
|
||||
| ----------------------------------------------------- | -------------------------- |
|
||||
| tidal-dl | Show interactive interface |
|
||||
| tidal-dl -h | Show help-message |
|
||||
| tidal-dl -l "https://tidal.com/browse/track/70973230" | Download link |
|
||||
| tidal-dl -g | Show simple-gui |
|
||||
|
||||
If you are using windows system, you can use [tidal-pro](https://github.com/yaronzz/Tidal-Media-Downloader-PRO)
|
||||
|
||||
### Nightly Builds
|
||||
|
||||
@@ -43,11 +55,6 @@
|
||||
[Actions]: https://github.com/yaronzz/Tidal-Media-Downloader/actions
|
||||
[Build]: https://github.com/yaronzz/Tidal-Media-Downloader/workflows/Tidal%20Media%20Downloader/badge.svg
|
||||
|
||||
## 📡 Telegram
|
||||
|
||||
- [Group](https://t.me/tidal_group) : Feed back
|
||||
- [Channel](https://t.me/Tidal_Media_Downloader) : Notify the new version
|
||||
|
||||
## 🤖 Features
|
||||
- Download album \ track \ video \ playlist \ artist-albums
|
||||
|
||||
@@ -59,6 +66,8 @@
|
||||
|
||||
<img src="https://i.loli.net/2020/08/19/gqW6zHI1SrKlomC.png" alt="image" style="zoom: 50%;" />
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Settings - Possible Tags
|
||||
@@ -97,6 +106,19 @@
|
||||
| {AudioQuality} | LOSSLESS |
|
||||
| {DurationSeconds} | 173 |
|
||||
| {Duration} | 02:53 |
|
||||
| {TrackID} | 55163244 |
|
||||
|
||||
### Video
|
||||
|
||||
| Tag | Example Value |
|
||||
| ----------------- | ------------------------------------------ |
|
||||
| {VideoNumber} | 00 |
|
||||
| {ArtistName} | DMX |
|
||||
| {ArtistsName} | DMX, Westside Gunn |
|
||||
| {VideoTitle} | Hood Blues |
|
||||
| {ExplicitFlag} | (*Explicit*) |
|
||||
| {VideoYear} | 2021 |
|
||||
| {TrackID} | 188932980 |
|
||||
|
||||
## ☕ Support
|
||||
|
||||
@@ -106,6 +128,7 @@ If you really like my projects and want to support me, you can buy me a coffee 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>
|
||||
|
||||
## 🎨 Libraries and reference
|
||||
|
||||
@@ -4,6 +4,8 @@ prettytable==3.1.1
|
||||
mutagen==1.45.1
|
||||
psutil==5.9.0
|
||||
pycryptodome==3.14.1
|
||||
aigpy==2022.1.20.1
|
||||
aigpy==2022.7.8.1
|
||||
lyricsgenius==3.0.1
|
||||
pydub==0.25.1
|
||||
PyQt5==5.15.7
|
||||
qt-material==2.12
|
||||
@@ -11,6 +11,6 @@ setup(
|
||||
packages=find_packages(exclude=['tidal_dl*']),
|
||||
include_package_data = True,
|
||||
platforms = "any",
|
||||
install_requires=["tidal-dl", "PyQt5"],
|
||||
install_requires=["tidal-dl"],
|
||||
entry_points={'console_scripts': [ 'tidal-gui = tidal_gui:main', ]}
|
||||
)
|
||||
|
||||
@@ -13,7 +13,10 @@ setup(
|
||||
packages=find_packages(exclude=['tidal_gui*']),
|
||||
include_package_data=False,
|
||||
platforms="any",
|
||||
install_requires=["aigpy>=2022.01.20.1", "requests>=2.22.0",
|
||||
"pycryptodome", "pydub", "prettytable", "lyricsgenius"],
|
||||
install_requires=["aigpy>=2022.7.8.1",
|
||||
"requests>=2.22.0",
|
||||
"pycryptodome",
|
||||
"pydub",
|
||||
"prettytable"],
|
||||
entry_points={'console_scripts': ['tidal-dl = tidal_dl:main', ]}
|
||||
)
|
||||
|
||||
@@ -1,244 +1,152 @@
|
||||
#!/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.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 startGui
|
||||
|
||||
|
||||
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
|
||||
continue
|
||||
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.select.MSG_PATH_ERR + SETTINGS.downloadPath)
|
||||
return
|
||||
|
||||
if showGui:
|
||||
startGui()
|
||||
return
|
||||
|
||||
if link is not None:
|
||||
if not loginByConfig():
|
||||
loginByWeb()
|
||||
Printf.info(LANG.select.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 apiKey.isItemValid(SETTINGS.apiKeyIndex):
|
||||
changeApiKey()
|
||||
loginByWeb()
|
||||
elif not loginByConfig():
|
||||
loginByWeb()
|
||||
|
||||
Printf.checkVersion()
|
||||
|
||||
while True:
|
||||
Printf.choices()
|
||||
choice = Printf.enter(LANG.select.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}"
|
||||
SETTINGS.multiThread = True
|
||||
|
||||
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 :
|
||||
"""
|
||||
@@ -60,13 +60,12 @@ __KEYS_JSON__ = '''
|
||||
'''
|
||||
__API_KEYS__ = json.loads(__KEYS_JSON__)
|
||||
__ERROR_KEY__ = {
|
||||
'platform': 'None',
|
||||
'formats': '',
|
||||
'clientId': '',
|
||||
'clientSecret': '',
|
||||
'platform': 'None',
|
||||
'formats': '',
|
||||
'clientId': '',
|
||||
'clientSecret': '',
|
||||
'valid': 'False',
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
|
||||
def getNum():
|
||||
@@ -79,6 +78,11 @@ def getItem(index: int):
|
||||
return __API_KEYS__['keys'][index]
|
||||
|
||||
|
||||
def isItemValid(index: int):
|
||||
item = getItem(index)
|
||||
return item['valid'] == 'True'
|
||||
|
||||
|
||||
def getItems():
|
||||
return __API_KEYS__['keys']
|
||||
|
||||
@@ -93,7 +97,8 @@ def getLimitIndexs():
|
||||
def getVersion():
|
||||
return __API_KEYS__['version']
|
||||
|
||||
#Load from gist
|
||||
|
||||
# Load from gist
|
||||
try:
|
||||
respond = requests.get('https://api.github.com/gists/48d01f5a24b4b7b37f19443977c22cd6')
|
||||
if respond.status_code == 200:
|
||||
@@ -101,5 +106,3 @@ try:
|
||||
__API_KEYS__ = json.loads(content)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@@ -8,177 +8,216 @@
|
||||
@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 *
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
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 __loadAPI__(user):
|
||||
API.key.accessToken = user.accessToken
|
||||
API.key.userId = user.userid
|
||||
API.key.countryCode = user.countryCode
|
||||
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 __downloadCover__(conf, album):
|
||||
if album == None:
|
||||
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 __track__(conf, obj):
|
||||
msg, album = API.getAlbum(obj.album.id)
|
||||
if conf.saveCovers:
|
||||
__downloadCover__(conf, album)
|
||||
downloadTrack(obj, album)
|
||||
|
||||
|
||||
def __video__(conf, obj):
|
||||
# Printf.video(obj)
|
||||
downloadVideo(obj, obj.album)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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
|
||||
|
||||
for index, item in enumerate(tracks):
|
||||
mag, album = API.getAlbum(item.album.id)
|
||||
item.trackNumberOnPlaylist = index + 1
|
||||
downloadTrack(item, album, obj)
|
||||
if conf.saveCovers and not conf.usePlaylistFolder:
|
||||
__downloadCover__(conf, album)
|
||||
for item in videos:
|
||||
downloadVideo(item, None)
|
||||
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)
|
||||
|
||||
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)
|
||||
m3u8content = requests.get(stream.m3u8Url).content
|
||||
if m3u8content is None:
|
||||
Printf.err(f"DL Video[{video.title}] getM3u8 failed.{str(e)}")
|
||||
return False, f"GetM3u8 failed.{str(e)}"
|
||||
|
||||
urls = aigpy.m3u8.parseTsUrls(m3u8content)
|
||||
if len(urls) <= 0:
|
||||
Printf.err(f"DL Video[{video.title}] getTsUrls failed.{str(e)}")
|
||||
return False, "GetTsUrls failed.{str(e)}"
|
||||
|
||||
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, msg
|
||||
except Exception as e:
|
||||
Printf.err(f"DL Video[{video.title}] failed.{str(e)}")
|
||||
return False, str(e)
|
||||
|
||||
|
||||
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 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 and not SETTINGS.multiThread:
|
||||
Printf.track(track, stream)
|
||||
|
||||
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, ''
|
||||
|
||||
# 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 and not SETTINGS.multiThread)
|
||||
if not check:
|
||||
Printf.err(f"DL Track[{track.title}] failed.{str(err)}")
|
||||
return False, str(err)
|
||||
|
||||
# encrypted -> decrypt and remove encrypted file
|
||||
__encrypted__(stream, path + '.part', path)
|
||||
|
||||
# contributors
|
||||
try:
|
||||
contributors = TIDAL_API.getTrackContributors(track.id)
|
||||
except:
|
||||
contributors = None
|
||||
|
||||
# lyrics
|
||||
try:
|
||||
lyrics = TIDAL_API.getLyrics(track.id).subtitles
|
||||
if SETTINGS.lyricFile:
|
||||
lrcPath = path.rsplit(".", 1)[0] + '.lrc'
|
||||
aigpy.file.write(lrcPath, lyrics, 'w')
|
||||
except:
|
||||
lyrics = ''
|
||||
|
||||
__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, str(e)
|
||||
|
||||
|
||||
def start(user, conf, string):
|
||||
__loadAPI__(user)
|
||||
if aigpy.string.isNull(string):
|
||||
Printf.err('Please enter something.')
|
||||
return
|
||||
def downloadTracks(tracks, album: Album = None, playlist : Playlist=None):
|
||||
def __getAlbum__(item: Track):
|
||||
album = TIDAL_API.getAlbum(item.album.id)
|
||||
if SETTINGS.saveCovers and not SETTINGS.usePlaylistFolder:
|
||||
downloadCover(album)
|
||||
return album
|
||||
|
||||
if not SETTINGS.multiThread:
|
||||
for index, item in enumerate(tracks):
|
||||
itemAlbum = album
|
||||
if itemAlbum is None:
|
||||
itemAlbum = __getAlbum__(item)
|
||||
item.trackNumberOnPlaylist = index + 1
|
||||
downloadTrack(item, itemAlbum, playlist)
|
||||
else:
|
||||
thread_pool = ThreadPoolExecutor(max_workers=5)
|
||||
for index, item in enumerate(tracks):
|
||||
itemAlbum = album
|
||||
if itemAlbum is None:
|
||||
itemAlbum = __getAlbum__(item)
|
||||
item.trackNumberOnPlaylist = index + 1
|
||||
thread_pool.submit(downloadTrack, item, itemAlbum, playlist)
|
||||
thread_pool.shutdown(wait=True)
|
||||
|
||||
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)
|
||||
def downloadVideos(videos, album: Album, playlist=None):
|
||||
for item in videos:
|
||||
downloadVideo(item, album, playlist)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
@File : enums.py
|
||||
@Time : 2020/08/08
|
||||
@Author : Yaronzz
|
||||
@Version : 2.0
|
||||
@Version : 3.0
|
||||
@Contact : yaronhuang@foxmail.com
|
||||
@Desc :
|
||||
'''
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
#!/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)
|
||||
downloadTracks(tracks, obj)
|
||||
downloadVideos(videos, 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):
|
||||
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)
|
||||
downloadTracks(tracks, None, obj)
|
||||
downloadVideos(videos, None, obj)
|
||||
|
||||
|
||||
def start_mix(obj: Mix):
|
||||
Printf.mix(obj)
|
||||
downloadTracks(obj.tracks, None, None)
|
||||
downloadVideos(obj.videos, None, 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.downloadPath = Printf.enterPath(
|
||||
LANG.select.CHANGE_DOWNLOAD_PATH,
|
||||
LANG.select.MSG_PATH_ERR,
|
||||
'0',
|
||||
SETTINGS.downloadPath)
|
||||
SETTINGS.albumFolderFormat = Printf.enterFormat(
|
||||
LANG.select.CHANGE_ALBUM_FOLDER_FORMAT,
|
||||
SETTINGS.albumFolderFormat,
|
||||
SETTINGS.getDefaultPathFormat(Type.Album))
|
||||
SETTINGS.trackFileFormat = Printf.enterFormat(
|
||||
LANG.select.CHANGE_TRACK_FILE_FORMAT,
|
||||
SETTINGS.trackFileFormat,
|
||||
SETTINGS.getDefaultPathFormat(Type.Track))
|
||||
SETTINGS.videoFileFormat = Printf.enterFormat(
|
||||
LANG.select.CHANGE_VIDEO_FILE_FORMAT,
|
||||
SETTINGS.videoFileFormat,
|
||||
SETTINGS.getDefaultPathFormat(Type.Video))
|
||||
SETTINGS.save()
|
||||
|
||||
|
||||
def changeQualitySettings():
|
||||
Printf.settings()
|
||||
SETTINGS.audioQuality = AudioQuality(
|
||||
int(Printf.enterLimit(LANG.select.CHANGE_AUDIO_QUALITY,
|
||||
LANG.select.MSG_INPUT_ERR,
|
||||
['0', '1', '2', '3'])))
|
||||
SETTINGS.videoQuality = VideoQuality(
|
||||
int(Printf.enterLimit(LANG.select.CHANGE_VIDEO_QUALITY,
|
||||
LANG.select.MSG_INPUT_ERR,
|
||||
['1080', '720', '480', '360'])))
|
||||
SETTINGS.save()
|
||||
|
||||
|
||||
def changeSettings():
|
||||
Printf.settings()
|
||||
SETTINGS.showProgress = Printf.enterBool(LANG.select.CHANGE_SHOW_PROGRESS)
|
||||
SETTINGS.showTrackInfo = Printf.enterBool(LANG.select.CHANGE_SHOW_TRACKINFO)
|
||||
SETTINGS.checkExist = Printf.enterBool(LANG.select.CHANGE_CHECK_EXIST)
|
||||
SETTINGS.includeEP = Printf.enterBool(LANG.select.CHANGE_INCLUDE_EP)
|
||||
SETTINGS.saveCovers = Printf.enterBool(LANG.select.CHANGE_SAVE_COVERS)
|
||||
SETTINGS.saveAlbumInfo = Printf.enterBool(LANG.select.CHANGE_SAVE_ALBUM_INFO)
|
||||
SETTINGS.lyricFile = Printf.enterBool(LANG.select.CHANGE_ADD_LRC_FILE)
|
||||
SETTINGS.multiThread = Printf.enterBool(LANG.select.CHANGE_MULITHREAD_DOWNLOAD)
|
||||
SETTINGS.usePlaylistFolder = Printf.enterBool(LANG.select.SETTING_USE_PLAYLIST_FOLDER + "('0'-No,'1'-Yes):")
|
||||
SETTINGS.language = Printf.enter(LANG.select.CHANGE_LANGUAGE + "(" + LANG.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.select.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.select.AUTH_START_LOGIN)
|
||||
# get device code
|
||||
url = TIDAL_API.getDeviceCode()
|
||||
|
||||
print(LANG.select.AUTH_NEXT_STEP.format(
|
||||
aigpy.cmd.green(url),
|
||||
aigpy.cmd.yellow(__displayTime__(TIDAL_API.key.authCheckTimeout))))
|
||||
print(LANG.select.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.select.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.select.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.select.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.select.MSG_INVALID_ACCESSTOKEN)
|
||||
if TIDAL_API.refreshAccessToken(TOKEN.refreshToken):
|
||||
Printf.success(LANG.select.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()
|
||||
@@ -0,0 +1,288 @@
|
||||
#!/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 sys
|
||||
import aigpy
|
||||
import _thread
|
||||
import importlib
|
||||
|
||||
from tidal_dl.events import *
|
||||
from tidal_dl.settings import *
|
||||
from tidal_dl.printf import *
|
||||
from tidal_dl.enums import *
|
||||
|
||||
|
||||
def enableGui():
|
||||
try:
|
||||
importlib.import_module('PyQt5')
|
||||
importlib.import_module('qt_material')
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
|
||||
if not enableGui():
|
||||
def startGui():
|
||||
Printf.err("Not support gui. Please type: `pip3 install PyQt5 qt_material`")
|
||||
else:
|
||||
from PyQt5.QtCore import Qt, QObject
|
||||
from PyQt5.QtGui import QTextCursor
|
||||
from PyQt5.QtCore import pyqtSignal
|
||||
from PyQt5 import QtWidgets
|
||||
from qt_material import apply_stylesheet
|
||||
|
||||
class SettingView(QtWidgets.QWidget):
|
||||
def __init__(self, ) -> None:
|
||||
super().__init__()
|
||||
self.initView()
|
||||
|
||||
def initView(self):
|
||||
self.c_pathDownload = QtWidgets.QLineEdit()
|
||||
self.c_pathAlbumFormat = QtWidgets.QLineEdit()
|
||||
self.c_pathTrackFormat = QtWidgets.QLineEdit()
|
||||
self.c_pathVideoFormat = QtWidgets.QLineEdit()
|
||||
|
||||
self.mainGrid = QtWidgets.QVBoxLayout(self)
|
||||
self.mainGrid.addWidget(self.c_pathDownload)
|
||||
self.mainGrid.addWidget(self.c_pathAlbumFormat)
|
||||
self.mainGrid.addWidget(self.c_pathTrackFormat)
|
||||
self.mainGrid.addWidget(self.c_pathVideoFormat)
|
||||
|
||||
class EmittingStream(QObject):
|
||||
textWritten = pyqtSignal(str)
|
||||
|
||||
def write(self, text):
|
||||
self.textWritten.emit(str(text))
|
||||
|
||||
class MainView(QtWidgets.QWidget):
|
||||
s_downloadEnd = pyqtSignal(str, bool, str)
|
||||
|
||||
def __init__(self, ) -> None:
|
||||
super().__init__()
|
||||
self.initView()
|
||||
self.setMinimumSize(600, 620)
|
||||
self.setWindowTitle("Tidal-dl")
|
||||
|
||||
def __info__(self, msg):
|
||||
QtWidgets.QMessageBox.information(self, 'Info', msg, QtWidgets.QMessageBox.Yes)
|
||||
|
||||
def __output__(self, text):
|
||||
cursor = self.c_printTextEdit.textCursor()
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
cursor.insertText(text)
|
||||
self.c_printTextEdit.setTextCursor(cursor)
|
||||
self.c_printTextEdit.ensureCursorVisible()
|
||||
|
||||
def initView(self):
|
||||
self.c_lineSearch = QtWidgets.QLineEdit()
|
||||
self.c_btnSearch = QtWidgets.QPushButton("Search")
|
||||
self.c_btnDownload = QtWidgets.QPushButton("Download")
|
||||
self.c_btnSetting = QtWidgets.QPushButton("Setting")
|
||||
self.c_combType = QtWidgets.QComboBox()
|
||||
self.c_combTQuality = QtWidgets.QComboBox()
|
||||
self.c_combVQuality = QtWidgets.QComboBox()
|
||||
self.c_widgetSetting = SettingView()
|
||||
self.c_widgetSetting.hide()
|
||||
|
||||
# Supported types for search
|
||||
self.m_supportType = [Type.Album, Type.Playlist, Type.Track, Type.Video, Type.Artist]
|
||||
for item in self.m_supportType:
|
||||
self.c_combType.addItem(item.name, item)
|
||||
|
||||
for item in AudioQuality:
|
||||
self.c_combTQuality.addItem(item.name, item)
|
||||
for item in VideoQuality:
|
||||
self.c_combVQuality.addItem(item.name, item)
|
||||
self.c_combTQuality.setCurrentText(SETTINGS.audioQuality.name)
|
||||
self.c_combVQuality.setCurrentText(SETTINGS.videoQuality.name)
|
||||
|
||||
# init table
|
||||
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)
|
||||
|
||||
# print
|
||||
self.c_printTextEdit = QtWidgets.QTextEdit()
|
||||
self.c_printTextEdit.setReadOnly(True)
|
||||
self.c_printTextEdit.setFixedHeight(150)
|
||||
sys.stdout = EmittingStream(textWritten=self.__output__)
|
||||
sys.stderr = EmittingStream(textWritten=self.__output__)
|
||||
|
||||
# layout
|
||||
self.lineGrid = QtWidgets.QHBoxLayout()
|
||||
self.lineGrid.addWidget(self.c_combType)
|
||||
self.lineGrid.addWidget(self.c_lineSearch)
|
||||
self.lineGrid.addWidget(self.c_btnSearch)
|
||||
|
||||
self.line2Grid = QtWidgets.QHBoxLayout()
|
||||
self.line2Grid.addWidget(QtWidgets.QLabel("QUALITY:"))
|
||||
self.line2Grid.addWidget(self.c_combTQuality)
|
||||
self.line2Grid.addWidget(self.c_combVQuality)
|
||||
self.line2Grid.addStretch(4)
|
||||
# self.line2Grid.addWidget(self.c_btnSetting)
|
||||
self.line2Grid.addWidget(self.c_btnDownload)
|
||||
|
||||
self.funcGrid = QtWidgets.QVBoxLayout()
|
||||
self.funcGrid.addLayout(self.lineGrid)
|
||||
self.funcGrid.addWidget(self.c_tableInfo)
|
||||
self.funcGrid.addLayout(self.line2Grid)
|
||||
self.funcGrid.addWidget(self.c_printTextEdit)
|
||||
|
||||
self.mainGrid = QtWidgets.QGridLayout(self)
|
||||
self.mainGrid.addLayout(self.funcGrid, 0, 0)
|
||||
self.mainGrid.addWidget(self.c_widgetSetting, 0, 0)
|
||||
|
||||
# connect
|
||||
self.c_btnSearch.clicked.connect(self.search)
|
||||
self.c_btnDownload.clicked.connect(self.download)
|
||||
self.s_downloadEnd.connect(self.downloadEnd)
|
||||
self.c_combTQuality.currentIndexChanged.connect(self.changeTQuality)
|
||||
self.c_combVQuality.currentIndexChanged.connect(self.changeVQuality)
|
||||
self.c_btnSetting.clicked.connect(self.showSettings)
|
||||
|
||||
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, '')
|
||||
elif self.s_type in [Type.Artist]:
|
||||
self.addItem(index, 1, item.name)
|
||||
self.addItem(index, 2, '')
|
||||
self.addItem(index, 3, '')
|
||||
self.c_tableInfo.viewport().update()
|
||||
|
||||
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)
|
||||
item_to_download = ""
|
||||
if isinstance(self.s_array[index], Artist):
|
||||
item_to_download = self.s_array[index].name
|
||||
else:
|
||||
item_to_download = self.s_array[index].title
|
||||
self.c_btnDownload.setText(f"Downloading [${item_to_download}]...")
|
||||
|
||||
def __thread_download__(model: MainView):
|
||||
downloading_item = ""
|
||||
try:
|
||||
type = model.s_type
|
||||
item = model.s_array[index]
|
||||
start_type(type, item)
|
||||
if isinstance(item, Artist):
|
||||
downloading_item = item.name
|
||||
else:
|
||||
downloading_item = item.title
|
||||
model.s_downloadEnd.emit(downloading_item, True, '')
|
||||
except Exception as e:
|
||||
model.s_downloadEnd.emit(downloading_item, False, str(e))
|
||||
|
||||
_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 changeTQuality(self, index):
|
||||
SETTINGS.audioQuality = self.c_combTQuality.itemData(index)
|
||||
SETTINGS.save()
|
||||
|
||||
def changeVQuality(self, index):
|
||||
SETTINGS.videoQuality = self.c_combVQuality.itemData(index)
|
||||
SETTINGS.save()
|
||||
|
||||
def showSettings(self):
|
||||
self.c_widgetSetting.show()
|
||||
|
||||
def startGui():
|
||||
aigpy.cmd.enableColor(False)
|
||||
|
||||
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,8 +31,9 @@ class LangArabic(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Multi thread download"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Album folder format"
|
||||
SETTING_TRACK_FILE_FORMAT = "Track file format"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Show progress"
|
||||
SETTING_SHOW_TRACKIFNO = "Show Track Info"
|
||||
SETTING_SHOW_TRACKINFO = "Show Track Info"
|
||||
SETTING_SAVE_ALBUMINFO = "Save AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Add lyrics"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Lyrics server proxy"
|
||||
@@ -79,6 +80,7 @@ class LangArabic(object):
|
||||
CHANGE_LANGUAGE = "اختر لغة"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Album folder format('0' not modify):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Track file format('0' not modify):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Show progress('0'-No,'1'-Yes):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show track info('0'-No,'1'-Yes):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Save AlbumInfo.txt('0'-No,'1'-Yes):"
|
||||
@@ -86,6 +88,7 @@ class LangArabic(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Lyrics server proxy('0' not modify):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Starting login process..."
|
||||
@@ -95,7 +98,7 @@ class LangArabic(object):
|
||||
AUTH_TIMEOUT = "Operation timed out."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken good for {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_INVALID_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_PATH_ERR = "!مجلد التنزيل خاطئ"
|
||||
MSG_INPUT_ERR = "!ادخال خاطئ"
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class LangChinese(object):
|
||||
VALUE = "值"
|
||||
SETTING_DOWNLOAD_PATH = "下载目录"
|
||||
SETTING_ONLY_M4A = "转换mp4为m4a"
|
||||
SETTING_ADD_EXPLICIT_TAG = "文件名添加脏话标签Explicit"
|
||||
SETTING_ADD_EXPLICIT_TAG = "文件名添加脏标Explicit"
|
||||
SETTING_ADD_HYPHEN = "文件名用'-'代替空格"
|
||||
SETTING_ADD_YEAR = "专辑文件夹添加年代标签"
|
||||
SETTING_USE_TRACK_NUM = "歌曲名称添加序号"
|
||||
@@ -31,27 +31,28 @@ class LangChinese(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "多线程下载"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "专辑目录格式"
|
||||
SETTING_TRACK_FILE_FORMAT = "歌曲文件名格式"
|
||||
SETTING_VIDEO_FILE_FORMAT = "视频文件格式"
|
||||
SETTING_SHOW_PROGRESS = "显示进度条"
|
||||
SETTING_SHOW_TRACKIFNO = "显示歌曲信息"
|
||||
SETTING_SHOW_TRACKINFO = "显示歌曲信息"
|
||||
SETTING_SAVE_ALBUMINFO = "保存AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "添加歌词"
|
||||
SETTING_LYRICS_SERVER_PROXY = "歌词服务器代理"
|
||||
SETTING_PATH = "Settings path"
|
||||
SETTINGS_ADD_LRC_FILE = "Save timed lyrics (.lrc file)"
|
||||
SETTING_PATH = "配置文件目录"
|
||||
SETTINGS_ADD_LRC_FILE = "保存歌词文件 (.lrc file)"
|
||||
SETTING_APIKEY = "APIKey支持"
|
||||
SETTING_ADD_TYPE_FOLDER = "Add Type-Folder"
|
||||
SETTING_ADD_TYPE_FOLDER = "添加类型文件夹"
|
||||
|
||||
CHOICE = "选项"
|
||||
FUNCTION = "功能"
|
||||
CHOICE_ENTER = "输入"
|
||||
CHOICE_ENTER_URLID = "输入 'Url或ID':"
|
||||
CHOICE_EXIT = "退出"
|
||||
CHOICE_LOGIN = "Check AccessToken"
|
||||
CHOICE_LOGIN = "检查AccessToken"
|
||||
CHOICE_SETTINGS = "配置"
|
||||
CHOICE_SET_ACCESS_TOKEN = "设置AccessToken"
|
||||
CHOICE_DOWNLOAD_BY_URL = "通过链接或ID下载"
|
||||
CHOICE_LOGOUT = "注销"
|
||||
CHOICE_APIKEY = "Select APIKey"
|
||||
CHOICE_APIKEY = "选择APIKey"
|
||||
|
||||
PRINT_ERR = "[错误]"
|
||||
PRINT_INFO = "[提示]"
|
||||
@@ -79,13 +80,15 @@ class LangChinese(object):
|
||||
CHANGE_LANGUAGE = "选择语言"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "专辑目录格式('0' 不修改):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "歌曲文件名格式('0' 不修改):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "视频文件名格式('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "显示进度条('0'-不,'1'-是):"
|
||||
CHANGE_SHOW_TRACKINFO = "显示歌曲信息('0'-否,'1'-是):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "保存AlbumInfo.txt('0'-否,'1'-是):"
|
||||
CHANGE_ADD_LYRICS = "添加歌词('0'-否,'1'-是):"
|
||||
CHANGE_LYRICS_SERVER_PROXY = "歌词服务器代理('0' 不修改):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-否,'1'-是):"
|
||||
CHANGE_ADD_LRC_FILE = "保存歌词文件 ('0'-否,'1'-是):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "多线程下载('0'-否,'1'-是):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "开始启动登录..."
|
||||
@@ -94,8 +97,8 @@ class LangChinese(object):
|
||||
AUTH_WAITING = "等待登录验证..."
|
||||
AUTH_TIMEOUT = "操作超时."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken保质期为 {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "AccessToken失效. 正在尝试更新它."
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken有效期为 {}."
|
||||
MSG_INVALID_ACCESSTOKEN = "AccessToken失效. 正在尝试更新它."
|
||||
MSG_PATH_ERR = "路径错误!"
|
||||
MSG_INPUT_ERR = "输入错误!"
|
||||
|
||||
@@ -110,7 +113,7 @@ class LangChinese(object):
|
||||
MODEL_VIDEO_NUMBER = '视频数量'
|
||||
MODEL_RELEASE_DATE = '发布时间'
|
||||
MODEL_VERSION = '版本'
|
||||
MODEL_EXPLICIT = '脏话标志'
|
||||
MODEL_EXPLICIT = '脏标'
|
||||
MODEL_ALBUM = '专辑'
|
||||
MODEL_ID = 'ID'
|
||||
MODEL_NAME = '名称'
|
||||
|
||||
@@ -31,8 +31,9 @@ class LangCroatian(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Multi thread download"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Album folder format"
|
||||
SETTING_TRACK_FILE_FORMAT = "Track file format"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Show progress"
|
||||
SETTING_SHOW_TRACKIFNO = "Show Track Info"
|
||||
SETTING_SHOW_TRACKINFO = "Show Track Info"
|
||||
SETTING_SAVE_ALBUMINFO = "Save AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Add lyrics"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Lyrics server proxy"
|
||||
@@ -79,6 +80,7 @@ class LangCroatian(object):
|
||||
CHANGE_LANGUAGE = "Odaberi jezik"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Album folder format('0' not modify):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Track file format('0' not modify):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Show progress('0'-No,'1'-Yes):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show track info('0'-No,'1'-Yes):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Save AlbumInfo.txt('0'-No,'1'-Yes):"
|
||||
@@ -86,6 +88,7 @@ class LangCroatian(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Lyrics server proxy('0' not modify):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Starting login process..."
|
||||
@@ -95,7 +98,7 @@ class LangCroatian(object):
|
||||
AUTH_TIMEOUT = "Operation timed out."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken good for {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_INVALID_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_PATH_ERR = "Pogreska putanje!"
|
||||
MSG_INPUT_ERR = "Pogreska unosa!"
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
'''
|
||||
@File : czech.py
|
||||
@Time : 2020/08/20
|
||||
@Author : Tomikk
|
||||
@Version : 1.0
|
||||
@Contact : justtomikk@gmail.com
|
||||
@Time : 2022/07/26
|
||||
@Author : Tomikk & Sweder
|
||||
@Version : 1.1
|
||||
@Contact : justtomikk@gmail.com & djsweder@gmail.com
|
||||
@Desc :
|
||||
'''
|
||||
|
||||
@@ -15,45 +15,46 @@ class LangCzech(object):
|
||||
VALUE = "Hodnota"
|
||||
SETTING_DOWNLOAD_PATH = "Umístění staženého souboru"
|
||||
SETTING_ONLY_M4A = "Konvertovat mp4 na m4a"
|
||||
SETTING_ADD_EXPLICIT_TAG = "Přidat explicitní značku"
|
||||
SETTING_ADD_HYPHEN = "Používat pomlčky místo mezer"
|
||||
SETTING_ADD_EXPLICIT_TAG = "Přidat označení explicity"
|
||||
SETTING_ADD_HYPHEN = "Místo mezer použít pomlčky"
|
||||
SETTING_ADD_YEAR = "Přidat rok před jméno složky"
|
||||
SETTING_USE_TRACK_NUM = "Přidat číslo skladby"
|
||||
SETTING_AUDIO_QUALITY = "Kvalita hudby"
|
||||
SETTING_VIDEO_QUALITY = "Kvalita videa"
|
||||
SETTING_CHECK_EXIST = "Zkontrolovat jestli soubor již existuje"
|
||||
SETTING_CHECK_EXIST = "Zkontrolovat existenci souboru"
|
||||
SETTING_ARTIST_BEFORE_TITLE = "Jméno interpreta před jménem skladby"
|
||||
SETTING_ALBUMID_BEFORE_FOLDER = "Id před složkou alba"
|
||||
SETTING_INCLUDE_EP = "Zahrnout single&ep"
|
||||
SETTING_INCLUDE_EP = "Zahrnout singly & EP"
|
||||
SETTING_SAVE_COVERS = "Uložit obal alba"
|
||||
SETTING_LANGUAGE = "Změna jazyka"
|
||||
SETTING_USE_PLAYLIST_FOLDER = "Use playlist folder"
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Multi thread download"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Album folder format"
|
||||
SETTING_TRACK_FILE_FORMAT = "Track file format"
|
||||
SETTING_SHOW_PROGRESS = "Show progress"
|
||||
SETTING_SHOW_TRACKIFNO = "Show Track Info"
|
||||
SETTING_SAVE_ALBUMINFO = "Save AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Add lyrics"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Lyrics server proxy"
|
||||
SETTINGS_ADD_LRC_FILE = "Save timed lyrics (.lrc file)"
|
||||
SETTING_PATH = "Settings path"
|
||||
SETTING_APIKEY = "APIKey support"
|
||||
SETTING_ADD_TYPE_FOLDER = "Add Type-Folder"
|
||||
SETTING_USE_PLAYLIST_FOLDER = "Používat složku playlistu"
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Stahování více vlákny"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Formát názvu složky alba"
|
||||
SETTING_TRACK_FILE_FORMAT = "Formát názvu souboru skladby"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Formát názvu souboru videa"
|
||||
SETTING_SHOW_PROGRESS = "Zobrazit indikátor stavu stahování"
|
||||
SETTING_SHOW_TRACKINFO = "Zobrazit informace o skladbě"
|
||||
SETTING_SAVE_ALBUMINFO = "Uložit soubor AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Přidat texty skladeb"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Server proxy pro texty skladeb"
|
||||
SETTINGS_ADD_LRC_FILE = "Uložit slova skladby s časováním (soubor .lrc)"
|
||||
SETTING_PATH = "Cesta k souboru s nastavením"
|
||||
SETTING_APIKEY = "APIKey podporuje"
|
||||
SETTING_ADD_TYPE_FOLDER = "Složky dle typu obsahu"
|
||||
|
||||
CHOICE = "Výběr"
|
||||
FUNCTION = "Funkce"
|
||||
CHOICE_ENTER = "Enter"
|
||||
CHOICE_ENTER_URLID = "Vložit 'Url/ID':"
|
||||
CHOICE_EXIT = "Ukončit"
|
||||
CHOICE_LOGIN = "Check AccessToken"
|
||||
CHOICE_LOGIN = "Zkontrolovat přístupový token"
|
||||
CHOICE_SETTINGS = "Nastavení"
|
||||
CHOICE_SET_ACCESS_TOKEN = "Nastavit přístupový token"
|
||||
CHOICE_DOWNLOAD_BY_URL = "Stáhnout buď url nebo id"
|
||||
CHOICE_LOGOUT = "Logout"
|
||||
CHOICE_APIKEY = "Select APIKey"
|
||||
CHOICE_LOGOUT = "Odhlásit"
|
||||
CHOICE_APIKEY = "Vybrat APIKey"
|
||||
|
||||
PRINT_ERR = "[Error]"
|
||||
PRINT_ERR = "[Chyba]"
|
||||
PRINT_INFO = "[Info]"
|
||||
PRINT_SUCCESS = "[Staženo]"
|
||||
|
||||
@@ -62,55 +63,57 @@ class LangCzech(object):
|
||||
# PRINT_USERNAME = "přihlašovací jméno:"
|
||||
# PRINT_PASSWORD = "heslo"
|
||||
|
||||
CHANGE_START_SETTINGS = "Start settings('0'-Zpět,'1'-Ano):"
|
||||
CHANGE_DOWNLOAD_PATH = "Cesta stažení('0' not modify):"
|
||||
CHANGE_AUDIO_QUALITY = "Kvalita hudby('0'-Normal,'1'-High,'2'-HiFi,'3'-Master):"
|
||||
CHANGE_VIDEO_QUALITY = "Kvalita videa(1080, 720, 480, 360):"
|
||||
CHANGE_ONLYM4A = "Konvertovat mp4 na m4a('0'-Ne,'1'-Ano):"
|
||||
CHANGE_ADD_EXPLICIT_TAG = "Přidat explicitní značku k souborům('0'-Ne,'1'-Ano):"
|
||||
CHANGE_ADD_HYPHEN = "V názvech souborů používat místo mezer pomlčky('0'-Ne,'1'-Ano):"
|
||||
CHANGE_ADD_YEAR = "Přidat rok vydání do názvu složky('0'-Ne,'1'-Ano):"
|
||||
CHANGE_USE_TRACK_NUM = "Přidat číslo skladby před název skladby('0'-Ne,'1'-Ano):"
|
||||
CHANGE_CHECK_EXIST = "Zkontrolovat existujicí soubor před stažením('0'-Ne,'1'-Ano):"
|
||||
CHANGE_ARTIST_BEFORE_TITLE = "Přidat jméno interpreta před názvem skladby('0'-Ne,'1'-Ano):"
|
||||
CHANGE_INCLUDE_EP = "Při stahování alba interpreta zahrnout singly a EP('0'-Ne,'1'-Ano):"
|
||||
CHANGE_ALBUMID_BEFORE_FOLDER = "Přidat ID před složku do alba('0'-Ne,'1'-Ano):"
|
||||
CHANGE_SAVE_COVERS = "Uložit obaly alb('0'-Ne,'1'-Ano):"
|
||||
CHANGE_START_SETTINGS = "Spustit nastavení ('0'-Zpět,'1'-Ano):"
|
||||
CHANGE_DOWNLOAD_PATH = "Umístění stažených souborů ('0' beze změny):"
|
||||
CHANGE_AUDIO_QUALITY = "Kvalita hudby ('0'-Normální,'1'-Vysoká,'2'-HiFi,'3'-Master):"
|
||||
CHANGE_VIDEO_QUALITY = "Kvalita videa (1080, 720, 480, 360):"
|
||||
CHANGE_ONLYM4A = "Konvertovat mp4 na m4a ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_ADD_EXPLICIT_TAG = "Přidat označení explicity k souborům ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_ADD_HYPHEN = "V názvech souborů používat místo mezer pomlčky ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_ADD_YEAR = "Přidat rok vydání do názvu složky ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_USE_TRACK_NUM = "Přidat číslo skladby před název skladby ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_CHECK_EXIST = "Zkontrolovat existenci souboru před stažením ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_ARTIST_BEFORE_TITLE = "Přidat jméno interpreta před název skladby ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_INCLUDE_EP = "Při stahování alb interpreta zahrnout singly a EP ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_ALBUMID_BEFORE_FOLDER = "Přidat ID před název složky s albem ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_SAVE_COVERS = "Uložit obaly alb ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_LANGUAGE = "Zvolit jazyk"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Album folder format('0' not modify):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Track file format('0' not modify):"
|
||||
CHANGE_SHOW_PROGRESS = "Show progress('0'-No,'1'-Yes):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show track info('0'-No,'1'-Yes):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Save AlbumInfo.txt('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_LYRICS = "Add lyrics('0'-No,'1'-Yes):"
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Lyrics server proxy('0' not modify):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Formát názvu složky alba ('0' beze změny):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Formát názvu složky skladny ('0' beze změny):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Formát názvu souboru videa ('0'-beze změny,'default'-pro nastavení výchozího názvu):"
|
||||
CHANGE_SHOW_PROGRESS = "Zobrazit indikátor stavu stahování ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show info o skladbě ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Uložit soubor AlbumInfo.txt ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_ADD_LYRICS = "Přidat texty skladeb ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Server proxy pro texty skladeb ('0' beze změny):"
|
||||
CHANGE_ADD_LRC_FILE = "Uložit slova skladby s časováním do souboru .lrc) ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Ukládat do složek dle typu obsahu, např. Album/Video/Playlist ('0'-Ne,'1'-Ano):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Více vláken pro stahování ('0'-Ne,'1'-Ano):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Starting login process..."
|
||||
AUTH_LOGIN_CODE = "Your login code is {}"
|
||||
AUTH_NEXT_STEP = "Go to {} within the next {} to complete setup."
|
||||
AUTH_WAITING = "Waiting for authorization..."
|
||||
AUTH_TIMEOUT = "Operation timed out."
|
||||
AUTH_START_LOGIN = "Spouštění přihlašovacího procesu..."
|
||||
AUTH_LOGIN_CODE = "Váš přihlašovací kód je {}"
|
||||
AUTH_NEXT_STEP = "K dokončení nastavení přejděte na stránku {} během následujích {}."
|
||||
AUTH_WAITING = "Čeká se na autorizaci..."
|
||||
AUTH_TIMEOUT = "Vypršel časový limit procesu."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken good for {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_VALID_ACCESSTOKEN = "Přístupový token fukční pro {}."
|
||||
MSG_INVALID_ACCESSTOKEN = "Platnost přístupového tokenu vypršela. Pokouším se o obnovení."
|
||||
MSG_PATH_ERR = "Cesta neexistuje!"
|
||||
MSG_INPUT_ERR = "Chyba vstupu!"
|
||||
MSG_INPUT_ERR = "Chyba zadání!"
|
||||
|
||||
MODEL_ALBUM_PROPERTY = "ALBUM-PROPERTY"
|
||||
MODEL_TRACK_PROPERTY = "TRACK-PROPERTY"
|
||||
MODEL_VIDEO_PROPERTY = "VIDEO-PROPERTY"
|
||||
MODEL_ARTIST_PROPERTY = "ARTIST-PROPERTY"
|
||||
MODEL_PLAYLIST_PROPERTY = "PLAYLIST-PROPERTY"
|
||||
MODEL_ALBUM_PROPERTY = "ALBUM-VLASTNOSTI"
|
||||
MODEL_TRACK_PROPERTY = "SKLADBA-VLASTNOSTI"
|
||||
MODEL_VIDEO_PROPERTY = "VIDEO-VLASTNOSTI"
|
||||
MODEL_ARTIST_PROPERTY = "INTERPRET-VLASTNOSTI"
|
||||
MODEL_PLAYLIST_PROPERTY = "PLAYLIST-VLASTNOSTI"
|
||||
|
||||
MODEL_TITLE = 'Název skladby'
|
||||
MODEL_TRACK_NUMBER = 'Číslo skladby'
|
||||
MODEL_VIDEO_NUMBER = 'Číslo videa'
|
||||
MODEL_RELEASE_DATE = 'Datum vydání'
|
||||
MODEL_VERSION = 'Verze'
|
||||
MODEL_EXPLICIT = 'Explicit'
|
||||
MODEL_EXPLICIT = 'Explicitní'
|
||||
MODEL_ALBUM = 'Album'
|
||||
MODEL_ID = 'ID'
|
||||
MODEL_NAME = 'Jméno'
|
||||
|
||||
@@ -31,8 +31,9 @@ class LangDanish(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Flertråede download"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Albummappens format"
|
||||
SETTING_TRACK_FILE_FORMAT = "Musiknummerets filformat"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Vis fremskridt"
|
||||
SETTING_SHOW_TRACKIFNO = "Show Track Info"
|
||||
SETTING_SHOW_TRACKINFO = "Show Track Info"
|
||||
SETTING_SAVE_ALBUMINFO = "Save AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Add lyrics"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Lyrics server proxy"
|
||||
@@ -79,6 +80,7 @@ class LangDanish(object):
|
||||
CHANGE_LANGUAGE = "Vælg sprog"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Albummappeformat('0' Ændrer ikke, 'default' for at indstille som standard):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Musiknummerets filformat('0' Ændrer ikke, 'default' for at indstille som standard):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Vis fremskridt('0'-Nej,'1'-Ja):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show track info('0'-No,'1'-Yes):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Save AlbumInfo.txt('0'-No,'1'-Yes):"
|
||||
@@ -86,6 +88,7 @@ class LangDanish(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Lyrics server proxy('0' not modify):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Starter login-processen."
|
||||
@@ -95,7 +98,7 @@ class LangDanish(object):
|
||||
AUTH_TIMEOUT = "Tiden løb ud."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken tilgængelig for {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "AccessToken udløb. Forsøger at opdatere"
|
||||
MSG_INVALID_ACCESSTOKEN = "AccessToken udløb. Forsøger at opdatere"
|
||||
MSG_PATH_ERR = "Sti fejl!"
|
||||
MSG_INPUT_ERR = "Indtastningsfejl!"
|
||||
|
||||
|
||||
@@ -31,8 +31,9 @@ class LangDutch(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Downloaden met meerdere threads"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Indeling albummap"
|
||||
SETTING_TRACK_FILE_FORMAT = "Bestandsindeling bijhouden"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Videobestandsindeling"
|
||||
SETTING_SHOW_PROGRESS = "Toon voortgang"
|
||||
SETTING_SHOW_TRACKIFNO = "Toon trackinfo"
|
||||
SETTING_SHOW_TRACKINFO = "Toon trackinfo"
|
||||
SETTING_SAVE_ALBUMINFO = "AlbumInfo.txt opslaan"
|
||||
SETTING_ADD_LYRICS = "Songtekst toevoegen"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Tekst server proxy"
|
||||
@@ -79,6 +80,7 @@ class LangDutch(object):
|
||||
CHANGE_LANGUAGE = "Selecteer taal"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Albummapindeling ('0'-niet wijzigen,'standaard'-om standaard in te stellen):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Bestandsformaat bijhouden ('0'-niet wijzigen,'standaard'-om standaard in te stellen):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Videobestandsindeling('0'-niet wijzigen,'standaard'-om standaard in te stellen):"
|
||||
CHANGE_SHOW_PROGRESS = "Voortgang weergeven('0'-Nee,'1'-Ja):"
|
||||
CHANGE_SHOW_TRACKINFO = "Toon trackinfo('0'-Nee,'1'-Ja):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Bewaar AlbumInfo.txt('0'-Nee,'1'-Ja):"
|
||||
@@ -86,6 +88,7 @@ class LangDutch(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Songtekst proxyserver('0'-niet wijzigen):"
|
||||
CHANGE_ADD_LRC_FILE = "Sla getimede songtekst .lrc-bestand op ('0'-Nee,'1'-Ja):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Type-map toevoegen, bijv. Album/Video/Playlist('0'-Nee,'1'-Ja):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Inlogproces starten..."
|
||||
@@ -95,7 +98,7 @@ class LangDutch(object):
|
||||
AUTH_TIMEOUT = "Operatie time-out."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "Toegangstoken goed voor {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Verlopen AccessToken. Poging om het te vernieuwen."
|
||||
MSG_INVALID_ACCESSTOKEN = "Verlopen AccessToken. Poging om het te vernieuwen."
|
||||
MSG_PATH_ERR = "Pad is incorrect!"
|
||||
MSG_INPUT_ERR = "Invoerfout!"
|
||||
|
||||
|
||||
@@ -31,8 +31,9 @@ class LangEnglish(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Multi thread download"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Album folder format"
|
||||
SETTING_TRACK_FILE_FORMAT = "Track file format"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Show progress"
|
||||
SETTING_SHOW_TRACKIFNO = "Show Track Info"
|
||||
SETTING_SHOW_TRACKINFO = "Show Track Info"
|
||||
SETTING_SAVE_ALBUMINFO = "Save AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Add lyrics"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Lyrics server proxy"
|
||||
@@ -79,6 +80,7 @@ class LangEnglish(object):
|
||||
CHANGE_LANGUAGE = "Select language"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Album folder format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Track file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Show progress('0'-No,'1'-Yes):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show track info('0'-No,'1'-Yes):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Save AlbumInfo.txt('0'-No,'1'-Yes):"
|
||||
@@ -86,6 +88,7 @@ class LangEnglish(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Lyrics server proxy('0'-not modify):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Starting login process..."
|
||||
@@ -95,7 +98,7 @@ class LangEnglish(object):
|
||||
AUTH_TIMEOUT = "Operation timed out."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken good for {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_INVALID_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_PATH_ERR = "Path is error!"
|
||||
MSG_INPUT_ERR = "Input error!"
|
||||
|
||||
|
||||
@@ -31,8 +31,9 @@ class LangFilipino(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Multi thread download"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Album folder format"
|
||||
SETTING_TRACK_FILE_FORMAT = "Track file format"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Show progress"
|
||||
SETTING_SHOW_TRACKIFNO = "Show Track Info"
|
||||
SETTING_SHOW_TRACKINFO = "Show Track Info"
|
||||
SETTING_SAVE_ALBUMINFO = "Save AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Add lyrics"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Lyrics server proxy"
|
||||
@@ -79,6 +80,7 @@ class LangFilipino(object):
|
||||
CHANGE_LANGUAGE = "Pumili ng lenggwahe"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Album folder format('0' not modify):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Track file format('0' not modify):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Show progress('0'-No,'1'-Yes):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show track info('0'-No,'1'-Yes):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Save AlbumInfo.txt('0'-No,'1'-Yes):"
|
||||
@@ -86,6 +88,7 @@ class LangFilipino(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Lyrics server proxy('0' not modify):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Starting login process..."
|
||||
@@ -95,7 +98,7 @@ class LangFilipino(object):
|
||||
AUTH_TIMEOUT = "Operation timed out."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken good for {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_INVALID_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_PATH_ERR = "May error sa paroroonan ng download!"
|
||||
MSG_INPUT_ERR = "May error sa pag-input!"
|
||||
|
||||
|
||||
@@ -31,27 +31,28 @@ class LangFrench(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Téléchargement multithread"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Format du dossier d'album"
|
||||
SETTING_TRACK_FILE_FORMAT = "Format du fichier de tracklist"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Afficher la Progression"
|
||||
SETTING_SHOW_TRACKIFNO = "Show Track Info"
|
||||
SETTING_SAVE_ALBUMINFO = "Save AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Add lyrics"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Lyrics server proxy"
|
||||
SETTING_PATH = "Settings path"
|
||||
SETTINGS_ADD_LRC_FILE = "Save timed lyrics (.lrc file)"
|
||||
SETTING_APIKEY = "APIKey support"
|
||||
SETTING_ADD_TYPE_FOLDER = "Add Type-Folder"
|
||||
SETTING_SHOW_TRACKINFO = "Afficher les information de la musique"
|
||||
SETTING_SAVE_ALBUMINFO = "Enregistrer AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Inclure les paroles"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Proxy du serveur de paroles"
|
||||
SETTING_PATH = "Emplacement des paramètres"
|
||||
SETTINGS_ADD_LRC_FILE = "Enregistrer les paroles synchronisées (fichier .lrc)"
|
||||
SETTING_APIKEY = "Prise en charge de la clé API"
|
||||
SETTING_ADD_TYPE_FOLDER = "Ajouter un dossier de type"
|
||||
|
||||
CHOICE = "CHOIX"
|
||||
FUNCTION = "FONCTION"
|
||||
CHOICE_ENTER = "Saisir"
|
||||
CHOICE_ENTER_URLID = "Saisir 'Url/ID':"
|
||||
CHOICE_EXIT = "Quitter"
|
||||
CHOICE_LOGIN = "Check AccessToken"
|
||||
CHOICE_LOGIN = "Vérifier le token d'accès"
|
||||
CHOICE_SETTINGS = "Réglages"
|
||||
CHOICE_SET_ACCESS_TOKEN = "Définir le jeton d'accès"
|
||||
CHOICE_DOWNLOAD_BY_URL = "Téléchargement par url ou id"
|
||||
CHOICE_LOGOUT = "Logout"
|
||||
CHOICE_APIKEY = "Select APIKey"
|
||||
CHOICE_LOGOUT = "Déconnexion"
|
||||
CHOICE_APIKEY = "Choisir la clé d'API"
|
||||
|
||||
PRINT_ERR = "[ERR]"
|
||||
PRINT_INFO = "[INFO]"
|
||||
@@ -79,23 +80,25 @@ class LangFrench(object):
|
||||
CHANGE_LANGUAGE = "Sélectionnez une langue"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Format du dossier d'album('0' ne pas modifier):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Format du fichier de tracklist('0' ne pas modifier):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Afficher la progression('0'-Non,'1'-Oui):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show track info('0'-No,'1'-Yes):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Save AlbumInfo.txt('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_LYRICS = "Add lyrics('0'-No,'1'-Yes):"
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Lyrics server proxy('0' not modify):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_SHOW_TRACKINFO = "Afficher les information de la piste ('0'-Non,'1'-Oui):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Enregistrer AlbumInfo.txt('0'-Non,'1'-Oui):"
|
||||
CHANGE_ADD_LYRICS = "Ajouter les paroles ('0'-Non,'1'-Oui):"
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Proxy du serveur de paroles('0' not modify):"
|
||||
CHANGE_ADD_LRC_FILE = "Enregistrer les paroles synchronisées (fichier.lrc) ('0'-Non,'1'-Oui):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Ajouter un dossier de type,Ex: Album/Video/Playlist('0'-Non,'1'-Oui):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-Non,'1'-Oui):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Starting login process..."
|
||||
AUTH_LOGIN_CODE = "Your login code is {}"
|
||||
AUTH_NEXT_STEP = "Go to {} within the next {} to complete setup."
|
||||
AUTH_WAITING = "Waiting for authorization..."
|
||||
AUTH_TIMEOUT = "Operation timed out."
|
||||
AUTH_START_LOGIN = "Démarrage du processus de connexion..."
|
||||
AUTH_LOGIN_CODE = "Votre code de connection est le suivant: {}"
|
||||
AUTH_NEXT_STEP = "Allez à {} avant {} pour finir la configuration."
|
||||
AUTH_WAITING = "En attente d'autorisation..."
|
||||
AUTH_TIMEOUT = "Temps écoulé."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken good for {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_VALID_ACCESSTOKEN = "Token d'accès valable {}."
|
||||
MSG_INVALID_ACCESSTOKEN = "Token d'accès expiré. Tentative de renouvellement automatique."
|
||||
MSG_PATH_ERR = "Erreur du chemin d'accès"
|
||||
MSG_INPUT_ERR = "Erreur de saisie !"
|
||||
|
||||
|
||||
@@ -31,8 +31,9 @@ class LangGerman(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Multi-Thread-Download"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Album-Ordnerformat"
|
||||
SETTING_TRACK_FILE_FORMAT = "Track-Dateiformat"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Fortschritt anzeigen"
|
||||
SETTING_SHOW_TRACKIFNO = "Show Track Info"
|
||||
SETTING_SHOW_TRACKINFO = "Show Track Info"
|
||||
SETTING_SAVE_ALBUMINFO = "Save AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Add lyrics"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Lyrics server proxy"
|
||||
@@ -79,6 +80,7 @@ class LangGerman(object):
|
||||
CHANGE_LANGUAGE = "Sprache auswählen"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Album-Ordnerformat('0' überspringen):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Track-Dateiformat('0' überspringen):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Fortschritt anzeigen('0'-Nein,'1'-Ja):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show track info('0'-No,'1'-Yes):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Save AlbumInfo.txt('0'-No,'1'-Yes):"
|
||||
@@ -86,6 +88,7 @@ class LangGerman(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Lyrics server proxy('0' not modify):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Starte Loginprozess..."
|
||||
@@ -95,7 +98,7 @@ class LangGerman(object):
|
||||
AUTH_TIMEOUT = "Zeitüberschreitung der Operation."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken gültig für {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "AccessToken abgelaufen. Versuche zu erneuern."
|
||||
MSG_INVALID_ACCESSTOKEN = "AccessToken abgelaufen. Versuche zu erneuern."
|
||||
MSG_PATH_ERR = "Ungültiger Pfad!"
|
||||
MSG_INPUT_ERR = "Eingabefehler!"
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
'''
|
||||
@File : hungarian.py
|
||||
@Time : 2022/02/10
|
||||
@Time : 2022/08/01
|
||||
@Author : Shanahan
|
||||
@Version : 1.1
|
||||
@Version : 1.2
|
||||
@Contact :
|
||||
@Desc :
|
||||
'''
|
||||
@@ -31,8 +31,9 @@ class LangHungarian(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Többszálú letöltés"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Album mappa formátum"
|
||||
SETTING_TRACK_FILE_FORMAT = "Track fájlformátum"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Videó fájlformátum"
|
||||
SETTING_SHOW_PROGRESS = "Haladás megjelenítése"
|
||||
SETTING_SHOW_TRACKIFNO = "Track infók megjelenítése"
|
||||
SETTING_SHOW_TRACKINFO = "Track infók megjelenítése"
|
||||
SETTING_SAVE_ALBUMINFO = "AlbumInfo.txt mentése"
|
||||
SETTING_ADD_LYRICS = "Dalszöveg hozzáadása"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Dalszöveg kiszolgáló proxy"
|
||||
@@ -60,7 +61,7 @@ class LangHungarian(object):
|
||||
PRINT_ENTER_CHOICE = "Válasszon:"
|
||||
PRINT_LATEST_VERSION = "Legújabb verzió:"
|
||||
# PRINT_USERNAME = "felhasználónév:"
|
||||
# PRINT_PASSWORD = "jelszó
|
||||
# PRINT_PASSWORD = "jelszó:"
|
||||
|
||||
CHANGE_START_SETTINGS = "Beállítások indítása('0'- Vissza, '1'-Igen):"
|
||||
CHANGE_DOWNLOAD_PATH = "Letöltési útvonal('0' nincs módosítás):"
|
||||
@@ -73,19 +74,21 @@ class LangHungarian(object):
|
||||
CHANGE_USE_TRACK_NUM = "Track szám hozzáadása a fájlnevek előtt('0'-Nem,'1'-Igen):"
|
||||
CHANGE_CHECK_EXIST = "Létező fájl ellenőrzése letöltés előtt('0'-Nem,'1'-Igen):"
|
||||
CHANGE_ARTIST_BEFORE_TITLE = "Előadó hozzáadása a szám címe előtt('0'-Nem,'1'-Igen):"
|
||||
CHANGE_INCLUDE_EP = "A kislemezek és EP-k letöltése('0'-nem, '1'-igen):"
|
||||
CHANGE_INCLUDE_EP = "A kislemezek és EP-k letöltése('0'-Nem, '1'-Igen):"
|
||||
CHANGE_ALBUMID_BEFORE_FOLDER = "Azonosító (ID) az album mappa előtt('0'-Nem,'1'-Igen):"
|
||||
CHANGE_SAVE_COVERS = "Borító mentése('0'-nem, '1'-igen):"
|
||||
CHANGE_SAVE_COVERS = "Borító mentése('0'-Nem, '1'-Igen):"
|
||||
CHANGE_LANGUAGE = "Nyelv kiválasztása"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Album mappa formátum('0' nincs módosítás,'default' az alapértelmezett beállításhoz):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Track fájl neve('0' nincs módosítás,'default' az alapértelmezett beállításhoz):"
|
||||
CHANGE_SHOW_PROGRESS = "Haladás megjelenítése('0'-nem, '1'-igen):"
|
||||
CHANGE_SHOW_TRACKINFO = "Track infók megjelenítése('0'-No,'1'-Yes):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "AlbumInfo.txt mentése('0'-nem, '1'-igen):"
|
||||
CHANGE_ADD_LYRICS = "Dalszöveg hozzáadása('0'-nem,'1'-igen):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-nincs módosítás,'default' az alapértelmezett beállításhoz):"
|
||||
CHANGE_SHOW_PROGRESS = "Haladás megjelenítése('0'-Nem, '1'-Igen):"
|
||||
CHANGE_SHOW_TRACKINFO = "Track infók megjelenítése('0'-Nem,'1'-Igen):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "AlbumInfo.txt mentése('0'-Nem, '1'-Igen):"
|
||||
CHANGE_ADD_LYRICS = "Dalszöveg hozzáadása('0'-Nem,'1'-Igen):"
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Dalszöveg kiszolgáló proxy('0' nincs módosítás):"
|
||||
CHANGE_ADD_LRC_FILE = "Dalszöveg mentése időbélyeggel .lrc fájl('0'-Nem,'1'-Igen):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Mappa típus hozzáadása, pl. Album/Video/Playlist('0'-Nem,'1'-Igen):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Többszálas letöltés('0'-Nem,'1'-Igen):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Bejelentkezési folyamat elindítása..."
|
||||
@@ -95,15 +98,15 @@ class LangHungarian(object):
|
||||
AUTH_TIMEOUT = "A művelet leállt."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken érvényessége {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Lejárt AccessToken. Megpróbálom frissíteni."
|
||||
MSG_INVALID_ACCESSTOKEN = "Lejárt AccessToken. Megpróbálom frissíteni."
|
||||
MSG_PATH_ERR = "Az útvonal hibás!"
|
||||
MSG_INPUT_ERR = "Beviteli hiba!"
|
||||
|
||||
MODEL_ALBUM_PROPERTY = "ALBUM-TULAJDONSÁG"
|
||||
MODEL_TRACK_PROPERTY = "TRACK-TULAJDONSÁG"
|
||||
MODEL_VIDEO_PROPERTY = "VIDEO-TULAJDONSÁG"
|
||||
MODEL_ARTIST_PROPERTY = "ELŐADÓ-TULAJDONSÁG"
|
||||
MODEL_PLAYLIST_PROPERTY = "LEJÁTSZÁSI LISTA-TULAJDONSÁG"
|
||||
MODEL_ALBUM_PROPERTY = "ALBUM-INFÓ"
|
||||
MODEL_TRACK_PROPERTY = "TRACK-INFÓ"
|
||||
MODEL_VIDEO_PROPERTY = "VIDEO-INFÓ"
|
||||
MODEL_ARTIST_PROPERTY = "ELŐADÓ-INFÓ"
|
||||
MODEL_PLAYLIST_PROPERTY = "LEJÁTSZÁSI LISTA-INFÓ"
|
||||
|
||||
MODEL_TITLE = 'Cím'
|
||||
MODEL_TRACK_NUMBER = 'Track száma'
|
||||
|
||||
@@ -31,8 +31,9 @@ class LangItalian(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Multi thread download"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Album folder format"
|
||||
SETTING_TRACK_FILE_FORMAT = "Track file format"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Show progress"
|
||||
SETTING_SHOW_TRACKIFNO = "Show Track Info"
|
||||
SETTING_SHOW_TRACKINFO = "Show Track Info"
|
||||
SETTING_SAVE_ALBUMINFO = "Save AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Add lyrics"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Lyrics server proxy"
|
||||
@@ -79,6 +80,7 @@ class LangItalian(object):
|
||||
CHANGE_LANGUAGE = "Selezionare lingua"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Album folder format('0' not modify):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Track file format('0' not modify):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Show progress('0'-No,'1'-Yes):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show track info('0'-No,'1'-Yes):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Save AlbumInfo.txt('0'-No,'1'-Yes):"
|
||||
@@ -86,6 +88,7 @@ class LangItalian(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Lyrics server proxy('0' not modify):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Starting login process..."
|
||||
@@ -95,7 +98,7 @@ class LangItalian(object):
|
||||
AUTH_TIMEOUT = "Operation timed out."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken good for {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_INVALID_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_PATH_ERR = "Percorso errato!"
|
||||
MSG_INPUT_ERR = "Inserimento errato!"
|
||||
|
||||
|
||||
@@ -31,8 +31,9 @@ class LangJapanese(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Multi thread download"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Album folder format"
|
||||
SETTING_TRACK_FILE_FORMAT = "Track file format"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Show progress"
|
||||
SETTING_SHOW_TRACKIFNO = "Show Track Info"
|
||||
SETTING_SHOW_TRACKINFO = "Show Track Info"
|
||||
SETTING_SAVE_ALBUMINFO = "Save AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "歌詞を追加"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Lyrics server proxy"
|
||||
@@ -79,6 +80,7 @@ class LangJapanese(object):
|
||||
CHANGE_LANGUAGE = "言語を選択する"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Album folder format('0'-変更しない,'default'-デフォルトを設定するには):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Track file format('0'-変更しない,'default'-デフォルトを設定するには):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Show progress('0'-いいえ,'1'-はい):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show track info('0'-いいえ,'1'-はい):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Save AlbumInfo.txt('0'-いいえ,'1'-はい):"
|
||||
@@ -86,6 +88,7 @@ class LangJapanese(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Lyrics server proxy('0'-変更しない):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-いいえ,'1'-はい):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Starting login process..."
|
||||
@@ -95,7 +98,7 @@ class LangJapanese(object):
|
||||
AUTH_TIMEOUT = "Operation timed out."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken good for {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_INVALID_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_PATH_ERR = "パスはエラーです!"
|
||||
MSG_INPUT_ERR = "入力エラー!"
|
||||
|
||||
|
||||
@@ -31,8 +31,9 @@ class LangKorean(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "다중 스레드 다운로드"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "앨범 폴더 형식"
|
||||
SETTING_TRACK_FILE_FORMAT = "트랙 파일 형식"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "진행 상태 표시"
|
||||
SETTING_SHOW_TRACKIFNO = "트랙 정보 표시"
|
||||
SETTING_SHOW_TRACKINFO = "트랙 정보 표시"
|
||||
SETTING_SAVE_ALBUMINFO = "AlbumInfo.txt 저장"
|
||||
SETTING_ADD_LYRICS = "가사 추가"
|
||||
SETTING_LYRICS_SERVER_PROXY = "가사 서버 프록시"
|
||||
@@ -79,6 +80,7 @@ class LangKorean(object):
|
||||
CHANGE_LANGUAGE = "언어 선택"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "앨범 폴더 형식('0'-변경 안 함,'default'-기본 설정):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "트랙 파일 형식('0'-변경 안 함,'default'-기본 설정):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "진행 상태 표시('0'-아니요,'1'-예):"
|
||||
CHANGE_SHOW_TRACKINFO = "트랙 정보 표시('0'-아니요,'1'-예):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "AlbumInfo.txt 저장('0'-아니요,'1'-예):"
|
||||
@@ -86,6 +88,7 @@ class LangKorean(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "가사 서버 프록시('0'-변경 안 함):"
|
||||
CHANGE_ADD_LRC_FILE = "timed 가사 .lrc 파일 저장 ('0'-아니요,'1'-예):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "로그인 중..."
|
||||
@@ -95,7 +98,7 @@ class LangKorean(object):
|
||||
AUTH_TIMEOUT = "작업 시간 초과"
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "{}에 대해 액세스 토큰이 유효합니다."
|
||||
MSG_INVAILD_ACCESSTOKEN = "만료된 액세스 토큰입니다. 새로 고침 중입니다."
|
||||
MSG_INVALID_ACCESSTOKEN = "만료된 액세스 토큰입니다. 새로 고침 중입니다."
|
||||
MSG_PATH_ERR = "경로 오류!"
|
||||
MSG_INPUT_ERR = "입력 오류!"
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ from tidal_dl.lang.french import LangFrench
|
||||
from tidal_dl.lang.german import LangGerman
|
||||
from tidal_dl.lang.hungarian import LangHungarian
|
||||
from tidal_dl.lang.italian import LangItalian
|
||||
from tidal_dl.lang.polish import LangPolish
|
||||
from tidal_dl.lang.portuguese import LangPortuguese
|
||||
from tidal_dl.lang.russian import LangRussian
|
||||
from tidal_dl.lang.spanish import LangSpanish
|
||||
@@ -30,117 +31,63 @@ from tidal_dl.lang.vietnamese import LangVietnamese
|
||||
from tidal_dl.lang.korean import LangKorean
|
||||
from tidal_dl.lang.japanese import LangJapanese
|
||||
|
||||
LANG = None
|
||||
_ALL_LANGUAGE_ = [
|
||||
['English', LangEnglish()],
|
||||
['中文', LangChinese()],
|
||||
['Turkish', LangTurkish()],
|
||||
['Italian', LangItalian()],
|
||||
['Czech', LangCzech()],
|
||||
['Arabic', LangArabic()],
|
||||
['Russian', LangRussian()],
|
||||
['Filipino', LangFilipino()],
|
||||
['Croatian', LangCroatian()],
|
||||
['Spanish', LangSpanish()],
|
||||
['Portuguese', LangPortuguese()],
|
||||
['Ukrainian', LangUkrainian()],
|
||||
['Vietnamese', LangVietnamese()],
|
||||
['French', LangFrench()],
|
||||
['German', LangGerman()],
|
||||
['Danish', LangDanish()],
|
||||
['Hungarian', LangHungarian()],
|
||||
['Korean', LangKorean()],
|
||||
['Japanese', LangJapanese()],
|
||||
['Dutch', LangDutch()],
|
||||
['Polish', LangPolish()],
|
||||
]
|
||||
|
||||
class Language(object):
|
||||
def __init__(self) -> None:
|
||||
self.select = LangEnglish()
|
||||
|
||||
def __toInt__(self, str):
|
||||
try:
|
||||
return int(str)
|
||||
except:
|
||||
return 0
|
||||
|
||||
def setLang(self, index):
|
||||
index = self.__toInt__(index)
|
||||
if index >= 0 and index < len(_ALL_LANGUAGE_):
|
||||
self.select = _ALL_LANGUAGE_[index][1]
|
||||
else:
|
||||
self.select = LangEnglish()
|
||||
|
||||
def getLangName(self, index):
|
||||
index = self.__toInt__(index)
|
||||
if index >= 0 and index < len(_ALL_LANGUAGE_):
|
||||
return _ALL_LANGUAGE_[index][0]
|
||||
return ""
|
||||
|
||||
def getLangChoicePrint(self):
|
||||
array = []
|
||||
index = 0
|
||||
while True:
|
||||
name = self.getLangName(index)
|
||||
if name == "":
|
||||
break
|
||||
array.append('\'' + str(index) + '\'-' + name)
|
||||
index += 1
|
||||
return ','.join(array)
|
||||
|
||||
|
||||
def initLang(index): # 初始化
|
||||
global LANG
|
||||
return setLang(index)
|
||||
|
||||
|
||||
def setLang(index):
|
||||
global LANG
|
||||
if str(index) == '0':
|
||||
LANG = LangEnglish()
|
||||
elif str(index) == '1':
|
||||
LANG = LangChinese()
|
||||
elif str(index) == '2':
|
||||
LANG = LangTurkish()
|
||||
elif str(index) == '3':
|
||||
LANG = LangItalian()
|
||||
elif str(index) == '4':
|
||||
LANG = LangCzech()
|
||||
elif str(index) == '5':
|
||||
LANG = LangArabic()
|
||||
elif str(index) == '6':
|
||||
LANG = LangRussian()
|
||||
elif str(index) == '7':
|
||||
LANG = LangFilipino()
|
||||
elif str(index) == '8':
|
||||
LANG = LangCroatian()
|
||||
elif str(index) == '9':
|
||||
LANG = LangSpanish()
|
||||
elif str(index) == '10':
|
||||
LANG = LangPortuguese()
|
||||
elif str(index) == '11':
|
||||
LANG = LangUkrainian()
|
||||
elif str(index) == '12':
|
||||
LANG = LangVietnamese()
|
||||
elif str(index) == '13':
|
||||
LANG = LangFrench()
|
||||
elif str(index) == '14':
|
||||
LANG = LangGerman()
|
||||
elif str(index) == '15':
|
||||
LANG = LangDanish()
|
||||
elif str(index) == '16':
|
||||
LANG = LangHungarian()
|
||||
elif str(index) == '17':
|
||||
LANG = LangKorean()
|
||||
elif str(index) == '18':
|
||||
LANG = LangJapanese()
|
||||
elif str(index) == '19':
|
||||
LANG = LangDutch()
|
||||
else:
|
||||
LANG = LangEnglish()
|
||||
return LANG
|
||||
|
||||
|
||||
def getLang():
|
||||
global LANG
|
||||
return LANG
|
||||
|
||||
|
||||
def getLangName(index):
|
||||
if str(index) == '0':
|
||||
return "English"
|
||||
if str(index) == '1':
|
||||
return "中文"
|
||||
if str(index) == '2':
|
||||
return "Turkish"
|
||||
if str(index) == '3':
|
||||
return "Italian"
|
||||
if str(index) == '4':
|
||||
return "Czech"
|
||||
if str(index) == '5':
|
||||
return "Arabic"
|
||||
if str(index) == '6':
|
||||
return "Russian"
|
||||
if str(index) == '7':
|
||||
return "Filipino"
|
||||
if str(index) == '8':
|
||||
return "Croatian"
|
||||
if str(index) == '9':
|
||||
return "Spanish"
|
||||
if str(index) == '10':
|
||||
return "Portuguese"
|
||||
if str(index) == '11':
|
||||
return "Ukrainian"
|
||||
if str(index) == '12':
|
||||
return "Vietnamese"
|
||||
if str(index) == '13':
|
||||
return "French"
|
||||
if str(index) == '14':
|
||||
return "German"
|
||||
if str(index) == '15':
|
||||
return "Danish"
|
||||
if str(index) == '16':
|
||||
return "Hungarian"
|
||||
if str(index) == '17':
|
||||
return "Korean"
|
||||
if str(index) == '18':
|
||||
return "Japanese"
|
||||
if str(index) == '19':
|
||||
return "Dutch"
|
||||
return ""
|
||||
|
||||
|
||||
def getLangChoicePrint():
|
||||
array = []
|
||||
index = 0
|
||||
while True:
|
||||
name = getLangName(index)
|
||||
if name == "":
|
||||
break
|
||||
array.append('\'' + str(index) + '\'-' + name)
|
||||
index += 1
|
||||
return ','.join(array)
|
||||
LANG = Language()
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
'''
|
||||
@File : polish.py
|
||||
@Time : 2022/03/04
|
||||
@Author : PatrykMis
|
||||
@Version : 1.0
|
||||
@Contact : patryk.mis@member.fsf.org
|
||||
@Desc :
|
||||
'''
|
||||
|
||||
|
||||
class LangPolish(object):
|
||||
SETTING = "USTAWIENIA"
|
||||
VALUE = "WARTOŚĆ"
|
||||
SETTING_DOWNLOAD_PATH = "Ścieżka pobierania"
|
||||
SETTING_ONLY_M4A = "Konwertuj mp4 do m4a"
|
||||
SETTING_ADD_EXPLICIT_TAG = "Dodaj tag jednoznaczne"
|
||||
SETTING_ADD_HYPHEN = "Dodaj myslnik"
|
||||
SETTING_ADD_YEAR = "Dodaj rok porzed folderem albumu"
|
||||
SETTING_USE_TRACK_NUM = "Dodaj numer utworu użytkownika"
|
||||
SETTING_AUDIO_QUALITY = "Jakość audio"
|
||||
SETTING_VIDEO_QUALITY = "Jakość wideo"
|
||||
SETTING_CHECK_EXIST = "Sprawdź istnienie"
|
||||
SETTING_ARTIST_BEFORE_TITLE = "Nazwa artysty przed tytułem utworu"
|
||||
SETTING_ALBUMID_BEFORE_FOLDER = "Id przed folderem albumu"
|
||||
SETTING_INCLUDE_EP = "Uwzględnij single i EP"
|
||||
SETTING_SAVE_COVERS = "Zapisz okładki"
|
||||
SETTING_LANGUAGE = "Język"
|
||||
SETTING_USE_PLAYLIST_FOLDER = "Użyj folder playlisty"
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Pobieranie wielowątkowe"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Format folderu albumu"
|
||||
SETTING_TRACK_FILE_FORMAT = "Format pliku utworu"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Pokaż postęp"
|
||||
SETTING_SHOW_TRACKINFO = "Pokaż informacje o utworze"
|
||||
SETTING_SAVE_ALBUMINFO = "Zapisz AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Dodaj teksty utworów"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Serwer proxy dla tekstów"
|
||||
SETTINGS_ADD_LRC_FILE = "Zapisz czasowe teksty utworów (plik .lrc)"
|
||||
SETTING_PATH = "Ścieżka ustawień"
|
||||
SETTING_APIKEY = "Obsługa APIKey"
|
||||
SETTING_ADD_TYPE_FOLDER = "Dodaj folder typu"
|
||||
|
||||
CHOICE = "WYBÓR"
|
||||
FUNCTION = "FUNKCJA"
|
||||
CHOICE_ENTER = "Wpisz"
|
||||
CHOICE_ENTER_URLID = "Wpisz 'Url/ID':"
|
||||
CHOICE_EXIT = "Wyjdź"
|
||||
CHOICE_LOGIN = "Sprawdź AccessToken"
|
||||
CHOICE_SETTINGS = "Ustawienia"
|
||||
CHOICE_SET_ACCESS_TOKEN = "Ustaw AccessToken"
|
||||
CHOICE_DOWNLOAD_BY_URL = "Pobierz według adresu URL lub ID"
|
||||
CHOICE_LOGOUT = "Wyloguj"
|
||||
CHOICE_APIKEY = "Wybierz APIKey"
|
||||
|
||||
PRINT_ERR = "[BŁĄD]"
|
||||
PRINT_INFO = "[INFO]"
|
||||
PRINT_SUCCESS = "[SUKCES]"
|
||||
|
||||
PRINT_ENTER_CHOICE = "Wprowadź Wybór:"
|
||||
PRINT_LATEST_VERSION = "Najnowsza wersja:"
|
||||
# PRINT_USERNAME = "nazwa użytkownika:"
|
||||
# PRINT_PASSWORD = "hasło:"
|
||||
|
||||
CHANGE_START_SETTINGS = "Uruchomić ustawienia('0'-Powrót,'1'-Tak):"
|
||||
CHANGE_DOWNLOAD_PATH = "Ścieżka pobierania('0'-bez zmian):"
|
||||
CHANGE_AUDIO_QUALITY = "Jakość audio('0'-Normalna,'1'-Wysoka,'2'-HiFi,'3'-Master):"
|
||||
CHANGE_VIDEO_QUALITY = "Jakość wideo(1080, 720, 480, 360):"
|
||||
CHANGE_ONLYM4A = "Konwertuj mp4 do m4a('0'-Nie,'1'-Tak):"
|
||||
CHANGE_ADD_EXPLICIT_TAG = "Dodaj tag jednoznaczne do nazwy pliku('0'-Nie,'1'-Tak):"
|
||||
CHANGE_ADD_HYPHEN = "Używaj myślników zamiast spacji w nazwach plików('0'-Nie,'1'-Tak):"
|
||||
CHANGE_ADD_YEAR = "Dodaj rok do nazw folderów albumów('0'-Nie,'1'-Tak):"
|
||||
CHANGE_USE_TRACK_NUM = "Dodaj numer utworu przed nazwami plików('0'-Nie,'1'-Tak):"
|
||||
CHANGE_CHECK_EXIST = "Sprawdź istniejący plik przed pobraniem utworu('0'-Nie,'1'-Tak):"
|
||||
CHANGE_ARTIST_BEFORE_TITLE = "Dodaj nazwę artysty przed tytułem utworu('0'-Nie,'1'-Tak):"
|
||||
CHANGE_INCLUDE_EP = "Uwzględnij single i EP podczas pobierania albumów wykonawcy('0'-Nie,'1'-Tak):"
|
||||
CHANGE_ALBUMID_BEFORE_FOLDER = "Dodaj ID przed folderem albumu('0'-Nie,'1'-Tak):"
|
||||
CHANGE_SAVE_COVERS = "Zapisz okładki('0'-Nie,'1'-Tak):"
|
||||
CHANGE_LANGUAGE = "Wybierz język"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Format folderu albumu('0'-nie modyfikuj,'default'-by ustawić domyślny):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Format pliku utworu('0'-nie modyfikuj,'default'-by ustawić domyślny):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Pokaż postęp('0'-Nie,'1'-Tak):"
|
||||
CHANGE_SHOW_TRACKINFO = "Pokaż informacje o utworze('0'-Nie,'1'-Tak):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Zapisz AlbumInfo.txt('0'-Nie,'1'-Tak):"
|
||||
CHANGE_ADD_LYRICS = "Dodaj teksty utworów('0'-Nie,'1'-Tak):"
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Serwer proxy dla tekstów('0'-nie modyfikuj):"
|
||||
CHANGE_ADD_LRC_FILE = "Zapisz plik .lrc czasowych tekstów utworów ('0'-Nie,'1'-Tak):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Dodaj folder typu, np. Album/wideo/lista odtwarzania('0'-Nie,'1'-Tak):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Rozpoczęcie procesu logowania..."
|
||||
AUTH_LOGIN_CODE = "Twój kod logowania to {}"
|
||||
AUTH_NEXT_STEP = "Przejdź do {} w ciągu następnych {} aby zakończyć konfigurację."
|
||||
AUTH_WAITING = "Oczekiwanie na autoryzację..."
|
||||
AUTH_TIMEOUT = "Przekroczono limit czasu operacji."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken dobry przez {}."
|
||||
MSG_INVALID_ACCESSTOKEN = "AccessToken wygasł. Próba jego odświeżenia."
|
||||
MSG_PATH_ERR = "Ścieżka jest błędna!"
|
||||
MSG_INPUT_ERR = "Błąd wejścia!"
|
||||
|
||||
MODEL_ALBUM_PROPERTY = "ALBUM-WŁAŚCIWOŚĆ"
|
||||
MODEL_TRACK_PROPERTY = "UTWÓR-WŁAŚCIWOŚĆ"
|
||||
MODEL_VIDEO_PROPERTY = "WIDEO-WŁAŚCIWOŚĆ"
|
||||
MODEL_ARTIST_PROPERTY = "ARTYSTA-WŁAŚCIWOŚĆ"
|
||||
MODEL_PLAYLIST_PROPERTY = "PLAYLISTA-WŁAŚCIWOŚĆ"
|
||||
|
||||
MODEL_TITLE = 'Tytuł'
|
||||
MODEL_TRACK_NUMBER = 'Numer Utworu'
|
||||
MODEL_VIDEO_NUMBER = 'Numer Wideo'
|
||||
MODEL_RELEASE_DATE = 'Data Wydania'
|
||||
MODEL_VERSION = 'Wersja'
|
||||
MODEL_EXPLICIT = 'Jednoznaczny'
|
||||
MODEL_ALBUM = 'Album'
|
||||
MODEL_ID = 'ID'
|
||||
MODEL_NAME = 'Nazwa'
|
||||
MODEL_TYPE = 'Typ'
|
||||
@@ -27,31 +27,32 @@ class LangPortuguese(object):
|
||||
SETTING_INCLUDE_EP = "Incluir Single & EP"
|
||||
SETTING_SAVE_COVERS = "Salvar Capas"
|
||||
SETTING_LANGUAGE = "idioma"
|
||||
SETTING_USE_PLAYLIST_FOLDER = "Use playlist folder"
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Multi thread download"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Album folder format"
|
||||
SETTING_USE_PLAYLIST_FOLDER = "Usar pasta de lista de reprodução"
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Download de vários tópicos"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Formato da pasta do álbum"
|
||||
SETTING_TRACK_FILE_FORMAT = "Track file format"
|
||||
SETTING_SHOW_PROGRESS = "Show progress"
|
||||
SETTING_SHOW_TRACKIFNO = "Show Track Info"
|
||||
SETTING_SAVE_ALBUMINFO = "Save AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Add lyrics"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Lyrics server proxy"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Mostrar progresso"
|
||||
SETTING_SHOW_TRACKINFO = "Mostrar informações da faixa"
|
||||
SETTING_SAVE_ALBUMINFO = "Salvar AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Adicionar Letra da Música"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Proxy do servidor de letras"
|
||||
SETTING_PATH = "Settings path"
|
||||
SETTINGS_ADD_LRC_FILE = "Save timed lyrics (.lrc file)"
|
||||
SETTING_APIKEY = "APIKey support"
|
||||
SETTING_ADD_TYPE_FOLDER = "Add Type-Folder"
|
||||
SETTINGS_ADD_LRC_FILE = "Salvar letras cronometradas (.lrc file)"
|
||||
SETTING_APIKEY = "Suporte APIKey"
|
||||
SETTING_ADD_TYPE_FOLDER = "Adicionar tipo de pasta"
|
||||
|
||||
CHOICE = "ESCOLHER"
|
||||
FUNCTION = "FUNÇÃO"
|
||||
CHOICE_ENTER = "Entrar"
|
||||
CHOICE_ENTER_URLID = "ADICIONAR 'Url/ID':"
|
||||
CHOICE_EXIT = "SAIR"
|
||||
CHOICE_LOGIN = "Check AccessToken"
|
||||
CHOICE_LOGIN = "Verificar o token de acesso"
|
||||
CHOICE_SETTINGS = "Configurações"
|
||||
CHOICE_SET_ACCESS_TOKEN = "Adicionar Token de Acesso"
|
||||
CHOICE_DOWNLOAD_BY_URL = "Download Por url ou id"
|
||||
CHOICE_LOGOUT = "Logout"
|
||||
CHOICE_APIKEY = "Select APIKey"
|
||||
CHOICE_APIKEY = "Selecione a APIKey"
|
||||
|
||||
PRINT_ERR = "[ERRO]"
|
||||
PRINT_INFO = "[INFORMAÇÕES]"
|
||||
@@ -77,25 +78,27 @@ class LangPortuguese(object):
|
||||
CHANGE_ALBUMID_BEFORE_FOLDER = "Adicionar id antes no nome da pasta do álbum('0'-Não,'1'-Sim):"
|
||||
CHANGE_SAVE_COVERS = "Salvar Capas('0'-Não,'1'-Sim):"
|
||||
CHANGE_LANGUAGE = "Selecionar idioma"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Album folder format('0' not modify):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Track file format('0' not modify):"
|
||||
CHANGE_SHOW_PROGRESS = "Show progress('0'-No,'1'-Yes):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show track info('0'-No,'1'-Yes):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Save AlbumInfo.txt('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_LYRICS = "Add lyrics('0'-No,'1'-Yes):"
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Lyrics server proxy('0' not modify):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Formato da pasta do álbum ('0' não modificar)"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Formato do arquivo de trilha ('0' não modificar):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Mostrar progresso('0'-Não,'1'-Sim):"
|
||||
CHANGE_SHOW_TRACKINFO = "Mostrar informações da faixa('0'-Não,'1'-Sim):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Salvar AlbumInfo.txt('0'-Não,'1'-Sim):"
|
||||
CHANGE_ADD_LYRICS = "Adicionar letras('0'-Não,'1'-Sim):"
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Proxy do servidor de letras ('0' não modificar):"
|
||||
CHANGE_ADD_LRC_FILE = "Salvar arquivo .lrc de letras cronometradas ('0'-Não,'1'-Sim):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Adicionar Tipo de Pasta, por exemplo, Álbum/Vídeo/Lista de Reprodução('0'-Não,'1'-Sim):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Starting login process..."
|
||||
AUTH_LOGIN_CODE = "Your login code is {}"
|
||||
AUTH_NEXT_STEP = "Go to {} within the next {} to complete setup."
|
||||
AUTH_WAITING = "Waiting for authorization..."
|
||||
AUTH_TIMEOUT = "Operation timed out."
|
||||
AUTH_START_LOGIN = "Iniciando o processo de login..."
|
||||
AUTH_LOGIN_CODE = "Seu código de login é {}"
|
||||
AUTH_NEXT_STEP = "Vá para {} no próximo {} para concluir a configuração."
|
||||
AUTH_WAITING = "Espera da autorização..."
|
||||
AUTH_TIMEOUT = "A operação expirou."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken good for {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_VALID_ACCESSTOKEN = "Token de acesso bom por {}."
|
||||
MSG_INVALID_ACCESSTOKEN = "Token de acesso expirado. Tentando atualizá-lo."
|
||||
MSG_PATH_ERR = "Erro no local de salvamento dos arquivos!"
|
||||
MSG_INPUT_ERR = "Erro de Entrada!"
|
||||
|
||||
|
||||
@@ -31,8 +31,9 @@ class LangRussian(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Многопоточная загрузка"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Маска имени альбома"
|
||||
SETTING_TRACK_FILE_FORMAT = "Маска имени трека"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Показывать процесс загрузки"
|
||||
SETTING_SHOW_TRACKIFNO = "Show Track Info"
|
||||
SETTING_SHOW_TRACKINFO = "Show Track Info"
|
||||
SETTING_SAVE_ALBUMINFO = "Сохранять AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Добавлять текст песень"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Прокси сервер для текстов песен"
|
||||
@@ -79,6 +80,7 @@ class LangRussian(object):
|
||||
CHANGE_LANGUAGE = "Выбрать язык"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Маска имени альбома('0' не менять):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Маска имени трека('0' не менять):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Показывать процесс загрузки('0'-Нет,'1'-Да):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show track info('0'-No,'1'-Yes):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Сохранять AlbumInfo.txt('0'-Нет,'1'-Да):"
|
||||
@@ -86,6 +88,7 @@ class LangRussian(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Прокси для сервера с текстом песень('0' не менять):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Входим в сервис..."
|
||||
@@ -95,7 +98,7 @@ class LangRussian(object):
|
||||
AUTH_TIMEOUT = "Закончилось время ожидания."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken успешно применён {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Срок действия AccessToken истек. Попытка обновления."
|
||||
MSG_INVALID_ACCESSTOKEN = "Срок действия AccessToken истек. Попытка обновления."
|
||||
MSG_PATH_ERR = "Неверное место!"
|
||||
MSG_INPUT_ERR = "Ошибка ввода!"
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
'''
|
||||
@File : spanish.py
|
||||
@Time : 2022/01/19
|
||||
@Time : 2022/07/07
|
||||
@Author : Frikilinux & JavierSC
|
||||
@Version : 2.3
|
||||
@Contact :
|
||||
@@ -31,8 +31,9 @@ class LangSpanish(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Descarga Multi-hilo"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Formato del nombre de carpeta del álbum"
|
||||
SETTING_TRACK_FILE_FORMAT = "Formato del nombre de archivo de la pista"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Mostrar progreso"
|
||||
SETTING_SHOW_TRACKIFNO = "Mostrar información de pista"
|
||||
SETTING_SHOW_TRACKINFO = "Mostrar información de pista"
|
||||
SETTING_SAVE_ALBUMINFO = "Guardar AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Añadir letras"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Proxy del servidor de letras"
|
||||
@@ -79,6 +80,7 @@ class LangSpanish(object):
|
||||
CHANGE_LANGUAGE = "Seleccione el idioma"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Formato del nombre de carpeta del álbum ('0' No modificar):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Formato del nombre de archivo de la pista ('0' No modificar):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Formato del archivo de video('0'-No modificar,'default'-por defecto):"
|
||||
CHANGE_SHOW_PROGRESS = "¿Mostrar progreso? ('0'-No,'1'-Sí):"
|
||||
CHANGE_SHOW_TRACKINFO = "¿Mostrar información de pista?('0'-No,'1'-Sí):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "¿Guardar AlbumInfo.txt?('0'-No,'1'-Sí):"
|
||||
@@ -86,6 +88,7 @@ class LangSpanish(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "¿Proxy del servidor de letras?('0' no modificar):"
|
||||
CHANGE_ADD_LRC_FILE = "¿Añadir letras cronometradas en un archivo .lrc? ('0'-No,'1'-Sí):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Añadir tipo de carpeta, ej: Album/Video/Playlist('0'-No,'1'-Sí):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "¿Descarga Multi-hilo?('0'-No,'1'-Sí:"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Iniciando sesión..."
|
||||
@@ -95,7 +98,7 @@ class LangSpanish(object):
|
||||
AUTH_TIMEOUT = "Se superó el tiempo de espera."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "Token de acceso válido por {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "El token de acceso ha expirado. Tratando de renovarlo."
|
||||
MSG_INVALID_ACCESSTOKEN = "El token de acceso ha expirado. Tratando de renovarlo."
|
||||
MSG_PATH_ERR = "¡La ruta no es correcta!"
|
||||
MSG_INPUT_ERR = "¡Error de entrada!"
|
||||
|
||||
@@ -106,8 +109,8 @@ class LangSpanish(object):
|
||||
MODEL_PLAYLIST_PROPERTY = "PROPIEDAD-DE-PLAYLIST"
|
||||
|
||||
MODEL_TITLE = 'Título'
|
||||
MODEL_TRACK_NUMBER = 'Numero de pistas'
|
||||
MODEL_VIDEO_NUMBER = 'Numero de videos'
|
||||
MODEL_TRACK_NUMBER = 'Número de pistas'
|
||||
MODEL_VIDEO_NUMBER = 'Número de videos'
|
||||
MODEL_RELEASE_DATE = 'Fecha de lanzamiento'
|
||||
MODEL_VERSION = 'Versión'
|
||||
MODEL_EXPLICIT = 'Explícito'
|
||||
|
||||
@@ -31,8 +31,9 @@ class LangTurkish(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Şarkılar tek tek indirilsin mi?"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Klasör ismi formatı:"
|
||||
SETTING_TRACK_FILE_FORMAT = "Dosya ismi formatı:"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "İndirme Çubuğu Görüntüleme:"
|
||||
SETTING_SHOW_TRACKIFNO = "Show Track Info"
|
||||
SETTING_SHOW_TRACKINFO = "Show Track Info"
|
||||
SETTING_SAVE_ALBUMINFO = "Save AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Add lyrics"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Lyrics server proxy"
|
||||
@@ -79,6 +80,7 @@ class LangTurkish(object):
|
||||
CHANGE_LANGUAGE = ">>> Lisan Seç "
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Albüm Klasör İsmi Formatı('0' aynı kalsın):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Dosya İsmi Formatı('0' aynı kalsın):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "İndirme Çubuğu Görüntülensin mi?('0'-Hayır,'1'-Evet):"
|
||||
CHANGE_SHOW_TRACKINFO = "Show track info('0'-No,'1'-Yes):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Save AlbumInfo.txt('0'-No,'1'-Yes):"
|
||||
@@ -86,6 +88,7 @@ class LangTurkish(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Lyrics server proxy('0' not modify):"
|
||||
CHANGE_ADD_LRC_FILE = "Save timed lyrics .lrc file ('0'-No,'1'-Yes):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Add Type-Folder,eg Album/Video/Playlist('0'-No,'1'-Yes):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Giriş işlemleri başlatıldı..."
|
||||
@@ -95,7 +98,7 @@ class LangTurkish(object):
|
||||
AUTH_TIMEOUT = "Lütfen size verilen süre içerisinde işlemleriniz tamamlayınız."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken good for {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_INVALID_ACCESSTOKEN = "Expired AccessToken. Attempting to refresh it."
|
||||
MSG_PATH_ERR = "İndirme konumu ile alakalı bir sorun var! ('/storage/emulated/0/Download/' şeklinde girebilirsiniz.)"
|
||||
MSG_INPUT_ERR = "Giriş Hatalı!"
|
||||
|
||||
|
||||
@@ -31,8 +31,9 @@ class LangUkrainian(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Багатопоточне завантаження"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Формат папки альбому"
|
||||
SETTING_TRACK_FILE_FORMAT = "Формат файлу треку"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Показувати прогрес"
|
||||
SETTING_SHOW_TRACKIFNO = "Показувати інформацію про трек"
|
||||
SETTING_SHOW_TRACKINFO = "Показувати інформацію про трек"
|
||||
SETTING_SAVE_ALBUMINFO = "Зберігати AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Додавати тексти пісень"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Проксі для сервера з текстами пісень"
|
||||
@@ -79,6 +80,7 @@ class LangUkrainian(object):
|
||||
CHANGE_LANGUAGE = "Обрати мову"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Формат теки альбому('0'-не змінювати):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Формат файлу треку('0'-не змінювати):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Показувати прогрес('0'-Ні,'1'-Так):"
|
||||
CHANGE_SHOW_TRACKINFO = "Показувати інформацію про трек('0'-Ні,'1'-Так):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Зберігати AlbumInfo.txt('0'-Ні,'1'-Так):"
|
||||
@@ -86,6 +88,7 @@ class LangUkrainian(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Проксі для сервера з текстами пісень('0'-не змінювати):"
|
||||
CHANGE_ADD_LRC_FILE = "Зберігати тексти пісень з відмітками часу в .lrc файл('0'-Ні,'1'-Так):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Додавати теку-тип,наприклад Альбом/Відео/Плейлист('0'-Ні,'1'-Так):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Початок процесу авторизації..."
|
||||
@@ -95,7 +98,7 @@ class LangUkrainian(object):
|
||||
AUTH_TIMEOUT = "Час очікування минув."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken хороший упродовж {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "Термін дії AccessToken'а закінчився. Пробуємо оновити його."
|
||||
MSG_INVALID_ACCESSTOKEN = "Термін дії AccessToken'а закінчився. Пробуємо оновити його."
|
||||
MSG_PATH_ERR = "Невірний шлях!"
|
||||
MSG_INPUT_ERR = "Помилка введення!"
|
||||
|
||||
|
||||
@@ -31,8 +31,9 @@ class LangVietnamese(object):
|
||||
SETTING_MULITHREAD_DOWNLOAD = "Tải về đa luồng"
|
||||
SETTING_ALBUM_FOLDER_FORMAT = "Định dạng thư mục album"
|
||||
SETTING_TRACK_FILE_FORMAT = "Định dạng tên tệp nhạc"
|
||||
SETTING_VIDEO_FILE_FORMAT = "Video file format"
|
||||
SETTING_SHOW_PROGRESS = "Hiện tiến trình"
|
||||
SETTING_SHOW_TRACKIFNO = "Hiện thông tin bài"
|
||||
SETTING_SHOW_TRACKINFO = "Hiện thông tin bài"
|
||||
SETTING_SAVE_ALBUMINFO = "Lưu AlbumInfo.txt"
|
||||
SETTING_ADD_LYRICS = "Thêm lời bài hát"
|
||||
SETTING_LYRICS_SERVER_PROXY = "Máy chủ proxy cho lyrics"
|
||||
@@ -79,6 +80,7 @@ class LangVietnamese(object):
|
||||
CHANGE_LANGUAGE = "Chọn ngôn ngữ"
|
||||
CHANGE_ALBUM_FOLDER_FORMAT = "Định dạng thư mục album('0' không đổi,'default' để đặt về mặc định):"
|
||||
CHANGE_TRACK_FILE_FORMAT = "Định dạng tên tệp nhạc('0' không đổi,'default' để đặt về mặc định):"
|
||||
CHANGE_VIDEO_FILE_FORMAT = "Video file format('0'-not modify,'default'-to set default):"
|
||||
CHANGE_SHOW_PROGRESS = "Hiện tiến trình('0'-Không,'1'-Có):"
|
||||
CHANGE_SHOW_TRACKINFO = "Hiện thông tin bài('0'-Không,'1'-Có):"
|
||||
CHANGE_SAVE_ALBUM_INFO = "Lưu AlbumInfo.txt('0'-Không,'1'-Có):"
|
||||
@@ -86,6 +88,7 @@ class LangVietnamese(object):
|
||||
CHANGE_LYRICS_SERVER_PROXY = "Máy chủ proxy cho lyrics('0' không đổi):"
|
||||
CHANGE_ADD_LRC_FILE = "Lưu timed lyrics tệp .lrc ('0'-Không,'1'-Có):"
|
||||
CHANGE_ADD_TYPE_FOLDER = "Thêm Loại-Thư mục, ví dụ Album/Video/Playlist('0'-Không,'1'-Có):"
|
||||
CHANGE_MULITHREAD_DOWNLOAD = "Multi thread download('0'-No,'1'-Yes):"
|
||||
|
||||
# {} are required in these strings
|
||||
AUTH_START_LOGIN = "Đang bắt đầu đăng nhập..."
|
||||
@@ -95,7 +98,7 @@ class LangVietnamese(object):
|
||||
AUTH_TIMEOUT = "Đã vượt quá thời gian chờ."
|
||||
|
||||
MSG_VALID_ACCESSTOKEN = "AccessToken vẫn tốt trong {}."
|
||||
MSG_INVAILD_ACCESSTOKEN = "AccessToken hết hạn. Đang cố làm mới."
|
||||
MSG_INVALID_ACCESSTOKEN = "AccessToken hết hạn. Đang cố làm mới."
|
||||
MSG_PATH_ERR = "Lỗi đường dẫn!"
|
||||
MSG_INPUT_ERR = "Lỗi nhập!"
|
||||
|
||||
|
||||
@@ -4,144 +4,204 @@
|
||||
@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(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.trackid = None
|
||||
self.url = None
|
||||
self.codec = None
|
||||
self.encryptionKey = None
|
||||
self.soundQuality = None
|
||||
|
||||
|
||||
class StreamUrl(ModelBase):
|
||||
trackid = None
|
||||
url = None
|
||||
codec = None
|
||||
encryptionKey = None
|
||||
soundQuality = None
|
||||
class VideoStreamUrl(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.codec = None
|
||||
self.resolution = None
|
||||
self.resolutions = None
|
||||
self.m3u8Url = None
|
||||
|
||||
|
||||
class VideoStreamUrl(ModelBase):
|
||||
codec = None
|
||||
resolution = None
|
||||
resolutions = None
|
||||
m3u8Url = None
|
||||
class Artist(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.id = None
|
||||
self.name = None
|
||||
self.type = None
|
||||
self.picture = None
|
||||
|
||||
|
||||
class Artist(ModelBase):
|
||||
id = None
|
||||
name = None
|
||||
type = None
|
||||
picture = None
|
||||
class Album(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.id = None
|
||||
self.title = None
|
||||
self.duration = 0
|
||||
self.numberOfTracks = 0
|
||||
self.numberOfVideos = 0
|
||||
self.numberOfVolumes = 0
|
||||
self.releaseDate = None
|
||||
self.type = None
|
||||
self.version = None
|
||||
self.cover = None
|
||||
self.explicit = False
|
||||
self.audioQuality = None
|
||||
self.audioModes = None
|
||||
self.artist = Artist()
|
||||
self.artists = Artist()
|
||||
|
||||
|
||||
class Album(ModelBase):
|
||||
id = None
|
||||
title = None
|
||||
duration = 0
|
||||
numberOfTracks = 0
|
||||
numberOfVideos = 0
|
||||
numberOfVolumes = 0
|
||||
releaseDate = None
|
||||
type = None
|
||||
version = None
|
||||
cover = None
|
||||
explicit = False
|
||||
audioQuality = None
|
||||
audioModes = None
|
||||
artist = Artist()
|
||||
artists = Artist()
|
||||
class Playlist(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.uuid = None
|
||||
self.title = None
|
||||
self.numberOfTracks = 0
|
||||
self.numberOfVideos = 0
|
||||
self.description = None
|
||||
self.duration = 0
|
||||
self.image = None
|
||||
self.squareImage = None
|
||||
|
||||
|
||||
class Playlist(ModelBase):
|
||||
uuid = None
|
||||
title = None
|
||||
numberOfTracks = 0
|
||||
numberOfVideos = 0
|
||||
description = None
|
||||
duration = 0
|
||||
image = None
|
||||
squareImage = None
|
||||
class Track(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.id = None
|
||||
self.title = None
|
||||
self.duration = 0
|
||||
self.trackNumber = 0
|
||||
self.volumeNumber = 0
|
||||
self.trackNumberOnPlaylist = 0
|
||||
self.version = None
|
||||
self.isrc = None
|
||||
self.explicit = False
|
||||
self.audioQuality = None
|
||||
self.copyRight = None
|
||||
self.artist = Artist()
|
||||
self.artists = Artist()
|
||||
self.album = Album()
|
||||
self.allowStreaming = False
|
||||
self.playlist = None
|
||||
|
||||
|
||||
class Track(ModelBase):
|
||||
id = None
|
||||
title = None
|
||||
duration = 0
|
||||
trackNumber = 0
|
||||
volumeNumber = 0
|
||||
trackNumberOnPlaylist = 0
|
||||
version = None
|
||||
isrc = None
|
||||
explicit = False
|
||||
audioQuality = None
|
||||
copyRight = None
|
||||
artist = Artist()
|
||||
artists = Artist()
|
||||
album = Album()
|
||||
allowStreaming = False
|
||||
playlist = None
|
||||
class Video(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.id = None
|
||||
self.title = None
|
||||
self.duration = 0
|
||||
self.imageID = None
|
||||
self.trackNumber = 0
|
||||
self.releaseDate = None
|
||||
self.version = None
|
||||
self.quality = None
|
||||
self.explicit = False
|
||||
self.artist = Artist()
|
||||
self.artists = Artist()
|
||||
self.album = Album()
|
||||
self.allowStreaming = False
|
||||
self.playlist = None
|
||||
|
||||
|
||||
class Video(ModelBase):
|
||||
id = None
|
||||
title = None
|
||||
duration = 0
|
||||
imageID = None
|
||||
trackNumber = 0
|
||||
releaseDate = None
|
||||
version = None
|
||||
quality = None
|
||||
explicit = False
|
||||
artist = Artist()
|
||||
artists = Artist()
|
||||
album = Album()
|
||||
allowStreaming = False
|
||||
playlist = None
|
||||
class Mix(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.id = None
|
||||
self.tracks = Track()
|
||||
self.videos = Video()
|
||||
|
||||
|
||||
class Mix(ModelBase):
|
||||
id = None
|
||||
tracks = Track()
|
||||
videos = Video()
|
||||
class Lyrics(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.trackId = None
|
||||
self.lyricsProvider = None
|
||||
self.providerCommontrackId = None
|
||||
self.providerLyricsId = None
|
||||
self.lyrics = None
|
||||
self.subtitles = None
|
||||
|
||||
|
||||
class Lyrics(ModelBase):
|
||||
trackId = None
|
||||
lyricsProvider = None
|
||||
providerCommontrackId = None
|
||||
providerLyricsId = None
|
||||
lyrics = None
|
||||
subtitles = None
|
||||
|
||||
|
||||
class SearchDataBase(ModelBase):
|
||||
limit = 0
|
||||
offset = 0
|
||||
totalNumberOfItems = 0
|
||||
class SearchDataBase(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.limit = 0
|
||||
self.offset = 0
|
||||
self.totalNumberOfItems = 0
|
||||
|
||||
|
||||
class SearchAlbums(SearchDataBase):
|
||||
items = Album()
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.items = Album()
|
||||
|
||||
|
||||
class SearchArtists(SearchDataBase):
|
||||
items = Artist()
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.items = Artist()
|
||||
|
||||
|
||||
class SearchTracks(SearchDataBase):
|
||||
items = Track()
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.items = Track()
|
||||
|
||||
|
||||
class SearchVideos(SearchDataBase):
|
||||
items = Video()
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.items = Video()
|
||||
|
||||
|
||||
class SearchPlaylists(SearchDataBase):
|
||||
items = Playlist()
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.items = Playlist()
|
||||
|
||||
|
||||
class SearchResult(ModelBase):
|
||||
artists = SearchArtists()
|
||||
albums = SearchAlbums()
|
||||
tracks = SearchTracks()
|
||||
videos = SearchVideos()
|
||||
playlists = SearchPlaylists()
|
||||
class SearchResult(aigpy.model.ModelBase):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.artists = SearchArtists()
|
||||
self.albums = SearchAlbums()
|
||||
self.tracks = SearchTracks()
|
||||
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
|
||||
|
||||
@@ -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.07.14.1'
|
||||
__LOGO__ = f'''
|
||||
/$$$$$$$$ /$$ /$$ /$$ /$$ /$$
|
||||
|__ $$__/|__/ | $$ | $$ | $$| $$
|
||||
| $$ /$$ /$$$$$$$ /$$$$$$ | $$ /$$$$$$$| $$
|
||||
@@ -29,86 +32,96 @@ __LOGO__ = '''
|
||||
|__/ |__/ \_______/ \_______/|__/ \_______/|__/
|
||||
|
||||
https://github.com/yaronzz/Tidal-Media-Downloader
|
||||
|
||||
{VERSION}
|
||||
'''
|
||||
VERSION = '2022.03.04.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"],
|
||||
["-g or --gui", "show simple-gui"],
|
||||
["-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 not None:
|
||||
icmp = aigpy.system.cmpVersion(onlineVer, VERSION)
|
||||
if icmp > 0:
|
||||
Printf.info(LANG.select.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_TRACKIFNO, 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_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.select.SETTING, LANG.select.VALUE], [
|
||||
#settings - path and format
|
||||
[LANG.select.SETTING_PATH, getProfilePath()],
|
||||
[LANG.select.SETTING_DOWNLOAD_PATH, data.downloadPath],
|
||||
[LANG.select.SETTING_ALBUM_FOLDER_FORMAT, data.albumFolderFormat],
|
||||
[LANG.select.SETTING_TRACK_FILE_FORMAT, data.trackFileFormat],
|
||||
[LANG.select.SETTING_VIDEO_FILE_FORMAT, data.videoFileFormat],
|
||||
|
||||
#settings - quality
|
||||
[LANG.select.SETTING_AUDIO_QUALITY, data.audioQuality],
|
||||
[LANG.select.SETTING_VIDEO_QUALITY, data.videoQuality],
|
||||
|
||||
#settings - else
|
||||
[LANG.select.SETTING_USE_PLAYLIST_FOLDER, data.usePlaylistFolder],
|
||||
[LANG.select.SETTING_CHECK_EXIST, data.checkExist],
|
||||
[LANG.select.SETTING_SHOW_PROGRESS, data.showProgress],
|
||||
[LANG.select.SETTING_SHOW_TRACKINFO, data.showTrackInfo],
|
||||
[LANG.select.SETTING_SAVE_ALBUMINFO, data.saveAlbumInfo],
|
||||
[LANG.select.SETTING_SAVE_COVERS, data.saveCovers],
|
||||
[LANG.select.SETTING_INCLUDE_EP, data.includeEP],
|
||||
[LANG.select.SETTING_LANGUAGE, LANG.getLangName(data.language)],
|
||||
[LANG.select.SETTINGS_ADD_LRC_FILE, data.lyricFile],
|
||||
[LANG.select.SETTING_MULITHREAD_DOWNLOAD, data.multiThread],
|
||||
[LANG.select.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.select.CHOICE, LANG.select.FUNCTION], [
|
||||
[aigpy.cmd.green(LANG.select.CHOICE_ENTER + " '0':"), LANG.select.CHOICE_EXIT],
|
||||
[aigpy.cmd.green(LANG.select.CHOICE_ENTER + " '1':"), LANG.select.CHOICE_LOGIN],
|
||||
[aigpy.cmd.green(LANG.select.CHOICE_ENTER + " '2':"), LANG.select.CHOICE_LOGOUT],
|
||||
[aigpy.cmd.green(LANG.select.CHOICE_ENTER + " '3':"), LANG.select.CHOICE_SET_ACCESS_TOKEN],
|
||||
[aigpy.cmd.green(LANG.select.CHOICE_ENTER + " '4':"), LANG.select.CHOICE_SETTINGS + '-Path'],
|
||||
[aigpy.cmd.green(LANG.select.CHOICE_ENTER + " '5':"), LANG.select.CHOICE_SETTINGS + '-Quality'],
|
||||
[aigpy.cmd.green(LANG.select.CHOICE_ENTER + " '6':"), LANG.select.CHOICE_SETTINGS + '-Else'],
|
||||
[aigpy.cmd.green(LANG.select.CHOICE_ENTER + " '7':"), LANG.select.CHOICE_APIKEY],
|
||||
[aigpy.cmd.green(LANG.select.CHOICE_ENTER_URLID), LANG.select.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("====================================================")
|
||||
|
||||
@@ -117,27 +130,31 @@ 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:
|
||||
return default
|
||||
elif ret == "":
|
||||
print(aigpy.cmd.red(LANG.PRINT_ERR + " ") + errmsg)
|
||||
print(aigpy.cmd.red(LANG.select.PRINT_ERR + " ") + errmsg)
|
||||
else:
|
||||
break
|
||||
return ret
|
||||
|
||||
@staticmethod
|
||||
def enterLimit(string, errmsg, limit=[]):
|
||||
LANG = getLang()
|
||||
while True:
|
||||
ret = aigpy.cmd.inputLimit(aigpy.cmd.yellow(string), limit)
|
||||
if ret is None:
|
||||
print(aigpy.cmd.red(LANG.PRINT_ERR + " ") + errmsg)
|
||||
print(aigpy.cmd.red(LANG.select.PRINT_ERR + " ") + errmsg)
|
||||
else:
|
||||
break
|
||||
return ret
|
||||
@@ -153,33 +170,28 @@ class Printf(object):
|
||||
|
||||
@staticmethod
|
||||
def err(string):
|
||||
LANG = getLang()
|
||||
print(aigpy.cmd.red(LANG.PRINT_ERR + " ") + string)
|
||||
logging.error(string)
|
||||
print(aigpy.cmd.red(LANG.select.PRINT_ERR + " ") + string)
|
||||
# logging.error(string)
|
||||
|
||||
@staticmethod
|
||||
def info(string):
|
||||
LANG = getLang()
|
||||
print(aigpy.cmd.blue(LANG.PRINT_INFO + " ") + string)
|
||||
print(aigpy.cmd.blue(LANG.select.PRINT_INFO + " ") + string)
|
||||
|
||||
@staticmethod
|
||||
def success(string):
|
||||
LANG = getLang()
|
||||
print(aigpy.cmd.green(LANG.PRINT_SUCCESS + " ") + string)
|
||||
print(aigpy.cmd.green(LANG.select.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.select.MODEL_ALBUM_PROPERTY, LANG.select.VALUE], [
|
||||
[LANG.select.MODEL_TITLE, data.title],
|
||||
["ID", data.id],
|
||||
[LANG.select.MODEL_TRACK_NUMBER, data.numberOfTracks],
|
||||
[LANG.select.MODEL_VIDEO_NUMBER, data.numberOfVideos],
|
||||
[LANG.select.MODEL_RELEASE_DATE, data.releaseDate],
|
||||
[LANG.select.MODEL_VERSION, data.version],
|
||||
[LANG.select.MODEL_EXPLICIT, data.explicit],
|
||||
])
|
||||
print(tb)
|
||||
logging.info("====album " + str(data.id) + "====\n" +
|
||||
"title:" + data.title + "\n" +
|
||||
@@ -189,16 +201,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.select.MODEL_TRACK_PROPERTY, LANG.select.VALUE], [
|
||||
[LANG.select.MODEL_TITLE, data.title],
|
||||
["ID", data.id],
|
||||
[LANG.select.MODEL_ALBUM, data.album.title],
|
||||
[LANG.select.MODEL_VERSION, data.version],
|
||||
[LANG.select.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)])
|
||||
@@ -210,19 +220,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.select.MODEL_VIDEO_PROPERTY, LANG.select.VALUE], [
|
||||
[LANG.select.MODEL_TITLE, data.title],
|
||||
[LANG.select.MODEL_ALBUM, data.album.title if data.album != None else None],
|
||||
[LANG.select.MODEL_VERSION, data.version],
|
||||
[LANG.select.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" +
|
||||
@@ -231,14 +238,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.select.MODEL_ARTIST_PROPERTY, LANG.select.VALUE], [
|
||||
[LANG.select.MODEL_ID, data.id],
|
||||
[LANG.select.MODEL_NAME, data.name],
|
||||
["Number of albums", num],
|
||||
[LANG.select.MODEL_TYPE, str(data.type)],
|
||||
])
|
||||
print(tb)
|
||||
logging.info("====artist " + str(data.id) + "====\n" +
|
||||
"name:" + data.name + "\n" +
|
||||
@@ -247,13 +252,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.select.MODEL_PLAYLIST_PROPERTY, LANG.select.VALUE], [
|
||||
[LANG.select.MODEL_TITLE, data.title],
|
||||
[LANG.select.MODEL_TRACK_NUMBER, data.numberOfTracks],
|
||||
[LANG.select.MODEL_VIDEO_NUMBER, data.numberOfVideos],
|
||||
])
|
||||
print(tb)
|
||||
logging.info("====playlist " + str(data.uuid) + "====\n" +
|
||||
"title:" + data.title + "\n" +
|
||||
@@ -263,13 +266,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.select.MODEL_PLAYLIST_PROPERTY, LANG.select.VALUE], [
|
||||
[LANG.select.MODEL_ID, data.id],
|
||||
[LANG.select.MODEL_TRACK_NUMBER, len(data.tracks)],
|
||||
[LANG.select.MODEL_VIDEO_NUMBER, len(data.videos)],
|
||||
])
|
||||
print(tb)
|
||||
logging.info("====Mix " + str(data.id) + "====\n" +
|
||||
"track num:" + str(len(data.tracks)) + "\n" +
|
||||
@@ -279,7 +280,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,164 +4,123 @@
|
||||
@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.lang.language import *
|
||||
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
|
||||
multiThread = 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
|
||||
|
||||
LANG.setLang(self.language)
|
||||
|
||||
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}"
|
||||
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 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,138 +4,72 @@
|
||||
@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
|
||||
|
||||
if 'totalNumberOfItems'in data:
|
||||
data = self.__get__(path, params)
|
||||
if 'totalNumberOfItems' in data:
|
||||
total = data['totalNumberOfItems']
|
||||
if total > 0 and total <= len(ret):
|
||||
return None, ret
|
||||
|
||||
num = 0
|
||||
for item in data["items"]:
|
||||
num += 1
|
||||
ret.append(item)
|
||||
return ret
|
||||
|
||||
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 = []
|
||||
txt = requests.get(url).text
|
||||
txt = requests.get(url).content.decode('utf-8')
|
||||
# array = txt.split("#EXT-X-STREAM-INF")
|
||||
array = txt.split("#")
|
||||
for item in array:
|
||||
@@ -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(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 == 2:
|
||||
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 getMix(self, id):
|
||||
msg, tracks, videos = self.getItems(id, Type.Mix)
|
||||
if msg is not None:
|
||||
return msg, None
|
||||
def getVideo(self, id) -> Video:
|
||||
return aigpy.model.dictToModel(self.__get__('videos/' + str(id)), Video())
|
||||
|
||||
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,15 @@ 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
|
||||
# else:
|
||||
# manifest = json.loads(base64.b64decode(resp.manifest).decode('utf-8'))
|
||||
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 +310,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 +356,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,527 +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
|
||||
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.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/'
|
||||
|
||||
# hyphen
|
||||
hyphen = ' - ' if conf.addHyphen else ' '
|
||||
# get number
|
||||
number = ''
|
||||
if conf.useTrackNumber:
|
||||
number = __getIndexStr__(video.trackNumber) + hyphen
|
||||
# get artist
|
||||
artist = ''
|
||||
if conf.artistBeforeTitle:
|
||||
artist = aigpy.path.replaceLimitChar(getArtistsName(video.artists), '-') + hyphen
|
||||
# get explicit
|
||||
explicit = "(Explicit)" if conf.addExplicitTag and video.explicit else ''
|
||||
# title
|
||||
title = aigpy.path.replaceLimitChar(video.title, '-')
|
||||
# extension
|
||||
extension = ".mp4"
|
||||
return base + number + artist.strip() + title + explicit + extension
|
||||
|
||||
|
||||
def 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(path, url):
|
||||
if CONF.checkExist and isNeedDownload(path, url) is False:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def convert(srcPath, stream):
|
||||
if CONF.onlyM4a:
|
||||
return convertToM4a(srcPath, stream.codec)
|
||||
return srcPath
|
||||
|
||||
|
||||
def downloadTrack(track: Track, album=None, playlist=None, userProgress=None, partSize=1048576):
|
||||
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)
|
||||
|
||||
# check exist
|
||||
if skip(path, stream.url):
|
||||
Printf.success(aigpy.path.getFileName(path) + " (skip:already exists!)")
|
||||
return True, ""
|
||||
|
||||
# download
|
||||
logging.info("[DL Track] name=" + aigpy.path.getFileName(path) + "\nurl=" + stream.url)
|
||||
tool = aigpy.download.DownloadTool(path + '.part', [stream.url])
|
||||
tool.setUserProgress(userProgress)
|
||||
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
|
||||
path = convert(path, stream)
|
||||
|
||||
# contributors
|
||||
msg, contributors = API.getTrackContributors(track.id)
|
||||
msg, tidalLyrics = API.getLyrics(track.id)
|
||||
|
||||
lyrics = '' if tidalLyrics is None else tidalLyrics.subtitles
|
||||
if CONF.lyricFile:
|
||||
if tidalLyrics is None:
|
||||
Printf.info(f'Failed to get lyrics from tidal!"{track.title}"')
|
||||
else:
|
||||
lrcPath = path.rsplit(".", 1)[0] + '.lrc'
|
||||
aigpy.fileHelper.write(lrcPath, tidalLyrics.subtitles, 'w')
|
||||
|
||||
setMetaData(track, album, path, contributors, lyrics)
|
||||
Printf.success(aigpy.path.getFileName(path))
|
||||
return True, ""
|
||||
except Exception as e:
|
||||
Printf.err("Download failed! " + track.title + ' (' + str(e) + ')')
|
||||
return False, str(e)
|
||||
|
||||
|
||||
def downloadVideo(video: Video, album=None, playlist=None):
|
||||
msg, stream = API.getVideoStreamUrl(video.id, CONF.videoQuality)
|
||||
Printf.video(video, stream)
|
||||
if not aigpy.string.isNull(msg):
|
||||
Printf.err(video.title + "." + msg)
|
||||
return False, msg
|
||||
path = getVideoPath(CONF, video, album, playlist)
|
||||
|
||||
logging.info("[DL Video] name=" + aigpy.path.getFileName(path) + "\nurl=" + stream.m3u8Url)
|
||||
m3u8content = 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_INVAILD_ACCESSTOKEN)
|
||||
msg, check = API.refreshAccessToken(TOKEN.refreshToken)
|
||||
if check:
|
||||
Printf.success(LANG.MSG_VALID_ACCESSTOKEN.format(displayTime(int(API.key.expiresIn))))
|
||||
TOKEN.userid = API.key.userId
|
||||
TOKEN.countryCode = API.key.countryCode
|
||||
TOKEN.accessToken = API.key.accessToken
|
||||
TOKEN.expiresAfter = time.time() + int(API.key.expiresIn)
|
||||
TokenSettings.save(TOKEN)
|
||||
return True
|
||||
else:
|
||||
tmp = TokenSettings() # clears saved tokens
|
||||
TokenSettings.save(tmp)
|
||||
return False
|
||||
|
||||
def loginByWeb():
|
||||
start = time.time()
|
||||
elapsed = 0
|
||||
while elapsed < API.key.authCheckTimeout:
|
||||
elapsed = time.time() - start
|
||||
msg, check = API.checkAuthStatus()
|
||||
if not check:
|
||||
if msg == "pending":
|
||||
time.sleep(API.key.authCheckInterval + 1)
|
||||
continue
|
||||
return False
|
||||
if check:
|
||||
Printf.success(LANG.MSG_VALID_ACCESSTOKEN.format(displayTime(int(API.key.expiresIn))))
|
||||
TOKEN.userid = API.key.userId
|
||||
TOKEN.countryCode = API.key.countryCode
|
||||
TOKEN.accessToken = API.key.accessToken
|
||||
TOKEN.refreshToken = API.key.refreshToken
|
||||
TOKEN.expiresAfter = time.time() + int(API.key.expiresIn)
|
||||
TokenSettings.save(TOKEN)
|
||||
return True
|
||||
|
||||
Printf.err(LANG.AUTH_TIMEOUT)
|
||||
return False
|
||||
|
||||
def getArtistsNames(artists): # : list[tidal_dl.model.Artist]
|
||||
ret = []
|
||||
for item in artists:
|
||||
ret.append(item.name)
|
||||
return ','.join(ret)
|
||||
|
||||
def getDurationString(seconds: int):
|
||||
m, s = divmod(seconds, 60)
|
||||
h, m = divmod(m, 60)
|
||||
return "%02d:%02d:%02d" % (h, m, s)
|
||||
|
||||
def getBasePath(model):
|
||||
if isinstance(model, tidal_dl.model.Album):
|
||||
return getAlbumPath(CONF, model)
|
||||
if isinstance(model, tidal_dl.model.Playlist):
|
||||
return getPlaylistPath(CONF, model)
|
||||
if isinstance(model, tidal_dl.model.Track):
|
||||
return getAlbumPath(CONF, model.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,34 +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.QtWidgets import QApplication
|
||||
|
||||
from tidal_gui import theme
|
||||
from tidal_gui.viewModel.mainModel import MainModel
|
||||
|
||||
|
||||
def main():
|
||||
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,42 +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):
|
||||
self._layout.insertWidget(self._numWidget, 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()
|
||||