commit 2a34d0cc932a81675b08c8b09ff20e5d7dc8caee Author: nmemmert <67961292+nmemmert@users.noreply.github.com> Date: Sat Nov 15 08:35:18 2025 -0500 Initial commit: Journey FM playlist automation script diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..c081c9a --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,78 @@ + +- [x] Verify that the copilot-instructions.md file in the .github directory is created. + +- [x] Clarify Project Requirements + + +- [x] Scaffold the Project + + +- [x] Customize the Project + + +- [x] Install Required Extensions + + +- [x] Compile the Project + + +- [x] Create and Run Task + + +- [x] Launch the Project + + +- [x] Ensure Documentation is Complete + + + +- Work through each checklist item systematically. +- Keep communication concise and focused. +- Follow development best practices. \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c078f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +# Python virtual environment +.venv/ +venv/ +env/ + +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Logs +playlist_log.txt +page.html + +# Selenium/ChromeDriver +*.log + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Environment variables (if you add a .env file later) +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..dc8f2d7 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Journey FM Playlist Creator + +This script automates the process of creating a Plex playlist 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 + +## Setup + +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` + +## Requirements + +- Python 3.6+ +- Plex server +- Local music library with ID3 tags + +## Note + +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. \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..ac36951 --- /dev/null +++ b/main.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 +""" +Journey FM Playlist Creator + +This script scrapes recently played songs from myjourneyfm.com, +checks if they exist in the local music library, +creates a playlist of matching songs, and imports it into Plex. +""" + +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 bs4 import BeautifulSoup +import os +from plexapi.myplex import MyPlexAccount +from mutagen.easyid3 import EasyID3 +import re +from datetime import datetime + +# Configuration +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""" + options = Options() + options.add_argument("--headless") + options.add_argument("--no-sandbox") + options.add_argument("--disable-dev-shm-usage") + options.add_argument("--disable-gpu") + 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 + + 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 + +def normalize_string(s): + """Normalize string for comparison""" + 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""" + for root, dirs, files in os.walk(library_path): + for file in files: + if file.endswith(('.mp3', '.flac', '.m4a')): # Add more formats if needed + try: + audio = EasyID3(os.path.join(root, file)) + file_artist = normalize_string(audio.get('artist', [''])[0]) + file_title = normalize_string(audio.get('title', [''])[0]) + if normalize_string(artist) == file_artist and normalize_string(title) == file_title: + return os.path.join(root, file) + except: + continue + return None + +def create_playlist_in_plex(plex, songs, playlist_name): + """Create playlist in Plex""" + # 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 + + for song in songs: + # Clean title and artist by removing text in parentheses and normalizing spaces + clean_title = re.sub(r'\([^)]*\)', '', song['title']).strip() + clean_title = re.sub(r'\s+', ' ', clean_title) # Replace multiple spaces with single space + clean_artist = re.sub(r'\([^)]*\)', '', song['artist']).strip() + clean_artist = re.sub(r'\s+', ' ', clean_artist) # Replace multiple spaces with single space + + # Skip invalid entries + if not clean_title or len(clean_title) < 3 or clean_title.lower() in ['by', 'recently played', 'now playing:', 'search']: + continue + + # Skip entries that are ONLY punctuation (but allow titles with punctuation in them) + if re.match(r'^[!?\s]+$', clean_title): + continue + + # Remove only ! and ? for searching, but keep apostrophes + search_title = re.sub(r'[!?]', '', clean_title).strip() + search_artist = re.sub(r'[!?]', '', clean_artist).strip() + + # Normalize & and + to "and" for both title and artist + search_title = re.sub(r'\s*[&+]\s*', ' and ', search_title, flags=re.IGNORECASE) + search_artist = re.sub(r'\s*[&+]\s*', ' and ', search_artist, flags=re.IGNORECASE) + + # Search for tracks by cleaned title + results = music_library.searchTracks(title=search_title) + if results: + # Try to match by artist as well + matched = False + for track in results: + track_artist = track.artist().title.lower() + 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() + + # Normalize & and + in track artist as well + track_artist = re.sub(r'\s*[&+]\s*', ' and ', track_artist) + + # Check if artist matches (case-insensitive, partial match) + # Match if main artist name is in the track artist or vice versa + if (search_artist_main in track_artist or track_artist in search_artist_main or + 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) + 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 + + if tracks: + try: + # Try to find existing playlist + try: + existing = plex.playlist(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] + + 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}") + else: + print(f"No new songs to add - all {len(tracks)} matching songs already in playlist.") + 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}") + except Exception as e: + print(f"Error with playlist: {e}") + else: + print("No matching tracks found in Plex.") + +def main(): + # 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.") + + # Connect to Plex via MyPlexAccount + account = MyPlexAccount(token=PLEX_TOKEN) + resources = account.resources() + + # Find the server by IP + server_resource = None + for r in resources: + for conn in r.connections: + if SERVER_IP in conn.address: + server_resource = r + break + if server_resource: + break + + if not server_resource: + 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 + # For now, assume all scraped songs are to be added if found in Plex + + # Create playlist + if songs: + create_playlist_in_plex(plex, songs, PLAYLIST_NAME) + else: + print("No songs found.") + + # 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") + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..755059f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +requests +beautifulsoup4 +plexapi +mutagen +selenium +webdriver-manager \ No newline at end of file diff --git a/run_journey_playlist.ps1 b/run_journey_playlist.ps1 new file mode 100644 index 0000000..9e55879 --- /dev/null +++ b/run_journey_playlist.ps1 @@ -0,0 +1,13 @@ +# Journey FM Playlist Updater - PowerShell Script +# Run this script to update your Plex playlist with recently played songs from Journey FM + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$pythonExe = "$scriptDir\.venv\Scripts\python.exe" +$mainScript = "$scriptDir\main.py" +$logFile = "$scriptDir\playlist_log.txt" + +# Run the Python script and capture output +$output = & $pythonExe $mainScript 2>&1 | Out-String + +# Append the full output to the log file +Add-Content -Path $logFile -Value $output diff --git a/setup_scheduler.ps1 b/setup_scheduler.ps1 new file mode 100644 index 0000000..ea6818a --- /dev/null +++ b/setup_scheduler.ps1 @@ -0,0 +1,26 @@ +# Setup Task Scheduler for Journey FM Playlist Updater +# Run this script as Administrator to create scheduled tasks + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$psScript = "$scriptDir\run_journey_playlist.ps1" + +# 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 + +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 diff --git a/test_scrape.py b/test_scrape.py new file mode 100644 index 0000000..b23ac0f --- /dev/null +++ b/test_scrape.py @@ -0,0 +1,23 @@ +import requests +from bs4 import BeautifulSoup + +url = 'https://www.myjourneyfm.com/recently-played/' +response = requests.get(url) +soup = BeautifulSoup(response.content, 'html.parser') + +# Try to find song elements +print("Song items:") +song_items = soup.find_all('div', class_='song-item') +for item in song_items: + print(item.prettify()) + +print("\nScripts with 'song' or 'played':") +scripts = soup.find_all('script') +for script in scripts: + if script.string and ('song' in script.string.lower() or 'played' in script.string.lower()): + print(script.string[:2000]) + +print("\nAll links:") +for a in soup.find_all('a'): + if 'recently' in a.get('href', '').lower(): + print(a) \ No newline at end of file