summaryrefslogtreecommitdiff
path: root/tidal_dl_ng/dialog_history.py
blob: 2e447df0268162060d2d0f025c56a24959f9d5d3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
"""
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}")