Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
e3085f22ae | ||
|
|
e643bb3c98 | ||
|
|
9a90811dea | ||
|
|
f12b50f9d0 | ||
|
|
589cbca2f6 | ||
|
|
3ff79549d9 | ||
|
|
e74845cab6 | ||
|
|
2893487134 | ||
|
|
be9a1a1b64 | ||
|
|
d5f2aa0dfd | ||
|
|
d357405fb7 | ||
|
|
5f88cdd92b | ||
|
|
9b63031c1e | ||
|
|
5e891ad139 | ||
|
|
c2b91f3f47 | ||
|
|
ce5175d8db | ||
|
|
345c40f40d |
No files matched your search
@@ -1,28 +1,214 @@
|
|||||||
# 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. **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.
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
Songs not in your library - Amazon search links:
|
||||||
|
|
||||||
|
Danny Gokey - I Got You
|
||||||
|
https://www.amazon.com/s?k=Danny%20Gokey%20I%20Got%20You&i=digital-music
|
||||||
|
|
||||||
|
Have For You Colton Dixon - The Love I
|
||||||
|
https://www.amazon.com/s?k=Have%20%20For%20%20You%20Colton%20%20Dixon%20The%20%20Love%20%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Done Tasha Layton - Look What You've
|
||||||
|
https://www.amazon.com/s?k=Done%20Tasha%20%20Layton%20Look%20%20What%20%20You%27ve%20&i=digital-music
|
||||||
|
|
||||||
|
Cochren & Co. - Who Can
|
||||||
|
https://www.amazon.com/s?k=Cochren%20%26%20%20Co.%20Who%20%20Can&i=digital-music
|
||||||
|
|
||||||
|
Have For You Colton Dixon - The Love I
|
||||||
|
https://www.amazon.com/s?k=Have%20%20For%20%20You%20Colton%20%20Dixon%20The%20%20Love%20%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Done Tasha Layton - Look What You've
|
||||||
|
https://www.amazon.com/s?k=Done%20Tasha%20%20Layton%20Look%20%20What%20%20You%27ve%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
Andrew Ripp - Breakdown
|
||||||
|
https://www.amazon.com/s?k=Andrew%20%20Ripp%20Breakdown&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Crowder - Still
|
||||||
|
https://www.amazon.com/s?k=Crowder%20Still&i=digital-music
|
||||||
|
|
||||||
|
Thing About Praise Benjamin William Hastings - That's The
|
||||||
|
https://www.amazon.com/s?k=Thing%20%20About%20%20Praise%20Benjamin%20%20William%20%20Hastings%20That%27s%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
Love Feat. Bear Rinehar We The Kingdom - Child Of
|
||||||
|
https://www.amazon.com/s?k=Love%20%20Feat.%20%20Bear%20%20Rinehar%20We%20%20The%20%20Kingdom%20Child%20%20Of%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Thing About Praise Benjamin William Hastings - That's The
|
||||||
|
https://www.amazon.com/s?k=Thing%20%20About%20%20Praise%20Benjamin%20%20William%20%20Hastings%20That%27s%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
Love Feat. Bear Rinehar We The Kingdom - Child Of
|
||||||
|
https://www.amazon.com/s?k=Love%20%20Feat.%20%20Bear%20%20Rinehar%20We%20%20The%20%20Kingdom%20Child%20%20Of%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
You've Done Tasha Layton - Look What
|
||||||
|
https://www.amazon.com/s?k=You%27ve%20%20Done%20Tasha%20%20Layton%20Look%20%20What%20&i=digital-music
|
||||||
|
|
||||||
|
Zach Williams - Jesus Loves
|
||||||
|
https://www.amazon.com/s?k=Zach%20Williams%20Jesus%20Loves&i=digital-music
|
||||||
|
|
||||||
|
Zach Williams - Jesus Loves
|
||||||
|
https://www.amazon.com/s?k=Zach%20%20Williams%20Jesus%20%20Loves&i=digital-music
|
||||||
|
|
||||||
|
Am They - Scars I
|
||||||
|
https://www.amazon.com/s?k=Am%20%20They%20Scars%20I%20&i=digital-music
|
||||||
|
|
||||||
|
Church Sing Tauren Wells - Let The
|
||||||
|
https://www.amazon.com/s?k=Church%20%20Sing%20Tauren%20%20Wells%20Let%20%20The%20&i=digital-music
|
||||||
|
|
||||||
|
You Danny Gokey - I Got
|
||||||
|
https://www.amazon.com/s?k=You%20Danny%20%20Gokey%20I%20%20Got%20&i=digital-music
|
||||||
|
|
||||||
|
Cochren & Co. - Church (Take Me Back)
|
||||||
|
https://www.amazon.com/s?k=Cochren%20%26%20Co.%20Church%20%28Take%20Me%20Back%29&i=digital-music
|
||||||
|
|
||||||
|
Blessing Offor - Your Love
|
||||||
|
https://www.amazon.com/s?k=Blessing%20Offor%20Your%20Love&i=digital-music
|
||||||
|
|
||||||
|
Anne Wilson - My Jesus
|
||||||
|
https://www.amazon.com/s?k=Anne%20Wilson%20My%20Jesus&i=digital-music
|
||||||
|
|
||||||
|
Sidewalk Prophets - You Love Me Anyway
|
||||||
|
https://www.amazon.com/s?k=Sidewalk%20Prophets%20You%20Love%20Me%20Anyway&i=digital-music
|
||||||
|
|
||||||
|
Casting Crowns (ft Matthew West) - Nobody
|
||||||
|
https://www.amazon.com/s?k=Casting%20Crowns%20%28ft%20Matthew%20West%29%20Nobody&i=digital-music
|
||||||
|
|
||||||
|
Phil Wickham - This Is Amazing Grace
|
||||||
|
https://www.amazon.com/s?k=Phil%20Wickham%20This%20Is%20Amazing%20Grace&i=digital-music
|
||||||
|
|
||||||
|
Crowder - STILL
|
||||||
|
https://www.amazon.com/s?k=Crowder%20STILL&i=digital-music
|
||||||
|
|
||||||
|
Josh Baldwin - Made for More
|
||||||
|
https://www.amazon.com/s?k=Josh%20Baldwin%20Made%20for%20More&i=digital-music
|
||||||
|
|
||||||
|
Leanna Crawford - Honest
|
||||||
|
https://www.amazon.com/s?k=Leanna%20Crawford%20Honest&i=digital-music
|
||||||
|
|
||||||
|
We The Kingdom - God So Loved
|
||||||
|
https://www.amazon.com/s?k=We%20The%20Kingdom%20God%20So%20Loved&i=digital-music
|
||||||
|
|
||||||
|
Dan Bremnes - Call Your Name
|
||||||
|
https://www.amazon.com/s?k=Dan%20Bremnes%20Call%20Your%20Name&i=digital-music
|
||||||
|
|
||||||
|
Big Daddy Weave - I Know
|
||||||
|
https://www.amazon.com/s?k=Big%20Daddy%20Weave%20I%20Know&i=digital-music
|
||||||
|
|
||||||
|
The Afters - God Is With Us
|
||||||
|
https://www.amazon.com/s?k=The%20Afters%20God%20Is%20With%20Us&i=digital-music
|
||||||
|
|
||||||
|
CAIN - Rise Up (Lazarus)
|
||||||
|
https://www.amazon.com/s?k=CAIN%20Rise%20Up%20%28Lazarus%29&i=digital-music
|
||||||
|
|
||||||
|
Katy Nichole - Have Your Way
|
||||||
|
https://www.amazon.com/s?k=Katy%20Nichole%20Have%20Your%20Way&i=digital-music
|
||||||
|
|
||||||
|
Ryan Stevenson - Preach
|
||||||
|
https://www.amazon.com/s?k=Ryan%20Stevenson%20Preach&i=digital-music
|
||||||
|
|
||||||
|
Elevation Worship - Praise
|
||||||
|
https://www.amazon.com/s?k=Elevation%20Worship%20Praise&i=digital-music
|
||||||
|
|
||||||
|
Andrew Ripp - Breakdown
|
||||||
|
https://www.amazon.com/s?k=Andrew%20Ripp%20Breakdown&i=digital-music
|
||||||
|
|
||||||
|
Chris Tomlin - Amazing Grace (My Chains Are Gon
|
||||||
|
https://www.amazon.com/s?k=Chris%20Tomlin%20Amazing%20Grace%20%28My%20Chains%20Are%20Gon&i=digital-music
|
||||||
|
|
||||||
|
Tauren Wells - Known
|
||||||
|
https://www.amazon.com/s?k=Tauren%20Wells%20Known&i=digital-music
|
||||||
|
|
||||||
|
Big Daddy Weave - All Things New (Single Mix)
|
||||||
|
https://www.amazon.com/s?k=Big%20Daddy%20Weave%20All%20Things%20New%20%28Single%20Mix%29&i=digital-music
|
||||||
|
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
2025-11-17 12:40:16.417598: Starting Journey FM Playlist app...
|
||||||
|
2025-11-17 12:40:16.417787: DISPLAY set, initializing GUI
|
||||||
|
2025-11-17 12:40:16.553130: Failed to start GUI application: 'MainWindow' object has no attribute 'update_playlist'
|
||||||
|
2025-11-17 12:42:52.626588: Starting Journey FM Playlist app...
|
||||||
|
2025-11-17 12:42:52.626688: DISPLAY set, initializing GUI
|
||||||
|
2025-11-17 12:42:52.761510: App initialized, starting event loop
|
||||||
|
2025-11-17 12:56:44.139926: Starting Journey FM Playlist app...
|
||||||
|
2025-11-17 12:56:44.140042: DISPLAY set, initializing GUI
|
||||||
|
2025-11-17 12:56:44.381412: App initialized, starting event loop
|
||||||
|
2025-11-17 13:01:57.291192: Starting Journey FM Playlist app...
|
||||||
|
2025-11-17 13:01:57.291370: DISPLAY set, initializing GUI
|
||||||
|
2025-11-17 13:01:57.440291: App initialized, starting event loop
|
||||||
|
2025-11-17 13:09:29.113472: Starting Journey FM Playlist app...
|
||||||
|
2025-11-17 13:09:29.113563: DISPLAY set, initializing GUI
|
||||||
|
2025-11-17 13:09:29.245236: App initialized, starting event loop
|
||||||
|
2025-11-17 13:12:14.103261: Starting Journey FM Playlist app...
|
||||||
|
2025-11-17 13:12:14.103373: DISPLAY set, initializing GUI
|
||||||
|
2025-11-17 13:12:14.295725: App initialized, starting event loop
|
||||||
|
2025-11-17 13:48:09.668000: Starting Journey FM Playlist app...
|
||||||
|
2025-11-17 13:48:09.668117: DISPLAY set, initializing GUI
|
||||||
|
2025-11-17 13:48:10.016465: App initialized, starting event loop
|
||||||
|
2025-11-17 13:55:31.548889: Starting Journey FM Playlist app...
|
||||||
|
2025-11-17 13:55:31.549017: DISPLAY set, initializing GUI
|
||||||
|
2025-11-17 13:55:31.715072: App initialized, starting event loop
|
||||||
|
2025-11-17 14:02:10.571449: Starting Journey FM Playlist app...
|
||||||
|
2025-11-17 14:02:10.571573: DISPLAY set, initializing GUI
|
||||||
|
2025-11-17 14:02:10.700170: App initialized, starting event loop
|
||||||
@@ -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")
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
#!/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 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"
|
||||||
+1050
File diff suppressed because it is too large.
Load diff
Executable
+26
@@ -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
|
||||||
@@ -11,91 +11,141 @@ import time
|
|||||||
from selenium import webdriver
|
from selenium import webdriver
|
||||||
from selenium.webdriver.chrome.options import Options
|
from selenium.webdriver.chrome.options import Options
|
||||||
from selenium.webdriver.chrome.service import Service
|
from selenium.webdriver.chrome.service import Service
|
||||||
from selenium.webdriver.common.by import By
|
|
||||||
from webdriver_manager.chrome import ChromeDriverManager
|
from webdriver_manager.chrome import ChromeDriverManager
|
||||||
|
from selenium.webdriver.common.by import By
|
||||||
|
from selenium.webdriver.support.ui import WebDriverWait
|
||||||
|
from selenium.webdriver.support import expected_conditions as EC
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import os
|
import os
|
||||||
from plexapi.myplex import MyPlexAccount
|
from plexapi.myplex import MyPlexAccount
|
||||||
from mutagen.easyid3 import EasyID3
|
from mutagen.easyid3 import EasyID3
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
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
|
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("--window-size=1920,1080")
|
||||||
|
options.add_argument("--disable-blink-features=AutomationControlled")
|
||||||
|
options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
|
||||||
|
options.add_experimental_option("excludeSwitches", ["enable-automation"])
|
||||||
|
options.add_experimental_option('useAutomationExtension', False)
|
||||||
|
options.binary_location = "/usr/bin/google-chrome-stable"
|
||||||
|
|
||||||
service = Service(ChromeDriverManager().install())
|
service = Service(ChromeDriverManager().install())
|
||||||
driver = webdriver.Chrome(service=service, options=options)
|
|
||||||
|
|
||||||
url = 'https://www.myjourneyfm.com/recently-played/'
|
|
||||||
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"""
|
||||||
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):
|
def find_song_in_library(artist, title, library_path):
|
||||||
"""Check if song exists in local library"""
|
"""Check if song exists in local library"""
|
||||||
@@ -113,11 +163,13 @@ def find_song_in_library(artist, title, library_path):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def create_playlist_in_plex(plex, songs, playlist_name):
|
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'
|
# Assuming Plex has a music library named 'Music'
|
||||||
music_library = plex.library.section('Music')
|
music_library = plex.library.section('Music')
|
||||||
tracks = []
|
tracks = []
|
||||||
seen_keys = set() # Track which ratingKeys we've already added to prevent duplicates within this run
|
seen_keys = set() # Track which ratingKeys we've already added to prevent duplicates within this run
|
||||||
|
missing = []
|
||||||
|
added_songs = []
|
||||||
|
|
||||||
for song in songs:
|
for song in songs:
|
||||||
# Clean title and artist by removing text in parentheses and normalizing spaces
|
# Clean title and artist by removing text in parentheses and normalizing spaces
|
||||||
@@ -131,7 +183,7 @@ def create_playlist_in_plex(plex, songs, playlist_name):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Skip entries that are ONLY punctuation (but allow titles with punctuation in them)
|
# 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
|
continue
|
||||||
|
|
||||||
# Remove only ! and ? for searching, but keep apostrophes
|
# Remove only ! and ? for searching, but keep apostrophes
|
||||||
@@ -152,7 +204,7 @@ def create_playlist_in_plex(plex, songs, playlist_name):
|
|||||||
search_artist_lower = search_artist.lower()
|
search_artist_lower = search_artist.lower()
|
||||||
|
|
||||||
# Clean featured artists from search (e.g., "W/ Emerson Day", "feat. Someone")
|
# 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
|
# Normalize & and + in track artist as well
|
||||||
track_artist = re.sub(r'\s*[&+]\s*', ' and ', track_artist)
|
track_artist = re.sub(r'\s*[&+]\s*', ' and ', track_artist)
|
||||||
@@ -163,14 +215,15 @@ 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
|
||||||
# If no artist match, skip this track (don't add wrong songs)
|
# If no artist match, skip this track (don't add wrong songs)
|
||||||
if not matched:
|
if not matched:
|
||||||
pass # Don't add if artist doesn't match # Take the first match
|
missing.append(song)
|
||||||
|
|
||||||
|
added = 0
|
||||||
if tracks:
|
if tracks:
|
||||||
try:
|
try:
|
||||||
# Try to find existing playlist
|
# Try to find existing playlist
|
||||||
@@ -179,36 +232,115 @@ 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_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
|
||||||
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_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_songs = []
|
||||||
else:
|
else:
|
||||||
print("No matching tracks found in Plex.")
|
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"Scheduler setup failed: {e}")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
global PLEX_TOKEN, SERVER_IP, PLAYLIST_NAME
|
||||||
|
# Load or prompt for config
|
||||||
|
selected_stations = ['journey_fm', 'spirit_fm'] # default
|
||||||
|
if os.path.exists('config.json'):
|
||||||
|
with open('config.json', 'r') as f:
|
||||||
|
config = json.load(f)
|
||||||
|
PLEX_TOKEN = config.get('PLEX_TOKEN', PLEX_TOKEN)
|
||||||
|
SERVER_IP = config.get('SERVER_IP', SERVER_IP)
|
||||||
|
PLAYLIST_NAME = config.get('PLAYLIST_NAME', PLAYLIST_NAME)
|
||||||
|
# Load selected stations
|
||||||
|
selected_stations_str = config.get('SELECTED_STATIONS', 'journey_fm,spirit_fm')
|
||||||
|
if isinstance(selected_stations_str, str) and selected_stations_str:
|
||||||
|
selected_stations = [s for s in selected_stations_str.split(',') if s]
|
||||||
|
# If empty or invalid, keep default
|
||||||
|
else:
|
||||||
|
# If no config file, use defaults (GUI should handle this)
|
||||||
|
pass
|
||||||
|
|
||||||
# Log start time
|
# 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")
|
|
||||||
|
|
||||||
# Scrape songs
|
# Initialize history database
|
||||||
songs = scrape_recently_played()
|
init_history_db()
|
||||||
print(f"Found {len(songs)} recently played songs.")
|
|
||||||
|
# Scrape songs with selected stations
|
||||||
|
songs = scrape_recently_played(selected_stations)
|
||||||
|
|
||||||
# Connect to Plex via MyPlexAccount
|
# Connect to Plex via MyPlexAccount
|
||||||
account = MyPlexAccount(token=PLEX_TOKEN)
|
account = MyPlexAccount(token=PLEX_TOKEN)
|
||||||
@@ -225,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
|
||||||
@@ -246,15 +374,68 @@ def main():
|
|||||||
|
|
||||||
# Create playlist
|
# Create playlist
|
||||||
if songs:
|
if songs:
|
||||||
create_playlist_in_plex(plex, songs, PLAYLIST_NAME)
|
added, added_songs, missing = create_playlist_in_plex(plex, songs, PLAYLIST_NAME)
|
||||||
else:
|
else:
|
||||||
print("No songs found.")
|
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:
|
||||||
|
|
||||||
|
# Read existing buy list
|
||||||
|
existing_songs = set()
|
||||||
|
if os.path.exists('amazon_buy_list.txt'):
|
||||||
|
try:
|
||||||
|
with open('amazon_buy_list.txt', 'r') as f:
|
||||||
|
content = f.read()
|
||||||
|
lines = content.split('\n')
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
if lines[i].startswith('Songs not in your library'):
|
||||||
|
i += 2 # Skip header
|
||||||
|
continue
|
||||||
|
if lines[i].strip() and not lines[i].startswith('http'):
|
||||||
|
if ' - ' in lines[i]:
|
||||||
|
artist, title = lines[i].split(' - ', 1)
|
||||||
|
existing_songs.add((artist.strip(), title.strip()))
|
||||||
|
i += 1
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Filter new missing songs
|
||||||
|
new_missing = []
|
||||||
|
for song in missing:
|
||||||
|
key = (song['artist'], song['title'])
|
||||||
|
if key not in existing_songs:
|
||||||
|
new_missing.append(song)
|
||||||
|
existing_songs.add(key)
|
||||||
|
|
||||||
|
if new_missing:
|
||||||
|
with open('amazon_buy_list.txt', 'a') as f: # Append mode
|
||||||
|
if not os.path.exists('amazon_buy_list.txt') or os.path.getsize('amazon_buy_list.txt') == 0:
|
||||||
|
f.write("Songs not in your library - Amazon search links:\n\n")
|
||||||
|
for song in new_missing:
|
||||||
|
artist = song['artist']
|
||||||
|
title = song['title']
|
||||||
|
# Create Amazon search URL
|
||||||
|
query = urllib.parse.quote(f"{artist} {title}")
|
||||||
|
url = f"https://www.amazon.com/s?k={query}&i=digital-music"
|
||||||
|
f.write(f"{artist} - {title}\n{url}\n\n")
|
||||||
|
else:
|
||||||
|
print("No new songs to add to buy list.")
|
||||||
|
|
||||||
|
print("Buy list updated.")
|
||||||
|
|
||||||
# Log completion
|
# 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()
|
||||||
@@ -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)
|
||||||
Binary file not shown.
+3
-1
@@ -3,4 +3,6 @@ beautifulsoup4
|
|||||||
plexapi
|
plexapi
|
||||||
mutagen
|
mutagen
|
||||||
selenium
|
selenium
|
||||||
webdriver-manager
|
webdriver-manager
|
||||||
|
PySide6
|
||||||
|
matplotlib
|
||||||
+12
@@ -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
|
||||||
Reference in new issue
Block a user