From e643bb3c9871540670d722d0338bd5ea1a984374 Mon Sep 17 00:00:00 2001 From: nmemmert Date: Mon, 17 Nov 2025 11:30:20 -0500 Subject: [PATCH] Add playlist history tracking with SQLite database and GUI dialog --- journey_fm_app.py | 78 ++++++++++++++++++++++++++++++++++++++++++++++- main.py | 65 ++++++++++++++++++++++++--------------- 2 files changed, 117 insertions(+), 26 deletions(-) diff --git a/journey_fm_app.py b/journey_fm_app.py index 79cb32b..54b8b54 100644 --- a/journey_fm_app.py +++ b/journey_fm_app.py @@ -20,7 +20,7 @@ from PySide6.QtWidgets import ( QLabel, QPushButton, QTextEdit, QLineEdit, QFormLayout, QDialog, QDialogButtonBox, QSystemTrayIcon, QMenu, QGroupBox, QCheckBox, QSpinBox, QComboBox, QMessageBox, - QProgressBar, QSplitter, QFrame, QScrollArea, QTextBrowser + QProgressBar, QSplitter, QFrame, QScrollArea, QTextBrowser, QTableWidget, QTableWidgetItem, QHeaderView ) from PySide6.QtCore import ( Qt, QTimer, QThread, Signal, QSettings, QSize @@ -294,6 +294,11 @@ class MainWindow(QMainWindow): self.buy_list_button.clicked.connect(self.show_buy_list) status_layout.addWidget(self.buy_list_button) + # History button + self.history_button = QPushButton("View History") + self.history_button.clicked.connect(self.show_history) + status_layout.addWidget(self.history_button) + status_group.setLayout(status_layout) splitter.addWidget(status_group) @@ -472,6 +477,77 @@ class MainWindow(QMainWindow): dialog.setLayout(layout) dialog.exec() # Use exec() for modal dialog + def show_history(self): + """Show the playlist history dialog""" + import sqlite3 + import json + from datetime import datetime + + dialog = QDialog(self) + dialog.setWindowTitle("Playlist History") + dialog.setModal(True) + dialog.resize(800, 600) + + layout = QVBoxLayout() + + table = QTableWidget() + table.setColumnCount(4) + table.setHorizontalHeaderLabels(["Date", "Added Songs", "Missing Songs", "Details"]) + table.horizontalHeader().setStretchLastSection(True) + + try: + conn = sqlite3.connect('playlist_history.db') + c = conn.cursor() + c.execute('SELECT date, added_count, added_songs, missing_count, missing_songs FROM history ORDER BY date DESC') + rows = c.fetchall() + conn.close() + + table.setRowCount(len(rows)) + for row_idx, (date, added_count, added_songs_json, missing_count, missing_songs_json) in enumerate(rows): + # Parse date + try: + dt = datetime.fromisoformat(date) + date_str = dt.strftime('%Y-%m-%d %H:%M') + except: + date_str = date + + # Added songs + try: + added_songs = json.loads(added_songs_json) + added_text = f"{added_count} songs\n" + "\n".join(added_songs[:3]) # Show first 3 + if len(added_songs) > 3: + added_text += f"\n... and {len(added_songs)-3} more" + except: + added_text = f"{added_count} songs" + + # Missing songs + try: + missing_songs = json.loads(missing_songs_json) + missing_text = f"{missing_count} songs\n" + "\n".join([f"{s['artist']} - {s['title']}" for s in missing_songs[:3]]) + if len(missing_songs) > 3: + missing_text += f"\n... and {len(missing_songs)-3} more" + except: + missing_text = f"{missing_count} songs" + + table.setItem(row_idx, 0, QTableWidgetItem(date_str)) + table.setItem(row_idx, 1, QTableWidgetItem(added_text)) + table.setItem(row_idx, 2, QTableWidgetItem(missing_text)) + table.setItem(row_idx, 3, QTableWidgetItem(f"Added: {added_count}, Missing: {missing_count}")) + + except Exception as e: + table.setRowCount(1) + table.setItem(0, 0, QTableWidgetItem("Error loading history")) + table.setItem(0, 1, QTableWidgetItem(str(e))) + + layout.addWidget(table) + + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close) + buttons.rejected.connect(dialog.close) + layout.addWidget(buttons) + + dialog.setLayout(layout) + dialog.exec() + def closeEvent(self, event): """Handle window close - minimize to tray instead of closing""" if self.tray_icon and self.tray_icon.isVisible(): diff --git a/main.py b/main.py index 3a2a3c3..d576897 100644 --- a/main.py +++ b/main.py @@ -22,6 +22,7 @@ from datetime import datetime import json import subprocess import urllib.parse +import sqlite3 import urllib.parse # Configuration (defaults, will be overridden by config.json) @@ -122,12 +123,13 @@ def find_song_in_library(artist, title, library_path): return None def create_playlist_in_plex(plex, songs, playlist_name): - """Create playlist in Plex and return added count and missing songs""" + """Create playlist in Plex and return added count, added songs, and missing songs""" # Assuming Plex has a music library named 'Music' music_library = plex.library.section('Music') tracks = [] seen_keys = set() # Track which ratingKeys we've already added to prevent duplicates within this run missing = [] + added_songs = [] for song in songs: # Clean title and artist by removing text in parentheses and normalizing spaces @@ -198,6 +200,7 @@ def create_playlist_in_plex(plex, songs, playlist_name): for track in new_tracks: print(f" + '{track.title}' by {track.artist().title}") added = len(new_tracks) + added_songs.extend([f"{track.title} by {track.artist().title}" for track in new_tracks]) else: print(f"No new songs to add - all {len(tracks)} matching songs already in playlist.") added = 0 @@ -208,14 +211,17 @@ def create_playlist_in_plex(plex, songs, playlist_name): for track in tracks: print(f" + '{track.title}' by {track.artist().title}") added = len(tracks) + added_songs.extend([f"{track.title} by {track.artist().title}" for track in tracks]) except Exception as e: print(f"Error with playlist: {e}") added = 0 + added_songs = [] else: print("No matching tracks found in Plex.") added = 0 + added_songs = [] - return added, missing + return added, added_songs, missing def prompt_for_config(): """Prompt user for configuration variables""" @@ -232,27 +238,30 @@ def prompt_for_config(): json.dump(config, f) return config -def prompt_for_scheduler(): - """Prompt user for scheduler setup""" - setup = input("Do you want to set up a scheduled task? (y/n): ").strip().lower() - if setup != 'y': - return None - print("Choose schedule type:") - print("1. Interval (e.g., every 15 minutes)") - print("2. At startup") - print("3. Both") - choice = input("Enter choice (1/2/3): ").strip() - if choice == '1': - interval = int(input("Enter interval number: ").strip()) - unit = input("Enter unit (Minutes/Hours/Days): ").strip() - return {'Type': 'Interval', 'Interval': interval, 'Unit': unit} - elif choice == '2': - return {'Type': 'Startup'} - elif choice == '3': - return {'Type': 'Both'} - else: - print("Invalid choice.") - return None +def init_history_db(): + """Initialize the history database""" + conn = sqlite3.connect('playlist_history.db') + c = conn.cursor() + c.execute('''CREATE TABLE IF NOT EXISTS history ( + id INTEGER PRIMARY KEY, + date TEXT, + added_count INTEGER, + added_songs TEXT, + missing_count INTEGER, + missing_songs TEXT + )''') + conn.commit() + conn.close() + +def save_history_entry(added_count, added_songs, missing_count, missing_songs): + """Save a history entry to the database""" + conn = sqlite3.connect('playlist_history.db') + c = conn.cursor() + date = datetime.now().isoformat() + c.execute('INSERT INTO history (date, added_count, added_songs, missing_count, missing_songs) VALUES (?, ?, ?, ?, ?)', + (date, added_count, json.dumps(added_songs), missing_count, json.dumps(missing_songs))) + conn.commit() + conn.close() def setup_scheduler(params): """Run the setup_scheduler.ps1 with parameters""" @@ -284,6 +293,9 @@ def main(): print(f"Journey FM Playlist Update - {start_time.strftime('%Y-%m-%d %I:%M:%S %p')}") print(f"{'='*60}\n") + # Initialize history database + init_history_db() + # Scrape songs songs = scrape_recently_played() print(f"Found {len(songs)} recently played songs.") @@ -324,10 +336,13 @@ def main(): # Create playlist if songs: - added, missing = create_playlist_in_plex(plex, songs, PLAYLIST_NAME) + added, added_songs, missing = create_playlist_in_plex(plex, songs, PLAYLIST_NAME) else: print("No songs found.") - added, missing = 0, [] + added, added_songs, missing = 0, [], [] + + # Save to history + save_history_entry(added, added_songs, len(missing), missing) # Create Amazon buy list for missing songs if missing: