Author SHA1 Message Date
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 731 additions and 209 deletions

No files matched your search

+215 -2
View File
@@ -1,8 +1,221 @@
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
https://www.amazon.com/s?k=Cochren%20%26%20%20Co.%20Who%20%20Can&i=digital-music
Matthew West - All In
https://www.amazon.com/s?k=Matthew%20%20West%20All%20%20In&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
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
fi
# Create application icon
echo "Creating application icon..."
python3 "$DIR/create_icon.py"
# Create desktop entry
echo "Creating desktop entry..."
mkdir -p ~/.local/share/applications
+314 -101
View File
@@ -15,22 +15,17 @@ from datetime import datetime
from pathlib import Path
# GUI imports
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QPushButton, QTextEdit, QLineEdit, QFormLayout,
QDialog, QDialogButtonBox, QSystemTrayIcon, QMenu,
QGroupBox, QCheckBox, QSpinBox, QComboBox, QMessageBox,
QProgressBar, QSplitter, QFrame, QScrollArea, QTextBrowser, QTableWidget, QTableWidgetItem, QHeaderView, QListWidget, QListWidgetItem
)
from PySide6.QtCore import (
Qt, QTimer, QThread, Signal, QSettings, QSize
)
from PySide6.QtGui import (
QIcon, QAction, QFont, QPixmap, QPainter, QColor, QDesktopServices
)
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
import csv
from urllib.parse import quote
import sqlite3
from datetime import datetime
# 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"""
@@ -64,6 +59,9 @@ class Config:
def save_config(self, config):
"""Save config to QSettings"""
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)
class SetupWizard(QDialog):
@@ -114,6 +112,22 @@ class SetupWizard(QDialog):
form_group.setLayout(form_layout)
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
sched_group = QGroupBox("Scheduling")
sched_layout = QVBoxLayout()
@@ -141,7 +155,7 @@ class SetupWizard(QDialog):
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.accepted.connect(self.validate_and_accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
@@ -152,16 +166,40 @@ class SetupWizard(QDialog):
self.server_input.setText(config.get('SERVER_IP', ''))
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):
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 {
'PLEX_TOKEN': self.token_input.text().strip(),
'SERVER_IP': self.server_input.text().strip(),
'PLAYLIST_NAME': self.playlist_input.text().strip(),
'AUTO_UPDATE': self.auto_update.isChecked(),
'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):
"""Widget for viewing application logs"""
def __init__(self, parent=None):
@@ -251,7 +289,9 @@ class MainWindow(QMainWindow):
self.config.load_config()
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)
# Create central widget
@@ -289,25 +329,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)
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)
splitter.addWidget(status_group)
@@ -330,6 +382,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
@@ -455,77 +514,90 @@ class MainWindow(QMainWindow):
def show_buy_list(self):
"""Show the Amazon buy list dialog with interactive features"""
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()
except FileNotFoundError:
QMessageBox.information(self, "No Buy List", "No buy list available. Run an update to generate the list.")
return
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to read buy list file: {e}")
return
# Parse content to get songs
lines = content.split('\n')
songs = []
i = 0
while i < len(lines):
if lines[i].startswith('Songs not in your library'):
i += 2 # Skip header
continue
if lines[i].strip() and not lines[i].startswith('http'):
artist_title = lines[i].strip()
if i + 1 < len(lines) and lines[i + 1].startswith('http'):
url = lines[i + 1].strip()
songs.append((artist_title, url))
i += 3 # Skip blank line
try:
# Parse content to get songs
lines = content.split('\n')
songs = []
i = 0
while i < len(lines):
if lines[i].startswith('Songs not in your library'):
i += 2 # Skip header
continue
if lines[i].strip() and not lines[i].startswith('http'):
artist_title = lines[i].strip()
if i + 1 < len(lines) and lines[i + 1].startswith('http'):
url = lines[i + 1].strip()
songs.append((artist_title, url))
i += 3 # Skip blank line
else:
i += 1
else:
i += 1
else:
i += 1
dialog = QDialog(self)
dialog.setWindowTitle("Amazon Buy List")
dialog.setModal(True)
dialog.resize(600, 400)
dialog = QDialog(self)
dialog.setWindowTitle("Amazon Buy List")
dialog.setModal(True)
dialog.resize(600, 400)
layout = QVBoxLayout()
layout = QVBoxLayout()
# Search box
search_layout = QHBoxLayout()
search_layout.addWidget(QLabel("Search:"))
self.search_input = QLineEdit()
self.search_input.textChanged.connect(lambda: self.filter_buy_list(songs))
search_layout.addWidget(self.search_input)
layout.addLayout(search_layout)
# Search box
search_layout = QHBoxLayout()
search_layout.addWidget(QLabel("Search:"))
self.search_input = QLineEdit()
self.search_input.textChanged.connect(lambda: self.filter_buy_list(songs))
search_layout.addWidget(self.search_input)
layout.addLayout(search_layout)
# List widget
self.buy_list_widget = QListWidget()
self.populate_buy_list(songs)
layout.addWidget(self.buy_list_widget)
# List widget
self.buy_list_widget = QListWidget()
self.buy_list_label = QLabel(f"Found {len(songs)} songs to buy:")
layout.addWidget(self.buy_list_label)
self.populate_buy_list(songs)
layout.addWidget(self.buy_list_widget)
# Buttons
button_layout = QHBoxLayout()
open_button = QPushButton("Open Selected")
open_button.clicked.connect(self.open_selected_buy_items)
button_layout.addWidget(open_button)
# Buttons
button_layout = QHBoxLayout()
open_button = QPushButton("Open Selected")
open_button.clicked.connect(self.open_selected_buy_items)
button_layout.addWidget(open_button)
remove_button = QPushButton("Remove Selected")
remove_button.clicked.connect(lambda: self.remove_selected_buy_items(songs, dialog))
button_layout.addWidget(remove_button)
remove_button = QPushButton("Remove Selected")
remove_button.clicked.connect(lambda: self.remove_selected_buy_items(songs, dialog))
button_layout.addWidget(remove_button)
close_button = QPushButton("Close")
close_button.clicked.connect(dialog.close)
button_layout.addWidget(close_button)
close_button = QPushButton("Close")
close_button.clicked.connect(dialog.close)
button_layout.addWidget(close_button)
layout.addLayout(button_layout)
layout.addLayout(button_layout)
dialog.setLayout(layout)
dialog.exec()
dialog.setLayout(layout)
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):
"""Populate the buy list widget"""
self.buy_list_widget.clear()
self.buy_list_label.setText(f"Found {len(songs)} songs to buy:")
for artist_title, url in songs:
item = QListWidgetItem(artist_title)
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)
def filter_buy_list(self, all_songs):
@@ -538,7 +610,7 @@ class MainWindow(QMainWindow):
"""Open selected buy list items in browser"""
for i in range(self.buy_list_widget.count()):
item = self.buy_list_widget.item(i)
if item.checkState() == 2: # Qt.CheckState.Checked
if item.checkState() == Qt.CheckState.Checked:
url = item.data(1)
QDesktopServices.openUrl(url)
@@ -547,7 +619,7 @@ class MainWindow(QMainWindow):
to_remove = []
for i in range(self.buy_list_widget.count()):
item = self.buy_list_widget.item(i)
if item.checkState() == 2: # Qt.CheckState.Checked
if item.checkState() == Qt.CheckState.Checked:
artist_title = item.text()
to_remove.append(artist_title)
@@ -618,6 +690,118 @@ class MainWindow(QMainWindow):
dialog.setLayout(layout)
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):
"""Show the playlist history dialog"""
import sqlite3
@@ -633,19 +817,19 @@ class MainWindow(QMainWindow):
table = QTableWidget()
table.setColumnCount(4)
table.setHorizontalHeaderLabels(["Date", "Added Songs", "Missing Songs", "Details"])
table.setHorizontalHeaderLabels(["Date", "Type", "Artist", "Song"])
table.horizontalHeader().setStretchLastSection(True)
try:
conn = sqlite3.connect('playlist_history.db')
c = conn.cursor()
c.execute('SELECT date, added_count, added_songs, missing_count, missing_songs FROM history ORDER BY date DESC')
c.execute('SELECT date, added_songs, missing_songs FROM history ORDER BY date DESC')
rows = c.fetchall()
conn.close()
table.setRowCount(len(rows))
for row_idx, (date, added_count, added_songs_json, missing_count, missing_songs_json) in enumerate(rows):
# Parse date
# Collect all entries
entries = []
for date, added_songs_json, missing_songs_json in rows:
try:
dt = datetime.fromisoformat(date)
date_str = dt.strftime('%Y-%m-%d %H:%M')
@@ -655,25 +839,33 @@ class MainWindow(QMainWindow):
# Added songs
try:
added_songs = json.loads(added_songs_json)
added_text = f"{added_count} songs\n" + "\n".join(added_songs[:3]) # Show first 3
if len(added_songs) > 3:
added_text += f"\n... and {len(added_songs)-3} more"
for song in added_songs:
# Format is "Title by Artist"
if " by " in song:
title, artist = song.split(" by ", 1)
else:
title = song
artist = "Unknown"
entries.append((date_str, "Added", artist, title))
except:
added_text = f"{added_count} songs"
pass
# Missing songs
try:
missing_songs = json.loads(missing_songs_json)
missing_text = f"{missing_count} songs\n" + "\n".join([f"{s['artist']} - {s['title']}" for s in missing_songs[:3]])
if len(missing_songs) > 3:
missing_text += f"\n... and {len(missing_songs)-3} more"
for song in missing_songs:
artist = song.get('artist', 'Unknown')
title = song.get('title', 'Unknown')
entries.append((date_str, "Missing", artist, title))
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, 1, QTableWidgetItem(added_text))
table.setItem(row_idx, 2, QTableWidgetItem(missing_text))
table.setItem(row_idx, 3, QTableWidgetItem(f"Added: {added_count}, Missing: {missing_count}"))
table.setItem(row_idx, 1, QTableWidgetItem(type_str))
table.setItem(row_idx, 2, QTableWidgetItem(artist))
table.setItem(row_idx, 3, QTableWidgetItem(song))
except Exception as e:
table.setRowCount(1)
@@ -824,14 +1016,35 @@ class SystemTrayApp:
def main():
"""Main application entry point"""
# Set up high DPI scaling
QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
)
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
)
# Create and run application
tray_app = SystemTrayApp()
sys.exit(tray_app.run())
# 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__":
main()
+173 -101
View File
@@ -13,6 +13,8 @@ from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
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
import os
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
PLAYLIST_NAME = 'Journey FM Recently Played'
def scrape_recently_played():
"""Scrape recently played songs from myjourneyfm.com using Selenium"""
def scrape_recently_played(selected_stations=None):
"""Scrape recently played songs from Journey FM and Spirit FM"""
if selected_stations is None:
selected_stations = ['journey_fm', 'spirit_fm']
options = Options()
options.add_argument("--headless")
options.add_argument("--headless=new")
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")
options.add_argument("--remote-debugging-port=9222")
options.add_argument("--user-data-dir=/tmp/chromium")
options.add_argument("--no-first-run")
options.add_argument("--disable-extensions")
options.binary_location = "/usr/bin/chromium-browser"
service = Service()
driver = webdriver.Chrome(service=service, options=options)
options.add_argument("--window-size=1920,1080")
options.add_argument("--disable-blink-features=AutomationControlled")
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_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False)
options.binary_location = "/usr/bin/google-chrome-stable"
url = 'https://www.myjourneyfm.com/recently-played/'
driver.get(url)
service = Service(ChromeDriverManager().install())
# Wait for initial load
time.sleep(5)
# Click "View More" button if exists
try:
more_button = driver.find_element(By.ID, "moreSongs")
more_button.click()
time.sleep(5) # Wait for more songs to load
except:
pass # Button not found or already loaded
driver = webdriver.Chrome(service=service, options=options)
except Exception as e:
print(f"Failed to start Chrome: {e}")
return []
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()
# Optional: save HTML to file for debugging (comment out if not needed)
# with open('page.html', 'w', encoding='utf-8') as f:
# 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
print(f"\nTotal unique songs found: {len(all_songs)}")
return all_songs
def normalize_string(s):
"""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):
# Only add if we haven't already added this track in this run
if track.ratingKey not in seen_keys:
tracks.append(track)
tracks.append((track, song))
seen_keys.add(track.ratingKey)
matched = True
break
@@ -192,28 +232,28 @@ def create_playlist_in_plex(plex, songs, playlist_name):
# Get existing track rating keys to avoid duplicates
existing_keys = {item.ratingKey for item in existing.items()}
# 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:
existing.addItems(new_tracks)
print(f"Added {len(new_tracks)} new songs to existing playlist '{playlist_name}':")
for track in new_tracks:
print(f" + '{track.title}' by {track.artist().title}")
existing.addItems([ts[0] for ts in new_tracks])
for track, song in new_tracks:
# 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}" for track in 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, tracks)
print(f"Created playlist '{playlist_name}' with {len(tracks)} songs:")
for track in tracks:
print(f" + '{track.title}' by {track.artist().title}")
plex.createPlaylist(playlist_name, [ts[0] for ts in tracks])
for track, song in tracks:
# Auto-tag added songs
track.addLabel("Journey FM")
track.rate(5)
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:
print(f"Error with playlist: {e}")
added = 0
added_songs = []
else:
@@ -272,33 +312,35 @@ 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
# Load or prompt for config
selected_stations = ['journey_fm', 'spirit_fm'] # default
if os.path.exists('config.json'):
with open('config.json', 'r') as f:
config = json.load(f)
PLEX_TOKEN = config.get('PLEX_TOKEN', PLEX_TOKEN)
SERVER_IP = config.get('SERVER_IP', SERVER_IP)
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:
# If no config file, use defaults (GUI should handle this)
pass
# 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.")
# Scrape songs with selected stations
songs = scrape_recently_played(selected_stations)
# Connect to Plex via MyPlexAccount
account = MyPlexAccount(token=PLEX_TOKEN)
@@ -315,20 +357,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
@@ -346,24 +384,58 @@ def main():
# Create Amazon buy list for missing songs
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:
f.write("Songs not in your library - Amazon search links:\n\n")
for song in 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")
print(f" - {artist} - {title}: {url}")
print("Buy list saved to amazon_buy_list.txt")
# Read existing buy list
existing_songs = set()
if os.path.exists('amazon_buy_list.txt'):
try:
with open('amazon_buy_list.txt', 'r') as f:
content = f.read()
lines = content.split('\n')
i = 0
while i < len(lines):
if lines[i].startswith('Songs not in your library'):
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
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.
+2 -1
View File
@@ -4,4 +4,5 @@ plexapi
mutagen
selenium
webdriver-manager
PySide6
PySide6
matplotlib