Add playlist export, enhanced buy list with search/filter/remove, and statistics dashboard

This commit is contained in:
nmemmert committed 2025-11-17 11:32:25 -05:00
1 parent e643bb3c98
commit e3085f22ae
1 file changed
+213 -16
+213 -16
View File
@@ -20,7 +20,7 @@ from PySide6.QtWidgets import (
QLabel, QPushButton, QTextEdit, QLineEdit, QFormLayout, QLabel, QPushButton, QTextEdit, QLineEdit, QFormLayout,
QDialog, QDialogButtonBox, QSystemTrayIcon, QMenu, QDialog, QDialogButtonBox, QSystemTrayIcon, QMenu,
QGroupBox, QCheckBox, QSpinBox, QComboBox, QMessageBox, QGroupBox, QCheckBox, QSpinBox, QComboBox, QMessageBox,
QProgressBar, QSplitter, QFrame, QScrollArea, QTextBrowser, QTableWidget, QTableWidgetItem, QHeaderView QProgressBar, QSplitter, QFrame, QScrollArea, QTextBrowser, QTableWidget, QTableWidgetItem, QHeaderView, QListWidget, QListWidgetItem
) )
from PySide6.QtCore import ( from PySide6.QtCore import (
Qt, QTimer, QThread, Signal, QSettings, QSize Qt, QTimer, QThread, Signal, QSettings, QSize
@@ -299,6 +299,16 @@ class MainWindow(QMainWindow):
self.history_button.clicked.connect(self.show_history) self.history_button.clicked.connect(self.show_history)
status_layout.addWidget(self.history_button) status_layout.addWidget(self.history_button)
# Export button
self.export_button = QPushButton("Export Playlist")
self.export_button.clicked.connect(self.export_playlist)
status_layout.addWidget(self.export_button)
# Statistics button
self.stats_button = QPushButton("Statistics")
self.stats_button.clicked.connect(self.show_statistics)
status_layout.addWidget(self.stats_button)
status_group.setLayout(status_layout) status_group.setLayout(status_layout)
splitter.addWidget(status_group) splitter.addWidget(status_group)
@@ -443,39 +453,170 @@ class MainWindow(QMainWindow):
QMessageBox.information(self, "Settings Saved", "Configuration updated successfully!") QMessageBox.information(self, "Settings Saved", "Configuration updated successfully!")
def show_buy_list(self): def show_buy_list(self):
"""Show the Amazon buy list dialog""" """Show the Amazon buy list dialog with interactive features"""
try: try:
with open('amazon_buy_list.txt', 'r') as f: with open('amazon_buy_list.txt', 'r') as f:
content = f.read() content = f.read()
except FileNotFoundError: except FileNotFoundError:
content = "No buy list available. Run an update to generate the list." QMessageBox.information(self, "No Buy List", "No buy list available. Run an update to generate the list.")
return
# Convert plain text to HTML with clickable links # Parse content to get songs
html_content = content.replace('\n', '<br>') lines = content.split('\n')
# Make URLs clickable songs = []
import re i = 0
html_content = re.sub(r'(https?://[^\s]+)', r'<a href="\1">\1</a>', html_content) while i < len(lines):
# Wrap in proper HTML if lines[i].startswith('Songs not in your library'):
html_content = f"<html><body>{html_content}</body></html>" i += 2 # Skip header
continue
if lines[i].strip() and not lines[i].startswith('http'):
artist_title = lines[i].strip()
if i + 1 < len(lines) and lines[i + 1].startswith('http'):
url = lines[i + 1].strip()
songs.append((artist_title, url))
i += 3 # Skip blank line
else:
i += 1
else:
i += 1
dialog = QDialog(self) dialog = QDialog(self)
dialog.setWindowTitle("Amazon Buy List") dialog.setWindowTitle("Amazon Buy List")
dialog.setModal(True) # Make it modal so it shows dialog.setModal(True)
dialog.resize(600, 400) dialog.resize(600, 400)
layout = QVBoxLayout() layout = QVBoxLayout()
text_browser = QTextBrowser() # Search box
text_browser.setHtml(html_content) search_layout = QHBoxLayout()
text_browser.setOpenExternalLinks(True) # Allow clicking links search_layout.addWidget(QLabel("Search:"))
layout.addWidget(text_browser) self.search_input = QLineEdit()
self.search_input.textChanged.connect(lambda: self.filter_buy_list(songs))
search_layout.addWidget(self.search_input)
layout.addLayout(search_layout)
# List widget
self.buy_list_widget = QListWidget()
self.populate_buy_list(songs)
layout.addWidget(self.buy_list_widget)
# Buttons
button_layout = QHBoxLayout()
open_button = QPushButton("Open Selected")
open_button.clicked.connect(self.open_selected_buy_items)
button_layout.addWidget(open_button)
remove_button = QPushButton("Remove Selected")
remove_button.clicked.connect(lambda: self.remove_selected_buy_items(songs, dialog))
button_layout.addWidget(remove_button)
close_button = QPushButton("Close")
close_button.clicked.connect(dialog.close)
button_layout.addWidget(close_button)
layout.addLayout(button_layout)
dialog.setLayout(layout)
dialog.exec()
def populate_buy_list(self, songs):
"""Populate the buy list widget"""
self.buy_list_widget.clear()
for artist_title, url in songs:
item = QListWidgetItem(artist_title)
item.setData(1, url) # Store URL in item data
item.setCheckState(0) # Qt.CheckState.Unchecked
self.buy_list_widget.addItem(item)
def filter_buy_list(self, all_songs):
"""Filter the buy list based on search text"""
search_text = self.search_input.text().lower()
filtered = [(at, url) for at, url in all_songs if search_text in at.lower()]
self.populate_buy_list(filtered)
def open_selected_buy_items(self):
"""Open selected buy list items in browser"""
for i in range(self.buy_list_widget.count()):
item = self.buy_list_widget.item(i)
if item.checkState() == 2: # Qt.CheckState.Checked
url = item.data(1)
QDesktopServices.openUrl(url)
def remove_selected_buy_items(self, all_songs, dialog):
"""Remove selected items from buy list"""
to_remove = []
for i in range(self.buy_list_widget.count()):
item = self.buy_list_widget.item(i)
if item.checkState() == 2: # Qt.CheckState.Checked
artist_title = item.text()
to_remove.append(artist_title)
if not to_remove:
QMessageBox.information(dialog, "No Selection", "Please select items to remove.")
return
# Confirm
reply = QMessageBox.question(dialog, "Confirm Removal",
f"Remove {len(to_remove)} selected items from buy list?",
QMessageBox.Yes | QMessageBox.No)
if reply != QMessageBox.Yes:
return
# Remove from all_songs
updated_songs = [(at, url) for at, url in all_songs if at not in to_remove]
# Rewrite file
with open('amazon_buy_list.txt', 'w') as f:
f.write("Songs not in your library - Amazon search links:\n\n")
for artist_title, url in updated_songs:
f.write(f"{artist_title}\n{url}\n\n")
# Update display
self.populate_buy_list(updated_songs)
QMessageBox.information(dialog, "Removed", f"Removed {len(to_remove)} items from buy list.")
def show_statistics(self):
"""Show statistics dashboard"""
import sqlite3
import json
dialog = QDialog(self)
dialog.setWindowTitle("Statistics Dashboard")
dialog.setModal(True)
dialog.resize(400, 300)
layout = QVBoxLayout()
try:
conn = sqlite3.connect('playlist_history.db')
c = conn.cursor()
c.execute('SELECT COUNT(*), SUM(added_count), SUM(missing_count) FROM history')
result = c.fetchone()
conn.close()
total_updates = result[0] or 0
total_added = result[1] or 0
total_missing = result[2] or 0
layout.addWidget(QLabel(f"Total Updates: {total_updates}"))
layout.addWidget(QLabel(f"Total Songs Added to Plex: {total_added}"))
layout.addWidget(QLabel(f"Total Songs in Buy List: {total_missing}"))
if total_updates > 0:
avg_added = total_added / total_updates
avg_missing = total_missing / total_updates
layout.addWidget(QLabel(f"Average Songs Added per Update: {avg_added:.1f}"))
layout.addWidget(QLabel(f"Average Missing Songs per Update: {avg_missing:.1f}"))
except Exception as e:
layout.addWidget(QLabel(f"Error loading statistics: {str(e)}"))
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
buttons.rejected.connect(dialog.close) buttons.rejected.connect(dialog.close)
layout.addWidget(buttons) layout.addWidget(buttons)
dialog.setLayout(layout) dialog.setLayout(layout)
dialog.exec() # Use exec() for modal dialog dialog.exec()
def show_history(self): def show_history(self):
"""Show the playlist history dialog""" """Show the playlist history dialog"""
@@ -548,6 +689,62 @@ class MainWindow(QMainWindow):
dialog.setLayout(layout) dialog.setLayout(layout)
dialog.exec() dialog.exec()
def export_playlist(self):
"""Export the current Plex playlist to CSV"""
try:
# Get config
plex_token = self.config.get('PLEX_TOKEN')
server_ip = self.config.get('SERVER_IP')
playlist_name = self.config.get('PLAYLIST_NAME', 'Journey FM Recently Played')
if not plex_token or not server_ip:
QMessageBox.warning(self, "Configuration Error", "Plex token and server IP are required. Please check settings.")
return
# Connect to Plex
from plexapi.myplex import MyPlexAccount
account = MyPlexAccount(token=plex_token)
resources = account.resources()
server_resource = None
for r in resources:
for conn in r.connections:
if server_ip in conn.address:
server_resource = r
break
if server_resource:
break
if not server_resource:
QMessageBox.warning(self, "Connection Error", f"Server at {server_ip} not found.")
return
plex = server_resource.connect()
# Get playlist
try:
playlist = plex.playlist(playlist_name)
tracks = playlist.items()
except:
QMessageBox.warning(self, "Playlist Error", f"Playlist '{playlist_name}' not found.")
return
# Export to CSV
import csv
with open('playlist_export.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['Title', 'Artist', 'Album'])
for track in tracks:
writer.writerow([
track.title,
track.artist().title if track.artist() else '',
track.album().title if track.album() else ''
])
QMessageBox.information(self, "Export Complete", f"Playlist exported to playlist_export.csv ({len(tracks)} songs)")
except Exception as e:
QMessageBox.critical(self, "Export Error", f"Failed to export playlist: {str(e)}")
def closeEvent(self, event): def closeEvent(self, event):
"""Handle window close - minimize to tray instead of closing""" """Handle window close - minimize to tray instead of closing"""
if self.tray_icon and self.tray_icon.isVisible(): if self.tray_icon and self.tray_icon.isVisible():