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
+188 -71

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
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
+43 -20
View File
@@ -15,7 +15,7 @@ from datetime import datetime
from pathlib import Path
# 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.QtGui import QIcon, QDesktopServices, QFont, QAction
import json
@@ -23,12 +23,9 @@ import csv
from urllib.parse import quote
import sqlite3
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
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:
"""Configuration management"""
@@ -287,30 +284,37 @@ class MainWindow(QMainWindow):
self.last_update_label = QLabel("Last update: Never")
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.clicked.connect(self.show_buy_list)
status_layout.addWidget(self.buy_list_button)
# History button
button_layout.addWidget(self.buy_list_button, 0, 1)
# Row 2
self.history_button = QPushButton("View History")
self.history_button.clicked.connect(self.show_history)
status_layout.addWidget(self.history_button)
# Export button
button_layout.addWidget(self.history_button, 1, 0)
self.export_button = QPushButton("Export Playlist")
self.export_button.clicked.connect(self.export_playlist)
status_layout.addWidget(self.export_button)
# Statistics button
button_layout.addWidget(self.export_button, 1, 1)
# Row 3
self.stats_button = QPushButton("Statistics")
self.stats_button.clicked.connect(self.show_statistics)
status_layout.addWidget(self.stats_button)
# Analytics button
button_layout.addWidget(self.stats_button, 2, 0)
self.analytics_button = QPushButton("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)
splitter.addWidget(status_group)
@@ -333,6 +337,13 @@ class MainWindow(QMainWindow):
if not self.config.get('PLEX_TOKEN'):
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):
"""Set the tray icon reference"""
self.tray_icon = tray_icon
@@ -639,6 +650,9 @@ class MainWindow(QMainWindow):
import json
from datetime import datetime
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.setWindowTitle("Playlist Analytics")
@@ -956,12 +970,16 @@ class SystemTrayApp:
def main():
"""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...")
try:
if not os.environ.get('DISPLAY'):
raise Exception("DISPLAY environment variable not set - no graphical display available")
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
QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
@@ -970,11 +988,16 @@ def main():
# Create and run application
tray_app = SystemTrayApp()
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())
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("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)
if __name__ == "__main__":
+16 -51
View File
@@ -86,16 +86,19 @@ def scrape_recently_played():
songs = []
# 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:
if source == 'Journey FM':
# Journey FM uses <strong> for title
spans = element.find_all('span')
if len(spans) >= 2:
title = spans[0].get_text().strip()
artist = spans[1].get_text().strip()
else:
# Check for strong
strong = element.find('strong')
if strong:
title = strong.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:
time_part = match.group(1)
before_time = full_text.split(time_part)[0].strip()
@@ -104,9 +107,9 @@ def scrape_recently_played():
artist = full_text.replace(title, '').strip()
artist = artist.strip()
else:
# Fallback
# Fallback to text parsing
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()
@@ -126,29 +129,6 @@ def scrape_recently_played():
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:
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:])
if title and artist:
songs.append({'artist': artist, 'title': title, 'source': source}) # Add to all_songs, avoiding duplicates
for song in songs:
@@ -252,30 +232,24 @@ def create_playlist_in_plex(plex, songs, playlist_name):
if 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:
print(f" + '{track.title}' by {track.artist().title} ({song['source']})")
# Auto-tag added songs
track.addLabel("Journey FM")
track.rate(5)
added = len(new_tracks)
added_songs.extend([f"{track.title} by {track.artist().title} ({song['source']})" for track, song in new_tracks])
else:
print(f"No new songs to add - all {len(tracks)} matching songs already in playlist.")
added = 0
except:
# Playlist doesn't exist, create it
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:
print(f" + '{track.title}' by {track.artist().title} ({song['source']})")
# Auto-tag added songs
track.addLabel("Journey FM")
track.rate(5)
added = len(tracks)
added_songs.extend([f"{track.title} by {track.artist().title} ({song['source']})" for track, song in tracks])
except Exception as e:
print(f"Error with playlist: {e}")
added = 0
added_songs = []
else:
@@ -334,7 +308,7 @@ def setup_scheduler(params):
subprocess.run(cmd, check=True)
print("Scheduler setup completed.")
except subprocess.CalledProcessError as e:
print(f"Error setting up scheduler: {e}")
print(f"Scheduler setup failed: {e}")
def main():
global PLEX_TOKEN, SERVER_IP, PLAYLIST_NAME
@@ -351,16 +325,12 @@ def main():
# Log start time
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
init_history_db()
# Scrape songs
songs = scrape_recently_played()
print(f"Found {len(songs)} recently played songs.")
# Connect to Plex via MyPlexAccount
account = MyPlexAccount(token=PLEX_TOKEN)
@@ -377,20 +347,16 @@ def main():
break
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.")
return
plex = server_resource.connect()
print(f"Connected to server: {plex.friendlyName}, ID: {plex.machineIdentifier}")
# Debug: list library sections
print(f"Library sections: {[s.title for s in plex.library.sections()]}")
# Debug: list existing playlists (handle unicode safely)
playlists = plex.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)
# 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
if missing:
print(f"\n{len(missing)} songs not found in your library. Updating Amazon buy list...")
# Read existing buy list
existing_songs = set()
@@ -428,7 +393,7 @@ def main():
existing_songs.add((artist.strip(), title.strip()))
i += 1
except Exception as e:
print(f"Warning: Could not read existing buy list: {e}")
pass
# Filter new missing songs
new_missing = []
@@ -449,8 +414,6 @@ def main():
query = urllib.parse.quote(f"{artist} {title}")
url = f"https://www.amazon.com/s?k={query}&i=digital-music"
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:
print("No new songs to add to buy list.")
@@ -459,8 +422,10 @@ def main():
# Log completion
end_time = datetime.now()
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__':
main()
Binary file not shown.