Files
tidal-dl-ng-webui/app/services/download_manager.py
2025-12-02 14:07:35 +01:00

310 lines
11 KiB
Python

import threading
import queue
import logging
import sys
from typing import Dict, List
from tidal_dl_ng.download import Download
from tidal_dl_ng.config import Settings, Tidal
class MockTask:
def __init__(self, total):
self.finished = False
self.percentage = 0
self.total = total
self.completed = 0
class MockProgressRich:
def __init__(self):
self.tasks = []
def add_task(self, *args, **kwargs):
total = kwargs.get("total", 100)
self.tasks.append(MockTask(total))
return len(self.tasks) - 1
def update(self, task_id, *args, **kwargs):
task = self.tasks[task_id]
if "completed" in kwargs:
task.completed = kwargs["completed"]
if "advance" in kwargs:
task.completed += kwargs["advance"]
if task.total and task.completed >= task.total:
task.finished = True
def advance(self, task_id, advance=1):
self.update(task_id, advance=advance)
def remove_task(self, *args, **kwargs):
pass
def start(self):
pass
def stop(self):
pass
from tidal_dl_ng.model.gui_data import ProgressBars
from .tidal_wrapper import TidalWrapper
logger = logging.getLogger(__name__)
class DownloadManager:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(DownloadManager, cls).__new__(cls)
cls._instance._initialized = False
return cls._instance
def __init__(self):
if self._initialized:
return
self.queue = queue.Queue()
self.active_downloads: Dict[str, Dict] = {}
self.history: List[Dict] = []
self.worker_thread = threading.Thread(target=self._worker, daemon=True)
self.worker_thread.start()
self.tidal_wrapper = TidalWrapper()
self._initialized = True
def add_to_queue(self, item_type: str, item_id: str):
task_id = f"{item_type}_{item_id}"
task = {
"id": task_id,
"type": item_type,
"item_id": item_id,
"status": "queued",
"progress": 0,
"name": "Fetching metadata..."
}
self.active_downloads[task_id] = task
self.queue.put(task)
return task
def get_queue(self):
return list(self.active_downloads.values()) + self.history
def _worker(self):
while True:
task = self.queue.get()
if task is None:
break
task_id = task["id"]
self.active_downloads[task_id]["status"] = "downloading"
try:
self._process_download(task)
self.active_downloads[task_id]["status"] = "completed"
self.active_downloads[task_id]["progress"] = 100
except Exception as e:
logger.error(f"Download failed: {e}")
self.active_downloads[task_id]["status"] = "failed"
self.active_downloads[task_id]["error"] = str(e)
finally:
# Move to history
self.history.append(self.active_downloads.pop(task_id))
self.queue.task_done()
def cancel_task(self, task_id: str):
if task_id in self.active_downloads:
self.active_downloads[task_id]["control"] = "cancel"
return True
return False
def pause_task(self, task_id: str):
if task_id in self.active_downloads:
self.active_downloads[task_id]["control"] = "pause"
return True
return False
def resume_task(self, task_id: str):
if task_id in self.active_downloads:
self.active_downloads[task_id]["control"] = "resume"
return True
return False
def _check_control(self, task):
# Check for control flags
while task.get("control") == "pause":
time.sleep(1)
# If cancelled while paused
if task.get("control") == "cancel":
break
if task.get("control") == "cancel":
task["status"] = "cancelled"
raise Exception("Download cancelled by user")
def _process_download(self, task):
# Mock Signal class to capture emit calls
class MockSignal:
def __init__(self, callback):
self.callback = callback
def emit(self, value):
self.callback(value)
# Mock ProgressBars class
class MockProgressBars:
def __init__(self, task_updater):
self.item = MockSignal(lambda p: task_updater("progress", p))
self.item_name = MockSignal(lambda n: task_updater("name", n))
self.list_item = MockSignal(lambda p: task_updater("list_progress", p))
self.list_name = MockSignal(lambda n: task_updater("list_name", n))
def update_task(key, value):
if key == "progress":
# For single track, this is the main progress
if task["type"] == "track":
self.active_downloads[task["id"]]["progress"] = value
elif key == "name":
# Update status with current track name
self.active_downloads[task["id"]]["current_item"] = value
elif key == "list_progress":
# For albums/playlists, this is the main progress
if task["type"] != "track":
self.active_downloads[task["id"]]["progress"] = value
mock_progress = MockProgressBars(update_task)
settings = Settings()
tidal = Tidal(settings)
# Attempt to load token from storage
tidal.login_token()
# Ensure we are logged in
if not tidal.session.check_login():
raise Exception("Not logged in")
# Use environment variable for download path, default to /app/downloads
import os
settings.data.download_base_path = os.getenv("DOWNLOAD_PATH", "/app/downloads")
settings.data.path_binary_ffmpeg = "ffmpeg"
# Configure logger
logger_tidal = logging.getLogger("tidal_dl_ng")
logger_tidal.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
if not logger_tidal.handlers:
logger_tidal.addHandler(handler)
mock_progress_rich = MockProgressRich()
event_abort = threading.Event()
event_run = threading.Event()
event_run.set() # Allow running
downloader = Download(
tidal_obj=tidal,
path_base=settings.data.download_base_path,
fn_logger=logger_tidal,
skip_existing=False,
progress_gui=mock_progress,
progress=mock_progress_rich,
event_abort=event_abort,
event_run=event_run
)
# Fetch media object
media = None
if task["type"] == "track":
media = tidal.session.track(task["item_id"])
elif task["type"] == "album":
media = tidal.session.album(task["item_id"])
elif task["type"] == "video":
media = tidal.session.video(task["item_id"])
elif task["type"] == "playlist":
media = tidal.session.playlist(task["item_id"])
elif task["type"] == "artist":
media = tidal.session.artist(task["item_id"])
if not media:
raise Exception("Media not found")
task["name"] = f"{media.name}"
if hasattr(media, "artist"):
task["name"] += f" - {media.artist.name}"
self._check_control(task)
if task["type"] == "track":
result, path = downloader.item(
file_template=settings.data.format_track,
media=media,
quality_audio=settings.data.quality_audio
)
if not result:
raise Exception("Download failed (downloader returned False)")
logger.info(f"Download successful: {path}")
elif task["type"] == "album":
tracks = media.tracks()
total = len(tracks)
task["total_items"] = total
for i, track in enumerate(tracks):
self._check_control(task)
task["current_index"] = i + 1
task["progress"] = int((i / total) * 100)
task["current_item"] = track.name
downloader.item(
file_template=settings.data.format_track,
media=track,
quality_audio=settings.data.quality_audio,
is_parent_album=True,
list_position=i+1,
list_total=total
)
elif task["type"] == "playlist":
tracks = media.tracks()
total = len(tracks)
task["total_items"] = total
for i, track in enumerate(tracks):
self._check_control(task)
task["current_index"] = i + 1
task["progress"] = int((i / total) * 100)
task["current_item"] = track.name
downloader.item(
file_template=settings.data.format_track,
media=track,
quality_audio=settings.data.quality_audio,
is_parent_album=False, # Playlist tracks are treated as individual tracks usually, or we can use playlist format
list_position=i+1,
list_total=total
)
elif task["type"] == "artist":
# For artist, download all albums
albums = media.get_albums()
total_albums = len(albums)
# We can't easily know total tracks upfront without fetching all albums first.
# Let's track progress by Album for the top level, and maybe tracks within?
# Or just flatten everything. Flattening is better for "Track X of Y".
all_tracks = []
for album in albums:
all_tracks.extend(album.tracks())
total = len(all_tracks)
task["total_items"] = total
for i, track in enumerate(all_tracks):
self._check_control(task)
task["current_index"] = i + 1
task["progress"] = int((i / total) * 100)
task["current_item"] = track.name
downloader.item(
file_template=settings.data.format_track,
media=track,
quality_audio=settings.data.quality_audio,
is_parent_album=True, # Treat as album tracks to keep folder structure
list_position=track.track_num, # Use original track num
list_total=track.album.num_tracks
)