Merge pull request #380 from exislow/149-feature-add-pauseresume-button-for-interrupting-the-download
✨ Downloads can be now paused / resumed. Fixes #149 & 324
This commit is contained in:
+1
-1
@@ -174,7 +174,7 @@ profile = "black"
|
|||||||
multi_line_output = 3
|
multi_line_output = 3
|
||||||
indent = 4
|
indent = 4
|
||||||
color_output = true
|
color_output = true
|
||||||
known_third_party = ["Crypto", "PySide6", "coloredlogs", "dataclasses_json", "ffmpeg", "m3u8", "mutagen", "pathvalidate", "requests", "rich", "tidalapi", "toml", "typer"]
|
known_third_party = ["Crypto", "PySide6", "coloredlogs", "config", "dataclasses_json", "ffmpeg", "m3u8", "mutagen", "pathvalidate", "requests", "rich", "tidalapi", "toml", "typer"]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
# https://mypy.readthedocs.io/en/latest/config_file.html#using-a-pyproject-toml-file
|
# https://mypy.readthedocs.io/en/latest/config_file.html#using-a-pyproject-toml-file
|
||||||
|
|||||||
+30
-1
@@ -1,9 +1,12 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
import signal
|
||||||
|
import sys
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Annotated, Optional
|
from typing import Annotated, Optional
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
|
from config import HandlingApp
|
||||||
from rich.console import Group
|
from rich.console import Group
|
||||||
from rich.live import Live
|
from rich.live import Live
|
||||||
from rich.progress import (
|
from rich.progress import (
|
||||||
@@ -64,6 +67,7 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
|
|||||||
|
|
||||||
# Create initial objects.
|
# Create initial objects.
|
||||||
settings: Settings = Settings()
|
settings: Settings = Settings()
|
||||||
|
handling_app: HandlingApp = HandlingApp()
|
||||||
progress: Progress = Progress(
|
progress: Progress = Progress(
|
||||||
TextColumn("[progress.description]{task.description}"),
|
TextColumn("[progress.description]{task.description}"),
|
||||||
SpinnerColumn(),
|
SpinnerColumn(),
|
||||||
@@ -92,8 +96,9 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
|
|||||||
fn_logger=fn_logger,
|
fn_logger=fn_logger,
|
||||||
progress=progress,
|
progress=progress,
|
||||||
progress_overall=progress_overall,
|
progress_overall=progress_overall,
|
||||||
|
event_abort=handling_app.event_abort,
|
||||||
|
event_run=handling_app.event_run,
|
||||||
)
|
)
|
||||||
|
|
||||||
progress_table = Table.grid()
|
progress_table = Table.grid()
|
||||||
|
|
||||||
# Style Progress display.
|
# Style Progress display.
|
||||||
@@ -112,6 +117,10 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
|
|||||||
for item in urls:
|
for item in urls:
|
||||||
media_type: MediaType | bool = False
|
media_type: MediaType | bool = False
|
||||||
|
|
||||||
|
# Exit loop if abort signal is set.
|
||||||
|
if handling_app.event_abort.is_set():
|
||||||
|
return False
|
||||||
|
|
||||||
# Extract media name and id from link.
|
# Extract media name and id from link.
|
||||||
if "http" in item:
|
if "http" in item:
|
||||||
media_type = get_tidal_media_type(item)
|
media_type = get_tidal_media_type(item)
|
||||||
@@ -143,6 +152,10 @@ def _download(ctx: typer.Context, urls: list[str], try_login: bool = True) -> bo
|
|||||||
item_ids.append(item_id)
|
item_ids.append(item_id)
|
||||||
|
|
||||||
for item_id in item_ids:
|
for item_id in item_ids:
|
||||||
|
# Exit loop if abort signal is set.
|
||||||
|
if handling_app.event_abort.is_set():
|
||||||
|
return False
|
||||||
|
|
||||||
dl.items(
|
dl.items(
|
||||||
media_id=item_id,
|
media_id=item_id,
|
||||||
media_type=media_type,
|
media_type=media_type,
|
||||||
@@ -379,5 +392,21 @@ def gui(ctx: typer.Context):
|
|||||||
gui_activate(ctx.obj[CTX_TIDAL])
|
gui_activate(ctx.obj[CTX_TIDAL])
|
||||||
|
|
||||||
|
|
||||||
|
def handle_sigint_term(signum, frame):
|
||||||
|
"""Set app abort event, so threads can check it and shutdown.
|
||||||
|
|
||||||
|
:param signum:
|
||||||
|
:param frame:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
handling_app: HandlingApp = HandlingApp()
|
||||||
|
|
||||||
|
handling_app.event_abort.set()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
# Catch CTRL+C
|
||||||
|
signal.signal(signal.SIGINT, handle_sigint_term)
|
||||||
|
signal.signal(signal.SIGTERM, handle_sigint_term)
|
||||||
|
|
||||||
app()
|
app()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import shutil
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from threading import Event
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import tidalapi
|
import tidalapi
|
||||||
@@ -189,3 +190,11 @@ class Tidal(BaseConfig, metaclass=SingletonMeta):
|
|||||||
del self.session
|
del self.session
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class HandlingApp(metaclass=SingletonMeta):
|
||||||
|
event_abort: Event = Event()
|
||||||
|
event_run: Event = Event()
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.event_run.set()
|
||||||
|
|||||||
+38
-3
@@ -6,6 +6,7 @@ import tempfile
|
|||||||
import time
|
import time
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from concurrent import futures
|
from concurrent import futures
|
||||||
|
from threading import Event
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
import m3u8
|
import m3u8
|
||||||
@@ -74,6 +75,8 @@ class Download:
|
|||||||
progress_gui: ProgressBars
|
progress_gui: ProgressBars
|
||||||
progress: Progress
|
progress: Progress
|
||||||
progress_overall: Progress
|
progress_overall: Progress
|
||||||
|
event_abort: Event
|
||||||
|
event_run: Event
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -84,6 +87,8 @@ class Download:
|
|||||||
progress_gui: ProgressBars = None,
|
progress_gui: ProgressBars = None,
|
||||||
progress: Progress = None,
|
progress: Progress = None,
|
||||||
progress_overall: Progress = None,
|
progress_overall: Progress = None,
|
||||||
|
event_abort: Event = None,
|
||||||
|
event_run: Event = None,
|
||||||
):
|
):
|
||||||
self.settings = Settings()
|
self.settings = Settings()
|
||||||
self.session = session
|
self.session = session
|
||||||
@@ -93,6 +98,8 @@ class Download:
|
|||||||
self.progress = progress
|
self.progress = progress
|
||||||
self.progress_overall = progress_overall
|
self.progress_overall = progress_overall
|
||||||
self.path_base = path_base
|
self.path_base = path_base
|
||||||
|
self.event_abort = event_abort
|
||||||
|
self.event_run = event_run
|
||||||
|
|
||||||
if not self.settings.data.path_binary_ffmpeg and (
|
if not self.settings.data.path_binary_ffmpeg and (
|
||||||
self.settings.data.video_convert_mp4 or self.settings.data.extract_flac
|
self.settings.data.video_convert_mp4 or self.settings.data.extract_flac
|
||||||
@@ -172,10 +179,11 @@ class Download:
|
|||||||
max_workers=self.settings.data.downloads_simultaneous_per_track_max
|
max_workers=self.settings.data.downloads_simultaneous_per_track_max
|
||||||
) as executor:
|
) as executor:
|
||||||
# Dispatch all download tasks to worker threads
|
# Dispatch all download tasks to worker threads
|
||||||
l_futures: [any] = [
|
l_futures: [futures.Future] = [
|
||||||
executor.submit(self._download_segment, url, path_base, block_size, p_task, progress_to_stdout)
|
executor.submit(self._download_segment, url, path_base, block_size, p_task, progress_to_stdout)
|
||||||
for url in urls
|
for url in urls
|
||||||
]
|
]
|
||||||
|
|
||||||
# Report results as they become available
|
# Report results as they become available
|
||||||
for future in futures.as_completed(l_futures):
|
for future in futures.as_completed(l_futures):
|
||||||
# Retrieve result
|
# Retrieve result
|
||||||
@@ -192,6 +200,14 @@ class Download:
|
|||||||
result_segments = False
|
result_segments = False
|
||||||
self.fn_logger.error(f"Something went wrong while downloading {media_name}. File is corrupt!")
|
self.fn_logger.error(f"Something went wrong while downloading {media_name}. File is corrupt!")
|
||||||
|
|
||||||
|
# If app is terminated (CTRL+C)
|
||||||
|
if self.event_abort.is_set():
|
||||||
|
# Cancel all not yet started tasks
|
||||||
|
for f in l_futures:
|
||||||
|
f.cancel()
|
||||||
|
|
||||||
|
return False, path_file
|
||||||
|
|
||||||
tmp_path_file_decrypted: pathlib.Path = path_file
|
tmp_path_file_decrypted: pathlib.Path = path_file
|
||||||
|
|
||||||
# Only if no error happened while downloading.
|
# Only if no error happened while downloading.
|
||||||
@@ -241,6 +257,15 @@ class Download:
|
|||||||
id_segment: int = int(filename_stem) if filename_stem.isdecimal() else 0
|
id_segment: int = int(filename_stem) if filename_stem.isdecimal() else 0
|
||||||
error: HTTPError | None = None
|
error: HTTPError | None = None
|
||||||
|
|
||||||
|
# If app is terminated (CTRL+C)
|
||||||
|
if self.event_abort.is_set():
|
||||||
|
return DownloadSegmentResult(
|
||||||
|
result=False, url=url, path_segment=path_segment, id_segment=id_segment, error=error
|
||||||
|
)
|
||||||
|
|
||||||
|
if not self.event_run.is_set():
|
||||||
|
self.event_run.wait()
|
||||||
|
|
||||||
# Retry download on failed segments, with an exponential delay between retries
|
# Retry download on failed segments, with an exponential delay between retries
|
||||||
with requests.Session() as s:
|
with requests.Session() as s:
|
||||||
retries = Retry(total=5, backoff_factor=1) # , status_forcelist=[ 502, 503, 504 ])
|
retries = Retry(total=5, backoff_factor=1) # , status_forcelist=[ 502, 503, 504 ])
|
||||||
@@ -478,7 +503,7 @@ class Download:
|
|||||||
|
|
||||||
# Whether a file was downloaded or skipped and the download delay is enabled, wait until the next download.
|
# Whether a file was downloaded or skipped and the download delay is enabled, wait until the next download.
|
||||||
# Only use this, if you have a list of several Track items.
|
# Only use this, if you have a list of several Track items.
|
||||||
if download_delay and not skip_file:
|
if (download_delay and not skip_file) and not self.event_abort.is_set():
|
||||||
time_sleep: float = round(
|
time_sleep: float = round(
|
||||||
random.SystemRandom().uniform(
|
random.SystemRandom().uniform(
|
||||||
self.settings.data.download_delay_sec_min, self.settings.data.download_delay_sec_max
|
self.settings.data.download_delay_sec_min, self.settings.data.download_delay_sec_max
|
||||||
@@ -726,7 +751,7 @@ class Download:
|
|||||||
while not progress.finished:
|
while not progress.finished:
|
||||||
with futures.ThreadPoolExecutor(max_workers=self.settings.data.downloads_concurrent_max) as executor:
|
with futures.ThreadPoolExecutor(max_workers=self.settings.data.downloads_concurrent_max) as executor:
|
||||||
# Dispatch all download tasks to worker threads
|
# Dispatch all download tasks to worker threads
|
||||||
l_futures: [any] = [
|
l_futures: [futures.Future] = [
|
||||||
executor.submit(
|
executor.submit(
|
||||||
self.item,
|
self.item,
|
||||||
media=item_media,
|
media=item_media,
|
||||||
@@ -738,6 +763,7 @@ class Download:
|
|||||||
)
|
)
|
||||||
for item_media in items
|
for item_media in items
|
||||||
]
|
]
|
||||||
|
|
||||||
# Report results as they become available
|
# Report results as they become available
|
||||||
for future in futures.as_completed(l_futures):
|
for future in futures.as_completed(l_futures):
|
||||||
# Retrieve result
|
# Retrieve result
|
||||||
@@ -752,6 +778,15 @@ class Download:
|
|||||||
if not progress_stdout:
|
if not progress_stdout:
|
||||||
self.progress_gui.list_item.emit(progress.tasks[p_task1].percentage)
|
self.progress_gui.list_item.emit(progress.tasks[p_task1].percentage)
|
||||||
|
|
||||||
|
# If app is terminated (CTRL+C)
|
||||||
|
if self.event_abort.is_set():
|
||||||
|
# Cancel all not yet started tasks
|
||||||
|
for f in l_futures:
|
||||||
|
f.cancel()
|
||||||
|
|
||||||
|
# End method here.
|
||||||
|
return
|
||||||
|
|
||||||
# Create playlist file
|
# Create playlist file
|
||||||
if self.settings.data.playlist_create:
|
if self.settings.data.playlist_create:
|
||||||
self.playlist_populate(set(result_dirs), list_media_name, is_album)
|
self.playlist_populate(set(result_dirs), list_media_name, is_album)
|
||||||
|
|||||||
+50
-2
@@ -49,6 +49,7 @@ import sys
|
|||||||
import time
|
import time
|
||||||
from collections.abc import Callable, Sequence
|
from collections.abc import Callable, Sequence
|
||||||
|
|
||||||
|
from config import HandlingApp
|
||||||
from requests.exceptions import HTTPError
|
from requests.exceptions import HTTPError
|
||||||
from tidalapi.session import LinkLogin
|
from tidalapi.session import LinkLogin
|
||||||
|
|
||||||
@@ -161,6 +162,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
self._populate_search_types(self.cb_search_type, SearchTypes)
|
self._populate_search_types(self.cb_search_type, SearchTypes)
|
||||||
self.apply_settings(self.settings)
|
self.apply_settings(self.settings)
|
||||||
self._init_signals()
|
self._init_signals()
|
||||||
|
self._init_buttons()
|
||||||
self.init_tidal(tidal)
|
self.init_tidal(tidal)
|
||||||
|
|
||||||
logger_gui.debug("All setup.")
|
logger_gui.debug("All setup.")
|
||||||
@@ -218,6 +220,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
list_name=self.s_list_name,
|
list_name=self.s_list_name,
|
||||||
)
|
)
|
||||||
progress: Progress = Progress()
|
progress: Progress = Progress()
|
||||||
|
handling_app: HandlingApp = HandlingApp()
|
||||||
self.dl = Download(
|
self.dl = Download(
|
||||||
session=self.tidal.session,
|
session=self.tidal.session,
|
||||||
skip_existing=self.tidal.settings.data.skip_existing,
|
skip_existing=self.tidal.settings.data.skip_existing,
|
||||||
@@ -225,6 +228,8 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
fn_logger=logger_gui,
|
fn_logger=logger_gui,
|
||||||
progress_gui=data_pb,
|
progress_gui=data_pb,
|
||||||
progress=progress,
|
progress=progress,
|
||||||
|
event_abort=handling_app.event_abort,
|
||||||
|
event_run=handling_app.event_run,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _init_progressbar(self):
|
def _init_progressbar(self):
|
||||||
@@ -751,6 +756,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
self.pb_queue_download_clear_all.clicked.connect(self.on_queue_download_clear_all)
|
self.pb_queue_download_clear_all.clicked.connect(self.on_queue_download_clear_all)
|
||||||
self.pb_queue_download_clear_finished.clicked.connect(self.on_queue_download_clear_finished)
|
self.pb_queue_download_clear_finished.clicked.connect(self.on_queue_download_clear_finished)
|
||||||
self.pb_queue_download_remove.clicked.connect(self.on_queue_download_remove)
|
self.pb_queue_download_remove.clicked.connect(self.on_queue_download_remove)
|
||||||
|
self.pb_queue_download_toggle.clicked.connect(self.on_pb_queue_download_toggle)
|
||||||
self.l_search.returnPressed.connect(
|
self.l_search.returnPressed.connect(
|
||||||
lambda: self.search_populate_results(self.l_search.text(), self.cb_search_type.currentData())
|
lambda: self.search_populate_results(self.l_search.text(), self.cb_search_type.currentData())
|
||||||
)
|
)
|
||||||
@@ -776,7 +782,7 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
self.s_update_show.connect(self.on_version)
|
self.s_update_show.connect(self.on_version)
|
||||||
|
|
||||||
# Menubar
|
# Menubar
|
||||||
self.a_exit.triggered.connect(sys.exit)
|
self.a_exit.triggered.connect(self.close)
|
||||||
self.a_version.triggered.connect(self.on_version)
|
self.a_version.triggered.connect(self.on_version)
|
||||||
self.a_preferences.triggered.connect(self.on_preferences)
|
self.a_preferences.triggered.connect(self.on_preferences)
|
||||||
self.a_logout.triggered.connect(self.on_logout)
|
self.a_logout.triggered.connect(self.on_logout)
|
||||||
@@ -793,6 +799,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
self.s_queue_download_item_failed.connect(self.on_queue_download_item_failed)
|
self.s_queue_download_item_failed.connect(self.on_queue_download_item_failed)
|
||||||
self.s_queue_download_item_skipped.connect(self.on_queue_download_item_skipped)
|
self.s_queue_download_item_skipped.connect(self.on_queue_download_item_skipped)
|
||||||
|
|
||||||
|
def _init_buttons(self):
|
||||||
|
self.pb_queue_download_run()
|
||||||
|
|
||||||
def on_logout(self):
|
def on_logout(self):
|
||||||
result: bool = self.tidal.logout()
|
result: bool = self.tidal.logout()
|
||||||
|
|
||||||
@@ -929,6 +938,36 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
else:
|
else:
|
||||||
logger_gui.info("Cannot remove a currently downloading item from queue.")
|
logger_gui.info("Cannot remove a currently downloading item from queue.")
|
||||||
|
|
||||||
|
def on_pb_queue_download_toggle(self) -> None:
|
||||||
|
"""Toggle download status (pause / resume) accordingly.
|
||||||
|
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
handling_app: HandlingApp = HandlingApp()
|
||||||
|
|
||||||
|
if handling_app.event_run.is_set():
|
||||||
|
self.pb_queue_download_pause()
|
||||||
|
else:
|
||||||
|
self.pb_queue_download_run()
|
||||||
|
|
||||||
|
def pb_queue_download_run(self):
|
||||||
|
handling_app: HandlingApp = HandlingApp()
|
||||||
|
|
||||||
|
handling_app.event_run.set()
|
||||||
|
|
||||||
|
icon = QtGui.QIcon(QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.MediaPlaybackStart))
|
||||||
|
self.pb_queue_download_toggle.setIcon(icon)
|
||||||
|
self.pb_queue_download_toggle.setStyleSheet("background-color: #218838; color: #fff")
|
||||||
|
|
||||||
|
def pb_queue_download_pause(self):
|
||||||
|
handling_app: HandlingApp = HandlingApp()
|
||||||
|
|
||||||
|
handling_app.event_run.clear()
|
||||||
|
|
||||||
|
icon = QtGui.QIcon(QtGui.QIcon.fromTheme(QtGui.QIcon.ThemeIcon.MediaPlaybackPause))
|
||||||
|
self.pb_queue_download_toggle.setIcon(icon)
|
||||||
|
self.pb_queue_download_toggle.setStyleSheet("background-color: #e0a800; color: #212529")
|
||||||
|
|
||||||
# TODO: Must happen in main thread. Do not thread this.
|
# TODO: Must happen in main thread. Do not thread this.
|
||||||
def on_download_results(self) -> None:
|
def on_download_results(self) -> None:
|
||||||
items: [HumanProxyModel | None] = self.tr_results.selectionModel().selectedRows()
|
items: [HumanProxyModel | None] = self.tr_results.selectionModel().selectedRows()
|
||||||
@@ -958,7 +997,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
self.tr_queue_download.addTopLevelItem(child)
|
self.tr_queue_download.addTopLevelItem(child)
|
||||||
|
|
||||||
def watcher_queue_download(self) -> None:
|
def watcher_queue_download(self) -> None:
|
||||||
while not self.shutdown:
|
handling_app: HandlingApp = HandlingApp()
|
||||||
|
|
||||||
|
while not handling_app.event_abort.is_set():
|
||||||
items: [QtWidgets.QTreeWidgetItem | None] = self.tr_queue_download.findItems(
|
items: [QtWidgets.QTreeWidgetItem | None] = self.tr_queue_download.findItems(
|
||||||
QueueDownloadStatus.Waiting, QtCore.Qt.MatchFlag.MatchExactly, column=0
|
QueueDownloadStatus.Waiting, QtCore.Qt.MatchFlag.MatchExactly, column=0
|
||||||
)
|
)
|
||||||
@@ -1107,6 +1148,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
self.shutdown = True
|
self.shutdown = True
|
||||||
|
|
||||||
|
handling_app: HandlingApp = HandlingApp()
|
||||||
|
handling_app.event_abort.set()
|
||||||
|
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
|
||||||
@@ -1114,7 +1158,9 @@ class MainWindow(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
def gui_activate(tidal: Tidal | None = None):
|
def gui_activate(tidal: Tidal | None = None):
|
||||||
# Set dark theme and create QT app.
|
# Set dark theme and create QT app.
|
||||||
qdarktheme.enable_hi_dpi()
|
qdarktheme.enable_hi_dpi()
|
||||||
|
|
||||||
app = QtWidgets.QApplication(sys.argv)
|
app = QtWidgets.QApplication(sys.argv)
|
||||||
|
|
||||||
# Fix for Windows: Tooltips have bright font color
|
# Fix for Windows: Tooltips have bright font color
|
||||||
# https://github.com/5yutan5/PyQtDarkTheme/issues/239
|
# https://github.com/5yutan5/PyQtDarkTheme/issues/239
|
||||||
# qdarktheme.setup_theme()
|
# qdarktheme.setup_theme()
|
||||||
@@ -1123,6 +1169,7 @@ def gui_activate(tidal: Tidal | None = None):
|
|||||||
# Create icon object and apply it to app window.
|
# Create icon object and apply it to app window.
|
||||||
pixmap: QtGui.QPixmap = QtGui.QPixmap("tidal_dl_ng/ui/icon.png")
|
pixmap: QtGui.QPixmap = QtGui.QPixmap("tidal_dl_ng/ui/icon.png")
|
||||||
icon: QtGui.QIcon = QtGui.QIcon(pixmap)
|
icon: QtGui.QIcon = QtGui.QIcon(pixmap)
|
||||||
|
|
||||||
app.setWindowIcon(icon)
|
app.setWindowIcon(icon)
|
||||||
|
|
||||||
# This bit gets the taskbar icon working properly in Windows
|
# This bit gets the taskbar icon working properly in Windows
|
||||||
@@ -1136,6 +1183,7 @@ def gui_activate(tidal: Tidal | None = None):
|
|||||||
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(my_app_id)
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(my_app_id)
|
||||||
|
|
||||||
window = MainWindow(tidal=tidal)
|
window = MainWindow(tidal=tidal)
|
||||||
|
|
||||||
window.show()
|
window.show()
|
||||||
# Check for updates
|
# Check for updates
|
||||||
window.s_update_check.emit(True)
|
window.s_update_check.emit(True)
|
||||||
|
|||||||
+17
-3
@@ -7,7 +7,7 @@
|
|||||||
################################################################################
|
################################################################################
|
||||||
|
|
||||||
from PySide6.QtCore import QCoreApplication, QLocale, QMetaObject, QRect, QSize, Qt
|
from PySide6.QtCore import QCoreApplication, QLocale, QMetaObject, QRect, QSize, Qt
|
||||||
from PySide6.QtGui import QAction, QFont, QPixmap
|
from PySide6.QtGui import QAction, QFont, QIcon, QPixmap
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
@@ -455,11 +455,24 @@ class Ui_MainWindow:
|
|||||||
|
|
||||||
self.lv_queue_download.addWidget(self.tr_queue_download)
|
self.lv_queue_download.addWidget(self.tr_queue_download)
|
||||||
|
|
||||||
|
self.horizontalLayout_4 = QHBoxLayout()
|
||||||
|
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
|
||||||
self.pb_queue_download_remove = QPushButton(self.w_central)
|
self.pb_queue_download_remove = QPushButton(self.w_central)
|
||||||
self.pb_queue_download_remove.setObjectName("pb_queue_download_remove")
|
self.pb_queue_download_remove.setObjectName("pb_queue_download_remove")
|
||||||
self.pb_queue_download_remove.setEnabled(True)
|
self.pb_queue_download_remove.setEnabled(True)
|
||||||
|
icon = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.EditClear))
|
||||||
|
self.pb_queue_download_remove.setIcon(icon)
|
||||||
|
|
||||||
self.lv_queue_download.addWidget(self.pb_queue_download_remove)
|
self.horizontalLayout_4.addWidget(self.pb_queue_download_remove)
|
||||||
|
|
||||||
|
self.pb_queue_download_toggle = QPushButton(self.w_central)
|
||||||
|
self.pb_queue_download_toggle.setObjectName("pb_queue_download_toggle")
|
||||||
|
icon1 = QIcon(QIcon.fromTheme(QIcon.ThemeIcon.MediaPlaybackStart))
|
||||||
|
self.pb_queue_download_toggle.setIcon(icon1)
|
||||||
|
|
||||||
|
self.horizontalLayout_4.addWidget(self.pb_queue_download_toggle)
|
||||||
|
|
||||||
|
self.lv_queue_download.addLayout(self.horizontalLayout_4)
|
||||||
|
|
||||||
self.horizontalLayout_3 = QHBoxLayout()
|
self.horizontalLayout_3 = QHBoxLayout()
|
||||||
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
|
self.horizontalLayout_3.setObjectName("horizontalLayout_3")
|
||||||
@@ -557,7 +570,7 @@ class Ui_MainWindow:
|
|||||||
def retranslateUi(self, MainWindow):
|
def retranslateUi(self, MainWindow):
|
||||||
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", "MainWindow", None))
|
MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", "MainWindow", None))
|
||||||
self.a_version.setText(QCoreApplication.translate("MainWindow", "Version", None))
|
self.a_version.setText(QCoreApplication.translate("MainWindow", "Version", None))
|
||||||
self.a_exit.setText(QCoreApplication.translate("MainWindow", "Exit", None))
|
self.a_exit.setText(QCoreApplication.translate("MainWindow", "Quit TIDAL-Downloader-NG", None))
|
||||||
self.a_logout.setText(QCoreApplication.translate("MainWindow", "Logout", None))
|
self.a_logout.setText(QCoreApplication.translate("MainWindow", "Logout", None))
|
||||||
self.a_updates_check.setText(QCoreApplication.translate("MainWindow", "Check for Updates", None))
|
self.a_updates_check.setText(QCoreApplication.translate("MainWindow", "Check for Updates", None))
|
||||||
___qtreewidgetitem = self.tr_lists_user.headerItem()
|
___qtreewidgetitem = self.tr_lists_user.headerItem()
|
||||||
@@ -585,6 +598,7 @@ class Ui_MainWindow:
|
|||||||
___qtreewidgetitem4.setText(2, QCoreApplication.translate("MainWindow", "Name", None))
|
___qtreewidgetitem4.setText(2, QCoreApplication.translate("MainWindow", "Name", None))
|
||||||
___qtreewidgetitem4.setText(1, QCoreApplication.translate("MainWindow", "obj", None))
|
___qtreewidgetitem4.setText(1, QCoreApplication.translate("MainWindow", "obj", None))
|
||||||
self.pb_queue_download_remove.setText(QCoreApplication.translate("MainWindow", "Remove", None))
|
self.pb_queue_download_remove.setText(QCoreApplication.translate("MainWindow", "Remove", None))
|
||||||
|
self.pb_queue_download_toggle.setText(QCoreApplication.translate("MainWindow", "Queue", None))
|
||||||
self.pb_queue_download_clear_finished.setText(QCoreApplication.translate("MainWindow", "Clear Finished", None))
|
self.pb_queue_download_clear_finished.setText(QCoreApplication.translate("MainWindow", "Clear Finished", None))
|
||||||
self.pb_queue_download_clear_all.setText(QCoreApplication.translate("MainWindow", "Clear All", None))
|
self.pb_queue_download_clear_all.setText(QCoreApplication.translate("MainWindow", "Clear All", None))
|
||||||
self.m_file.setTitle(QCoreApplication.translate("MainWindow", "File", None))
|
self.m_file.setTitle(QCoreApplication.translate("MainWindow", "File", None))
|
||||||
|
|||||||
+26
-9
@@ -650,14 +650,31 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="pb_queue_download_remove">
|
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||||
<property name="enabled">
|
<item>
|
||||||
<bool>true</bool>
|
<widget class="QPushButton" name="pb_queue_download_remove">
|
||||||
</property>
|
<property name="enabled">
|
||||||
<property name="text">
|
<bool>true</bool>
|
||||||
<string>Remove</string>
|
</property>
|
||||||
</property>
|
<property name="text">
|
||||||
</widget>
|
<string>Remove</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="QIcon::ThemeIcon::EditClear"/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="pb_queue_download_toggle">
|
||||||
|
<property name="text">
|
||||||
|
<string>Queue</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="QIcon::ThemeIcon::MediaPlaybackStart"/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
@@ -787,7 +804,7 @@
|
|||||||
</action>
|
</action>
|
||||||
<action name="a_exit">
|
<action name="a_exit">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Exit</string>
|
<string>Quit TIDAL-Downloader-NG</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
<action name="a_logout">
|
<action name="a_logout">
|
||||||
|
|||||||
Reference in New Issue
Block a user