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:
|
Songs not in your library - Amazon search links:
|
||||||
|
|
||||||
|
Danny Gokey - I Got You
|
||||||
|
https://www.amazon.com/s?k=Danny%20Gokey%20I%20Got%20You&i=digital-music
|
||||||
|
|
||||||
|
Have For You Colton Dixon - The Love I
|
||||||
|
https://www.amazon.com/s?k=Have%20%20For%20%20You%20Colton%20%20Dixon%20The%20%20Love%20%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Done Tasha Layton - Look What You've
|
||||||
|
https://www.amazon.com/s?k=Done%20Tasha%20%20Layton%20Look%20%20What%20%20You%27ve%20&i=digital-music
|
||||||
|
|
||||||
Cochren & Co. - Who Can
|
Cochren & Co. - Who Can
|
||||||
https://www.amazon.com/s?k=Cochren%20%26%20%20Co.%20Who%20%20Can&i=digital-music
|
https://www.amazon.com/s?k=Cochren%20%26%20%20Co.%20Who%20%20Can&i=digital-music
|
||||||
|
|
||||||
Matthew West - All In
|
Have For You Colton Dixon - The Love I
|
||||||
https://www.amazon.com/s?k=Matthew%20%20West%20All%20%20In&i=digital-music
|
https://www.amazon.com/s?k=Have%20%20For%20%20You%20Colton%20%20Dixon%20The%20%20Love%20%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Done Tasha Layton - Look What You've
|
||||||
|
https://www.amazon.com/s?k=Done%20Tasha%20%20Layton%20Look%20%20What%20%20You%27ve%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
Andrew Ripp - Breakdown
|
||||||
|
https://www.amazon.com/s?k=Andrew%20%20Ripp%20Breakdown&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Crowder - Still
|
||||||
|
https://www.amazon.com/s?k=Crowder%20Still&i=digital-music
|
||||||
|
|
||||||
|
Thing About Praise Benjamin William Hastings - That's The
|
||||||
|
https://www.amazon.com/s?k=Thing%20%20About%20%20Praise%20Benjamin%20%20William%20%20Hastings%20That%27s%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
Love Feat. Bear Rinehar We The Kingdom - Child Of
|
||||||
|
https://www.amazon.com/s?k=Love%20%20Feat.%20%20Bear%20%20Rinehar%20We%20%20The%20%20Kingdom%20Child%20%20Of%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Thing About Praise Benjamin William Hastings - That's The
|
||||||
|
https://www.amazon.com/s?k=Thing%20%20About%20%20Praise%20Benjamin%20%20William%20%20Hastings%20That%27s%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
Love Feat. Bear Rinehar We The Kingdom - Child Of
|
||||||
|
https://www.amazon.com/s?k=Love%20%20Feat.%20%20Bear%20%20Rinehar%20We%20%20The%20%20Kingdom%20Child%20%20Of%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Zach Williams - Jesus Loves
|
||||||
|
https://www.amazon.com/s?k=Zach%20Williams%20Jesus%20Loves&i=digital-music
|
||||||
|
|
||||||
|
Zach Williams - Jesus Loves
|
||||||
|
https://www.amazon.com/s?k=Zach%20%20Williams%20Jesus%20%20Loves&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
Cochren & Co. - Church (Take Me Back)
|
||||||
|
https://www.amazon.com/s?k=Cochren%20%26%20Co.%20Church%20%28Take%20Me%20Back%29&i=digital-music
|
||||||
|
|
||||||
|
Blessing Offor - Your Love
|
||||||
|
https://www.amazon.com/s?k=Blessing%20Offor%20Your%20Love&i=digital-music
|
||||||
|
|
||||||
|
Anne Wilson - My Jesus
|
||||||
|
https://www.amazon.com/s?k=Anne%20Wilson%20My%20Jesus&i=digital-music
|
||||||
|
|
||||||
|
Sidewalk Prophets - You Love Me Anyway
|
||||||
|
https://www.amazon.com/s?k=Sidewalk%20Prophets%20You%20Love%20Me%20Anyway&i=digital-music
|
||||||
|
|
||||||
|
Casting Crowns (ft Matthew West) - Nobody
|
||||||
|
https://www.amazon.com/s?k=Casting%20Crowns%20%28ft%20Matthew%20West%29%20Nobody&i=digital-music
|
||||||
|
|
||||||
|
Phil Wickham - This Is Amazing Grace
|
||||||
|
https://www.amazon.com/s?k=Phil%20Wickham%20This%20Is%20Amazing%20Grace&i=digital-music
|
||||||
|
|
||||||
|
Crowder - STILL
|
||||||
|
https://www.amazon.com/s?k=Crowder%20STILL&i=digital-music
|
||||||
|
|
||||||
|
Josh Baldwin - Made for More
|
||||||
|
https://www.amazon.com/s?k=Josh%20Baldwin%20Made%20for%20More&i=digital-music
|
||||||
|
|
||||||
|
Leanna Crawford - Honest
|
||||||
|
https://www.amazon.com/s?k=Leanna%20Crawford%20Honest&i=digital-music
|
||||||
|
|
||||||
|
We The Kingdom - God So Loved
|
||||||
|
https://www.amazon.com/s?k=We%20The%20Kingdom%20God%20So%20Loved&i=digital-music
|
||||||
|
|
||||||
|
Dan Bremnes - Call Your Name
|
||||||
|
https://www.amazon.com/s?k=Dan%20Bremnes%20Call%20Your%20Name&i=digital-music
|
||||||
|
|
||||||
|
Big Daddy Weave - I Know
|
||||||
|
https://www.amazon.com/s?k=Big%20Daddy%20Weave%20I%20Know&i=digital-music
|
||||||
|
|
||||||
|
The Afters - God Is With Us
|
||||||
|
https://www.amazon.com/s?k=The%20Afters%20God%20Is%20With%20Us&i=digital-music
|
||||||
|
|
||||||
|
CAIN - Rise Up (Lazarus)
|
||||||
|
https://www.amazon.com/s?k=CAIN%20Rise%20Up%20%28Lazarus%29&i=digital-music
|
||||||
|
|
||||||
|
Katy Nichole - Have Your Way
|
||||||
|
https://www.amazon.com/s?k=Katy%20Nichole%20Have%20Your%20Way&i=digital-music
|
||||||
|
|
||||||
|
Ryan Stevenson - Preach
|
||||||
|
https://www.amazon.com/s?k=Ryan%20Stevenson%20Preach&i=digital-music
|
||||||
|
|
||||||
|
Elevation Worship - Praise
|
||||||
|
https://www.amazon.com/s?k=Elevation%20Worship%20Praise&i=digital-music
|
||||||
|
|
||||||
|
Andrew Ripp - Breakdown
|
||||||
|
https://www.amazon.com/s?k=Andrew%20Ripp%20Breakdown&i=digital-music
|
||||||
|
|
||||||
|
Chris Tomlin - Amazing Grace (My Chains Are Gon
|
||||||
|
https://www.amazon.com/s?k=Chris%20Tomlin%20Amazing%20Grace%20%28My%20Chains%20Are%20Gon&i=digital-music
|
||||||
|
|
||||||
|
Tauren Wells - Known
|
||||||
|
https://www.amazon.com/s?k=Tauren%20Wells%20Known&i=digital-music
|
||||||
|
|
||||||
|
Big Daddy Weave - All Things New (Single Mix)
|
||||||
|
https://www.amazon.com/s?k=Big%20Daddy%20Weave%20All%20Things%20New%20%28Single%20Mix%29&i=digital-music
|
||||||
|
|
||||||
+27
@@ -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
|
sudo apt update && sudo apt install -y chromium-browser
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create application icon
|
|
||||||
echo "Creating application icon..."
|
|
||||||
python3 "$DIR/create_icon.py"
|
|
||||||
|
|
||||||
# Create desktop entry
|
# Create desktop entry
|
||||||
echo "Creating desktop entry..."
|
echo "Creating desktop entry..."
|
||||||
mkdir -p ~/.local/share/applications
|
mkdir -p ~/.local/share/applications
|
||||||
|
|||||||
+311
-98
@@ -15,22 +15,17 @@ from datetime import datetime
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# GUI imports
|
# GUI imports
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QPushButton, QTextEdit, QLineEdit, QLabel, QHBoxLayout, QDialog, QListWidget, QListWidgetItem, QTableWidget, QTableWidgetItem, QHeaderView, QCheckBox, QInputDialog, QMessageBox, QProgressBar, QSystemTrayIcon, QMenu, QComboBox, QGroupBox, QFormLayout, QSpinBox, QTextBrowser, QTabWidget, QDialogButtonBox, QSplitter, QGridLayout
|
||||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
from PySide6.QtCore import QTimer, Qt, QThread, Signal, QSettings, QUrl
|
||||||
QLabel, QPushButton, QTextEdit, QLineEdit, QFormLayout,
|
from PySide6.QtGui import QIcon, QDesktopServices, QFont, QAction
|
||||||
QDialog, QDialogButtonBox, QSystemTrayIcon, QMenu,
|
import json
|
||||||
QGroupBox, QCheckBox, QSpinBox, QComboBox, QMessageBox,
|
import csv
|
||||||
QProgressBar, QSplitter, QFrame, QScrollArea, QTextBrowser, QTableWidget, QTableWidgetItem, QHeaderView, QListWidget, QListWidgetItem
|
from urllib.parse import quote
|
||||||
)
|
import sqlite3
|
||||||
from PySide6.QtCore import (
|
from datetime import datetime
|
||||||
Qt, QTimer, QThread, Signal, QSettings, QSize
|
|
||||||
)
|
|
||||||
from PySide6.QtGui import (
|
|
||||||
QIcon, QAction, QFont, QPixmap, QPainter, QColor, QDesktopServices
|
|
||||||
)
|
|
||||||
|
|
||||||
# Import our existing functionality
|
# Import our existing functionality
|
||||||
from main import scrape_recently_played, create_playlist_in_plex, PLEX_TOKEN, SERVER_IP, PLAYLIST_NAME
|
from main import scrape_recently_played, create_playlist_in_plex, update_playlist, PLEX_TOKEN, SERVER_IP, PLAYLIST_NAME
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
"""Configuration management"""
|
"""Configuration management"""
|
||||||
@@ -64,6 +59,9 @@ class Config:
|
|||||||
def save_config(self, config):
|
def save_config(self, config):
|
||||||
"""Save config to QSettings"""
|
"""Save config to QSettings"""
|
||||||
for key, value in config.items():
|
for key, value in config.items():
|
||||||
|
# Handle list values (convert to comma-separated string)
|
||||||
|
if isinstance(value, list):
|
||||||
|
value = ','.join(value)
|
||||||
self.set(key, value)
|
self.set(key, value)
|
||||||
|
|
||||||
class SetupWizard(QDialog):
|
class SetupWizard(QDialog):
|
||||||
@@ -114,6 +112,22 @@ class SetupWizard(QDialog):
|
|||||||
form_group.setLayout(form_layout)
|
form_group.setLayout(form_layout)
|
||||||
layout.addWidget(form_group)
|
layout.addWidget(form_group)
|
||||||
|
|
||||||
|
# Station Selection
|
||||||
|
station_group = QGroupBox("Station Selection")
|
||||||
|
station_layout = QVBoxLayout()
|
||||||
|
|
||||||
|
station_layout.addWidget(QLabel("Select which stations to monitor:"))
|
||||||
|
self.journey_fm_checkbox = QCheckBox("Journey FM (myjourneyfm.com)")
|
||||||
|
self.journey_fm_checkbox.setChecked(True)
|
||||||
|
station_layout.addWidget(self.journey_fm_checkbox)
|
||||||
|
|
||||||
|
self.spirit_fm_checkbox = QCheckBox("Spirit FM (spiritfm.com)")
|
||||||
|
self.spirit_fm_checkbox.setChecked(True)
|
||||||
|
station_layout.addWidget(self.spirit_fm_checkbox)
|
||||||
|
|
||||||
|
station_group.setLayout(station_layout)
|
||||||
|
layout.addWidget(station_group)
|
||||||
|
|
||||||
# Scheduling options
|
# Scheduling options
|
||||||
sched_group = QGroupBox("Scheduling")
|
sched_group = QGroupBox("Scheduling")
|
||||||
sched_layout = QVBoxLayout()
|
sched_layout = QVBoxLayout()
|
||||||
@@ -141,7 +155,7 @@ class SetupWizard(QDialog):
|
|||||||
buttons = QDialogButtonBox(
|
buttons = QDialogButtonBox(
|
||||||
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
)
|
)
|
||||||
buttons.accepted.connect(self.accept)
|
buttons.accepted.connect(self.validate_and_accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
layout.addWidget(buttons)
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
@@ -152,16 +166,40 @@ class SetupWizard(QDialog):
|
|||||||
self.server_input.setText(config.get('SERVER_IP', ''))
|
self.server_input.setText(config.get('SERVER_IP', ''))
|
||||||
self.playlist_input.setText(config.get('PLAYLIST_NAME', 'Journey FM Recently Played'))
|
self.playlist_input.setText(config.get('PLAYLIST_NAME', 'Journey FM Recently Played'))
|
||||||
|
|
||||||
|
# Load station selections
|
||||||
|
selected_stations = config.get('SELECTED_STATIONS', 'journey_fm,spirit_fm')
|
||||||
|
if isinstance(selected_stations, str):
|
||||||
|
selected_stations = selected_stations.split(',')
|
||||||
|
else:
|
||||||
|
selected_stations = ['journey_fm', 'spirit_fm']
|
||||||
|
self.journey_fm_checkbox.setChecked('journey_fm' in selected_stations)
|
||||||
|
self.spirit_fm_checkbox.setChecked('spirit_fm' in selected_stations)
|
||||||
|
|
||||||
def get_config(self):
|
def get_config(self):
|
||||||
|
selected_stations = []
|
||||||
|
if self.journey_fm_checkbox.isChecked():
|
||||||
|
selected_stations.append('journey_fm')
|
||||||
|
if self.spirit_fm_checkbox.isChecked():
|
||||||
|
selected_stations.append('spirit_fm')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'PLEX_TOKEN': self.token_input.text().strip(),
|
'PLEX_TOKEN': self.token_input.text().strip(),
|
||||||
'SERVER_IP': self.server_input.text().strip(),
|
'SERVER_IP': self.server_input.text().strip(),
|
||||||
'PLAYLIST_NAME': self.playlist_input.text().strip(),
|
'PLAYLIST_NAME': self.playlist_input.text().strip(),
|
||||||
'AUTO_UPDATE': self.auto_update.isChecked(),
|
'AUTO_UPDATE': self.auto_update.isChecked(),
|
||||||
'UPDATE_INTERVAL': self.interval_spin.value(),
|
'UPDATE_INTERVAL': self.interval_spin.value(),
|
||||||
'UPDATE_UNIT': self.interval_unit.currentText()
|
'UPDATE_UNIT': self.interval_unit.currentText(),
|
||||||
|
'SELECTED_STATIONS': ','.join(selected_stations)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def validate_and_accept(self):
|
||||||
|
"""Validate configuration before accepting"""
|
||||||
|
if not self.journey_fm_checkbox.isChecked() and not self.spirit_fm_checkbox.isChecked():
|
||||||
|
QMessageBox.warning(self, "Validation Error",
|
||||||
|
"Please select at least one station to monitor.")
|
||||||
|
return
|
||||||
|
self.accept()
|
||||||
|
|
||||||
class LogViewer(QWidget):
|
class LogViewer(QWidget):
|
||||||
"""Widget for viewing application logs"""
|
"""Widget for viewing application logs"""
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@@ -251,7 +289,9 @@ class MainWindow(QMainWindow):
|
|||||||
self.config.load_config()
|
self.config.load_config()
|
||||||
|
|
||||||
self.setWindowTitle("Journey FM Playlist Creator")
|
self.setWindowTitle("Journey FM Playlist Creator")
|
||||||
self.setWindowIcon(QIcon("icon.png")) # You'll need to add an icon
|
icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icon.png')
|
||||||
|
if os.path.exists(icon_path):
|
||||||
|
self.setWindowIcon(QIcon(icon_path))
|
||||||
self.resize(800, 600)
|
self.resize(800, 600)
|
||||||
|
|
||||||
# Create central widget
|
# Create central widget
|
||||||
@@ -289,25 +329,37 @@ class MainWindow(QMainWindow):
|
|||||||
self.last_update_label = QLabel("Last update: Never")
|
self.last_update_label = QLabel("Last update: Never")
|
||||||
status_layout.addWidget(self.last_update_label)
|
status_layout.addWidget(self.last_update_label)
|
||||||
|
|
||||||
# Buy list button
|
# Buttons in grid layout
|
||||||
|
button_layout = QGridLayout()
|
||||||
|
|
||||||
|
# Row 1
|
||||||
|
self.update_button = QPushButton("Update Playlist")
|
||||||
|
self.update_button.clicked.connect(self.run_update_playlist)
|
||||||
|
button_layout.addWidget(self.update_button, 0, 0)
|
||||||
|
|
||||||
self.buy_list_button = QPushButton("Show Buy List")
|
self.buy_list_button = QPushButton("Show Buy List")
|
||||||
self.buy_list_button.clicked.connect(self.show_buy_list)
|
self.buy_list_button.clicked.connect(self.show_buy_list)
|
||||||
status_layout.addWidget(self.buy_list_button)
|
button_layout.addWidget(self.buy_list_button, 0, 1)
|
||||||
|
|
||||||
# History button
|
# Row 2
|
||||||
self.history_button = QPushButton("View History")
|
self.history_button = QPushButton("View History")
|
||||||
self.history_button.clicked.connect(self.show_history)
|
self.history_button.clicked.connect(self.show_history)
|
||||||
status_layout.addWidget(self.history_button)
|
button_layout.addWidget(self.history_button, 1, 0)
|
||||||
|
|
||||||
# Export button
|
|
||||||
self.export_button = QPushButton("Export Playlist")
|
self.export_button = QPushButton("Export Playlist")
|
||||||
self.export_button.clicked.connect(self.export_playlist)
|
self.export_button.clicked.connect(self.export_playlist)
|
||||||
status_layout.addWidget(self.export_button)
|
button_layout.addWidget(self.export_button, 1, 1)
|
||||||
|
|
||||||
# Statistics button
|
# Row 3
|
||||||
self.stats_button = QPushButton("Statistics")
|
self.stats_button = QPushButton("Statistics")
|
||||||
self.stats_button.clicked.connect(self.show_statistics)
|
self.stats_button.clicked.connect(self.show_statistics)
|
||||||
status_layout.addWidget(self.stats_button)
|
button_layout.addWidget(self.stats_button, 2, 0)
|
||||||
|
|
||||||
|
self.analytics_button = QPushButton("Analytics")
|
||||||
|
self.analytics_button.clicked.connect(self.show_analytics)
|
||||||
|
button_layout.addWidget(self.analytics_button, 2, 1)
|
||||||
|
|
||||||
|
status_layout.addLayout(button_layout)
|
||||||
|
|
||||||
status_group.setLayout(status_layout)
|
status_group.setLayout(status_layout)
|
||||||
splitter.addWidget(status_group)
|
splitter.addWidget(status_group)
|
||||||
@@ -330,6 +382,13 @@ class MainWindow(QMainWindow):
|
|||||||
if not self.config.get('PLEX_TOKEN'):
|
if not self.config.get('PLEX_TOKEN'):
|
||||||
self.show_setup_wizard()
|
self.show_setup_wizard()
|
||||||
|
|
||||||
|
def run_update_playlist(self):
|
||||||
|
"""Run playlist update in background thread"""
|
||||||
|
import threading
|
||||||
|
self.status_label.setText("Updating playlist...")
|
||||||
|
self.update_button.setEnabled(False)
|
||||||
|
threading.Thread(target=update_playlist, daemon=True).start()
|
||||||
|
|
||||||
def set_tray_icon(self, tray_icon):
|
def set_tray_icon(self, tray_icon):
|
||||||
"""Set the tray icon reference"""
|
"""Set the tray icon reference"""
|
||||||
self.tray_icon = tray_icon
|
self.tray_icon = tray_icon
|
||||||
@@ -455,77 +514,90 @@ class MainWindow(QMainWindow):
|
|||||||
def show_buy_list(self):
|
def show_buy_list(self):
|
||||||
"""Show the Amazon buy list dialog with interactive features"""
|
"""Show the Amazon buy list dialog with interactive features"""
|
||||||
try:
|
try:
|
||||||
with open('amazon_buy_list.txt', 'r') as f:
|
buy_list_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'amazon_buy_list.txt')
|
||||||
|
with open(buy_list_path, 'r') as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
QMessageBox.information(self, "No Buy List", "No buy list available. Run an update to generate the list.")
|
QMessageBox.information(self, "No Buy List", "No buy list available. Run an update to generate the list.")
|
||||||
return
|
return
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Error", f"Failed to read buy list file: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
# Parse content to get songs
|
try:
|
||||||
lines = content.split('\n')
|
# Parse content to get songs
|
||||||
songs = []
|
lines = content.split('\n')
|
||||||
i = 0
|
songs = []
|
||||||
while i < len(lines):
|
i = 0
|
||||||
if lines[i].startswith('Songs not in your library'):
|
while i < len(lines):
|
||||||
i += 2 # Skip header
|
if lines[i].startswith('Songs not in your library'):
|
||||||
continue
|
i += 2 # Skip header
|
||||||
if lines[i].strip() and not lines[i].startswith('http'):
|
continue
|
||||||
artist_title = lines[i].strip()
|
if lines[i].strip() and not lines[i].startswith('http'):
|
||||||
if i + 1 < len(lines) and lines[i + 1].startswith('http'):
|
artist_title = lines[i].strip()
|
||||||
url = lines[i + 1].strip()
|
if i + 1 < len(lines) and lines[i + 1].startswith('http'):
|
||||||
songs.append((artist_title, url))
|
url = lines[i + 1].strip()
|
||||||
i += 3 # Skip blank line
|
songs.append((artist_title, url))
|
||||||
|
i += 3 # Skip blank line
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
else:
|
else:
|
||||||
i += 1
|
i += 1
|
||||||
else:
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
dialog = QDialog(self)
|
dialog = QDialog(self)
|
||||||
dialog.setWindowTitle("Amazon Buy List")
|
dialog.setWindowTitle("Amazon Buy List")
|
||||||
dialog.setModal(True)
|
dialog.setModal(True)
|
||||||
dialog.resize(600, 400)
|
dialog.resize(600, 400)
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
# Search box
|
# Search box
|
||||||
search_layout = QHBoxLayout()
|
search_layout = QHBoxLayout()
|
||||||
search_layout.addWidget(QLabel("Search:"))
|
search_layout.addWidget(QLabel("Search:"))
|
||||||
self.search_input = QLineEdit()
|
self.search_input = QLineEdit()
|
||||||
self.search_input.textChanged.connect(lambda: self.filter_buy_list(songs))
|
self.search_input.textChanged.connect(lambda: self.filter_buy_list(songs))
|
||||||
search_layout.addWidget(self.search_input)
|
search_layout.addWidget(self.search_input)
|
||||||
layout.addLayout(search_layout)
|
layout.addLayout(search_layout)
|
||||||
|
|
||||||
# List widget
|
# List widget
|
||||||
self.buy_list_widget = QListWidget()
|
self.buy_list_widget = QListWidget()
|
||||||
self.populate_buy_list(songs)
|
self.buy_list_label = QLabel(f"Found {len(songs)} songs to buy:")
|
||||||
layout.addWidget(self.buy_list_widget)
|
layout.addWidget(self.buy_list_label)
|
||||||
|
self.populate_buy_list(songs)
|
||||||
|
layout.addWidget(self.buy_list_widget)
|
||||||
|
|
||||||
# Buttons
|
# Buttons
|
||||||
button_layout = QHBoxLayout()
|
button_layout = QHBoxLayout()
|
||||||
open_button = QPushButton("Open Selected")
|
open_button = QPushButton("Open Selected")
|
||||||
open_button.clicked.connect(self.open_selected_buy_items)
|
open_button.clicked.connect(self.open_selected_buy_items)
|
||||||
button_layout.addWidget(open_button)
|
button_layout.addWidget(open_button)
|
||||||
|
|
||||||
remove_button = QPushButton("Remove Selected")
|
remove_button = QPushButton("Remove Selected")
|
||||||
remove_button.clicked.connect(lambda: self.remove_selected_buy_items(songs, dialog))
|
remove_button.clicked.connect(lambda: self.remove_selected_buy_items(songs, dialog))
|
||||||
button_layout.addWidget(remove_button)
|
button_layout.addWidget(remove_button)
|
||||||
|
|
||||||
close_button = QPushButton("Close")
|
close_button = QPushButton("Close")
|
||||||
close_button.clicked.connect(dialog.close)
|
close_button.clicked.connect(dialog.close)
|
||||||
button_layout.addWidget(close_button)
|
button_layout.addWidget(close_button)
|
||||||
|
|
||||||
layout.addLayout(button_layout)
|
layout.addLayout(button_layout)
|
||||||
|
|
||||||
dialog.setLayout(layout)
|
dialog.setLayout(layout)
|
||||||
dialog.exec()
|
dialog.show()
|
||||||
|
dialog.raise_()
|
||||||
|
dialog.activateWindow()
|
||||||
|
dialog.exec()
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Error", f"Failed to show buy list dialog: {e}")
|
||||||
|
|
||||||
def populate_buy_list(self, songs):
|
def populate_buy_list(self, songs):
|
||||||
"""Populate the buy list widget"""
|
"""Populate the buy list widget"""
|
||||||
self.buy_list_widget.clear()
|
self.buy_list_widget.clear()
|
||||||
|
self.buy_list_label.setText(f"Found {len(songs)} songs to buy:")
|
||||||
for artist_title, url in songs:
|
for artist_title, url in songs:
|
||||||
item = QListWidgetItem(artist_title)
|
item = QListWidgetItem(artist_title)
|
||||||
item.setData(1, url) # Store URL in item data
|
item.setData(1, url) # Store URL in item data
|
||||||
item.setCheckState(0) # Qt.CheckState.Unchecked
|
item.setCheckState(Qt.CheckState.Unchecked)
|
||||||
self.buy_list_widget.addItem(item)
|
self.buy_list_widget.addItem(item)
|
||||||
|
|
||||||
def filter_buy_list(self, all_songs):
|
def filter_buy_list(self, all_songs):
|
||||||
@@ -538,7 +610,7 @@ class MainWindow(QMainWindow):
|
|||||||
"""Open selected buy list items in browser"""
|
"""Open selected buy list items in browser"""
|
||||||
for i in range(self.buy_list_widget.count()):
|
for i in range(self.buy_list_widget.count()):
|
||||||
item = self.buy_list_widget.item(i)
|
item = self.buy_list_widget.item(i)
|
||||||
if item.checkState() == 2: # Qt.CheckState.Checked
|
if item.checkState() == Qt.CheckState.Checked:
|
||||||
url = item.data(1)
|
url = item.data(1)
|
||||||
QDesktopServices.openUrl(url)
|
QDesktopServices.openUrl(url)
|
||||||
|
|
||||||
@@ -547,7 +619,7 @@ class MainWindow(QMainWindow):
|
|||||||
to_remove = []
|
to_remove = []
|
||||||
for i in range(self.buy_list_widget.count()):
|
for i in range(self.buy_list_widget.count()):
|
||||||
item = self.buy_list_widget.item(i)
|
item = self.buy_list_widget.item(i)
|
||||||
if item.checkState() == 2: # Qt.CheckState.Checked
|
if item.checkState() == Qt.CheckState.Checked:
|
||||||
artist_title = item.text()
|
artist_title = item.text()
|
||||||
to_remove.append(artist_title)
|
to_remove.append(artist_title)
|
||||||
|
|
||||||
@@ -618,6 +690,118 @@ class MainWindow(QMainWindow):
|
|||||||
dialog.setLayout(layout)
|
dialog.setLayout(layout)
|
||||||
dialog.exec()
|
dialog.exec()
|
||||||
|
|
||||||
|
def show_analytics(self):
|
||||||
|
"""Show analytics dashboard with charts"""
|
||||||
|
import sqlite3
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from collections import Counter
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
||||||
|
from matplotlib.figure import Figure
|
||||||
|
|
||||||
|
dialog = QDialog(self)
|
||||||
|
dialog.setWindowTitle("Playlist Analytics")
|
||||||
|
dialog.setModal(True)
|
||||||
|
dialog.resize(1000, 700)
|
||||||
|
|
||||||
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
|
tab_widget = QTabWidget()
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = sqlite3.connect('playlist_history.db')
|
||||||
|
c = conn.cursor()
|
||||||
|
c.execute('SELECT date, added_songs, missing_songs FROM history ORDER BY date')
|
||||||
|
rows = c.fetchall()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
# Prepare data
|
||||||
|
dates = []
|
||||||
|
cumulative_added = 0
|
||||||
|
cumulative_list = []
|
||||||
|
artist_counter = Counter()
|
||||||
|
update_freq = []
|
||||||
|
|
||||||
|
for date, added_json, missing_json in rows:
|
||||||
|
dt = datetime.fromisoformat(date)
|
||||||
|
dates.append(dt.strftime('%Y-%m-%d'))
|
||||||
|
|
||||||
|
added_songs = json.loads(added_json) if added_json else []
|
||||||
|
cumulative_added += len(added_songs)
|
||||||
|
cumulative_list.append(cumulative_added)
|
||||||
|
update_freq.append(len(added_songs))
|
||||||
|
|
||||||
|
for song in added_songs:
|
||||||
|
# Parse artist from "Title by Artist"
|
||||||
|
if " by " in song:
|
||||||
|
artist = song.split(" by ", 1)[1]
|
||||||
|
artist_counter[artist] += 1
|
||||||
|
|
||||||
|
# Tab 1: Playlist Growth
|
||||||
|
growth_tab = QWidget()
|
||||||
|
growth_layout = QVBoxLayout()
|
||||||
|
fig1 = Figure(figsize=(8, 6))
|
||||||
|
ax1 = fig1.add_subplot(111)
|
||||||
|
ax1.plot(dates, cumulative_list, marker='o')
|
||||||
|
ax1.set_title('Playlist Growth Over Time')
|
||||||
|
ax1.set_xlabel('Date')
|
||||||
|
ax1.set_ylabel('Total Songs')
|
||||||
|
ax1.tick_params(axis='x', rotation=45)
|
||||||
|
canvas1 = FigureCanvas(fig1)
|
||||||
|
growth_layout.addWidget(canvas1)
|
||||||
|
growth_tab.setLayout(growth_layout)
|
||||||
|
tab_widget.addTab(growth_tab, "Growth")
|
||||||
|
|
||||||
|
# Tab 2: Top Artists
|
||||||
|
artists_tab = QWidget()
|
||||||
|
artists_layout = QVBoxLayout()
|
||||||
|
fig2 = Figure(figsize=(8, 6))
|
||||||
|
ax2 = fig2.add_subplot(111)
|
||||||
|
top_artists = artist_counter.most_common(10)
|
||||||
|
if top_artists:
|
||||||
|
artists, counts = zip(*top_artists)
|
||||||
|
ax2.bar(artists, counts)
|
||||||
|
ax2.set_title('Top 10 Artists')
|
||||||
|
ax2.set_xlabel('Artist')
|
||||||
|
ax2.set_ylabel('Songs Added')
|
||||||
|
ax2.tick_params(axis='x', rotation=45)
|
||||||
|
canvas2 = FigureCanvas(fig2)
|
||||||
|
artists_layout.addWidget(canvas2)
|
||||||
|
artists_tab.setLayout(artists_layout)
|
||||||
|
tab_widget.addTab(artists_tab, "Top Artists")
|
||||||
|
|
||||||
|
# Tab 3: Update Frequency
|
||||||
|
freq_tab = QWidget()
|
||||||
|
freq_layout = QVBoxLayout()
|
||||||
|
fig3 = Figure(figsize=(8, 6))
|
||||||
|
ax3 = fig3.add_subplot(111)
|
||||||
|
ax3.bar(dates, update_freq)
|
||||||
|
ax3.set_title('Songs Added per Update')
|
||||||
|
ax3.set_xlabel('Date')
|
||||||
|
ax3.set_ylabel('Songs Added')
|
||||||
|
ax3.tick_params(axis='x', rotation=45)
|
||||||
|
canvas3 = FigureCanvas(fig3)
|
||||||
|
freq_layout.addWidget(canvas3)
|
||||||
|
freq_tab.setLayout(freq_layout)
|
||||||
|
tab_widget.addTab(freq_tab, "Update Frequency")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_tab = QWidget()
|
||||||
|
error_layout = QVBoxLayout()
|
||||||
|
error_layout.addWidget(QLabel(f"Error loading analytics: {str(e)}"))
|
||||||
|
error_tab.setLayout(error_layout)
|
||||||
|
tab_widget.addTab(error_tab, "Error")
|
||||||
|
|
||||||
|
layout.addWidget(tab_widget)
|
||||||
|
|
||||||
|
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
||||||
|
buttons.rejected.connect(dialog.close)
|
||||||
|
layout.addWidget(buttons)
|
||||||
|
|
||||||
|
dialog.setLayout(layout)
|
||||||
|
dialog.exec()
|
||||||
|
|
||||||
def show_history(self):
|
def show_history(self):
|
||||||
"""Show the playlist history dialog"""
|
"""Show the playlist history dialog"""
|
||||||
import sqlite3
|
import sqlite3
|
||||||
@@ -633,19 +817,19 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
table = QTableWidget()
|
table = QTableWidget()
|
||||||
table.setColumnCount(4)
|
table.setColumnCount(4)
|
||||||
table.setHorizontalHeaderLabels(["Date", "Added Songs", "Missing Songs", "Details"])
|
table.setHorizontalHeaderLabels(["Date", "Type", "Artist", "Song"])
|
||||||
table.horizontalHeader().setStretchLastSection(True)
|
table.horizontalHeader().setStretchLastSection(True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = sqlite3.connect('playlist_history.db')
|
conn = sqlite3.connect('playlist_history.db')
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
c.execute('SELECT date, added_count, added_songs, missing_count, missing_songs FROM history ORDER BY date DESC')
|
c.execute('SELECT date, added_songs, missing_songs FROM history ORDER BY date DESC')
|
||||||
rows = c.fetchall()
|
rows = c.fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
table.setRowCount(len(rows))
|
# Collect all entries
|
||||||
for row_idx, (date, added_count, added_songs_json, missing_count, missing_songs_json) in enumerate(rows):
|
entries = []
|
||||||
# Parse date
|
for date, added_songs_json, missing_songs_json in rows:
|
||||||
try:
|
try:
|
||||||
dt = datetime.fromisoformat(date)
|
dt = datetime.fromisoformat(date)
|
||||||
date_str = dt.strftime('%Y-%m-%d %H:%M')
|
date_str = dt.strftime('%Y-%m-%d %H:%M')
|
||||||
@@ -655,25 +839,33 @@ class MainWindow(QMainWindow):
|
|||||||
# Added songs
|
# Added songs
|
||||||
try:
|
try:
|
||||||
added_songs = json.loads(added_songs_json)
|
added_songs = json.loads(added_songs_json)
|
||||||
added_text = f"{added_count} songs\n" + "\n".join(added_songs[:3]) # Show first 3
|
for song in added_songs:
|
||||||
if len(added_songs) > 3:
|
# Format is "Title by Artist"
|
||||||
added_text += f"\n... and {len(added_songs)-3} more"
|
if " by " in song:
|
||||||
|
title, artist = song.split(" by ", 1)
|
||||||
|
else:
|
||||||
|
title = song
|
||||||
|
artist = "Unknown"
|
||||||
|
entries.append((date_str, "Added", artist, title))
|
||||||
except:
|
except:
|
||||||
added_text = f"{added_count} songs"
|
pass
|
||||||
|
|
||||||
# Missing songs
|
# Missing songs
|
||||||
try:
|
try:
|
||||||
missing_songs = json.loads(missing_songs_json)
|
missing_songs = json.loads(missing_songs_json)
|
||||||
missing_text = f"{missing_count} songs\n" + "\n".join([f"{s['artist']} - {s['title']}" for s in missing_songs[:3]])
|
for song in missing_songs:
|
||||||
if len(missing_songs) > 3:
|
artist = song.get('artist', 'Unknown')
|
||||||
missing_text += f"\n... and {len(missing_songs)-3} more"
|
title = song.get('title', 'Unknown')
|
||||||
|
entries.append((date_str, "Missing", artist, title))
|
||||||
except:
|
except:
|
||||||
missing_text = f"{missing_count} songs"
|
pass
|
||||||
|
|
||||||
|
table.setRowCount(len(entries))
|
||||||
|
for row_idx, (date_str, type_str, artist, song) in enumerate(entries):
|
||||||
table.setItem(row_idx, 0, QTableWidgetItem(date_str))
|
table.setItem(row_idx, 0, QTableWidgetItem(date_str))
|
||||||
table.setItem(row_idx, 1, QTableWidgetItem(added_text))
|
table.setItem(row_idx, 1, QTableWidgetItem(type_str))
|
||||||
table.setItem(row_idx, 2, QTableWidgetItem(missing_text))
|
table.setItem(row_idx, 2, QTableWidgetItem(artist))
|
||||||
table.setItem(row_idx, 3, QTableWidgetItem(f"Added: {added_count}, Missing: {missing_count}"))
|
table.setItem(row_idx, 3, QTableWidgetItem(song))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
table.setRowCount(1)
|
table.setRowCount(1)
|
||||||
@@ -824,14 +1016,35 @@ class SystemTrayApp:
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main application entry point"""
|
"""Main application entry point"""
|
||||||
# Set up high DPI scaling
|
with open('app_log.txt', 'a') as log:
|
||||||
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
log.write(f"{datetime.now()}: Starting Journey FM Playlist app...\n")
|
||||||
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
print("Starting Journey FM Playlist app...")
|
||||||
)
|
try:
|
||||||
|
if not os.environ.get('DISPLAY'):
|
||||||
|
raise Exception("DISPLAY environment variable not set - no graphical display available")
|
||||||
|
|
||||||
# Create and run application
|
print("Initializing GUI...")
|
||||||
tray_app = SystemTrayApp()
|
with open('app_log.txt', 'a') as log:
|
||||||
sys.exit(tray_app.run())
|
log.write(f"{datetime.now()}: DISPLAY set, initializing GUI\n")
|
||||||
|
# Set up high DPI scaling
|
||||||
|
QApplication.setHighDpiScaleFactorRoundingPolicy(
|
||||||
|
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create and run application
|
||||||
|
tray_app = SystemTrayApp()
|
||||||
|
print("App initialized, starting event loop...")
|
||||||
|
with open('app_log.txt', 'a') as log:
|
||||||
|
log.write(f"{datetime.now()}: App initialized, starting event loop\n")
|
||||||
|
sys.exit(tray_app.run())
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"Failed to start GUI application: {e}"
|
||||||
|
print(error_msg)
|
||||||
|
print("Make sure you have a graphical display available (X11/Wayland) and DISPLAY environment variable is set.")
|
||||||
|
print("If running remotely, use X forwarding or a VNC session.")
|
||||||
|
with open('app_log.txt', 'a') as log:
|
||||||
|
log.write(f"{datetime.now()}: {error_msg}\n")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -13,6 +13,8 @@ from selenium.webdriver.chrome.options import Options
|
|||||||
from selenium.webdriver.chrome.service import Service
|
from selenium.webdriver.chrome.service import Service
|
||||||
from webdriver_manager.chrome import ChromeDriverManager
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
from selenium.webdriver.common.by import By
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import os
|
import os
|
||||||
from plexapi.myplex import MyPlexAccount
|
from plexapi.myplex import MyPlexAccount
|
||||||
@@ -30,78 +32,116 @@ PLEX_TOKEN = 'pU-m3HWYUZU6iXJFhJyA' # Your Plex.tv token
|
|||||||
SERVER_IP = '172.16.16.106' # Your local server IP
|
SERVER_IP = '172.16.16.106' # Your local server IP
|
||||||
PLAYLIST_NAME = 'Journey FM Recently Played'
|
PLAYLIST_NAME = 'Journey FM Recently Played'
|
||||||
|
|
||||||
def scrape_recently_played():
|
def scrape_recently_played(selected_stations=None):
|
||||||
"""Scrape recently played songs from myjourneyfm.com using Selenium"""
|
"""Scrape recently played songs from Journey FM and Spirit FM"""
|
||||||
|
if selected_stations is None:
|
||||||
|
selected_stations = ['journey_fm', 'spirit_fm']
|
||||||
|
|
||||||
options = Options()
|
options = Options()
|
||||||
options.add_argument("--headless")
|
options.add_argument("--headless=new")
|
||||||
options.add_argument("--no-sandbox")
|
options.add_argument("--no-sandbox")
|
||||||
options.add_argument("--disable-dev-shm-usage")
|
options.add_argument("--disable-dev-shm-usage")
|
||||||
options.add_argument("--disable-gpu")
|
options.add_argument("--disable-gpu")
|
||||||
options.add_argument("--remote-debugging-port=9222")
|
options.add_argument("--window-size=1920,1080")
|
||||||
options.add_argument("--user-data-dir=/tmp/chromium")
|
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||||
options.add_argument("--no-first-run")
|
options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||||
options.add_argument("--disable-extensions")
|
options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||||
options.binary_location = "/usr/bin/chromium-browser"
|
options.add_experimental_option('useAutomationExtension', False)
|
||||||
service = Service()
|
options.binary_location = "/usr/bin/google-chrome-stable"
|
||||||
driver = webdriver.Chrome(service=service, options=options)
|
|
||||||
|
|
||||||
url = 'https://www.myjourneyfm.com/recently-played/'
|
service = Service(ChromeDriverManager().install())
|
||||||
driver.get(url)
|
|
||||||
|
|
||||||
# Wait for initial load
|
|
||||||
time.sleep(5)
|
|
||||||
|
|
||||||
# Click "View More" button if exists
|
|
||||||
try:
|
try:
|
||||||
more_button = driver.find_element(By.ID, "moreSongs")
|
driver = webdriver.Chrome(service=service, options=options)
|
||||||
more_button.click()
|
except Exception as e:
|
||||||
time.sleep(5) # Wait for more songs to load
|
print(f"Failed to start Chrome: {e}")
|
||||||
except:
|
return []
|
||||||
pass # Button not found or already loaded
|
|
||||||
|
all_songs = []
|
||||||
|
seen = set()
|
||||||
|
|
||||||
|
# Journey FM - uses <strong> for title, normal text for artist
|
||||||
|
if 'journey_fm' in selected_stations:
|
||||||
|
try:
|
||||||
|
print("Scraping Journey FM...")
|
||||||
|
driver.get('https://www.myjourneyfm.com/recently-played/')
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
|
# Click "View More" button if exists
|
||||||
|
try:
|
||||||
|
more_button = driver.find_element(By.ID, "moreSongs")
|
||||||
|
more_button.click()
|
||||||
|
time.sleep(5)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
soup = BeautifulSoup(driver.page_source, 'html.parser')
|
||||||
|
|
||||||
|
# Find all song items using the proper class structure
|
||||||
|
song_items = soup.find_all('div', class_='rp-item')
|
||||||
|
|
||||||
|
for item in song_items:
|
||||||
|
try:
|
||||||
|
title_elem = item.find('h5', class_='song-title')
|
||||||
|
artist_elem = item.find('p', class_='song-artist')
|
||||||
|
|
||||||
|
if title_elem and artist_elem:
|
||||||
|
title = title_elem.get_text().strip()
|
||||||
|
artist = artist_elem.get_text().strip()
|
||||||
|
|
||||||
|
if title and artist:
|
||||||
|
key = (title.lower(), artist.lower())
|
||||||
|
if key not in seen:
|
||||||
|
all_songs.append({'title': title, 'artist': artist, 'source': 'Journey FM'})
|
||||||
|
seen.add(key)
|
||||||
|
print(f" Found: {title} by {artist}")
|
||||||
|
except Exception as e:
|
||||||
|
continue
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error scraping Journey FM: {e}")
|
||||||
|
|
||||||
|
# Spirit FM - loads songs from iframe text file
|
||||||
|
if 'spirit_fm' in selected_stations:
|
||||||
|
try:
|
||||||
|
print("Scraping Spirit FM...")
|
||||||
|
driver.get('https://spiritfm.com/ajax/now_playing_history.txt')
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
# Parse the plain text format: "Mon 01:45PM Artist - Title"
|
||||||
|
soup = BeautifulSoup(driver.page_source, 'html.parser')
|
||||||
|
text_content = soup.get_text()
|
||||||
|
|
||||||
|
# Split into lines and parse each
|
||||||
|
for line in text_content.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Match pattern: Day HH:MM(AM/PM) Artist - Title
|
||||||
|
match = re.match(r'^\w+\s+\d+:\d+[AP]M\s+(.+?)\s+-\s+(.+)$', line)
|
||||||
|
if match:
|
||||||
|
artist = match.group(1).strip()
|
||||||
|
title = match.group(2).strip()
|
||||||
|
|
||||||
|
# Clean up HTML entities
|
||||||
|
artist = artist.replace('&', '&')
|
||||||
|
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()
|
driver.quit()
|
||||||
|
|
||||||
# Optional: save HTML to file for debugging (comment out if not needed)
|
print(f"\nTotal unique songs found: {len(all_songs)}")
|
||||||
# with open('page.html', 'w', encoding='utf-8') as f:
|
return all_songs
|
||||||
# f.write(html)
|
|
||||||
|
|
||||||
soup = BeautifulSoup(html, 'html.parser')
|
|
||||||
|
|
||||||
songs = []
|
|
||||||
# Find elements (div, li) that contain time in their text
|
|
||||||
song_elements = soup.find_all(lambda tag: tag.name in ['div', 'li', 'p'] and re.search(r'\d+:\d+ [AP]M', tag.get_text()))
|
|
||||||
for element in song_elements:
|
|
||||||
spans = element.find_all('span')
|
|
||||||
if len(spans) >= 3:
|
|
||||||
title = spans[0].get_text().strip()
|
|
||||||
artist = spans[1].get_text().strip()
|
|
||||||
# Ignore time
|
|
||||||
songs.append({'artist': artist, 'title': title})
|
|
||||||
else:
|
|
||||||
# Fallback to text parsing
|
|
||||||
full_text = element.get_text().strip()
|
|
||||||
match = re.search(r'(\d+:\d+ [AP]M)', full_text)
|
|
||||||
if match:
|
|
||||||
play_time = match.group(1)
|
|
||||||
before = full_text.split(play_time)[0].strip()
|
|
||||||
if ' by ' in before:
|
|
||||||
title, artist = before.split(' by ', 1)
|
|
||||||
title = title.strip()
|
|
||||||
artist = artist.strip()
|
|
||||||
else:
|
|
||||||
# Split on capital letters
|
|
||||||
parts = re.findall(r'[A-Z][^A-Z]*', before)
|
|
||||||
if len(parts) == 4:
|
|
||||||
title = ' '.join(parts[:2])
|
|
||||||
artist = ' '.join(parts[2:])
|
|
||||||
else:
|
|
||||||
title = ' '.join(parts[:3])
|
|
||||||
artist = ' '.join(parts[3:])
|
|
||||||
if title and artist:
|
|
||||||
songs.append({'artist': artist, 'title': title})
|
|
||||||
|
|
||||||
return songs
|
|
||||||
|
|
||||||
def normalize_string(s):
|
def normalize_string(s):
|
||||||
"""Normalize string for comparison"""
|
"""Normalize string for comparison"""
|
||||||
@@ -175,7 +215,7 @@ def create_playlist_in_plex(plex, songs, playlist_name):
|
|||||||
search_artist_lower in track_artist or track_artist in search_artist_lower):
|
search_artist_lower in track_artist or track_artist in search_artist_lower):
|
||||||
# Only add if we haven't already added this track in this run
|
# Only add if we haven't already added this track in this run
|
||||||
if track.ratingKey not in seen_keys:
|
if track.ratingKey not in seen_keys:
|
||||||
tracks.append(track)
|
tracks.append((track, song))
|
||||||
seen_keys.add(track.ratingKey)
|
seen_keys.add(track.ratingKey)
|
||||||
matched = True
|
matched = True
|
||||||
break
|
break
|
||||||
@@ -192,28 +232,28 @@ def create_playlist_in_plex(plex, songs, playlist_name):
|
|||||||
# Get existing track rating keys to avoid duplicates
|
# Get existing track rating keys to avoid duplicates
|
||||||
existing_keys = {item.ratingKey for item in existing.items()}
|
existing_keys = {item.ratingKey for item in existing.items()}
|
||||||
# Filter out tracks already in playlist
|
# Filter out tracks already in playlist
|
||||||
new_tracks = [track for track in tracks if track.ratingKey not in existing_keys]
|
new_tracks = [track_song for track_song in tracks if track_song[0].ratingKey not in existing_keys]
|
||||||
|
|
||||||
if new_tracks:
|
if new_tracks:
|
||||||
existing.addItems(new_tracks)
|
existing.addItems([ts[0] for ts in new_tracks])
|
||||||
print(f"Added {len(new_tracks)} new songs to existing playlist '{playlist_name}':")
|
for track, song in new_tracks:
|
||||||
for track in new_tracks:
|
# Auto-tag added songs
|
||||||
print(f" + '{track.title}' by {track.artist().title}")
|
track.addLabel("Journey FM")
|
||||||
|
track.rate(5)
|
||||||
added = len(new_tracks)
|
added = len(new_tracks)
|
||||||
added_songs.extend([f"{track.title} by {track.artist().title}" for track in new_tracks])
|
added_songs.extend([f"{track.title} by {track.artist().title} ({song['source']})" for track, song in new_tracks])
|
||||||
else:
|
else:
|
||||||
print(f"No new songs to add - all {len(tracks)} matching songs already in playlist.")
|
|
||||||
added = 0
|
added = 0
|
||||||
except:
|
except:
|
||||||
# Playlist doesn't exist, create it
|
# Playlist doesn't exist, create it
|
||||||
plex.createPlaylist(playlist_name, tracks)
|
plex.createPlaylist(playlist_name, [ts[0] for ts in tracks])
|
||||||
print(f"Created playlist '{playlist_name}' with {len(tracks)} songs:")
|
for track, song in tracks:
|
||||||
for track in tracks:
|
# Auto-tag added songs
|
||||||
print(f" + '{track.title}' by {track.artist().title}")
|
track.addLabel("Journey FM")
|
||||||
|
track.rate(5)
|
||||||
added = len(tracks)
|
added = len(tracks)
|
||||||
added_songs.extend([f"{track.title} by {track.artist().title}" for track in tracks])
|
added_songs.extend([f"{track.title} by {track.artist().title} ({song['source']})" for track, song in tracks])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error with playlist: {e}")
|
|
||||||
added = 0
|
added = 0
|
||||||
added_songs = []
|
added_songs = []
|
||||||
else:
|
else:
|
||||||
@@ -272,33 +312,35 @@ def setup_scheduler(params):
|
|||||||
subprocess.run(cmd, check=True)
|
subprocess.run(cmd, check=True)
|
||||||
print("Scheduler setup completed.")
|
print("Scheduler setup completed.")
|
||||||
except subprocess.CalledProcessError as e:
|
except subprocess.CalledProcessError as e:
|
||||||
print(f"Error setting up scheduler: {e}")
|
print(f"Scheduler setup failed: {e}")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global PLEX_TOKEN, SERVER_IP, PLAYLIST_NAME
|
global PLEX_TOKEN, SERVER_IP, PLAYLIST_NAME
|
||||||
# Load or prompt for config
|
# Load or prompt for config
|
||||||
|
selected_stations = ['journey_fm', 'spirit_fm'] # default
|
||||||
if os.path.exists('config.json'):
|
if os.path.exists('config.json'):
|
||||||
with open('config.json', 'r') as f:
|
with open('config.json', 'r') as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
PLEX_TOKEN = config.get('PLEX_TOKEN', PLEX_TOKEN)
|
PLEX_TOKEN = config.get('PLEX_TOKEN', PLEX_TOKEN)
|
||||||
SERVER_IP = config.get('SERVER_IP', SERVER_IP)
|
SERVER_IP = config.get('SERVER_IP', SERVER_IP)
|
||||||
PLAYLIST_NAME = config.get('PLAYLIST_NAME', PLAYLIST_NAME)
|
PLAYLIST_NAME = config.get('PLAYLIST_NAME', PLAYLIST_NAME)
|
||||||
|
# Load selected stations
|
||||||
|
selected_stations_str = config.get('SELECTED_STATIONS', 'journey_fm,spirit_fm')
|
||||||
|
if isinstance(selected_stations_str, str) and selected_stations_str:
|
||||||
|
selected_stations = [s for s in selected_stations_str.split(',') if s]
|
||||||
|
# If empty or invalid, keep default
|
||||||
else:
|
else:
|
||||||
# If no config file, use defaults (GUI should handle this)
|
# If no config file, use defaults (GUI should handle this)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Log start time
|
# Log start time
|
||||||
start_time = datetime.now()
|
start_time = datetime.now()
|
||||||
print(f"\n{'='*60}")
|
|
||||||
print(f"Journey FM Playlist Update - {start_time.strftime('%Y-%m-%d %I:%M:%S %p')}")
|
|
||||||
print(f"{'='*60}\n")
|
|
||||||
|
|
||||||
# Initialize history database
|
# Initialize history database
|
||||||
init_history_db()
|
init_history_db()
|
||||||
|
|
||||||
# Scrape songs
|
# Scrape songs with selected stations
|
||||||
songs = scrape_recently_played()
|
songs = scrape_recently_played(selected_stations)
|
||||||
print(f"Found {len(songs)} recently played songs.")
|
|
||||||
|
|
||||||
# Connect to Plex via MyPlexAccount
|
# Connect to Plex via MyPlexAccount
|
||||||
account = MyPlexAccount(token=PLEX_TOKEN)
|
account = MyPlexAccount(token=PLEX_TOKEN)
|
||||||
@@ -315,20 +357,16 @@ def main():
|
|||||||
break
|
break
|
||||||
|
|
||||||
if not server_resource:
|
if not server_resource:
|
||||||
print(f"Server at {SERVER_IP} not found in your claimed resources.")
|
|
||||||
print("Please claim the server: Open http://172.16.16.106:32400/web, sign in with your Plex account, and follow the claim prompts.")
|
print("Please claim the server: Open http://172.16.16.106:32400/web, sign in with your Plex account, and follow the claim prompts.")
|
||||||
return
|
return
|
||||||
|
|
||||||
plex = server_resource.connect()
|
plex = server_resource.connect()
|
||||||
print(f"Connected to server: {plex.friendlyName}, ID: {plex.machineIdentifier}")
|
|
||||||
|
|
||||||
# Debug: list library sections
|
# Debug: list library sections
|
||||||
print(f"Library sections: {[s.title for s in plex.library.sections()]}")
|
|
||||||
|
|
||||||
# Debug: list existing playlists (handle unicode safely)
|
# Debug: list existing playlists (handle unicode safely)
|
||||||
playlists = plex.playlists()
|
playlists = plex.playlists()
|
||||||
playlist_names = [p.title.encode('ascii', 'ignore').decode('ascii') if not p.title.isascii() else p.title for p in playlists]
|
playlist_names = [p.title.encode('ascii', 'ignore').decode('ascii') if not p.title.isascii() else p.title for p in playlists]
|
||||||
print(f"Existing playlists: {len(playlists)} total")
|
|
||||||
|
|
||||||
# Find matching songs in local library (optional, since Plex search might suffice)
|
# Find matching songs in local library (optional, since Plex search might suffice)
|
||||||
# But to filter only those in local library, perhaps skip or adjust
|
# But to filter only those in local library, perhaps skip or adjust
|
||||||
@@ -346,24 +384,58 @@ def main():
|
|||||||
|
|
||||||
# Create Amazon buy list for missing songs
|
# Create Amazon buy list for missing songs
|
||||||
if missing:
|
if missing:
|
||||||
print(f"\n{len(missing)} songs not found in your library. Creating Amazon buy list...")
|
|
||||||
with open('amazon_buy_list.txt', 'w') as f:
|
# Read existing buy list
|
||||||
f.write("Songs not in your library - Amazon search links:\n\n")
|
existing_songs = set()
|
||||||
for song in missing:
|
if os.path.exists('amazon_buy_list.txt'):
|
||||||
artist = song['artist']
|
try:
|
||||||
title = song['title']
|
with open('amazon_buy_list.txt', 'r') as f:
|
||||||
# Create Amazon search URL
|
content = f.read()
|
||||||
query = urllib.parse.quote(f"{artist} {title}")
|
lines = content.split('\n')
|
||||||
url = f"https://www.amazon.com/s?k={query}&i=digital-music"
|
i = 0
|
||||||
f.write(f"{artist} - {title}\n{url}\n\n")
|
while i < len(lines):
|
||||||
print(f" - {artist} - {title}: {url}")
|
if lines[i].startswith('Songs not in your library'):
|
||||||
print("Buy list saved to amazon_buy_list.txt")
|
i += 2 # Skip header
|
||||||
|
continue
|
||||||
|
if lines[i].strip() and not lines[i].startswith('http'):
|
||||||
|
if ' - ' in lines[i]:
|
||||||
|
artist, title = lines[i].split(' - ', 1)
|
||||||
|
existing_songs.add((artist.strip(), title.strip()))
|
||||||
|
i += 1
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Filter new missing songs
|
||||||
|
new_missing = []
|
||||||
|
for song in missing:
|
||||||
|
key = (song['artist'], song['title'])
|
||||||
|
if key not in existing_songs:
|
||||||
|
new_missing.append(song)
|
||||||
|
existing_songs.add(key)
|
||||||
|
|
||||||
|
if new_missing:
|
||||||
|
with open('amazon_buy_list.txt', 'a') as f: # Append mode
|
||||||
|
if not os.path.exists('amazon_buy_list.txt') or os.path.getsize('amazon_buy_list.txt') == 0:
|
||||||
|
f.write("Songs not in your library - Amazon search links:\n\n")
|
||||||
|
for song in new_missing:
|
||||||
|
artist = song['artist']
|
||||||
|
title = song['title']
|
||||||
|
# Create Amazon search URL
|
||||||
|
query = urllib.parse.quote(f"{artist} {title}")
|
||||||
|
url = f"https://www.amazon.com/s?k={query}&i=digital-music"
|
||||||
|
f.write(f"{artist} - {title}\n{url}\n\n")
|
||||||
|
else:
|
||||||
|
print("No new songs to add to buy list.")
|
||||||
|
|
||||||
|
print("Buy list updated.")
|
||||||
|
|
||||||
# Log completion
|
# Log completion
|
||||||
end_time = datetime.now()
|
end_time = datetime.now()
|
||||||
duration = (end_time - start_time).total_seconds()
|
duration = (end_time - start_time).total_seconds()
|
||||||
print(f"\nCompleted in {duration:.1f} seconds at {end_time.strftime('%I:%M:%S %p')}")
|
|
||||||
print(f"{'='*60}\n")
|
def update_playlist():
|
||||||
|
"""Function to update playlist, callable from GUI"""
|
||||||
|
main()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
Binary file not shown.
@@ -5,3 +5,4 @@ mutagen
|
|||||||
selenium
|
selenium
|
||||||
webdriver-manager
|
webdriver-manager
|
||||||
PySide6
|
PySide6
|
||||||
|
matplotlib
|
||||||
Reference in new issue
Block a user