Compare commits
30
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ea9c6647a | ||
|
|
f0d9b06ef4 | ||
|
|
013edb952f | ||
|
|
d5da0f710e | ||
|
|
2d027d339a | ||
|
|
dd985796e6 | ||
|
|
e762c4252d | ||
|
|
b50b430a5f | ||
|
|
8b68b382ff | ||
|
|
8393e5d659 | ||
|
|
bf7aa42460 | ||
|
|
59689dcbf6 | ||
|
|
38131fb653 | ||
|
|
5066b0b5e9 | ||
|
|
cdb6e53fb6 | ||
|
|
f89b970c94 | ||
|
|
62174895ff | ||
|
|
723e12fe11 | ||
|
|
2dcd4be86d | ||
|
|
fbe0c4a8e1 | ||
|
|
30c2e1c907 | ||
|
|
32d87cf0c2 | ||
|
|
7e7754f533 | ||
|
|
1339365941 | ||
|
|
43e93c5abc | ||
|
|
eb61051c7c | ||
|
|
3d18efb88f | ||
|
|
f958ea0983 | ||
|
|
2da02dc7df | ||
|
|
3f377e165d |
No files matched your search
+215
-2
@@ -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
@@ -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
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 639 B After Width: | Height: | Size: 1.2 MiB |
@@ -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
@@ -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()
|
||||
@@ -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('&', '&')
|
||||
title = title.replace('&', '&')
|
||||
|
||||
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
@@ -4,4 +4,5 @@ plexapi
|
||||
mutagen
|
||||
selenium
|
||||
webdriver-manager
|
||||
PySide6
|
||||
PySide6
|
||||
matplotlib
|
||||
Reference in new issue
Block a user