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
|
||||
|
||||
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/
|
||||
- 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.
|
||||
@@ -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.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 selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from bs4 import BeautifulSoup
|
||||
import os
|
||||
from plexapi.myplex import MyPlexAccount
|
||||
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'
|
||||
|
||||
def scrape_recently_played():
|
||||
"""Scrape recently played songs from myjourneyfm.com using Selenium"""
|
||||
def scrape_recently_played(selected_stations=None):
|
||||
"""Scrape recently played songs from Journey FM and Spirit FM"""
|
||||
if selected_stations is None:
|
||||
selected_stations = ['journey_fm', 'spirit_fm']
|
||||
|
||||
options = Options()
|
||||
options.add_argument("--headless")
|
||||
options.add_argument("--headless=new")
|
||||
options.add_argument("--no-sandbox")
|
||||
options.add_argument("--disable-dev-shm-usage")
|
||||
options.add_argument("--disable-gpu")
|
||||
options.add_argument("--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())
|
||||
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:
|
||||
more_button = driver.find_element(By.ID, "moreSongs")
|
||||
more_button.click()
|
||||
time.sleep(5) # Wait for more songs to load
|
||||
except:
|
||||
pass # Button not found or already loaded
|
||||
driver = webdriver.Chrome(service=service, options=options)
|
||||
except Exception as e:
|
||||
print(f"Failed to start Chrome: {e}")
|
||||
return []
|
||||
|
||||
all_songs = []
|
||||
seen = set()
|
||||
|
||||
# Journey FM - uses <strong> for title, normal text for artist
|
||||
if 'journey_fm' in selected_stations:
|
||||
try:
|
||||
print("Scraping Journey FM...")
|
||||
driver.get('https://www.myjourneyfm.com/recently-played/')
|
||||
time.sleep(5)
|
||||
|
||||
# Click "View More" button if exists
|
||||
try:
|
||||
more_button = driver.find_element(By.ID, "moreSongs")
|
||||
more_button.click()
|
||||
time.sleep(5)
|
||||
except:
|
||||
pass
|
||||
|
||||
soup = BeautifulSoup(driver.page_source, 'html.parser')
|
||||
|
||||
# Find all song items using the proper class structure
|
||||
song_items = soup.find_all('div', class_='rp-item')
|
||||
|
||||
for item in song_items:
|
||||
try:
|
||||
title_elem = item.find('h5', class_='song-title')
|
||||
artist_elem = item.find('p', class_='song-artist')
|
||||
|
||||
if title_elem and artist_elem:
|
||||
title = title_elem.get_text().strip()
|
||||
artist = artist_elem.get_text().strip()
|
||||
|
||||
if title and artist:
|
||||
key = (title.lower(), artist.lower())
|
||||
if key not in seen:
|
||||
all_songs.append({'title': title, 'artist': artist, 'source': 'Journey FM'})
|
||||
seen.add(key)
|
||||
print(f" Found: {title} by {artist}")
|
||||
except Exception as e:
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error scraping Journey FM: {e}")
|
||||
|
||||
# Spirit FM - loads songs from iframe text file
|
||||
if 'spirit_fm' in selected_stations:
|
||||
try:
|
||||
print("Scraping Spirit FM...")
|
||||
driver.get('https://spiritfm.com/ajax/now_playing_history.txt')
|
||||
time.sleep(3)
|
||||
|
||||
# Parse the plain text format: "Mon 01:45PM Artist - Title"
|
||||
soup = BeautifulSoup(driver.page_source, 'html.parser')
|
||||
text_content = soup.get_text()
|
||||
|
||||
# Split into lines and parse each
|
||||
for line in text_content.split('\n'):
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
|
||||
# Match pattern: Day HH:MM(AM/PM) Artist - Title
|
||||
match = re.match(r'^\w+\s+\d+:\d+[AP]M\s+(.+?)\s+-\s+(.+)$', line)
|
||||
if match:
|
||||
artist = match.group(1).strip()
|
||||
title = match.group(2).strip()
|
||||
|
||||
# Clean up HTML entities
|
||||
artist = artist.replace('&', '&')
|
||||
title = title.replace('&', '&')
|
||||
|
||||
if title and artist:
|
||||
key = (title.lower(), artist.lower())
|
||||
if key not in seen:
|
||||
all_songs.append({'title': title, 'artist': artist, 'source': 'Spirit FM'})
|
||||
seen.add(key)
|
||||
print(f" Found: {title} by {artist}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error scraping Spirit FM: {e}")
|
||||
|
||||
html = driver.page_source
|
||||
driver.quit()
|
||||
|
||||
# Optional: save HTML to file for debugging (comment out if not needed)
|
||||
# with open('page.html', 'w', encoding='utf-8') as f:
|
||||
# f.write(html)
|
||||
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
|
||||
songs = []
|
||||
# Find elements (div, li) that contain time in their text
|
||||
song_elements = soup.find_all(lambda tag: tag.name in ['div', 'li', 'p'] and re.search(r'\d+:\d+ [AP]M', tag.get_text()))
|
||||
for element in song_elements:
|
||||
spans = element.find_all('span')
|
||||
if len(spans) >= 3:
|
||||
title = spans[0].get_text().strip()
|
||||
artist = spans[1].get_text().strip()
|
||||
# Ignore time
|
||||
songs.append({'artist': artist, 'title': title})
|
||||
else:
|
||||
# Fallback to text parsing
|
||||
full_text = element.get_text().strip()
|
||||
match = re.search(r'(\d+:\d+ [AP]M)', full_text)
|
||||
if match:
|
||||
play_time = match.group(1)
|
||||
before = full_text.split(play_time)[0].strip()
|
||||
if ' by ' in before:
|
||||
title, artist = before.split(' by ', 1)
|
||||
title = title.strip()
|
||||
artist = artist.strip()
|
||||
else:
|
||||
# Split on capital letters
|
||||
parts = re.findall(r'[A-Z][^A-Z]*', before)
|
||||
if len(parts) == 4:
|
||||
title = ' '.join(parts[:2])
|
||||
artist = ' '.join(parts[2:])
|
||||
else:
|
||||
title = ' '.join(parts[:3])
|
||||
artist = ' '.join(parts[3:])
|
||||
if title and artist:
|
||||
songs.append({'artist': artist, 'title': title})
|
||||
|
||||
return songs
|
||||
print(f"\nTotal unique songs found: {len(all_songs)}")
|
||||
return all_songs
|
||||
|
||||
def normalize_string(s):
|
||||
"""Normalize string for comparison"""
|
||||
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 +163,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 +183,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 +204,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)
|
||||
@@ -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):
|
||||
# Only add if we haven't already added this track in this run
|
||||
if track.ratingKey not in seen_keys:
|
||||
tracks.append(track)
|
||||
tracks.append((track, song))
|
||||
seen_keys.add(track.ratingKey)
|
||||
matched = True
|
||||
break
|
||||
# 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
|
||||
@@ -179,36 +232,115 @@ def create_playlist_in_plex(plex, songs, playlist_name):
|
||||
# Get existing track rating keys to avoid duplicates
|
||||
existing_keys = {item.ratingKey for item in existing.items()}
|
||||
# Filter out tracks already in playlist
|
||||
new_tracks = [track for track in tracks if track.ratingKey not in existing_keys]
|
||||
new_tracks = [track_song for track_song in tracks if track_song[0].ratingKey not in existing_keys]
|
||||
|
||||
if new_tracks:
|
||||
existing.addItems(new_tracks)
|
||||
print(f"Added {len(new_tracks)} new songs to existing playlist '{playlist_name}':")
|
||||
for track in new_tracks:
|
||||
print(f" + '{track.title}' by {track.artist().title}")
|
||||
existing.addItems([ts[0] for ts in new_tracks])
|
||||
for track, song in new_tracks:
|
||||
# Auto-tag added songs
|
||||
track.addLabel("Journey FM")
|
||||
track.rate(5)
|
||||
added = len(new_tracks)
|
||||
added_songs.extend([f"{track.title} by {track.artist().title} ({song['source']})" for track, song in new_tracks])
|
||||
else:
|
||||
print(f"No new songs to add - all {len(tracks)} matching songs already in playlist.")
|
||||
added = 0
|
||||
except:
|
||||
# Playlist doesn't exist, create it
|
||||
plex.createPlaylist(playlist_name, tracks)
|
||||
print(f"Created playlist '{playlist_name}' with {len(tracks)} songs:")
|
||||
for track in tracks:
|
||||
print(f" + '{track.title}' by {track.artist().title}")
|
||||
plex.createPlaylist(playlist_name, [ts[0] for ts in tracks])
|
||||
for track, song in tracks:
|
||||
# Auto-tag added songs
|
||||
track.addLabel("Journey FM")
|
||||
track.rate(5)
|
||||
added = len(tracks)
|
||||
added_songs.extend([f"{track.title} by {track.artist().title} ({song['source']})" for track, song 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"Scheduler setup failed: {e}")
|
||||
|
||||
def main():
|
||||
global PLEX_TOKEN, SERVER_IP, PLAYLIST_NAME
|
||||
# Load or prompt for config
|
||||
selected_stations = ['journey_fm', 'spirit_fm'] # default
|
||||
if os.path.exists('config.json'):
|
||||
with open('config.json', 'r') as f:
|
||||
config = json.load(f)
|
||||
PLEX_TOKEN = config.get('PLEX_TOKEN', PLEX_TOKEN)
|
||||
SERVER_IP = config.get('SERVER_IP', SERVER_IP)
|
||||
PLAYLIST_NAME = config.get('PLAYLIST_NAME', PLAYLIST_NAME)
|
||||
# Load selected stations
|
||||
selected_stations_str = config.get('SELECTED_STATIONS', 'journey_fm,spirit_fm')
|
||||
if isinstance(selected_stations_str, str) and selected_stations_str:
|
||||
selected_stations = [s for s in selected_stations_str.split(',') if s]
|
||||
# If empty or invalid, keep default
|
||||
else:
|
||||
# If no config file, use defaults (GUI should handle this)
|
||||
pass
|
||||
|
||||
# Log start time
|
||||
start_time = datetime.now()
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Journey FM Playlist Update - {start_time.strftime('%Y-%m-%d %I:%M:%S %p')}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
# Scrape songs
|
||||
songs = scrape_recently_played()
|
||||
print(f"Found {len(songs)} recently played songs.")
|
||||
# Initialize history database
|
||||
init_history_db()
|
||||
|
||||
# Scrape songs with selected stations
|
||||
songs = scrape_recently_played(selected_stations)
|
||||
|
||||
# Connect to Plex via MyPlexAccount
|
||||
account = MyPlexAccount(token=PLEX_TOKEN)
|
||||
@@ -225,20 +357,16 @@ def main():
|
||||
break
|
||||
|
||||
if not server_resource:
|
||||
print(f"Server at {SERVER_IP} not found in your claimed resources.")
|
||||
print("Please claim the server: Open http://172.16.16.106:32400/web, sign in with your Plex account, and follow the claim prompts.")
|
||||
return
|
||||
|
||||
plex = server_resource.connect()
|
||||
print(f"Connected to server: {plex.friendlyName}, ID: {plex.machineIdentifier}")
|
||||
|
||||
# Debug: list library sections
|
||||
print(f"Library sections: {[s.title for s in plex.library.sections()]}")
|
||||
|
||||
# Debug: list existing playlists (handle unicode safely)
|
||||
playlists = plex.playlists()
|
||||
playlist_names = [p.title.encode('ascii', 'ignore').decode('ascii') if not p.title.isascii() else p.title for p in playlists]
|
||||
print(f"Existing playlists: {len(playlists)} total")
|
||||
|
||||
# Find matching songs in local library (optional, since Plex search might suffice)
|
||||
# But to filter only those in local library, perhaps skip or adjust
|
||||
@@ -246,15 +374,68 @@ 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:
|
||||
|
||||
# Read existing buy list
|
||||
existing_songs = set()
|
||||
if os.path.exists('amazon_buy_list.txt'):
|
||||
try:
|
||||
with open('amazon_buy_list.txt', 'r') as f:
|
||||
content = f.read()
|
||||
lines = content.split('\n')
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
if lines[i].startswith('Songs not in your library'):
|
||||
i += 2 # Skip header
|
||||
continue
|
||||
if lines[i].strip() and not lines[i].startswith('http'):
|
||||
if ' - ' in lines[i]:
|
||||
artist, title = lines[i].split(' - ', 1)
|
||||
existing_songs.add((artist.strip(), title.strip()))
|
||||
i += 1
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
# Filter new missing songs
|
||||
new_missing = []
|
||||
for song in missing:
|
||||
key = (song['artist'], song['title'])
|
||||
if key not in existing_songs:
|
||||
new_missing.append(song)
|
||||
existing_songs.add(key)
|
||||
|
||||
if new_missing:
|
||||
with open('amazon_buy_list.txt', 'a') as f: # Append mode
|
||||
if not os.path.exists('amazon_buy_list.txt') or os.path.getsize('amazon_buy_list.txt') == 0:
|
||||
f.write("Songs not in your library - Amazon search links:\n\n")
|
||||
for song in new_missing:
|
||||
artist = song['artist']
|
||||
title = song['title']
|
||||
# Create Amazon search URL
|
||||
query = urllib.parse.quote(f"{artist} {title}")
|
||||
url = f"https://www.amazon.com/s?k={query}&i=digital-music"
|
||||
f.write(f"{artist} - {title}\n{url}\n\n")
|
||||
else:
|
||||
print("No new songs to add to buy list.")
|
||||
|
||||
print("Buy list updated.")
|
||||
|
||||
# Log completion
|
||||
end_time = datetime.now()
|
||||
duration = (end_time - start_time).total_seconds()
|
||||
print(f"\nCompleted in {duration:.1f} seconds at {end_time.strftime('%I:%M:%S %p')}")
|
||||
print(f"{'='*60}\n")
|
||||
|
||||
def update_playlist():
|
||||
"""Function to update playlist, callable from GUI"""
|
||||
main()
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -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.
@@ -4,3 +4,5 @@ plexapi
|
||||
mutagen
|
||||
selenium
|
||||
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
|
||||
+17
-21
@@ -1,30 +1,26 @@
|
||||
# Setup Task Scheduler for Journey FM Playlist Updater
|
||||
# Run this script as Administrator to create scheduled tasks
|
||||
|
||||
param(
|
||||
[string]$Type = "Both", # Interval, Startup, Both
|
||||
[int]$Interval = 15,
|
||||
[string]$Unit = "Minutes" # Minutes, Hours, Days
|
||||
)
|
||||
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$psScript = "$scriptDir\run_journey_playlist.ps1"
|
||||
|
||||
if ($Type -eq "Interval" -or $Type -eq "Both") {
|
||||
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$psScript`""
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -$Unit $Interval)
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
||||
Register-ScheduledTask -TaskName "Journey FM Playlist Updater - Every $Interval $Unit" -Action $action -Trigger $trigger -Settings $settings -Description "Updates Plex playlist with recently played songs every $Interval $Unit"
|
||||
Write-Host "Created task: Every $Interval $Unit" -ForegroundColor Green
|
||||
}
|
||||
# Task 1: Run every 15 minutes
|
||||
$action1 = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$psScript`""
|
||||
$trigger1 = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes 15)
|
||||
$settings1 = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
||||
|
||||
if ($Type -eq "Startup" -or $Type -eq "Both") {
|
||||
$action = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$psScript`""
|
||||
$trigger = New-ScheduledTaskTrigger -AtStartup
|
||||
$trigger.Delay = "PT1M" # 1 minute delay
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
||||
Register-ScheduledTask -TaskName "Journey FM Playlist Updater - At Startup" -Action $action -Trigger $trigger -Settings $settings -Description "Updates Plex playlist with recently played songs at startup"
|
||||
Write-Host "Created task: At Startup" -ForegroundColor Green
|
||||
}
|
||||
Register-ScheduledTask -TaskName "Journey FM Playlist Updater - Every 15 Minutes" -Action $action1 -Trigger $trigger1 -Settings $settings1 -Description "Updates Plex playlist with recently played songs from Journey FM every 15 minutes" -Force
|
||||
|
||||
# Task 2: Run at startup
|
||||
$action2 = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument "-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$psScript`""
|
||||
$trigger2 = New-ScheduledTaskTrigger -AtStartup
|
||||
$trigger2.Delay = "PT1M" # 1 minute delay after startup
|
||||
$settings2 = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
||||
|
||||
Register-ScheduledTask -TaskName "Journey FM Playlist Updater - At Startup" -Action $action2 -Trigger $trigger2 -Settings $settings2 -Description "Updates Plex playlist with recently played songs from Journey FM at computer startup" -Force
|
||||
|
||||
Write-Host "Scheduled tasks created successfully!" -ForegroundColor Green
|
||||
Write-Host "Task 1: Runs every 15 minutes" -ForegroundColor Cyan
|
||||
Write-Host "Task 2: Runs at computer startup (with 60 second delay)" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "You can view and manage these tasks in Task Scheduler." -ForegroundColor Yellow
|
||||
Reference in new issue
Block a user