Author SHA1 Message Date
nmemmert e3085f22ae Add playlist export, enhanced buy list with search/filter/remove, and statistics dashboard 2025-11-17 11:32:25 -05:00
nmemmert e643bb3c98 Add playlist history tracking with SQLite database and GUI dialog 2025-11-17 11:30:20 -05:00
nmemmert 9a90811dea Change to QTextBrowser for better HTML and link display 2025-11-17 11:24:51 -05:00
nmemmert f12b50f9d0 Fix buy list dialog to show properly with modal exec and proper HTML 2025-11-17 11:24:06 -05:00
nmemmert 589cbca2f6 Add debug message to buy list button 2025-11-17 11:23:25 -05:00
nmemmert 3ff79549d9 Add buy list button and dialog to GUI 2025-11-17 11:22:23 -05:00
nmemmert e74845cab6 Fix Amazon URL encoding and update buy list 2025-11-17 11:21:01 -05:00
nmemmert 2893487134 Add Amazon buy list feature for missing songs 2025-11-17 11:17:43 -05:00
nmemmert be9a1a1b64 linux fix app launch 2025-11-17 09:29:21 -05:00
nmemmert d5f2aa0dfd Update README with Linux installer and detailed Plex token instructions; add help button in app for Plex token 2025-11-17 09:26:47 -05:00
nmemmert d357405fb7 Merge branch 'linux' 2025-11-17 09:24:35 -05:00
nmemmert 5f88cdd92b Fix ChromeDriver compatibility and update desktop icon 2025-11-17 09:24:16 -05:00
nmemmert 9b63031c1e Add Linux installer script 2025-11-17 09:18:32 -05:00
nmemmert 5e891ad139 Update Linux app to use Firefox and auto-install dependencies 2025-11-17 08:24:50 -05:00
Nate Emmert c2b91f3f47 windows updaate 2025-11-17 08:22:25 -05:00
Nate Emmert ce5175d8db Add desktop GUI application with system tray support
- Created PySide6-based desktop application
- Added system tray integration for Windows and Linux
- Built-in log viewer and configuration wizard
- Easy setup and automatic playlist updates
- Cross-platform compatibility
- Updated README with comprehensive instructions
2025-11-17 08:08:20 -05:00
nmemmert 345c40f40d Update main.py with a new script 2025-11-15 08:49:41 -05:00
11 changed files with 1332 additions and 29 deletions

No files matched your search

+204 -18
View File
@@ -1,28 +1,214 @@
# Journey FM Playlist Creator
This script automates the process of creating a Plex playlist from recently played songs on My Journey FM radio station.
A cross-platform desktop application that automatically creates Plex playlists from recently played songs on My Journey FM radio station.
## Features
![Application Screenshot](screenshot.png)
- Scrapes recently played songs from https://www.myjourneyfm.com/recently-played/
- Checks your local music library for matching songs
- Creates a playlist in Plex with the found songs
## ✨ Features
## Setup
- **System Tray Integration**: Runs minimized in the taskbar/system tray
- **Automatic Updates**: Configurable automatic playlist updates
- **Cross-Platform**: Works on Windows and Linux
- **Log Viewer**: Built-in log viewer for monitoring updates
- **Easy Setup**: Guided setup wizard for initial configuration
- **Duplicate Prevention**: Smart duplicate detection and prevention
- **Artist Matching**: Handles featured artists, punctuation, and name variations
1. Install dependencies: `pip install -r requirements.txt`
2. Configure the script:
- Set `PLEX_URL` to your Plex server URL
- Set `PLEX_TOKEN` to your Plex authentication token
- Set `MUSIC_LIBRARY_PATH` to the path of your music library
3. Run the script: `python main.py`
## 🚀 Quick Start
## Requirements
### Windows
- Python 3.6+
- Plex server
- Local music library with ID3 tags
1. **Install Dependencies**
```bash
pip install -r requirements.txt
```
## Note
2. **Create Application Icon**
```bash
python create_icon.py
```
The HTML scraping part may need adjustment based on the actual structure of the website. Inspect the page to find the correct selectors for artist and title.
3. **Run the Application**
```bash
python journey_fm_app.py
```
Or use the batch file: `run_app.bat`
### Linux
1. **Run the Installer**
```bash
./install.sh
```
This will:
- Install Python dependencies
- Set up virtual environment
- Install Chromium browser
- Create desktop icon
- Add to applications menu
2. **Launch from Menu**
- Search for "Journey FM Playlist" in your applications menu
- Or run: `./journey_fm_app.sh`
## ⚙️ Configuration
### First-Time Setup
When you first run the application, a setup wizard will guide you through:
1. **Plex Token**: See below for how to get your Plex token
2. **Server IP**: Your Plex server's local IP address (e.g., 192.168.1.100)
3. **Playlist Name**: Name for your Journey FM playlist (default: "Journey FM Recently Played")
4. **Auto-Update Settings**: Enable automatic updates and set intervals
#### How to Get Your Plex Token
1. Go to [https://plex.tv/claim](https://plex.tv/claim) in your web browser
2. Sign in with your Plex account
3. Copy the claim token from the page (it looks like: `pU-mHWYUZU6iXJFhJyA`)
4. Paste it into the application setup
**Note**: This token is used to authenticate with your Plex server. Keep it secure and don't share it.
### Manual Configuration
You can also edit settings anytime through the Settings menu in the application.
## 📱 System Tray Features
- **Minimize to Tray**: The app minimizes to system tray instead of closing
- **Right-click Menu**:
- Show: Restore the main window
- Update Now: Manually trigger playlist update
- Quit: Exit the application
- **Double-click**: Restore the main window
- **Notifications**: Get notified when automatic updates complete
## 📋 Log Viewer
The built-in log viewer shows:
- Update timestamps
- Songs added to playlist
- Error messages
- Update duration
- Connection status
Logs are automatically saved to `playlist_log.txt` for external viewing.
## 🔄 Automatic Updates
Configure automatic updates to run every:
- 5-120 minutes
- Hours (up to several hours)
The app will:
- Run in the background
- Show tray notifications when updates complete
- Prevent duplicate songs
- Handle connection errors gracefully
## 🛠️ Troubleshooting
### Common Issues
**"Server not found"**
- Ensure your Plex server is running
- Check that you've claimed the server at http://[SERVER_IP]:32400/web
- Verify the server IP address in settings
**"No matching tracks found"**
- Ensure your music library is properly scanned in Plex
- Check that song metadata matches (artist names, titles)
- The app handles variations like "&" vs "and", featured artists, etc.
**System tray not working**
- On Linux, ensure you have a system tray (like KDE/GNOME)
- Some desktop environments may need additional packages
### Manual Testing
Test the core functionality:
```bash
python main.py
```
This runs the update once without the GUI.
## 📁 Project Structure
```
journey-fm-playlist/
├── journey_fm_app.py # Main GUI application
├── main.py # Core update logic
├── create_icon.py # Icon generator
├── run_app.bat # Windows launcher
├── journey_fm_app.sh # Linux launcher
├── requirements.txt # Python dependencies
├── config.json # Configuration (auto-generated)
├── playlist_log.txt # Update logs
├── icon.png # Application icon
└── README.md # This file
```
## 🔧 Development
### Adding Features
The application is built with PyQt6 for cross-platform compatibility. Key components:
- `MainWindow`: Main application window
- `SystemTrayApp`: Tray icon and menu
- `SetupWizard`: Configuration wizard
- `LogViewer`: Log display widget
- `UpdateWorker`: Background update thread
### Building Standalone Executables
**Windows (PyInstaller)**
```bash
pip install pyinstaller
pyinstaller --onefile --windowed --icon=icon.ico journey_fm_app.py
```
**Linux (PyInstaller)**
```bash
pip install pyinstaller
pyinstaller --onefile --icon=icon.png journey_fm_app.py
```
## 📋 Requirements
- **Python**: 3.8+
- **PyQt6**: GUI framework
- **Selenium**: Web scraping
- **PlexAPI**: Plex integration
- **BeautifulSoup4**: HTML parsing
### System Requirements
- **Windows**: 10+ with system tray support
- **Linux**: Modern desktop environment with system tray
- **Plex Server**: Local or remote Plex server access
## 🤝 Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test on both Windows and Linux
5. Submit a pull request
## 📄 License
This project is open source. Feel free to use and modify as needed.
## 🆘 Support
If you encounter issues:
1. Check the application logs
2. Verify your Plex server connection
3. Ensure all dependencies are installed
4. Test with `python main.py` first
For bugs or feature requests, please create an issue on GitHub.
+8
View File
@@ -0,0 +1,8 @@
Songs not in your library - Amazon search links:
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
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""
Create application icon
"""
from PySide6.QtGui import QIcon, QPixmap, QPainter, QColor, QFont
from PySide6.QtCore import QSize
def create_icon():
"""Create a simple application icon"""
pixmap = QPixmap(64, 64)
pixmap.fill(QColor(30, 144, 255)) # Dodger blue background
painter = QPainter(pixmap)
painter.setPen(QColor(255, 255, 255))
painter.setFont(QFont("Arial", 32, QFont.Weight.Bold))
# Draw music note symbol
painter.drawText(16, 48, "♪")
painter.end()
icon = QIcon(pixmap)
return icon
if __name__ == "__main__":
from PySide6.QtWidgets import QApplication
import sys
app = QApplication(sys.argv)
icon = create_icon()
pixmap = icon.pixmap(QSize(64, 64))
pixmap.save("icon.png")
print("Icon created: icon.png")
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Executable
+63
View File
@@ -0,0 +1,63 @@
#!/bin/bash
# Journey FM Playlist Creator - Linux Installer
echo "Journey FM Playlist Creator - Linux Installer"
echo "=============================================="
# Get the directory where this script is located
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
echo "Installing in: $DIR"
# Check if Python 3 is installed
if ! command -v python3 > /dev/null 2>&1; then
echo "Python 3 not found. Installing Python 3..."
sudo apt update && sudo apt install -y python3 python3-pip python3-venv
fi
# Create virtual environment
echo "Creating virtual environment..."
python3 -m venv "$DIR/.venv"
# Activate virtual environment
source "$DIR/.venv/bin/activate"
# Install Python dependencies
echo "Installing Python dependencies..."
pip install -r "$DIR/requirements.txt"
# Install Chromium browser
if ! command -v chromium-browser > /dev/null 2>&1; then
echo "Installing Chromium browser..."
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
cat > ~/.local/share/applications/journey-fm.desktop << EOF
[Desktop Entry]
Name=Journey FM Playlist
Exec=$DIR/journey_fm_app.sh
Icon=$DIR/icon.png
Type=Application
Categories=Utility;
EOF
chmod +x ~/.local/share/applications/journey-fm.desktop
# Update desktop database
echo "Updating desktop database..."
update-desktop-database ~/.local/share/applications
# Make scripts executable
chmod +x "$DIR/journey_fm_app.sh"
echo "Installation complete!"
echo "You can now find 'Journey FM Playlist' in your applications menu."
echo "Or run: $DIR/journey_fm_app.sh"
+837
View File
@@ -0,0 +1,837 @@
#!/usr/bin/env python3
"""
Journey FM Playlist Creator - Desktop Application
A cross-platform desktop application for automating Journey FM playlist updates.
Features system tray integration, log viewing, and easy setup.
"""
import sys
import os
import json
import threading
import time
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
)
# Import our existing functionality
from main import scrape_recently_played, create_playlist_in_plex, PLEX_TOKEN, SERVER_IP, PLAYLIST_NAME
class Config:
"""Configuration management"""
def __init__(self):
self.settings = QSettings('JourneyFM', 'PlaylistCreator')
self.config_file = Path('config.json')
def get(self, key, default=None):
return self.settings.value(key, default)
def set(self, key, value):
self.settings.setValue(key, value)
self.settings.sync()
def load_config(self):
"""Load config from JSON file for backward compatibility"""
if self.config_file.exists():
try:
with open(self.config_file, 'r') as f:
config = json.load(f)
# Migrate to QSettings
for key, value in config.items():
self.set(key, value)
# Remove old file
self.config_file.unlink()
return config
except:
pass
return {}
def save_config(self, config):
"""Save config to QSettings"""
for key, value in config.items():
self.set(key, value)
class SetupWizard(QDialog):
"""Setup wizard for initial configuration"""
def __init__(self, config, parent=None):
super().__init__(parent)
self.config = config
self.setWindowTitle("Journey FM Setup")
self.setModal(True)
self.resize(500, 400)
layout = QVBoxLayout()
# Welcome message
welcome = QLabel("Welcome to Journey FM Playlist Creator!")
welcome.setFont(QFont("Arial", 14, QFont.Weight.Bold))
layout.addWidget(welcome)
desc = QLabel("This wizard will help you set up automatic playlist updates from Journey FM.")
desc.setWordWrap(True)
layout.addWidget(desc)
# Configuration form
form_group = QGroupBox("Plex Configuration")
form_layout = QFormLayout()
self.token_input = QLineEdit()
self.token_input.setPlaceholderText("Get this from https://plex.tv/claim")
# Token input with help button
token_layout = QHBoxLayout()
token_layout.addWidget(self.token_input)
help_button = QPushButton("?")
help_button.setMaximumWidth(30)
help_button.setToolTip("Click to open Plex token page")
help_button.clicked.connect(lambda: QDesktopServices.openUrl("https://plex.tv/claim"))
token_layout.addWidget(help_button)
form_layout.addRow("Plex Token:", token_layout)
self.server_input = QLineEdit()
self.server_input.setPlaceholderText("e.g., 192.168.1.100")
form_layout.addRow("Server IP:", self.server_input)
self.playlist_input = QLineEdit("Journey FM Recently Played")
form_layout.addRow("Playlist Name:", self.playlist_input)
form_group.setLayout(form_layout)
layout.addWidget(form_group)
# Scheduling options
sched_group = QGroupBox("Scheduling")
sched_layout = QVBoxLayout()
self.auto_update = QCheckBox("Enable automatic updates")
self.auto_update.setChecked(True)
sched_layout.addWidget(self.auto_update)
interval_layout = QHBoxLayout()
interval_layout.addWidget(QLabel("Update every"))
self.interval_spin = QSpinBox()
self.interval_spin.setValue(15)
self.interval_spin.setRange(5, 120)
interval_layout.addWidget(self.interval_spin)
self.interval_unit = QComboBox()
self.interval_unit.addItems(["Minutes", "Hours"])
interval_layout.addWidget(self.interval_unit)
interval_layout.addStretch()
sched_layout.addLayout(interval_layout)
sched_group.setLayout(sched_layout)
layout.addWidget(sched_group)
# Buttons
buttons = QDialogButtonBox(
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
)
buttons.accepted.connect(self.accept)
buttons.rejected.connect(self.reject)
layout.addWidget(buttons)
self.setLayout(layout)
# Load existing config
self.token_input.setText(config.get('PLEX_TOKEN', ''))
self.server_input.setText(config.get('SERVER_IP', ''))
self.playlist_input.setText(config.get('PLAYLIST_NAME', 'Journey FM Recently Played'))
def get_config(self):
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()
}
class LogViewer(QWidget):
"""Widget for viewing application logs"""
def __init__(self, parent=None):
super().__init__(parent)
layout = QVBoxLayout()
# Header
header = QHBoxLayout()
header.addWidget(QLabel("Application Logs"))
clear_btn = QPushButton("Clear")
clear_btn.clicked.connect(self.clear_logs)
header.addWidget(clear_btn)
header.addStretch()
layout.addLayout(header)
# Log text area
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
self.log_text.setFont(QFont("Consolas", 9))
layout.addWidget(self.log_text)
# Load existing logs
self.load_logs()
self.setLayout(layout)
def load_logs(self):
"""Load logs from file"""
log_file = Path('playlist_log.txt')
if log_file.exists():
try:
with open(log_file, 'r', encoding='utf-8') as f:
content = f.read()
self.log_text.setPlainText(content)
# Scroll to bottom
cursor = self.log_text.textCursor()
cursor.movePosition(cursor.MoveOperation.End)
self.log_text.setTextCursor(cursor)
except Exception as e:
self.log_text.setPlainText(f"Error loading logs: {e}")
def clear_logs(self):
"""Clear the log file"""
try:
with open('playlist_log.txt', 'w', encoding='utf-8') as f:
f.write("")
self.log_text.clear()
except Exception as e:
QMessageBox.warning(self, "Error", f"Failed to clear logs: {e}")
def refresh_logs(self):
"""Refresh the log display"""
self.load_logs()
class UpdateWorker(QThread):
"""Worker thread for running playlist updates"""
finished = Signal(str)
progress = Signal(str)
def run(self):
try:
self.progress.emit("Starting playlist update...")
# Import here to avoid circular imports
from main import main as update_main
# Capture output by redirecting stdout
import io
from contextlib import redirect_stdout
output_buffer = io.StringIO()
with redirect_stdout(output_buffer):
update_main()
result = output_buffer.getvalue()
self.finished.emit(result)
except Exception as e:
self.finished.emit(f"Error: {str(e)}")
class MainWindow(QMainWindow):
"""Main application window"""
def __init__(self):
super().__init__()
self.tray_icon = None # Will be set later
self.config = Config()
self.config.load_config()
self.setWindowTitle("Journey FM Playlist Creator")
self.setWindowIcon(QIcon("icon.png")) # You'll need to add an icon
self.resize(800, 600)
# Create central widget
central = QWidget()
self.setCentralWidget(central)
layout = QVBoxLayout(central)
# Toolbar
toolbar = self.addToolBar("Main")
toolbar.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
update_action = QAction("Update Now", self)
update_action.triggered.connect(self.manual_update)
toolbar.addAction(update_action)
settings_action = QAction("Settings", self)
settings_action.triggered.connect(self.show_settings)
toolbar.addAction(settings_action)
# Progress bar
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
layout.addWidget(self.progress_bar)
# Splitter for main content
splitter = QSplitter(Qt.Orientation.Vertical)
# Status section
status_group = QGroupBox("Status")
status_layout = QVBoxLayout()
self.status_label = QLabel("Ready")
status_layout.addWidget(self.status_label)
self.last_update_label = QLabel("Last update: Never")
status_layout.addWidget(self.last_update_label)
# Buy list button
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
self.history_button = QPushButton("View History")
self.history_button.clicked.connect(self.show_history)
status_layout.addWidget(self.history_button)
# Export button
self.export_button = QPushButton("Export Playlist")
self.export_button.clicked.connect(self.export_playlist)
status_layout.addWidget(self.export_button)
# Statistics button
self.stats_button = QPushButton("Statistics")
self.stats_button.clicked.connect(self.show_statistics)
status_layout.addWidget(self.stats_button)
status_group.setLayout(status_layout)
splitter.addWidget(status_group)
# Log viewer
self.log_viewer = LogViewer()
splitter.addWidget(self.log_viewer)
splitter.setSizes([100, 400])
layout.addWidget(splitter)
# Setup timer for automatic updates
self.update_timer = QTimer()
self.update_timer.timeout.connect(self.auto_update)
# Load settings and start auto-update if enabled
self.load_settings()
# Check if first run
if not self.config.get('PLEX_TOKEN'):
self.show_setup_wizard()
def set_tray_icon(self, tray_icon):
"""Set the tray icon reference"""
self.tray_icon = tray_icon
def show_setup_wizard(self):
"""Show the setup wizard"""
wizard = SetupWizard(self.config, self)
if wizard.exec() == QDialog.DialogCode.Accepted:
config = wizard.get_config()
self.config.save_config(config)
self.load_settings()
QMessageBox.information(self, "Setup Complete",
"Configuration saved! The app will now update your playlist automatically.")
def load_settings(self):
"""Load settings and configure auto-update"""
auto_update = self.config.get('AUTO_UPDATE', False)
if auto_update:
interval = int(self.config.get('UPDATE_INTERVAL', 15))
unit = self.config.get('UPDATE_UNIT', 'Minutes')
# Convert to milliseconds
if unit == 'Hours':
interval_ms = interval * 60 * 60 * 1000
else:
interval_ms = interval * 60 * 1000
self.update_timer.start(interval_ms)
self.status_label.setText(f"Auto-update enabled (every {interval} {unit.lower()})")
else:
self.update_timer.stop()
self.status_label.setText("Auto-update disabled")
def manual_update(self):
"""Manually trigger playlist update"""
if hasattr(self, 'worker') and self.worker.isRunning():
QMessageBox.information(self, "Update in Progress",
"An update is already running. Please wait.")
return
self.worker = UpdateWorker()
self.worker.progress.connect(self.update_progress)
self.worker.finished.connect(self.update_finished)
self.worker.start()
self.progress_bar.setVisible(True)
self.progress_bar.setRange(0, 0) # Indeterminate progress
self.status_label.setText("Updating playlist...")
def update_progress(self, message):
"""Update progress display"""
self.status_label.setText(message)
def update_finished(self, result):
"""Handle update completion"""
self.progress_bar.setVisible(False)
self.last_update_label.setText(f"Last update: {datetime.now().strftime('%Y-%m-%d %I:%M:%S %p')}")
self.status_label.setText("Ready")
# Append result to log file
try:
with open('playlist_log.txt', 'a', encoding='utf-8') as f:
f.write(result + '\n')
except Exception as e:
print(f"Error writing to log: {e}")
# Refresh logs
self.log_viewer.refresh_logs()
# Show result in status
if "Error" in result:
QMessageBox.warning(self, "Update Error", result)
else:
# Extract summary from result
lines = result.strip().split('\n')
for line in reversed(lines):
if line.startswith("Added") or line.startswith("No new songs") or line.startswith("No matching"):
self.status_label.setText(line)
break
def auto_update(self):
"""Automatic update (runs in background)"""
if hasattr(self, 'worker') and self.worker.isRunning():
return # Skip if already running
self.worker = UpdateWorker()
self.worker.finished.connect(self.auto_update_finished)
self.worker.start()
def auto_update_finished(self, result):
"""Handle automatic update completion"""
self.last_update_label.setText(f"Last update: {datetime.now().strftime('%Y-%m-%d %I:%M:%S %p')}")
# Append result to log file
try:
with open('playlist_log.txt', 'a', encoding='utf-8') as f:
f.write(result + '\n')
except Exception as e:
print(f"Error writing to log: {e}")
# Refresh logs
self.log_viewer.refresh_logs()
# Show tray notification if minimized
if self.isMinimized() or not self.isVisible():
if self.tray_icon:
self.tray_icon.showMessage(
"Journey FM Update",
"Playlist updated successfully",
QSystemTrayIcon.MessageIcon.Information,
3000
)
def show_settings(self):
"""Show settings dialog"""
wizard = SetupWizard(self.config, self)
if wizard.exec() == QDialog.DialogCode.Accepted:
config = wizard.get_config()
self.config.save_config(config)
self.load_settings()
QMessageBox.information(self, "Settings Saved", "Configuration updated successfully!")
def show_buy_list(self):
"""Show the Amazon buy list dialog with interactive features"""
try:
with open('amazon_buy_list.txt', '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
# 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
dialog = QDialog(self)
dialog.setWindowTitle("Amazon Buy List")
dialog.setModal(True)
dialog.resize(600, 400)
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)
# List widget
self.buy_list_widget = QListWidget()
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)
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)
layout.addLayout(button_layout)
dialog.setLayout(layout)
dialog.exec()
def populate_buy_list(self, songs):
"""Populate the buy list widget"""
self.buy_list_widget.clear()
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
self.buy_list_widget.addItem(item)
def filter_buy_list(self, all_songs):
"""Filter the buy list based on search text"""
search_text = self.search_input.text().lower()
filtered = [(at, url) for at, url in all_songs if search_text in at.lower()]
self.populate_buy_list(filtered)
def open_selected_buy_items(self):
"""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
url = item.data(1)
QDesktopServices.openUrl(url)
def remove_selected_buy_items(self, all_songs, dialog):
"""Remove selected items from buy list"""
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
artist_title = item.text()
to_remove.append(artist_title)
if not to_remove:
QMessageBox.information(dialog, "No Selection", "Please select items to remove.")
return
# Confirm
reply = QMessageBox.question(dialog, "Confirm Removal",
f"Remove {len(to_remove)} selected items from buy list?",
QMessageBox.Yes | QMessageBox.No)
if reply != QMessageBox.Yes:
return
# Remove from all_songs
updated_songs = [(at, url) for at, url in all_songs if at not in to_remove]
# Rewrite file
with open('amazon_buy_list.txt', 'w') as f:
f.write("Songs not in your library - Amazon search links:\n\n")
for artist_title, url in updated_songs:
f.write(f"{artist_title}\n{url}\n\n")
# Update display
self.populate_buy_list(updated_songs)
QMessageBox.information(dialog, "Removed", f"Removed {len(to_remove)} items from buy list.")
def show_statistics(self):
"""Show statistics dashboard"""
import sqlite3
import json
dialog = QDialog(self)
dialog.setWindowTitle("Statistics Dashboard")
dialog.setModal(True)
dialog.resize(400, 300)
layout = QVBoxLayout()
try:
conn = sqlite3.connect('playlist_history.db')
c = conn.cursor()
c.execute('SELECT COUNT(*), SUM(added_count), SUM(missing_count) FROM history')
result = c.fetchone()
conn.close()
total_updates = result[0] or 0
total_added = result[1] or 0
total_missing = result[2] or 0
layout.addWidget(QLabel(f"Total Updates: {total_updates}"))
layout.addWidget(QLabel(f"Total Songs Added to Plex: {total_added}"))
layout.addWidget(QLabel(f"Total Songs in Buy List: {total_missing}"))
if total_updates > 0:
avg_added = total_added / total_updates
avg_missing = total_missing / total_updates
layout.addWidget(QLabel(f"Average Songs Added per Update: {avg_added:.1f}"))
layout.addWidget(QLabel(f"Average Missing Songs per Update: {avg_missing:.1f}"))
except Exception as e:
layout.addWidget(QLabel(f"Error loading statistics: {str(e)}"))
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
import json
from datetime import datetime
dialog = QDialog(self)
dialog.setWindowTitle("Playlist History")
dialog.setModal(True)
dialog.resize(800, 600)
layout = QVBoxLayout()
table = QTableWidget()
table.setColumnCount(4)
table.setHorizontalHeaderLabels(["Date", "Added Songs", "Missing Songs", "Details"])
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')
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
try:
dt = datetime.fromisoformat(date)
date_str = dt.strftime('%Y-%m-%d %H:%M')
except:
date_str = date
# 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"
except:
added_text = f"{added_count} songs"
# 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"
except:
missing_text = f"{missing_count} songs"
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}"))
except Exception as e:
table.setRowCount(1)
table.setItem(0, 0, QTableWidgetItem("Error loading history"))
table.setItem(0, 1, QTableWidgetItem(str(e)))
layout.addWidget(table)
buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
buttons.rejected.connect(dialog.close)
layout.addWidget(buttons)
dialog.setLayout(layout)
dialog.exec()
def export_playlist(self):
"""Export the current Plex playlist to CSV"""
try:
# Get config
plex_token = self.config.get('PLEX_TOKEN')
server_ip = self.config.get('SERVER_IP')
playlist_name = self.config.get('PLAYLIST_NAME', 'Journey FM Recently Played')
if not plex_token or not server_ip:
QMessageBox.warning(self, "Configuration Error", "Plex token and server IP are required. Please check settings.")
return
# Connect to Plex
from plexapi.myplex import MyPlexAccount
account = MyPlexAccount(token=plex_token)
resources = account.resources()
server_resource = None
for r in resources:
for conn in r.connections:
if server_ip in conn.address:
server_resource = r
break
if server_resource:
break
if not server_resource:
QMessageBox.warning(self, "Connection Error", f"Server at {server_ip} not found.")
return
plex = server_resource.connect()
# Get playlist
try:
playlist = plex.playlist(playlist_name)
tracks = playlist.items()
except:
QMessageBox.warning(self, "Playlist Error", f"Playlist '{playlist_name}' not found.")
return
# Export to CSV
import csv
with open('playlist_export.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['Title', 'Artist', 'Album'])
for track in tracks:
writer.writerow([
track.title,
track.artist().title if track.artist() else '',
track.album().title if track.album() else ''
])
QMessageBox.information(self, "Export Complete", f"Playlist exported to playlist_export.csv ({len(tracks)} songs)")
except Exception as e:
QMessageBox.critical(self, "Export Error", f"Failed to export playlist: {str(e)}")
def closeEvent(self, event):
"""Handle window close - minimize to tray instead of closing"""
if self.tray_icon and self.tray_icon.isVisible():
self.hide()
self.tray_icon.showMessage(
"Journey FM",
"Application minimized to system tray",
QSystemTrayIcon.MessageIcon.Information,
2000
)
event.ignore()
else:
event.accept()
class SystemTrayApp:
"""System tray application wrapper"""
def __init__(self):
self.app = QApplication(sys.argv)
self.app.setApplicationName("Journey FM Playlist Creator")
self.app.setApplicationVersion("1.0")
self.app.setOrganizationName("JourneyFM")
# Create main window first
self.main_window = MainWindow()
# Create system tray
self.create_tray_icon()
# Set tray icon reference in main window
self.main_window.set_tray_icon(self.tray_icon)
# Show window initially
self.main_window.show()
def create_tray_icon(self):
"""Create system tray icon"""
if not QSystemTrayIcon.isSystemTrayAvailable():
QMessageBox.critical(None, "System Tray",
"System tray not available on this system")
return
self.tray_icon = QSystemTrayIcon(self.main_window)
self.tray_icon.setIcon(self.main_window.windowIcon())
# Create tray menu
tray_menu = QMenu()
show_action = QAction("Show", self.main_window)
show_action.triggered.connect(self.main_window.show)
tray_menu.addAction(show_action)
update_action = QAction("Update Now", self.main_window)
update_action.triggered.connect(self.main_window.manual_update)
tray_menu.addAction(update_action)
tray_menu.addSeparator()
quit_action = QAction("Quit", self.main_window)
quit_action.triggered.connect(self.app.quit)
tray_menu.addAction(quit_action)
self.tray_icon.setContextMenu(tray_menu)
self.tray_icon.activated.connect(self.tray_activated)
self.tray_icon.show()
def tray_activated(self, reason):
"""Handle tray icon activation"""
if reason == QSystemTrayIcon.ActivationReason.DoubleClick:
self.main_window.show()
self.main_window.raise_()
self.main_window.activateWindow()
def run(self):
"""Run the application"""
return self.app.exec()
def main():
"""Main application entry point"""
# Set up high DPI scaling
QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
)
# Create and run application
tray_app = SystemTrayApp()
sys.exit(tray_app.run())
if __name__ == "__main__":
main()
+26
View File
@@ -0,0 +1,26 @@
#!/bin/bash
# Journey FM Playlist Creator - Linux Startup Script
# Get the directory where this script is located
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
# Create virtual environment if it doesn't exist
if [ ! -d "$DIR/.venv" ]; then
python3 -m venv "$DIR/.venv"
fi
# Activate virtual environment
source "$DIR/.venv/bin/activate"
# Install dependencies
pip install -r "$DIR/requirements.txt"
# Check if Chromium is installed, install if not
if ! command -v chromium-browser > /dev/null 2>&1; then
echo "Chromium not found. Installing Chromium..."
sudo apt update && sudo apt install -y chromium-browser
fi
# Run the application
cd "$DIR"
python3 journey_fm_app.py
+119 -10
View File
@@ -11,16 +11,21 @@ import time
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from webdriver_manager.chrome import ChromeDriverManager
from selenium.webdriver.common.by import By
from bs4 import BeautifulSoup
import os
from plexapi.myplex import MyPlexAccount
from mutagen.easyid3 import EasyID3
import re
from datetime import datetime
import json
import subprocess
import urllib.parse
import sqlite3
import urllib.parse
# Configuration
# Configuration (defaults, will be overridden by config.json)
PLEX_TOKEN = 'pU-m3HWYUZU6iXJFhJyA' # Your Plex.tv token
SERVER_IP = '172.16.16.106' # Your local server IP
PLAYLIST_NAME = 'Journey FM Recently Played'
@@ -32,7 +37,12 @@ def scrape_recently_played():
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")
options.add_argument("--disable-gpu")
service = Service(ChromeDriverManager().install())
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)
url = 'https://www.myjourneyfm.com/recently-played/'
@@ -95,7 +105,7 @@ def scrape_recently_played():
def normalize_string(s):
"""Normalize string for comparison"""
return re.sub(r'[^\w\s]', '', s).lower().strip()
return re.sub(r'[^\\w\\s]', '', s).lower().strip()
def find_song_in_library(artist, title, library_path):
"""Check if song exists in local library"""
@@ -113,11 +123,13 @@ def find_song_in_library(artist, title, library_path):
return None
def create_playlist_in_plex(plex, songs, playlist_name):
"""Create playlist in Plex"""
"""Create playlist in Plex and return added count, added songs, and missing songs"""
# Assuming Plex has a music library named 'Music'
music_library = plex.library.section('Music')
tracks = []
seen_keys = set() # Track which ratingKeys we've already added to prevent duplicates within this run
missing = []
added_songs = []
for song in songs:
# Clean title and artist by removing text in parentheses and normalizing spaces
@@ -131,7 +143,7 @@ def create_playlist_in_plex(plex, songs, playlist_name):
continue
# Skip entries that are ONLY punctuation (but allow titles with punctuation in them)
if re.match(r'^[!?\s]+$', clean_title):
if re.match(r'^[!?\\\\s]+$', clean_title):
continue
# Remove only ! and ? for searching, but keep apostrophes
@@ -152,7 +164,7 @@ def create_playlist_in_plex(plex, songs, playlist_name):
search_artist_lower = search_artist.lower()
# Clean featured artists from search (e.g., "W/ Emerson Day", "feat. Someone")
search_artist_main = re.sub(r'\s+(w/|feat\.|featuring|ft\.).*', '', search_artist_lower).strip()
search_artist_main = re.sub(r'\s+(w/|feat\.||featuring|ft\.).*', '', search_artist_lower).strip()
# Normalize & and + in track artist as well
track_artist = re.sub(r'\s*[&+]\s*', ' and ', track_artist)
@@ -169,8 +181,9 @@ def create_playlist_in_plex(plex, songs, playlist_name):
break
# If no artist match, skip this track (don't add wrong songs)
if not matched:
pass # Don't add if artist doesn't match # Take the first match
missing.append(song)
added = 0
if tracks:
try:
# Try to find existing playlist
@@ -186,26 +199,103 @@ def create_playlist_in_plex(plex, songs, playlist_name):
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}")
added = len(new_tracks)
added_songs.extend([f"{track.title} by {track.artist().title}" for track 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}")
added = len(tracks)
added_songs.extend([f"{track.title} by {track.artist().title}" for track in tracks])
except Exception as e:
print(f"Error with playlist: {e}")
added = 0
added_songs = []
else:
print("No matching tracks found in Plex.")
added = 0
added_songs = []
return added, added_songs, missing
def prompt_for_config():
"""Prompt user for configuration variables"""
print("Configuration not found. Please provide the following:")
plex_token = input("Enter your Plex token: ").strip()
server_ip = input("Enter your Plex server IP: ").strip()
playlist_name = input("Enter the playlist name: ").strip()
config = {
'PLEX_TOKEN': plex_token,
'SERVER_IP': server_ip,
'PLAYLIST_NAME': playlist_name
}
with open('config.json', 'w') as f:
json.dump(config, f)
return config
def init_history_db():
"""Initialize the history database"""
conn = sqlite3.connect('playlist_history.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS history (
id INTEGER PRIMARY KEY,
date TEXT,
added_count INTEGER,
added_songs TEXT,
missing_count INTEGER,
missing_songs TEXT
)''')
conn.commit()
conn.close()
def save_history_entry(added_count, added_songs, missing_count, missing_songs):
"""Save a history entry to the database"""
conn = sqlite3.connect('playlist_history.db')
c = conn.cursor()
date = datetime.now().isoformat()
c.execute('INSERT INTO history (date, added_count, added_songs, missing_count, missing_songs) VALUES (?, ?, ?, ?, ?)',
(date, added_count, json.dumps(added_songs), missing_count, json.dumps(missing_songs)))
conn.commit()
conn.close()
def setup_scheduler(params):
"""Run the setup_scheduler.ps1 with parameters"""
cmd = ['powershell.exe', '-File', 'setup_scheduler.ps1']
for k, v in params.items():
cmd.extend(['-' + k, str(v)])
try:
subprocess.run(cmd, check=True)
print("Scheduler setup completed.")
except subprocess.CalledProcessError as e:
print(f"Error setting up scheduler: {e}")
def main():
global PLEX_TOKEN, SERVER_IP, PLAYLIST_NAME
# Load or prompt for config
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)
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.")
@@ -246,9 +336,28 @@ def main():
# Create playlist
if songs:
create_playlist_in_plex(plex, songs, PLAYLIST_NAME)
added, added_songs, missing = create_playlist_in_plex(plex, songs, PLAYLIST_NAME)
else:
print("No songs found.")
added, added_songs, missing = 0, [], []
# Save to history
save_history_entry(added, added_songs, len(missing), missing)
# 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")
# Log completion
end_time = datetime.now()
@@ -257,4 +366,4 @@ def main():
print(f"{'='*60}\n")
if __name__ == '__main__':
main()
main()
+28
View File
@@ -0,0 +1,28 @@
Requirement already satisfied: requests in ./.venv/lib/python3.13/site-packages (from -r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 1)) (2.32.5)
Requirement already satisfied: beautifulsoup4 in ./.venv/lib/python3.13/site-packages (from -r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 2)) (4.14.2)
Requirement already satisfied: plexapi in ./.venv/lib/python3.13/site-packages (from -r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 3)) (4.17.1)
Requirement already satisfied: mutagen in ./.venv/lib/python3.13/site-packages (from -r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 4)) (1.47.0)
Requirement already satisfied: selenium in ./.venv/lib/python3.13/site-packages (from -r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 5)) (4.38.0)
Requirement already satisfied: webdriver-manager in ./.venv/lib/python3.13/site-packages (from -r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 6)) (4.0.2)
Requirement already satisfied: PySide6 in ./.venv/lib/python3.13/site-packages (from -r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 7)) (6.10.0)
Requirement already satisfied: charset_normalizer<4,>=2 in ./.venv/lib/python3.13/site-packages (from requests->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 1)) (3.4.4)
Requirement already satisfied: idna<4,>=2.5 in ./.venv/lib/python3.13/site-packages (from requests->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 1)) (3.11)
Requirement already satisfied: urllib3<3,>=1.21.1 in ./.venv/lib/python3.13/site-packages (from requests->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 1)) (2.5.0)
Requirement already satisfied: certifi>=2017.4.17 in ./.venv/lib/python3.13/site-packages (from requests->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 1)) (2025.11.12)
Requirement already satisfied: soupsieve>1.2 in ./.venv/lib/python3.13/site-packages (from beautifulsoup4->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 2)) (2.8)
Requirement already satisfied: typing-extensions>=4.0.0 in ./.venv/lib/python3.13/site-packages (from beautifulsoup4->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 2)) (4.15.0)
Requirement already satisfied: trio<1.0,>=0.31.0 in ./.venv/lib/python3.13/site-packages (from selenium->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 5)) (0.32.0)
Requirement already satisfied: trio-websocket<1.0,>=0.12.2 in ./.venv/lib/python3.13/site-packages (from selenium->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 5)) (0.12.2)
Requirement already satisfied: websocket-client<2.0,>=1.8.0 in ./.venv/lib/python3.13/site-packages (from selenium->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 5)) (1.9.0)
Requirement already satisfied: attrs>=23.2.0 in ./.venv/lib/python3.13/site-packages (from trio<1.0,>=0.31.0->selenium->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 5)) (25.4.0)
Requirement already satisfied: sortedcontainers in ./.venv/lib/python3.13/site-packages (from trio<1.0,>=0.31.0->selenium->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 5)) (2.4.0)
Requirement already satisfied: outcome in ./.venv/lib/python3.13/site-packages (from trio<1.0,>=0.31.0->selenium->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 5)) (1.3.0.post0)
Requirement already satisfied: sniffio>=1.3.0 in ./.venv/lib/python3.13/site-packages (from trio<1.0,>=0.31.0->selenium->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 5)) (1.3.1)
Requirement already satisfied: wsproto>=0.14 in ./.venv/lib/python3.13/site-packages (from trio-websocket<1.0,>=0.12.2->selenium->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 5)) (1.3.1)
Requirement already satisfied: pysocks!=1.5.7,<2.0,>=1.5.6 in ./.venv/lib/python3.13/site-packages (from urllib3[socks]<3.0,>=2.5.0->selenium->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 5)) (1.7.1)
Requirement already satisfied: python-dotenv in ./.venv/lib/python3.13/site-packages (from webdriver-manager->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 6)) (1.2.1)
Requirement already satisfied: packaging in ./.venv/lib/python3.13/site-packages (from webdriver-manager->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 6)) (25.0)
Requirement already satisfied: shiboken6==6.10.0 in ./.venv/lib/python3.13/site-packages (from PySide6->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 7)) (6.10.0)
Requirement already satisfied: PySide6_Essentials==6.10.0 in ./.venv/lib/python3.13/site-packages (from PySide6->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 7)) (6.10.0)
Requirement already satisfied: PySide6_Addons==6.10.0 in ./.venv/lib/python3.13/site-packages (from PySide6->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 7)) (6.10.0)
Requirement already satisfied: h11<1,>=0.16.0 in ./.venv/lib/python3.13/site-packages (from wsproto>=0.14->trio-websocket<1.0,>=0.12.2->selenium->-r /home/nmemmert/journey/journey-fm-playlist/requirements.txt (line 5)) (0.16.0)
+2 -1
View File
@@ -3,4 +3,5 @@ beautifulsoup4
plexapi
mutagen
selenium
webdriver-manager
webdriver-manager
PySide6
+12
View File
@@ -0,0 +1,12 @@
@echo off
REM Journey FM Playlist Creator - Windows Launcher
cd /d "%~dp0"
REM Activate virtual environment if it exists
if exist .venv\Scripts\activate.bat (
call .venv\Scripts\activate.bat
)
REM Run the application without console window
pythonw journey_fm_app.py