Improve song parsing for Journey FM and Spirit FM, fix app startup issues

This commit is contained in:
nmemmert committed 2025-11-17 12:56:21 -05:00
1 parent 59689dcbf6
commit bf7aa42460
5 files changed
+184 -67

No files matched your search

+123
View File
@@ -18,3 +18,126 @@ https://www.amazon.com/s?k=Have%20%20For%20%20You%20Colton%20%20Dixon%20The%20%2
Done Tasha Layton - Look What You've Done Tasha Layton - Look What You've
https://www.amazon.com/s?k=Done%20Tasha%20%20Layton%20Look%20%20What%20%20You%27ve%20&i=digital-music https://www.amazon.com/s?k=Done%20Tasha%20%20Layton%20Look%20%20What%20%20You%27ve%20&i=digital-music
Church Sing Tauren Wells - Let The
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
You Danny Gokey - I Got
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
Andrew Ripp - Breakdown
https://www.amazon.com/s?k=Andrew%20%20Ripp%20Breakdown&i=digital-music
You've Done Tasha Layton - Look What
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
Crowder - Still
https://www.amazon.com/s?k=Crowder%20Still&i=digital-music
Thing About Praise Benjamin William Hastings - That's The
https://www.amazon.com/s?k=Thing%20%20About%20%20Praise%20Benjamin%20%20William%20%20Hastings%20That%27s%20%20The%20&i=digital-music
Love Feat. Bear Rinehar We The Kingdom - Child Of
https://www.amazon.com/s?k=Love%20%20Feat.%20%20Bear%20%20Rinehar%20We%20%20The%20%20Kingdom%20Child%20%20Of%20&i=digital-music
Am They - Scars I
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
Church Sing Tauren Wells - Let The
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
You Danny Gokey - I Got
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
You've Done Tasha Layton - Look What
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
Am They - Scars I
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
Church Sing Tauren Wells - Let The
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
You Danny Gokey - I Got
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
You've Done Tasha Layton - Look What
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
Am They - Scars I
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
Church Sing Tauren Wells - Let The
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
You Danny Gokey - I Got
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
You've Done Tasha Layton - Look What
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
Am They - Scars I
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
Church Sing Tauren Wells - Let The
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
You Danny Gokey - I Got
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
You've Done Tasha Layton - Look What
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
Am They - Scars I
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
Church Sing Tauren Wells - Let The
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
You Danny Gokey - I Got
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
You've Done Tasha Layton - Look What
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
Am They - Scars I
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
Church Sing Tauren Wells - Let The
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
You Danny Gokey - I Got
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
You've Done Tasha Layton - Look What
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
Thing About Praise Benjamin William Hastings - That's The
https://www.amazon.com/s?k=Thing%20%20About%20%20Praise%20Benjamin%20%20William%20%20Hastings%20That%27s%20%20The%20&i=digital-music
Love Feat. Bear Rinehar We The Kingdom - Child Of
https://www.amazon.com/s?k=Love%20%20Feat.%20%20Bear%20%20Rinehar%20We%20%20The%20%20Kingdom%20Child%20%20Of%20&i=digital-music
Am They - Scars I
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
Church Sing Tauren Wells - Let The
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
You Danny Gokey - I Got
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
You've Done Tasha Layton - Look What
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
Am They - Scars I
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
Church Sing Tauren Wells - Let The
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
You Danny Gokey - I Got
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
You've Done Tasha Layton - Look What
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
+6
View File
@@ -0,0 +1,6 @@
2025-11-17 12:40:16.417598: Starting Journey FM Playlist app...
2025-11-17 12:40:16.417787: DISPLAY set, initializing GUI
2025-11-17 12:40:16.553130: Failed to start GUI application: 'MainWindow' object has no attribute 'update_playlist'
2025-11-17 12:42:52.626588: Starting Journey FM Playlist app...
2025-11-17 12:42:52.626688: DISPLAY set, initializing GUI
2025-11-17 12:42:52.761510: App initialized, starting event loop
+39 -16
View File
@@ -15,7 +15,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
# GUI imports # GUI imports
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QTextEdit, QLineEdit, QLabel, QHBoxLayout, QDialog, QListWidget, QListWidgetItem, QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox, QInputDialog, QMessageBox, QProgressBar, QSystemTrayIcon, QMenu, QComboBox, QGroupBox, QFormLayout, QSpinBox, QTextBrowser, QTabWidget, QDialogButtonBox, QSplitter from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QTextEdit, QLineEdit, QLabel, QHBoxLayout, QDialog, QListWidget, QListWidgetItem, QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox, QInputDialog, QMessageBox, QProgressBar, QSystemTrayIcon, QMenu, QComboBox, QGroupBox, QFormLayout, QSpinBox, QTextBrowser, QTabWidget, QDialogButtonBox, QSplitter, QGridLayout
from PySide6.QtCore import QTimer, Qt, QThread, Signal, QSettings, QUrl from PySide6.QtCore import QTimer, Qt, QThread, Signal, QSettings, QUrl
from PySide6.QtGui import QIcon, QDesktopServices, QFont, QAction from PySide6.QtGui import QIcon, QDesktopServices, QFont, QAction
import json import json
@@ -23,12 +23,9 @@ import csv
from urllib.parse import quote from urllib.parse import quote
import sqlite3 import sqlite3
from datetime import datetime from datetime import datetime
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
# Import our existing functionality # Import our existing functionality
from main import scrape_recently_played, create_playlist_in_plex, PLEX_TOKEN, SERVER_IP, PLAYLIST_NAME from main import scrape_recently_played, create_playlist_in_plex, update_playlist, PLEX_TOKEN, SERVER_IP, PLAYLIST_NAME
class Config: class Config:
"""Configuration management""" """Configuration management"""
@@ -287,30 +284,37 @@ class MainWindow(QMainWindow):
self.last_update_label = QLabel("Last update: Never") self.last_update_label = QLabel("Last update: Never")
status_layout.addWidget(self.last_update_label) status_layout.addWidget(self.last_update_label)
# Buy list button # Buttons in grid layout
button_layout = QGridLayout()
# Row 1
self.update_button = QPushButton("Update Playlist")
self.update_button.clicked.connect(self.run_update_playlist)
button_layout.addWidget(self.update_button, 0, 0)
self.buy_list_button = QPushButton("Show Buy List") self.buy_list_button = QPushButton("Show Buy List")
self.buy_list_button.clicked.connect(self.show_buy_list) self.buy_list_button.clicked.connect(self.show_buy_list)
status_layout.addWidget(self.buy_list_button) button_layout.addWidget(self.buy_list_button, 0, 1)
# History button # Row 2
self.history_button = QPushButton("View History") self.history_button = QPushButton("View History")
self.history_button.clicked.connect(self.show_history) self.history_button.clicked.connect(self.show_history)
status_layout.addWidget(self.history_button) button_layout.addWidget(self.history_button, 1, 0)
# Export button
self.export_button = QPushButton("Export Playlist") self.export_button = QPushButton("Export Playlist")
self.export_button.clicked.connect(self.export_playlist) self.export_button.clicked.connect(self.export_playlist)
status_layout.addWidget(self.export_button) button_layout.addWidget(self.export_button, 1, 1)
# Statistics button # Row 3
self.stats_button = QPushButton("Statistics") self.stats_button = QPushButton("Statistics")
self.stats_button.clicked.connect(self.show_statistics) self.stats_button.clicked.connect(self.show_statistics)
status_layout.addWidget(self.stats_button) button_layout.addWidget(self.stats_button, 2, 0)
# Analytics button
self.analytics_button = QPushButton("Analytics") self.analytics_button = QPushButton("Analytics")
self.analytics_button.clicked.connect(self.show_analytics) self.analytics_button.clicked.connect(self.show_analytics)
status_layout.addWidget(self.analytics_button) button_layout.addWidget(self.analytics_button, 2, 1)
status_layout.addLayout(button_layout)
status_group.setLayout(status_layout) status_group.setLayout(status_layout)
splitter.addWidget(status_group) splitter.addWidget(status_group)
@@ -333,6 +337,13 @@ class MainWindow(QMainWindow):
if not self.config.get('PLEX_TOKEN'): if not self.config.get('PLEX_TOKEN'):
self.show_setup_wizard() self.show_setup_wizard()
def run_update_playlist(self):
"""Run playlist update in background thread"""
import threading
self.status_label.setText("Updating playlist...")
self.update_button.setEnabled(False)
threading.Thread(target=update_playlist, daemon=True).start()
def set_tray_icon(self, tray_icon): def set_tray_icon(self, tray_icon):
"""Set the tray icon reference""" """Set the tray icon reference"""
self.tray_icon = tray_icon self.tray_icon = tray_icon
@@ -639,6 +650,9 @@ class MainWindow(QMainWindow):
import json import json
from datetime import datetime from datetime import datetime
from collections import Counter from collections import Counter
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
dialog = QDialog(self) dialog = QDialog(self)
dialog.setWindowTitle("Playlist Analytics") dialog.setWindowTitle("Playlist Analytics")
@@ -956,12 +970,16 @@ class SystemTrayApp:
def main(): def main():
"""Main application entry point""" """Main application entry point"""
with open('app_log.txt', 'a') as log:
log.write(f"{datetime.now()}: Starting Journey FM Playlist app...\n")
print("Starting Journey FM Playlist app...") print("Starting Journey FM Playlist app...")
try: try:
if not os.environ.get('DISPLAY'): if not os.environ.get('DISPLAY'):
raise Exception("DISPLAY environment variable not set - no graphical display available") raise Exception("DISPLAY environment variable not set - no graphical display available")
print("Initializing GUI...") print("Initializing GUI...")
with open('app_log.txt', 'a') as log:
log.write(f"{datetime.now()}: DISPLAY set, initializing GUI\n")
# Set up high DPI scaling # Set up high DPI scaling
QApplication.setHighDpiScaleFactorRoundingPolicy( QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
@@ -970,11 +988,16 @@ def main():
# Create and run application # Create and run application
tray_app = SystemTrayApp() tray_app = SystemTrayApp()
print("App initialized, starting event loop...") print("App initialized, starting event loop...")
with open('app_log.txt', 'a') as log:
log.write(f"{datetime.now()}: App initialized, starting event loop\n")
sys.exit(tray_app.run()) sys.exit(tray_app.run())
except Exception as e: except Exception as e:
print(f"Failed to start GUI application: {e}") error_msg = f"Failed to start GUI application: {e}"
print(error_msg)
print("Make sure you have a graphical display available (X11/Wayland) and DISPLAY environment variable is set.") print("Make sure you have a graphical display available (X11/Wayland) and DISPLAY environment variable is set.")
print("If running remotely, use X forwarding or a VNC session.") print("If running remotely, use X forwarding or a VNC session.")
with open('app_log.txt', 'a') as log:
log.write(f"{datetime.now()}: {error_msg}\n")
sys.exit(1) sys.exit(1)
if __name__ == "__main__": if __name__ == "__main__":
+16 -51
View File
@@ -86,16 +86,19 @@ def scrape_recently_played():
songs = [] songs = []
# Find elements (div, li) that contain time in their text # Find elements (div, li) that contain time in their text
song_elements = soup.find_all(lambda tag: tag.name in ['div', 'li', 'p'] and re.search(r'\d+:\d+ [AP]M', tag.get_text())) song_elements = soup.find_all(lambda tag: tag.name in ['div', 'li', 'p'] and re.search(r'\d+:\d+', tag.get_text()))
for element in song_elements: for element in song_elements:
if source == 'Journey FM': spans = element.find_all('span')
# Journey FM uses <strong> for title if len(spans) >= 2:
title = spans[0].get_text().strip()
artist = spans[1].get_text().strip()
else:
# Check for strong
strong = element.find('strong') strong = element.find('strong')
if strong: if strong:
title = strong.get_text().strip() title = strong.get_text().strip()
full_text = element.get_text().strip() full_text = element.get_text().strip()
# Remove the title and time match = re.search(r'(\d+:\d+(?: [AP]M)?)', full_text)
match = re.search(r'(\d+:\d+ [AP]M)', full_text)
if match: if match:
time_part = match.group(1) time_part = match.group(1)
before_time = full_text.split(time_part)[0].strip() before_time = full_text.split(time_part)[0].strip()
@@ -104,32 +107,9 @@ def scrape_recently_played():
artist = full_text.replace(title, '').strip() artist = full_text.replace(title, '').strip()
artist = artist.strip() artist = artist.strip()
else: else:
# Fallback # Fallback to text parsing
full_text = element.get_text().strip() full_text = element.get_text().strip()
match = re.search(r'(\d+:\d+ [AP]M)', full_text) match = re.search(r'(\d+:\d+(?: [AP]M)?)', full_text)
if match:
play_time = match.group(1)
before = full_text.split(play_time)[0].strip()
if ' by ' in before:
title, artist = before.split(' by ', 1)
title = title.strip()
artist = artist.strip()
elif ' - ' in before:
title, artist = before.split(' - ', 1)
title = title.strip()
artist = artist.strip()
else:
parts = re.findall(r'[A-Z][^A-Z]*', before)
if len(parts) >= 4:
title = ' '.join(parts[:2])
artist = ' '.join(parts[2:])
else:
title = ' '.join(parts[:len(parts)//2])
artist = ' '.join(parts[len(parts)//2:])
else:
# Spirit FM uses text with -
full_text = element.get_text().strip()
match = re.search(r'(\d+:\d+ [AP]M)', full_text)
if match: if match:
play_time = match.group(1) play_time = match.group(1)
before = full_text.split(play_time)[0].strip() before = full_text.split(play_time)[0].strip()
@@ -252,30 +232,24 @@ def create_playlist_in_plex(plex, songs, playlist_name):
if new_tracks: if new_tracks:
existing.addItems([ts[0] for ts in new_tracks]) existing.addItems([ts[0] for ts in new_tracks])
print(f"Added {len(new_tracks)} new songs to existing playlist '{playlist_name}':")
for track, song in new_tracks: for track, song in new_tracks:
print(f" + '{track.title}' by {track.artist().title} ({song['source']})")
# Auto-tag added songs # Auto-tag added songs
track.addLabel("Journey FM") track.addLabel("Journey FM")
track.rate(5) track.rate(5)
added = len(new_tracks) added = len(new_tracks)
added_songs.extend([f"{track.title} by {track.artist().title} ({song['source']})" for track, song in new_tracks]) added_songs.extend([f"{track.title} by {track.artist().title} ({song['source']})" for track, song in new_tracks])
else: else:
print(f"No new songs to add - all {len(tracks)} matching songs already in playlist.")
added = 0 added = 0
except: except:
# Playlist doesn't exist, create it # Playlist doesn't exist, create it
plex.createPlaylist(playlist_name, [ts[0] for ts in tracks]) plex.createPlaylist(playlist_name, [ts[0] for ts in tracks])
print(f"Created playlist '{playlist_name}' with {len(tracks)} songs:")
for track, song in tracks: for track, song in tracks:
print(f" + '{track.title}' by {track.artist().title} ({song['source']})")
# Auto-tag added songs # Auto-tag added songs
track.addLabel("Journey FM") track.addLabel("Journey FM")
track.rate(5) track.rate(5)
added = len(tracks) added = len(tracks)
added_songs.extend([f"{track.title} by {track.artist().title} ({song['source']})" for track, song in tracks]) added_songs.extend([f"{track.title} by {track.artist().title} ({song['source']})" for track, song in tracks])
except Exception as e: except Exception as e:
print(f"Error with playlist: {e}")
added = 0 added = 0
added_songs = [] added_songs = []
else: else:
@@ -334,7 +308,7 @@ def setup_scheduler(params):
subprocess.run(cmd, check=True) subprocess.run(cmd, check=True)
print("Scheduler setup completed.") print("Scheduler setup completed.")
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print(f"Error setting up scheduler: {e}") print(f"Scheduler setup failed: {e}")
def main(): def main():
global PLEX_TOKEN, SERVER_IP, PLAYLIST_NAME global PLEX_TOKEN, SERVER_IP, PLAYLIST_NAME
@@ -351,16 +325,12 @@ def main():
# Log start time # Log start time
start_time = datetime.now() start_time = datetime.now()
print(f"\n{'='*60}")
print(f"Journey FM Playlist Update - {start_time.strftime('%Y-%m-%d %I:%M:%S %p')}")
print(f"{'='*60}\n")
# Initialize history database # Initialize history database
init_history_db() init_history_db()
# Scrape songs # Scrape songs
songs = scrape_recently_played() songs = scrape_recently_played()
print(f"Found {len(songs)} recently played songs.")
# Connect to Plex via MyPlexAccount # Connect to Plex via MyPlexAccount
account = MyPlexAccount(token=PLEX_TOKEN) account = MyPlexAccount(token=PLEX_TOKEN)
@@ -377,20 +347,16 @@ def main():
break break
if not server_resource: if not server_resource:
print(f"Server at {SERVER_IP} not found in your claimed resources.")
print("Please claim the server: Open http://172.16.16.106:32400/web, sign in with your Plex account, and follow the claim prompts.") print("Please claim the server: Open http://172.16.16.106:32400/web, sign in with your Plex account, and follow the claim prompts.")
return return
plex = server_resource.connect() plex = server_resource.connect()
print(f"Connected to server: {plex.friendlyName}, ID: {plex.machineIdentifier}")
# Debug: list library sections # Debug: list library sections
print(f"Library sections: {[s.title for s in plex.library.sections()]}")
# Debug: list existing playlists (handle unicode safely) # Debug: list existing playlists (handle unicode safely)
playlists = plex.playlists() playlists = plex.playlists()
playlist_names = [p.title.encode('ascii', 'ignore').decode('ascii') if not p.title.isascii() else p.title for p in playlists] playlist_names = [p.title.encode('ascii', 'ignore').decode('ascii') if not p.title.isascii() else p.title for p in playlists]
print(f"Existing playlists: {len(playlists)} total")
# Find matching songs in local library (optional, since Plex search might suffice) # Find matching songs in local library (optional, since Plex search might suffice)
# But to filter only those in local library, perhaps skip or adjust # But to filter only those in local library, perhaps skip or adjust
@@ -408,7 +374,6 @@ def main():
# Create Amazon buy list for missing songs # Create Amazon buy list for missing songs
if missing: if missing:
print(f"\n{len(missing)} songs not found in your library. Updating Amazon buy list...")
# Read existing buy list # Read existing buy list
existing_songs = set() existing_songs = set()
@@ -428,7 +393,7 @@ def main():
existing_songs.add((artist.strip(), title.strip())) existing_songs.add((artist.strip(), title.strip()))
i += 1 i += 1
except Exception as e: except Exception as e:
print(f"Warning: Could not read existing buy list: {e}") pass
# Filter new missing songs # Filter new missing songs
new_missing = [] new_missing = []
@@ -449,8 +414,6 @@ def main():
query = urllib.parse.quote(f"{artist} {title}") query = urllib.parse.quote(f"{artist} {title}")
url = f"https://www.amazon.com/s?k={query}&i=digital-music" url = f"https://www.amazon.com/s?k={query}&i=digital-music"
f.write(f"{artist} - {title}\n{url}\n\n") f.write(f"{artist} - {title}\n{url}\n\n")
print(f" - {artist} - {title}: {url}")
print(f"Added {len(new_missing)} new songs to buy list.")
else: else:
print("No new songs to add to buy list.") print("No new songs to add to buy list.")
@@ -459,8 +422,10 @@ def main():
# Log completion # Log completion
end_time = datetime.now() end_time = datetime.now()
duration = (end_time - start_time).total_seconds() duration = (end_time - start_time).total_seconds()
print(f"\nCompleted in {duration:.1f} seconds at {end_time.strftime('%I:%M:%S %p')}")
print(f"{'='*60}\n") def update_playlist():
"""Function to update playlist, callable from GUI"""
main()
if __name__ == '__main__': if __name__ == '__main__':
main() main()
Binary file not shown.