From bf7aa42460b3eb631e3656ce42b88e0931fdaabc Mon Sep 17 00:00:00 2001 From: nmemmert Date: Mon, 17 Nov 2025 12:56:21 -0500 Subject: [PATCH] Improve song parsing for Journey FM and Spirit FM, fix app startup issues --- amazon_buy_list.txt | 123 ++++++++++++++++++++++++++++++++++++++++++++ app_log.txt | 6 +++ journey_fm_app.py | 63 ++++++++++++++++------- main.py | 67 ++++++------------------ playlist_history.db | Bin 8192 -> 16384 bytes 5 files changed, 188 insertions(+), 71 deletions(-) create mode 100644 app_log.txt diff --git a/amazon_buy_list.txt b/amazon_buy_list.txt index 81b2a4b..85be51c 100644 --- a/amazon_buy_list.txt +++ b/amazon_buy_list.txt @@ -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 + diff --git a/app_log.txt b/app_log.txt new file mode 100644 index 0000000..37b2d83 --- /dev/null +++ b/app_log.txt @@ -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 diff --git a/journey_fm_app.py b/journey_fm_app.py index a9ecd55..e84de5b 100644 --- a/journey_fm_app.py +++ b/journey_fm_app.py @@ -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__": diff --git a/main.py b/main.py index bd899fb..e3527a5 100644 --- a/main.py +++ b/main.py @@ -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 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() diff --git a/playlist_history.db b/playlist_history.db index d45d0574c6d5f3a298a1c25dc9b7a724c3c561f5..7ec095e6c73c4c583f2cb965cbbd0080567cb62f 100644 GIT binary patch literal 16384 zcmeI2TTkOg6vrJ1_XXOAst@~eluBh$S=g?muy|OE4Jv264{x_jAwr5WafWnQh!==LmPIvf51Xeg%5;W zPWTc)5QG_g=ka}!a(I*ed{H*&IwOcbKAXnU+-yO>7bPM<1c(3;AOb{y2oM1xKm>>Y z5g-EZDS;Qw+}OwW@8?dZL$=+q9XANM-#fnyRW_>SMim<6#ab24GvUD`oF}&pt6w#$ z%he5dvaz~e-q?b*>ej;R@;1ZZyHWkNahb*1ZM*&54)1#5Mcs5^z`ea7RqenH0@vFc zRPmztvTDB%^CL(-H~VMAJ|AD+Q!`MvhyW2F0z`la5CI}U1c(3;AOb{y2oQlA5*VI{ z&gLhCr^4*o^dI+roBC*cYgEmDF|>?XH%q6%#JEr%|1kISydo+3Ls@<(7aOu-X{IHc zqFFK&O>VYEn?DtpAL3?8!Gc2h0HER6y}|+%LO1N#QD%LI`2nm#lo{}@zthisjCY>h zgQfMt@d8|Jq~hRr9Dq7*gFu6IecJ=rv^$+3(QM5Q`wz?9?iTODBj$NMfMvd)>f~ya z8D?&)?6rOS2w=lKJWTXz(YM)tn;&^uM~l6hdu{_f;vViGvA|&fHP#DxzlGNjYn<;R z1qc7!k+tQD&yU)+pXgpabUU5F4YW>!@c~vhEKL(lN!Db8tbXULJ~qJWx}|8MDk&w^ zB&**stH1nY6sM@1nYT;xjj}VbS)!X9qX~z$U5(m!Zg8iD!VMB5R;J znG61%DC_$SEZHm+0UyNtNK<*XgEyO~iXyjyHSANHA0&iK#c?}rfUkGoActBfVLo2h z&OS@}u2i&SLo|xAVHBIKe8Lz1Ng@I&w!sQ2iE{;KOn@bhql(mLg?CWbq1JPMNXfhY zIAa#sg>rVu7(G2wnOXZ?;X6(wyFUf&vzP$C>2SU+m`A!iVx9^UGTA9r=Z5x3wQ>XQsTQ*EJx2E(y3T!NON~P?%`i%(v@DB5 zh(XgyKY%!QhG-aRsDopM)Ck_5!P~cxiDL2xxz57Si2}Dx8za`MQEIu<0a*14$E_Di zF2;T`ch!9nyJ~4A(a?*Mg6=z(XsB5aJ;r4kl@*$($ zuF=kFQ?b#0ZC=&;XffH+WKlOuimo