Author SHA1 Message Date
nmemmert 6ea9c6647a Merge pull request #1 from nmemmert/copilot/add-playlist-name-customization 2025-11-28 11:35:12 -05:00
copilot-swe-agent[bot]andnmemmert f0d9b06ef4 Add validation for station selection to require at least one station
Co-authored-by: nmemmert <67961292+nmemmert@users.noreply.github.com>
2025-11-28 16:31:36 +00:00
copilot-swe-agent[bot]andnmemmert 013edb952f Add station selection feature to SetupWizard and scrape function
Co-authored-by: nmemmert <67961292+nmemmert@users.noreply.github.com>
2025-11-28 16:29:29 +00:00
copilot-swe-agent[bot] d5da0f710e Initial plan 2025-11-28 16:25:25 +00:00
nmemmert 2d027d339a Use custom icon.png and fix buy list path to use absolute path 2025-11-17 14:04:24 -05:00
nmemmert dd985796e6 Fix Spirit FM parsing - bypass Cloudflare and use direct text file 2025-11-17 13:47:41 -05:00
nmemmert e762c4252d Fix song scraping - use proper HTML structure parsing 2025-11-17 13:41:25 -05:00
nmemmert b50b430a5f Switch back to Chrome webdriver, user should run with xvfb-run for headless support 2025-11-17 13:12:14 -05:00
nmemmert 8b68b382ff Switch to Firefox webdriver to avoid Chrome DevTools issues 2025-11-17 13:09:16 -05:00
nmemmert 8393e5d659 Fix Selenium Chrome session issues by simplifying options and adding error handling 2025-11-17 13:01:35 -05:00
nmemmert bf7aa42460 Improve song parsing for Journey FM and Spirit FM, fix app startup issues 2025-11-17 12:56:21 -05:00
nmemmert 59689dcbf6 windows 2025-11-17 12:18:25 -05:00
nmemmert 38131fb653 Fix buy list label to update on filter 2025-11-17 12:11:24 -05:00
nmemmert 5066b0b5e9 Fix syntax error: add missing newline after return statement 2025-11-17 12:05:47 -05:00
nmemmert cdb6e53fb6 Add debug prints to track GUI startup progress 2025-11-17 12:05:02 -05:00
nmemmert f89b970c94 Add display environment check and better error handling for GUI startup 2025-11-17 12:03:53 -05:00
nmemmert 62174895ff Add source tracking for scraped songs and display station in logs and history 2025-11-17 12:02:54 -05:00
nmemmert 723e12fe11 Modify buy list generation to append new missing songs instead of overwriting 2025-11-17 12:01:37 -05:00
nmemmert 2dcd4be86d Add missing QSplitter import to QtWidgets 2025-11-17 11:59:19 -05:00
nmemmert fbe0c4a8e1 Fix QAction import - move from QtWidgets to QtGui 2025-11-17 11:58:35 -05:00
nmemmert 30c2e1c907 Fix matplotlib backend import for PySide6 (Qt6) compatibility 2025-11-17 11:57:40 -05:00
nmemmert 32d87cf0c2 Add scraping from Spirit FM recently played list, combine with Journey FM and deduplicate songs 2025-11-17 11:56:26 -05:00
nmemmert 7e7754f533 Add Playlist Analytics Dashboard with charts for growth, top artists, and update frequency using matplotlib 2025-11-17 11:53:32 -05:00
nmemmert 1339365941 Add auto-playlist tagging: automatically add 'Journey FM' label and 5-star rating to songs added to Plex playlist 2025-11-17 11:49:27 -05:00
nmemmert 43e93c5abc Split artist and song into separate columns in history table 2025-11-17 11:46:15 -05:00
nmemmert eb61051c7c Redesign history table to show one song per row for better readability 2025-11-17 11:44:52 -05:00
nmemmert 3d18efb88f Improve history dialog to show full list of added and missing songs with word wrap and auto row sizing 2025-11-17 11:43:29 -05:00
nmemmert f958ea0983 Fix QListWidgetItem checkState usage to use Qt.CheckState enum instead of int 2025-11-17 11:40:44 -05:00
nmemmert 2da02dc7df Add debug message and better error handling for buy list dialog 2025-11-17 11:39:52 -05:00
nmemmert 3f377e165d Fix buy list dialog visibility by ensuring it shows on top and add song count label 2025-11-17 11:37:26 -05:00
8 changed files with 727 additions and 205 deletions

No files matched your search

+215 -2
View File
@@ -1,8 +1,221 @@
Songs not in your library - Amazon search links: Songs not in your library - Amazon search links:
Danny Gokey - I Got You
https://www.amazon.com/s?k=Danny%20Gokey%20I%20Got%20You&i=digital-music
Have For You Colton Dixon - The Love I
https://www.amazon.com/s?k=Have%20%20For%20%20You%20Colton%20%20Dixon%20The%20%20Love%20%20I%20&i=digital-music
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
Cochren & Co. - Who Can Cochren & Co. - Who Can
https://www.amazon.com/s?k=Cochren%20%26%20%20Co.%20Who%20%20Can&i=digital-music https://www.amazon.com/s?k=Cochren%20%26%20%20Co.%20Who%20%20Can&i=digital-music
Matthew West - All In Have For You Colton Dixon - The Love I
https://www.amazon.com/s?k=Matthew%20%20West%20All%20%20In&i=digital-music https://www.amazon.com/s?k=Have%20%20For%20%20You%20Colton%20%20Dixon%20The%20%20Love%20%20I%20&i=digital-music
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
Zach Williams - Jesus Loves
https://www.amazon.com/s?k=Zach%20Williams%20Jesus%20Loves&i=digital-music
Zach Williams - Jesus Loves
https://www.amazon.com/s?k=Zach%20%20Williams%20Jesus%20%20Loves&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
Cochren & Co. - Church (Take Me Back)
https://www.amazon.com/s?k=Cochren%20%26%20Co.%20Church%20%28Take%20Me%20Back%29&i=digital-music
Blessing Offor - Your Love
https://www.amazon.com/s?k=Blessing%20Offor%20Your%20Love&i=digital-music
Anne Wilson - My Jesus
https://www.amazon.com/s?k=Anne%20Wilson%20My%20Jesus&i=digital-music
Sidewalk Prophets - You Love Me Anyway
https://www.amazon.com/s?k=Sidewalk%20Prophets%20You%20Love%20Me%20Anyway&i=digital-music
Casting Crowns (ft Matthew West) - Nobody
https://www.amazon.com/s?k=Casting%20Crowns%20%28ft%20Matthew%20West%29%20Nobody&i=digital-music
Phil Wickham - This Is Amazing Grace
https://www.amazon.com/s?k=Phil%20Wickham%20This%20Is%20Amazing%20Grace&i=digital-music
Crowder - STILL
https://www.amazon.com/s?k=Crowder%20STILL&i=digital-music
Josh Baldwin - Made for More
https://www.amazon.com/s?k=Josh%20Baldwin%20Made%20for%20More&i=digital-music
Leanna Crawford - Honest
https://www.amazon.com/s?k=Leanna%20Crawford%20Honest&i=digital-music
We The Kingdom - God So Loved
https://www.amazon.com/s?k=We%20The%20Kingdom%20God%20So%20Loved&i=digital-music
Dan Bremnes - Call Your Name
https://www.amazon.com/s?k=Dan%20Bremnes%20Call%20Your%20Name&i=digital-music
Big Daddy Weave - I Know
https://www.amazon.com/s?k=Big%20Daddy%20Weave%20I%20Know&i=digital-music
The Afters - God Is With Us
https://www.amazon.com/s?k=The%20Afters%20God%20Is%20With%20Us&i=digital-music
CAIN - Rise Up (Lazarus)
https://www.amazon.com/s?k=CAIN%20Rise%20Up%20%28Lazarus%29&i=digital-music
Katy Nichole - Have Your Way
https://www.amazon.com/s?k=Katy%20Nichole%20Have%20Your%20Way&i=digital-music
Ryan Stevenson - Preach
https://www.amazon.com/s?k=Ryan%20Stevenson%20Preach&i=digital-music
Elevation Worship - Praise
https://www.amazon.com/s?k=Elevation%20Worship%20Praise&i=digital-music
Andrew Ripp - Breakdown
https://www.amazon.com/s?k=Andrew%20Ripp%20Breakdown&i=digital-music
Chris Tomlin - Amazing Grace (My Chains Are Gon
https://www.amazon.com/s?k=Chris%20Tomlin%20Amazing%20Grace%20%28My%20Chains%20Are%20Gon&i=digital-music
Tauren Wells - Known
https://www.amazon.com/s?k=Tauren%20Wells%20Known&i=digital-music
Big Daddy Weave - All Things New (Single Mix)
https://www.amazon.com/s?k=Big%20Daddy%20Weave%20All%20Things%20New%20%28Single%20Mix%29&i=digital-music
+27
View File
@@ -0,0 +1,27 @@
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
2025-11-17 12:56:44.139926: Starting Journey FM Playlist app...
2025-11-17 12:56:44.140042: DISPLAY set, initializing GUI
2025-11-17 12:56:44.381412: App initialized, starting event loop
2025-11-17 13:01:57.291192: Starting Journey FM Playlist app...
2025-11-17 13:01:57.291370: DISPLAY set, initializing GUI
2025-11-17 13:01:57.440291: App initialized, starting event loop
2025-11-17 13:09:29.113472: Starting Journey FM Playlist app...
2025-11-17 13:09:29.113563: DISPLAY set, initializing GUI
2025-11-17 13:09:29.245236: App initialized, starting event loop
2025-11-17 13:12:14.103261: Starting Journey FM Playlist app...
2025-11-17 13:12:14.103373: DISPLAY set, initializing GUI
2025-11-17 13:12:14.295725: App initialized, starting event loop
2025-11-17 13:48:09.668000: Starting Journey FM Playlist app...
2025-11-17 13:48:09.668117: DISPLAY set, initializing GUI
2025-11-17 13:48:10.016465: App initialized, starting event loop
2025-11-17 13:55:31.548889: Starting Journey FM Playlist app...
2025-11-17 13:55:31.549017: DISPLAY set, initializing GUI
2025-11-17 13:55:31.715072: App initialized, starting event loop
2025-11-17 14:02:10.571449: Starting Journey FM Playlist app...
2025-11-17 14:02:10.571573: DISPLAY set, initializing GUI
2025-11-17 14:02:10.700170: App initialized, starting event loop
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 639 B

After

Width:  |  Height:  |  Size: 1.2 MiB

-4
View File
@@ -32,10 +32,6 @@ if ! command -v chromium-browser > /dev/null 2>&1; then
sudo apt update && sudo apt install -y chromium-browser sudo apt update && sudo apt install -y chromium-browser
fi fi
# Create application icon
echo "Creating application icon..."
python3 "$DIR/create_icon.py"
# Create desktop entry # Create desktop entry
echo "Creating desktop entry..." echo "Creating desktop entry..."
mkdir -p ~/.local/share/applications mkdir -p ~/.local/share/applications
+311 -98
View File
@@ -15,22 +15,17 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
# GUI imports # GUI imports
from PySide6.QtWidgets import ( 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
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, from PySide6.QtCore import QTimer, Qt, QThread, Signal, QSettings, QUrl
QLabel, QPushButton, QTextEdit, QLineEdit, QFormLayout, from PySide6.QtGui import QIcon, QDesktopServices, QFont, QAction
QDialog, QDialogButtonBox, QSystemTrayIcon, QMenu, import json
QGroupBox, QCheckBox, QSpinBox, QComboBox, QMessageBox, import csv
QProgressBar, QSplitter, QFrame, QScrollArea, QTextBrowser, QTableWidget, QTableWidgetItem, QHeaderView, QListWidget, QListWidgetItem from urllib.parse import quote
) import sqlite3
from PySide6.QtCore import ( from datetime import datetime
Qt, QTimer, QThread, Signal, QSettings, QSize
)
from PySide6.QtGui import (
QIcon, QAction, QFont, QPixmap, QPainter, QColor, QDesktopServices
)
# 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"""
@@ -64,6 +59,9 @@ class Config:
def save_config(self, config): def save_config(self, config):
"""Save config to QSettings""" """Save config to QSettings"""
for key, value in config.items(): for key, value in config.items():
# Handle list values (convert to comma-separated string)
if isinstance(value, list):
value = ','.join(value)
self.set(key, value) self.set(key, value)
class SetupWizard(QDialog): class SetupWizard(QDialog):
@@ -114,6 +112,22 @@ class SetupWizard(QDialog):
form_group.setLayout(form_layout) form_group.setLayout(form_layout)
layout.addWidget(form_group) layout.addWidget(form_group)
# Station Selection
station_group = QGroupBox("Station Selection")
station_layout = QVBoxLayout()
station_layout.addWidget(QLabel("Select which stations to monitor:"))
self.journey_fm_checkbox = QCheckBox("Journey FM (myjourneyfm.com)")
self.journey_fm_checkbox.setChecked(True)
station_layout.addWidget(self.journey_fm_checkbox)
self.spirit_fm_checkbox = QCheckBox("Spirit FM (spiritfm.com)")
self.spirit_fm_checkbox.setChecked(True)
station_layout.addWidget(self.spirit_fm_checkbox)
station_group.setLayout(station_layout)
layout.addWidget(station_group)
# Scheduling options # Scheduling options
sched_group = QGroupBox("Scheduling") sched_group = QGroupBox("Scheduling")
sched_layout = QVBoxLayout() sched_layout = QVBoxLayout()
@@ -141,7 +155,7 @@ class SetupWizard(QDialog):
buttons = QDialogButtonBox( buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
) )
buttons.accepted.connect(self.accept) buttons.accepted.connect(self.validate_and_accept)
buttons.rejected.connect(self.reject) buttons.rejected.connect(self.reject)
layout.addWidget(buttons) layout.addWidget(buttons)
@@ -152,16 +166,40 @@ class SetupWizard(QDialog):
self.server_input.setText(config.get('SERVER_IP', '')) self.server_input.setText(config.get('SERVER_IP', ''))
self.playlist_input.setText(config.get('PLAYLIST_NAME', 'Journey FM Recently Played')) self.playlist_input.setText(config.get('PLAYLIST_NAME', 'Journey FM Recently Played'))
# Load station selections
selected_stations = config.get('SELECTED_STATIONS', 'journey_fm,spirit_fm')
if isinstance(selected_stations, str):
selected_stations = selected_stations.split(',')
else:
selected_stations = ['journey_fm', 'spirit_fm']
self.journey_fm_checkbox.setChecked('journey_fm' in selected_stations)
self.spirit_fm_checkbox.setChecked('spirit_fm' in selected_stations)
def get_config(self): def get_config(self):
selected_stations = []
if self.journey_fm_checkbox.isChecked():
selected_stations.append('journey_fm')
if self.spirit_fm_checkbox.isChecked():
selected_stations.append('spirit_fm')
return { return {
'PLEX_TOKEN': self.token_input.text().strip(), 'PLEX_TOKEN': self.token_input.text().strip(),
'SERVER_IP': self.server_input.text().strip(), 'SERVER_IP': self.server_input.text().strip(),
'PLAYLIST_NAME': self.playlist_input.text().strip(), 'PLAYLIST_NAME': self.playlist_input.text().strip(),
'AUTO_UPDATE': self.auto_update.isChecked(), 'AUTO_UPDATE': self.auto_update.isChecked(),
'UPDATE_INTERVAL': self.interval_spin.value(), 'UPDATE_INTERVAL': self.interval_spin.value(),
'UPDATE_UNIT': self.interval_unit.currentText() 'UPDATE_UNIT': self.interval_unit.currentText(),
'SELECTED_STATIONS': ','.join(selected_stations)
} }
def validate_and_accept(self):
"""Validate configuration before accepting"""
if not self.journey_fm_checkbox.isChecked() and not self.spirit_fm_checkbox.isChecked():
QMessageBox.warning(self, "Validation Error",
"Please select at least one station to monitor.")
return
self.accept()
class LogViewer(QWidget): class LogViewer(QWidget):
"""Widget for viewing application logs""" """Widget for viewing application logs"""
def __init__(self, parent=None): def __init__(self, parent=None):
@@ -251,7 +289,9 @@ class MainWindow(QMainWindow):
self.config.load_config() self.config.load_config()
self.setWindowTitle("Journey FM Playlist Creator") self.setWindowTitle("Journey FM Playlist Creator")
self.setWindowIcon(QIcon("icon.png")) # You'll need to add an icon icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icon.png')
if os.path.exists(icon_path):
self.setWindowIcon(QIcon(icon_path))
self.resize(800, 600) self.resize(800, 600)
# Create central widget # Create central widget
@@ -289,25 +329,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)
self.analytics_button = QPushButton("Analytics")
self.analytics_button.clicked.connect(self.show_analytics)
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)
@@ -330,6 +382,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
@@ -455,77 +514,90 @@ class MainWindow(QMainWindow):
def show_buy_list(self): def show_buy_list(self):
"""Show the Amazon buy list dialog with interactive features""" """Show the Amazon buy list dialog with interactive features"""
try: try:
with open('amazon_buy_list.txt', 'r') as f: buy_list_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'amazon_buy_list.txt')
with open(buy_list_path, 'r') as f:
content = f.read() content = f.read()
except FileNotFoundError: except FileNotFoundError:
QMessageBox.information(self, "No Buy List", "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 return
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to read buy list file: {e}")
return
# Parse content to get songs try:
lines = content.split('\n') # Parse content to get songs
songs = [] lines = content.split('\n')
i = 0 songs = []
while i < len(lines): i = 0
if lines[i].startswith('Songs not in your library'): while i < len(lines):
i += 2 # Skip header if lines[i].startswith('Songs not in your library'):
continue i += 2 # Skip header
if lines[i].strip() and not lines[i].startswith('http'): continue
artist_title = lines[i].strip() if lines[i].strip() and not lines[i].startswith('http'):
if i + 1 < len(lines) and lines[i + 1].startswith('http'): artist_title = lines[i].strip()
url = lines[i + 1].strip() if i + 1 < len(lines) and lines[i + 1].startswith('http'):
songs.append((artist_title, url)) url = lines[i + 1].strip()
i += 3 # Skip blank line songs.append((artist_title, url))
i += 3 # Skip blank line
else:
i += 1
else: else:
i += 1 i += 1
else:
i += 1
dialog = QDialog(self) dialog = QDialog(self)
dialog.setWindowTitle("Amazon Buy List") dialog.setWindowTitle("Amazon Buy List")
dialog.setModal(True) dialog.setModal(True)
dialog.resize(600, 400) dialog.resize(600, 400)
layout = QVBoxLayout() layout = QVBoxLayout()
# Search box # Search box
search_layout = QHBoxLayout() search_layout = QHBoxLayout()
search_layout.addWidget(QLabel("Search:")) search_layout.addWidget(QLabel("Search:"))
self.search_input = QLineEdit() self.search_input = QLineEdit()
self.search_input.textChanged.connect(lambda: self.filter_buy_list(songs)) self.search_input.textChanged.connect(lambda: self.filter_buy_list(songs))
search_layout.addWidget(self.search_input) search_layout.addWidget(self.search_input)
layout.addLayout(search_layout) layout.addLayout(search_layout)
# List widget # List widget
self.buy_list_widget = QListWidget() self.buy_list_widget = QListWidget()
self.populate_buy_list(songs) self.buy_list_label = QLabel(f"Found {len(songs)} songs to buy:")
layout.addWidget(self.buy_list_widget) layout.addWidget(self.buy_list_label)
self.populate_buy_list(songs)
layout.addWidget(self.buy_list_widget)
# Buttons # Buttons
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
open_button = QPushButton("Open Selected") open_button = QPushButton("Open Selected")
open_button.clicked.connect(self.open_selected_buy_items) open_button.clicked.connect(self.open_selected_buy_items)
button_layout.addWidget(open_button) button_layout.addWidget(open_button)
remove_button = QPushButton("Remove Selected") remove_button = QPushButton("Remove Selected")
remove_button.clicked.connect(lambda: self.remove_selected_buy_items(songs, dialog)) remove_button.clicked.connect(lambda: self.remove_selected_buy_items(songs, dialog))
button_layout.addWidget(remove_button) button_layout.addWidget(remove_button)
close_button = QPushButton("Close") close_button = QPushButton("Close")
close_button.clicked.connect(dialog.close) close_button.clicked.connect(dialog.close)
button_layout.addWidget(close_button) button_layout.addWidget(close_button)
layout.addLayout(button_layout) layout.addLayout(button_layout)
dialog.setLayout(layout) dialog.setLayout(layout)
dialog.exec() dialog.show()
dialog.raise_()
dialog.activateWindow()
dialog.exec()
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to show buy list dialog: {e}")
def populate_buy_list(self, songs): def populate_buy_list(self, songs):
"""Populate the buy list widget""" """Populate the buy list widget"""
self.buy_list_widget.clear() self.buy_list_widget.clear()
self.buy_list_label.setText(f"Found {len(songs)} songs to buy:")
for artist_title, url in songs: for artist_title, url in songs:
item = QListWidgetItem(artist_title) item = QListWidgetItem(artist_title)
item.setData(1, url) # Store URL in item data item.setData(1, url) # Store URL in item data
item.setCheckState(0) # Qt.CheckState.Unchecked item.setCheckState(Qt.CheckState.Unchecked)
self.buy_list_widget.addItem(item) self.buy_list_widget.addItem(item)
def filter_buy_list(self, all_songs): def filter_buy_list(self, all_songs):
@@ -538,7 +610,7 @@ class MainWindow(QMainWindow):
"""Open selected buy list items in browser""" """Open selected buy list items in browser"""
for i in range(self.buy_list_widget.count()): for i in range(self.buy_list_widget.count()):
item = self.buy_list_widget.item(i) item = self.buy_list_widget.item(i)
if item.checkState() == 2: # Qt.CheckState.Checked if item.checkState() == Qt.CheckState.Checked:
url = item.data(1) url = item.data(1)
QDesktopServices.openUrl(url) QDesktopServices.openUrl(url)
@@ -547,7 +619,7 @@ class MainWindow(QMainWindow):
to_remove = [] to_remove = []
for i in range(self.buy_list_widget.count()): for i in range(self.buy_list_widget.count()):
item = self.buy_list_widget.item(i) item = self.buy_list_widget.item(i)
if item.checkState() == 2: # Qt.CheckState.Checked if item.checkState() == Qt.CheckState.Checked:
artist_title = item.text() artist_title = item.text()
to_remove.append(artist_title) to_remove.append(artist_title)
@@ -618,6 +690,118 @@ class MainWindow(QMainWindow):
dialog.setLayout(layout) dialog.setLayout(layout)
dialog.exec() dialog.exec()
def show_analytics(self):
"""Show analytics dashboard with charts"""
import sqlite3
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")
dialog.setModal(True)
dialog.resize(1000, 700)
layout = QVBoxLayout()
tab_widget = QTabWidget()
try:
conn = sqlite3.connect('playlist_history.db')
c = conn.cursor()
c.execute('SELECT date, added_songs, missing_songs FROM history ORDER BY date')
rows = c.fetchall()
conn.close()
# Prepare data
dates = []
cumulative_added = 0
cumulative_list = []
artist_counter = Counter()
update_freq = []
for date, added_json, missing_json in rows:
dt = datetime.fromisoformat(date)
dates.append(dt.strftime('%Y-%m-%d'))
added_songs = json.loads(added_json) if added_json else []
cumulative_added += len(added_songs)
cumulative_list.append(cumulative_added)
update_freq.append(len(added_songs))
for song in added_songs:
# Parse artist from "Title by Artist"
if " by " in song:
artist = song.split(" by ", 1)[1]
artist_counter[artist] += 1
# Tab 1: Playlist Growth
growth_tab = QWidget()
growth_layout = QVBoxLayout()
fig1 = Figure(figsize=(8, 6))
ax1 = fig1.add_subplot(111)
ax1.plot(dates, cumulative_list, marker='o')
ax1.set_title('Playlist Growth Over Time')
ax1.set_xlabel('Date')
ax1.set_ylabel('Total Songs')
ax1.tick_params(axis='x', rotation=45)
canvas1 = FigureCanvas(fig1)
growth_layout.addWidget(canvas1)
growth_tab.setLayout(growth_layout)
tab_widget.addTab(growth_tab, "Growth")
# Tab 2: Top Artists
artists_tab = QWidget()
artists_layout = QVBoxLayout()
fig2 = Figure(figsize=(8, 6))
ax2 = fig2.add_subplot(111)
top_artists = artist_counter.most_common(10)
if top_artists:
artists, counts = zip(*top_artists)
ax2.bar(artists, counts)
ax2.set_title('Top 10 Artists')
ax2.set_xlabel('Artist')
ax2.set_ylabel('Songs Added')
ax2.tick_params(axis='x', rotation=45)
canvas2 = FigureCanvas(fig2)
artists_layout.addWidget(canvas2)
artists_tab.setLayout(artists_layout)
tab_widget.addTab(artists_tab, "Top Artists")
# Tab 3: Update Frequency
freq_tab = QWidget()
freq_layout = QVBoxLayout()
fig3 = Figure(figsize=(8, 6))
ax3 = fig3.add_subplot(111)
ax3.bar(dates, update_freq)
ax3.set_title('Songs Added per Update')
ax3.set_xlabel('Date')
ax3.set_ylabel('Songs Added')
ax3.tick_params(axis='x', rotation=45)
canvas3 = FigureCanvas(fig3)
freq_layout.addWidget(canvas3)
freq_tab.setLayout(freq_layout)
tab_widget.addTab(freq_tab, "Update Frequency")
except Exception as e:
error_tab = QWidget()
error_layout = QVBoxLayout()
error_layout.addWidget(QLabel(f"Error loading analytics: {str(e)}"))
error_tab.setLayout(error_layout)
tab_widget.addTab(error_tab, "Error")
layout.addWidget(tab_widget)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
buttons.rejected.connect(dialog.close)
layout.addWidget(buttons)
dialog.setLayout(layout)
dialog.exec()
def show_history(self): def show_history(self):
"""Show the playlist history dialog""" """Show the playlist history dialog"""
import sqlite3 import sqlite3
@@ -633,19 +817,19 @@ class MainWindow(QMainWindow):
table = QTableWidget() table = QTableWidget()
table.setColumnCount(4) table.setColumnCount(4)
table.setHorizontalHeaderLabels(["Date", "Added Songs", "Missing Songs", "Details"]) table.setHorizontalHeaderLabels(["Date", "Type", "Artist", "Song"])
table.horizontalHeader().setStretchLastSection(True) table.horizontalHeader().setStretchLastSection(True)
try: try:
conn = sqlite3.connect('playlist_history.db') conn = sqlite3.connect('playlist_history.db')
c = conn.cursor() c = conn.cursor()
c.execute('SELECT date, added_count, added_songs, missing_count, missing_songs FROM history ORDER BY date DESC') c.execute('SELECT date, added_songs, missing_songs FROM history ORDER BY date DESC')
rows = c.fetchall() rows = c.fetchall()
conn.close() conn.close()
table.setRowCount(len(rows)) # Collect all entries
for row_idx, (date, added_count, added_songs_json, missing_count, missing_songs_json) in enumerate(rows): entries = []
# Parse date for date, added_songs_json, missing_songs_json in rows:
try: try:
dt = datetime.fromisoformat(date) dt = datetime.fromisoformat(date)
date_str = dt.strftime('%Y-%m-%d %H:%M') date_str = dt.strftime('%Y-%m-%d %H:%M')
@@ -655,25 +839,33 @@ class MainWindow(QMainWindow):
# Added songs # Added songs
try: try:
added_songs = json.loads(added_songs_json) added_songs = json.loads(added_songs_json)
added_text = f"{added_count} songs\n" + "\n".join(added_songs[:3]) # Show first 3 for song in added_songs:
if len(added_songs) > 3: # Format is "Title by Artist"
added_text += f"\n... and {len(added_songs)-3} more" if " by " in song:
title, artist = song.split(" by ", 1)
else:
title = song
artist = "Unknown"
entries.append((date_str, "Added", artist, title))
except: except:
added_text = f"{added_count} songs" pass
# Missing songs # Missing songs
try: try:
missing_songs = json.loads(missing_songs_json) 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]]) for song in missing_songs:
if len(missing_songs) > 3: artist = song.get('artist', 'Unknown')
missing_text += f"\n... and {len(missing_songs)-3} more" title = song.get('title', 'Unknown')
entries.append((date_str, "Missing", artist, title))
except: except:
missing_text = f"{missing_count} songs" pass
table.setRowCount(len(entries))
for row_idx, (date_str, type_str, artist, song) in enumerate(entries):
table.setItem(row_idx, 0, QTableWidgetItem(date_str)) table.setItem(row_idx, 0, QTableWidgetItem(date_str))
table.setItem(row_idx, 1, QTableWidgetItem(added_text)) table.setItem(row_idx, 1, QTableWidgetItem(type_str))
table.setItem(row_idx, 2, QTableWidgetItem(missing_text)) table.setItem(row_idx, 2, QTableWidgetItem(artist))
table.setItem(row_idx, 3, QTableWidgetItem(f"Added: {added_count}, Missing: {missing_count}")) table.setItem(row_idx, 3, QTableWidgetItem(song))
except Exception as e: except Exception as e:
table.setRowCount(1) table.setRowCount(1)
@@ -824,14 +1016,35 @@ class SystemTrayApp:
def main(): def main():
"""Main application entry point""" """Main application entry point"""
# Set up high DPI scaling with open('app_log.txt', 'a') as log:
QApplication.setHighDpiScaleFactorRoundingPolicy( log.write(f"{datetime.now()}: Starting Journey FM Playlist app...\n")
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough print("Starting Journey FM Playlist app...")
) try:
if not os.environ.get('DISPLAY'):
raise Exception("DISPLAY environment variable not set - no graphical display available")
# Create and run application print("Initializing GUI...")
tray_app = SystemTrayApp() with open('app_log.txt', 'a') as log:
sys.exit(tray_app.run()) log.write(f"{datetime.now()}: DISPLAY set, initializing GUI\n")
# Set up high DPI scaling
QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
)
# 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:
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__": if __name__ == "__main__":
main() main()
+173 -101
View File
@@ -13,6 +13,8 @@ from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import os import os
from plexapi.myplex import MyPlexAccount from plexapi.myplex import MyPlexAccount
@@ -30,78 +32,116 @@ PLEX_TOKEN = 'pU-m3HWYUZU6iXJFhJyA' # Your Plex.tv token
SERVER_IP = '172.16.16.106' # Your local server IP SERVER_IP = '172.16.16.106' # Your local server IP
PLAYLIST_NAME = 'Journey FM Recently Played' PLAYLIST_NAME = 'Journey FM Recently Played'
def scrape_recently_played(): def scrape_recently_played(selected_stations=None):
"""Scrape recently played songs from myjourneyfm.com using Selenium""" """Scrape recently played songs from Journey FM and Spirit FM"""
if selected_stations is None:
selected_stations = ['journey_fm', 'spirit_fm']
options = Options() options = Options()
options.add_argument("--headless") options.add_argument("--headless=new")
options.add_argument("--no-sandbox") options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage") options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu") options.add_argument("--disable-gpu")
options.add_argument("--remote-debugging-port=9222") options.add_argument("--window-size=1920,1080")
options.add_argument("--user-data-dir=/tmp/chromium") options.add_argument("--disable-blink-features=AutomationControlled")
options.add_argument("--no-first-run") options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
options.add_argument("--disable-extensions") options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.binary_location = "/usr/bin/chromium-browser" options.add_experimental_option('useAutomationExtension', False)
service = Service() options.binary_location = "/usr/bin/google-chrome-stable"
driver = webdriver.Chrome(service=service, options=options)
url = 'https://www.myjourneyfm.com/recently-played/' service = Service(ChromeDriverManager().install())
driver.get(url)
# Wait for initial load
time.sleep(5)
# Click "View More" button if exists
try: try:
more_button = driver.find_element(By.ID, "moreSongs") driver = webdriver.Chrome(service=service, options=options)
more_button.click() except Exception as e:
time.sleep(5) # Wait for more songs to load print(f"Failed to start Chrome: {e}")
except: return []
pass # Button not found or already loaded
all_songs = []
seen = set()
# Journey FM - uses <strong> for title, normal text for artist
if 'journey_fm' in selected_stations:
try:
print("Scraping Journey FM...")
driver.get('https://www.myjourneyfm.com/recently-played/')
time.sleep(5)
# Click "View More" button if exists
try:
more_button = driver.find_element(By.ID, "moreSongs")
more_button.click()
time.sleep(5)
except:
pass
soup = BeautifulSoup(driver.page_source, 'html.parser')
# Find all song items using the proper class structure
song_items = soup.find_all('div', class_='rp-item')
for item in song_items:
try:
title_elem = item.find('h5', class_='song-title')
artist_elem = item.find('p', class_='song-artist')
if title_elem and artist_elem:
title = title_elem.get_text().strip()
artist = artist_elem.get_text().strip()
if title and artist:
key = (title.lower(), artist.lower())
if key not in seen:
all_songs.append({'title': title, 'artist': artist, 'source': 'Journey FM'})
seen.add(key)
print(f" Found: {title} by {artist}")
except Exception as e:
continue
except Exception as e:
print(f"Error scraping Journey FM: {e}")
# Spirit FM - loads songs from iframe text file
if 'spirit_fm' in selected_stations:
try:
print("Scraping Spirit FM...")
driver.get('https://spiritfm.com/ajax/now_playing_history.txt')
time.sleep(3)
# Parse the plain text format: "Mon 01:45PM Artist - Title"
soup = BeautifulSoup(driver.page_source, 'html.parser')
text_content = soup.get_text()
# Split into lines and parse each
for line in text_content.split('\n'):
line = line.strip()
if not line:
continue
# Match pattern: Day HH:MM(AM/PM) Artist - Title
match = re.match(r'^\w+\s+\d+:\d+[AP]M\s+(.+?)\s+-\s+(.+)$', line)
if match:
artist = match.group(1).strip()
title = match.group(2).strip()
# Clean up HTML entities
artist = artist.replace('&amp;', '&')
title = title.replace('&amp;', '&')
if title and artist:
key = (title.lower(), artist.lower())
if key not in seen:
all_songs.append({'title': title, 'artist': artist, 'source': 'Spirit FM'})
seen.add(key)
print(f" Found: {title} by {artist}")
except Exception as e:
print(f"Error scraping Spirit FM: {e}")
html = driver.page_source
driver.quit() driver.quit()
# Optional: save HTML to file for debugging (comment out if not needed) print(f"\nTotal unique songs found: {len(all_songs)}")
# with open('page.html', 'w', encoding='utf-8') as f: return all_songs
# f.write(html)
soup = BeautifulSoup(html, 'html.parser')
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()))
for element in song_elements:
spans = element.find_all('span')
if len(spans) >= 3:
title = spans[0].get_text().strip()
artist = spans[1].get_text().strip()
# Ignore time
songs.append({'artist': artist, 'title': title})
else:
# Fallback to text parsing
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()
else:
# Split on capital letters
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[:3])
artist = ' '.join(parts[3:])
if title and artist:
songs.append({'artist': artist, 'title': title})
return songs
def normalize_string(s): def normalize_string(s):
"""Normalize string for comparison""" """Normalize string for comparison"""
@@ -175,7 +215,7 @@ def create_playlist_in_plex(plex, songs, playlist_name):
search_artist_lower in track_artist or track_artist in search_artist_lower): search_artist_lower in track_artist or track_artist in search_artist_lower):
# Only add if we haven't already added this track in this run # Only add if we haven't already added this track in this run
if track.ratingKey not in seen_keys: if track.ratingKey not in seen_keys:
tracks.append(track) tracks.append((track, song))
seen_keys.add(track.ratingKey) seen_keys.add(track.ratingKey)
matched = True matched = True
break break
@@ -192,28 +232,28 @@ def create_playlist_in_plex(plex, songs, playlist_name):
# Get existing track rating keys to avoid duplicates # Get existing track rating keys to avoid duplicates
existing_keys = {item.ratingKey for item in existing.items()} existing_keys = {item.ratingKey for item in existing.items()}
# Filter out tracks already in playlist # Filter out tracks already in playlist
new_tracks = [track for track in tracks if track.ratingKey not in existing_keys] new_tracks = [track_song for track_song in tracks if track_song[0].ratingKey not in existing_keys]
if new_tracks: if new_tracks:
existing.addItems(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 in new_tracks: # Auto-tag added songs
print(f" + '{track.title}' by {track.artist().title}") track.addLabel("Journey FM")
track.rate(5)
added = len(new_tracks) added = len(new_tracks)
added_songs.extend([f"{track.title} by {track.artist().title}" for track 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, 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 in tracks: # Auto-tag added songs
print(f" + '{track.title}' by {track.artist().title}") track.addLabel("Journey FM")
track.rate(5)
added = len(tracks) added = len(tracks)
added_songs.extend([f"{track.title} by {track.artist().title}" for track 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:
@@ -272,33 +312,35 @@ 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
# Load or prompt for config # Load or prompt for config
selected_stations = ['journey_fm', 'spirit_fm'] # default
if os.path.exists('config.json'): if os.path.exists('config.json'):
with open('config.json', 'r') as f: with open('config.json', 'r') as f:
config = json.load(f) config = json.load(f)
PLEX_TOKEN = config.get('PLEX_TOKEN', PLEX_TOKEN) PLEX_TOKEN = config.get('PLEX_TOKEN', PLEX_TOKEN)
SERVER_IP = config.get('SERVER_IP', SERVER_IP) SERVER_IP = config.get('SERVER_IP', SERVER_IP)
PLAYLIST_NAME = config.get('PLAYLIST_NAME', PLAYLIST_NAME) PLAYLIST_NAME = config.get('PLAYLIST_NAME', PLAYLIST_NAME)
# Load selected stations
selected_stations_str = config.get('SELECTED_STATIONS', 'journey_fm,spirit_fm')
if isinstance(selected_stations_str, str) and selected_stations_str:
selected_stations = [s for s in selected_stations_str.split(',') if s]
# If empty or invalid, keep default
else: else:
# If no config file, use defaults (GUI should handle this) # If no config file, use defaults (GUI should handle this)
pass pass
# 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 with selected stations
songs = scrape_recently_played() songs = scrape_recently_played(selected_stations)
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)
@@ -315,20 +357,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
@@ -346,24 +384,58 @@ 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. Creating Amazon buy list...")
with open('amazon_buy_list.txt', 'w') as f: # Read existing buy list
f.write("Songs not in your library - Amazon search links:\n\n") existing_songs = set()
for song in missing: if os.path.exists('amazon_buy_list.txt'):
artist = song['artist'] try:
title = song['title'] with open('amazon_buy_list.txt', 'r') as f:
# Create Amazon search URL content = f.read()
query = urllib.parse.quote(f"{artist} {title}") lines = content.split('\n')
url = f"https://www.amazon.com/s?k={query}&i=digital-music" i = 0
f.write(f"{artist} - {title}\n{url}\n\n") while i < len(lines):
print(f" - {artist} - {title}: {url}") if lines[i].startswith('Songs not in your library'):
print("Buy list saved to amazon_buy_list.txt") i += 2 # Skip header
continue
if lines[i].strip() and not lines[i].startswith('http'):
if ' - ' in lines[i]:
artist, title = lines[i].split(' - ', 1)
existing_songs.add((artist.strip(), title.strip()))
i += 1
except Exception as e:
pass
# Filter new missing songs
new_missing = []
for song in missing:
key = (song['artist'], song['title'])
if key not in existing_songs:
new_missing.append(song)
existing_songs.add(key)
if new_missing:
with open('amazon_buy_list.txt', 'a') as f: # Append mode
if not os.path.exists('amazon_buy_list.txt') or os.path.getsize('amazon_buy_list.txt') == 0:
f.write("Songs not in your library - Amazon search links:\n\n")
for song in new_missing:
artist = song['artist']
title = song['title']
# Create Amazon search URL
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")
else:
print("No new songs to add to buy list.")
print("Buy list updated.")
# 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.
+1
View File
@@ -5,3 +5,4 @@ mutagen
selenium selenium
webdriver-manager webdriver-manager
PySide6 PySide6
matplotlib