Files

328 lines
12 KiB
Python
Raw Permalink Normal View History

"""
dialog_history.py
Dialog for viewing and managing download history.
"""
import os
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from PySide6 import QtCore, QtWidgets
from tidal_dl_ng.history import HistoryService
from tidal_dl_ng.logger import logger_gui
class DialogHistory(QtWidgets.QDialog):
"""Dialog for managing download history.
Displays tracks grouped by source (album, playlist, mix) with ability to:
- View download dates and source information
- Export/Import history
- Clear history or remove selected items
- View statistics
"""
def __init__(self, history_service: HistoryService, parent=None):
"""Initialize the history dialog.
Args:
history_service: The HistoryService instance.
parent: Parent widget.
"""
super().__init__(parent)
# Import the generated UI
from tidal_dl_ng.ui.dialog_history import Ui_DialogHistory
self.ui = Ui_DialogHistory()
self.ui.setupUi(self)
self.history_service = history_service
self._init_ui()
self._connect_signals()
self._load_history()
self.exec()
def _init_ui(self):
"""Initialize UI elements."""
# Set file path
file_path = self.history_service.get_history_file_path()
self.ui.le_file_path.setText(file_path)
# Configure tree widget
self.ui.tw_history.setColumnWidth(0, 400)
self.ui.tw_history.setColumnWidth(1, 100)
self.ui.tw_history.setColumnWidth(2, 180)
self.ui.tw_history.setColumnWidth(3, 100)
# Set window size
self.resize(900, 600)
def _connect_signals(self):
"""Connect UI signals to handlers."""
self.ui.pb_refresh.clicked.connect(self._load_history)
self.ui.pb_export.clicked.connect(self._on_export)
self.ui.pb_import.clicked.connect(self._on_import)
self.ui.pb_clear_history.clicked.connect(self._on_clear_history)
self.ui.pb_remove_selected.clicked.connect(self._on_remove_selected)
self.ui.pb_close.clicked.connect(self.close)
self.ui.pb_open_folder.clicked.connect(self._on_open_folder)
def _load_history(self):
"""Load and display the download history."""
# Clear existing items
self.ui.tw_history.clear()
# Get history grouped by source
grouped_history = self.history_service.get_history_by_source()
# Get statistics
stats = self.history_service.get_statistics()
self._update_statistics(stats)
# Sort sources by name
sorted_sources = sorted(grouped_history.items(), key=lambda x: x[0])
# Populate tree
for _, tracks in sorted_sources:
if not tracks:
continue
# Create parent item for source
source_item = self._create_source_item(tracks)
self.ui.tw_history.addTopLevelItem(source_item)
# Add tracks as children
for track_data in sorted(tracks, key=lambda x: x.get("download_date", ""), reverse=True):
track_item = self._create_track_item(track_data)
source_item.addChild(track_item)
# Expand all top-level items
self.ui.tw_history.expandAll()
logger_gui.info(f"Loaded {stats['total_tracks']} tracks from history")
def _create_source_item(self, tracks: list) -> QtWidgets.QTreeWidgetItem:
"""Create a tree widget item for a source.
Args:
tracks: List of track data dictionaries.
Returns:
QTreeWidgetItem for the source.
"""
if not tracks:
return QtWidgets.QTreeWidgetItem()
first_track = tracks[0]
source_type = first_track.get("source_type", "unknown")
source_name = first_track.get("source_name", "Unknown")
source_id = first_track.get("source_id", "")
# Format source name
if source_type == "manual" or not source_name:
display_name = f"📝 Manual Downloads ({len(tracks)} tracks)"
elif source_type == "album":
display_name = f"💿 {source_name} ({len(tracks)} tracks)"
elif source_type == "playlist":
display_name = f"📋 {source_name} ({len(tracks)} tracks)"
elif source_type == "mix":
display_name = f"🎵 {source_name} ({len(tracks)} tracks)"
elif source_type == "track":
display_name = f"🎼 Individual Tracks ({len(tracks)} tracks)"
else:
display_name = f"{source_name} ({len(tracks)} tracks)"
item = QtWidgets.QTreeWidgetItem()
item.setText(0, display_name)
item.setText(1, source_type.capitalize())
item.setText(2, "")
item.setText(3, source_id or "")
# Make it bold
font = item.font(0)
font.setBold(True)
item.setFont(0, font)
return item
def _create_track_item(self, track_data: dict) -> QtWidgets.QTreeWidgetItem:
"""Create a tree widget item for a track.
Args:
track_data: Dictionary with track information.
Returns:
QTreeWidgetItem for the track.
"""
track_id = track_data.get("track_id", "")
download_date = track_data.get("download_date", "")
# Format date
try:
if download_date:
dt = datetime.fromisoformat(download_date.replace("Z", "+00:00"))
formatted_date = dt.strftime("%Y-%m-%d %H:%M:%S")
else:
formatted_date = "Unknown"
except:
formatted_date = download_date
item = QtWidgets.QTreeWidgetItem()
item.setText(0, f" Track {track_id}")
item.setText(1, "Track")
item.setText(2, formatted_date)
item.setText(3, track_id)
# Store track ID for later use
item.setData(0, QtCore.Qt.ItemDataRole.UserRole, track_id)
return item
def _update_statistics(self, stats: dict):
"""Update statistics labels.
Args:
stats: Dictionary with statistics.
"""
total = stats.get("total_tracks", 0)
by_type = stats.get("by_source_type", {})
self.ui.l_total_tracks.setText(f"Total Tracks: {total}")
self.ui.l_by_albums.setText(f"Albums: {by_type.get('album', 0)}")
self.ui.l_by_playlists.setText(f"Playlists: {by_type.get('playlist', 0)}")
self.ui.l_by_mixes.setText(f"Mixes: {by_type.get('mix', 0)}")
self.ui.l_by_manual.setText(f"Manual: {by_type.get('manual', 0)}")
def _on_export(self):
"""Handle export button click."""
file_path, _ = QtWidgets.QFileDialog.getSaveFileName(
self, "Export Download History", "download_history_export.json", "JSON Files (*.json)"
)
if file_path:
success, message = self.history_service.export_history(file_path)
if success:
QtWidgets.QMessageBox.information(self, "Export Successful", message)
logger_gui.info(f"Exported history to: {file_path}")
else:
QtWidgets.QMessageBox.critical(self, "Export Failed", f"Failed to export history:\n{message}")
logger_gui.error(f"Export failed: {message}")
def _on_import(self):
"""Handle import button click."""
file_path, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Import Download History", "", "JSON Files (*.json)")
if not file_path:
return
# Ask merge or replace
reply = QtWidgets.QMessageBox.question(
self,
"Import Mode",
"Do you want to MERGE with existing history?\n\n"
"Yes = Merge (add new tracks, keep existing)\n"
"No = Replace (delete all existing, import only new)\n"
"Cancel = Abort import",
QtWidgets.QMessageBox.StandardButton.Yes
| QtWidgets.QMessageBox.StandardButton.No
| QtWidgets.QMessageBox.StandardButton.Cancel,
)
if reply == QtWidgets.QMessageBox.StandardButton.Cancel:
return
merge = reply == QtWidgets.QMessageBox.StandardButton.Yes
# Perform import
success, message = self.history_service.import_history(file_path, merge=merge)
if success:
QtWidgets.QMessageBox.information(self, "Import Successful", message)
logger_gui.info(f"Imported history from: {file_path}")
self._load_history() # Refresh display
else:
QtWidgets.QMessageBox.critical(self, "Import Failed", f"Failed to import history:\n{message}")
logger_gui.error(f"Import failed: {message}")
def _on_clear_history(self):
"""Handle clear history button click."""
reply = QtWidgets.QMessageBox.warning(
self,
"Clear Download History",
"Are you sure you want to clear ALL download history?\n\n" "This action cannot be undone!",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
self.history_service.clear_history()
QtWidgets.QMessageBox.information(self, "History Cleared", "Download history has been cleared.")
logger_gui.info("Download history cleared")
self._load_history() # Refresh display
def _on_remove_selected(self):
"""Handle remove selected button click."""
selected_items = self.ui.tw_history.selectedItems()
if not selected_items:
QtWidgets.QMessageBox.warning(self, "No Selection", "Please select one or more tracks to remove.")
return
# Collect track IDs from selected items (only child items, not parents)
track_ids = []
for item in selected_items:
# Check if it's a track (child item) by checking if it has a parent
if item.parent() is not None:
track_id = item.data(0, QtCore.Qt.ItemDataRole.UserRole)
if track_id:
track_ids.append(track_id)
if not track_ids:
QtWidgets.QMessageBox.warning(
self, "No Tracks Selected", "Please select individual tracks (not source groups) to remove."
)
return
reply = QtWidgets.QMessageBox.question(
self,
"Remove Tracks",
f"Are you sure you want to remove {len(track_ids)} track(s) from history?",
QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No,
)
if reply == QtWidgets.QMessageBox.StandardButton.Yes:
removed_count = 0
for track_id in track_ids:
if self.history_service.remove_track_from_history(track_id):
removed_count += 1
QtWidgets.QMessageBox.information(self, "Tracks Removed", f"Removed {removed_count} track(s) from history.")
logger_gui.info(f"Removed {removed_count} tracks from history")
self._load_history() # Refresh display
def _on_open_folder(self):
"""Open the folder containing the history file."""
file_path = Path(self.history_service.get_history_file_path())
folder_path = file_path.parent
try:
if sys.platform == "win32":
os.startfile(folder_path) # noqa: S606
elif sys.platform == "darwin":
# Security: folder_path is from config, not user input
subprocess.run(["open", str(folder_path)], check=False) # noqa: S603, S607
else:
# Security: folder_path is from config, not user input
subprocess.run(["xdg-open", str(folder_path)], check=False) # noqa: S603, S607
logger_gui.info(f"Opened folder: {folder_path}")
except Exception as e:
QtWidgets.QMessageBox.warning(self, "Cannot Open Folder", f"Failed to open folder:\n{e!s}")
logger_gui.error(f"Failed to open folder: {e}")