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
This commit is contained in:
1 parent
345c40f40d
commit
ce5175d8db
8 files changed
+760
-29
No files matched your search
@@ -1,28 +1,205 @@
|
|||||||
# Journey FM Playlist Creator
|
# 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
|

|
||||||
|
|
||||||
- Scrapes recently played songs from https://www.myjourneyfm.com/recently-played/
|
## ✨ Features
|
||||||
- Checks your local music library for matching songs
|
|
||||||
- Creates a playlist in Plex with the found songs
|
|
||||||
|
|
||||||
## 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`
|
## 🚀 Quick Start
|
||||||
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`
|
|
||||||
|
|
||||||
## Requirements
|
### Windows
|
||||||
|
|
||||||
- Python 3.6+
|
1. **Install Dependencies**
|
||||||
- Plex server
|
```bash
|
||||||
- Local music library with ID3 tags
|
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. **Install Dependencies**
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Make Scripts Executable**
|
||||||
|
```bash
|
||||||
|
chmod +x journey_fm_app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Run the Application**
|
||||||
|
```bash
|
||||||
|
./journey_fm_app.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ Configuration
|
||||||
|
|
||||||
|
### First-Time Setup
|
||||||
|
|
||||||
|
When you first run the application, a setup wizard will guide you through:
|
||||||
|
|
||||||
|
1. **Plex Token**: Get this from https://plex.tv/claim
|
||||||
|
2. **Server IP**: Your Plex server's local IP address
|
||||||
|
3. **Playlist Name**: Name for your Journey FM playlist
|
||||||
|
4. **Auto-Update Settings**: Enable automatic updates and set intervals
|
||||||
|
|
||||||
|
### 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.
|
||||||
@@ -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")
|
||||||
@@ -0,0 +1,500 @@
|
|||||||
|
#!/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
|
||||||
|
)
|
||||||
|
from PySide6.QtCore import (
|
||||||
|
Qt, QTimer, QThread, Signal, QSettings, QSize
|
||||||
|
)
|
||||||
|
from PySide6.QtGui import (
|
||||||
|
QIcon, QAction, QFont, QPixmap, QPainter, QColor
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
form_layout.addRow("Plex Token:", self.token_input)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 = 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")
|
||||||
|
|
||||||
|
# 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')}")
|
||||||
|
|
||||||
|
# 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 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()
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#!/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 )"
|
||||||
|
|
||||||
|
# Activate virtual environment if it exists
|
||||||
|
if [ -d "$DIR/.venv" ]; then
|
||||||
|
source "$DIR/.venv/bin/activate"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the application
|
||||||
|
cd "$DIR"
|
||||||
|
python journey_fm_app.py
|
||||||
@@ -53,7 +53,6 @@ def scrape_recently_played():
|
|||||||
html = driver.page_source
|
html = driver.page_source
|
||||||
driver.quit()
|
driver.quit()
|
||||||
|
|
||||||
tweet = scrape(tweet_code)
|
|
||||||
# Optional: save HTML to file for debugging (comment out if not needed)
|
# Optional: save HTML to file for debugging (comment out if not needed)
|
||||||
# with open('page.html', 'w', encoding='utf-8') as f:
|
# with open('page.html', 'w', encoding='utf-8') as f:
|
||||||
# f.write(html)
|
# f.write(html)
|
||||||
@@ -259,15 +258,8 @@ def main():
|
|||||||
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)
|
||||||
else:
|
else:
|
||||||
config = prompt_for_config()
|
# If no config file, use defaults (GUI should handle this)
|
||||||
PLEX_TOKEN = config['PLEX_TOKEN']
|
pass
|
||||||
SERVER_IP = config['SERVER_IP']
|
|
||||||
PLAYLIST_NAME = config['PLAYLIST_NAME']
|
|
||||||
|
|
||||||
# Prompt for scheduler setup
|
|
||||||
sched_params = prompt_for_scheduler()
|
|
||||||
if sched_params:
|
|
||||||
setup_scheduler(sched_params)
|
|
||||||
|
|
||||||
# Log start time
|
# Log start time
|
||||||
start_time = datetime.now()
|
start_time = datetime.now()
|
||||||
|
|||||||
+2
-1
@@ -3,4 +3,5 @@ beautifulsoup4
|
|||||||
plexapi
|
plexapi
|
||||||
mutagen
|
mutagen
|
||||||
selenium
|
selenium
|
||||||
webdriver-manager
|
webdriver-manager
|
||||||
|
PySide6
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
@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
|
||||||
|
python journey_fm_app.py
|
||||||
|
|
||||||
|
pause
|
||||||
Reference in new issue
Block a user