Files
journey-fm-playlist/journeyfm/update_service.py
T
nmemmertandClaude Sonnet 4.6 7efb9265e6 Add per-playlist station selection
Each playlist now has its own station checkboxes in Settings.
Sync scrapes the union of all stations once, then filters songs
by source before syncing each playlist separately.

Config stored as PLAYLISTS JSON: [{name, stations}, ...].
Backward-compatible with flat PLAYLIST_NAME + SELECTED_STATIONS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-26 09:04:34 -04:00

177 lines
7.0 KiB
Python

import logging
import os
import urllib.parse
from journeyfm.config_store import load_runtime_config
from journeyfm.history_service import init_history_db, save_history_entry
from journeyfm.paths import data_path
from journeyfm.plex_service import PlexConnectionError, connect_to_plex_server, create_or_update_playlist
from journeyfm.scraper_service import scrape_recently_played
logger = logging.getLogger(__name__)
def update_buy_list(missing_songs, buy_list_path=None):
buy_list_path = buy_list_path or data_path("amazon_buy_list.txt")
if not missing_songs:
logger.info("No new songs to add to buy list.")
return []
existing_songs = set()
if os.path.exists(buy_list_path):
try:
with open(buy_list_path, "r", encoding="utf-8") as file_handle:
lines = file_handle.read().splitlines()
for line in lines:
if line.strip() and not line.startswith("http") and " - " in line:
artist, title = line.split(" - ", 1)
existing_songs.add((artist.strip(), title.strip()))
except Exception:
pass
new_missing = []
for song in missing_songs:
key = (song["artist"], song["title"])
if key in existing_songs:
continue
existing_songs.add(key)
new_missing.append(song)
if not new_missing:
logger.info("No new songs to add to buy list.")
return []
file_exists = os.path.exists(buy_list_path)
file_empty = not file_exists or os.path.getsize(buy_list_path) == 0
with open(buy_list_path, "a", encoding="utf-8") as file_handle:
if file_empty:
file_handle.write("Songs not in your library - Amazon search links:\n\n")
for song in new_missing:
query = urllib.parse.quote(f"{song['artist']} {song['title']}")
file_handle.write(f"{song['artist']} - {song['title']}\n")
file_handle.write(f"https://www.amazon.com/s?k={query}&i=digital-music\n\n")
logger.info("Buy list updated.")
return new_missing
def format_result_summary(result):
station_bits = []
for station in result.get("station_breakdown", []):
if station.get("success"):
pattern = station.get("parse_pattern", "unknown")
payload_bytes = int(station.get("raw_payload_bytes", 0) or 0)
payload_kib = payload_bytes / 1024.0
station_bits.append(
f"{station['display_name']}: {station['scraped_count']} [{pattern}, {payload_kib:.1f} KiB]"
)
else:
station_bits.append(f"{station['display_name']}: failed")
station_summary = ", ".join(station_bits) or "No station data"
lines = [
f"Scraped {result.get('scraped_count', 0)} songs ({station_summary})",
f"Matched {result.get('matched_count', 0)} in Plex",
f"Added {result.get('added_count', 0)} to playlist",
f"Skipped {result.get('skipped_count', 0)} invalid entries",
f"Suppressed {result.get('duplicate_count', 0)} duplicates already in playlist",
f"Missing {result.get('missing_count', 0)} from Plex library",
]
if result.get("status") == "error":
lines.append(f"Error: {result.get('error_message', 'Unknown error')}")
return "\n".join(lines)
def run_update_job(config=None, dry_run=False, persist_history=True, write_buy_list=True):
config = config or load_runtime_config()
result = {
"status": "success",
"error_message": "",
"station_breakdown": [],
"scraped_count": 0,
"matched_count": 0,
"added_count": 0,
"added_songs": [],
"missing_count": 0,
"missing_songs": [],
"duplicate_count": 0,
"duplicate_songs": [],
"skipped_count": 0,
"skipped_songs": [],
}
import json as _json
# station_key → display name used in song["source"]
STATION_SOURCE = {"journey_fm": "Journey FM", "spirit_fm": "Spirit FM", "klove": "K-LOVE"}
token = config.get("PLEX_TOKEN", "").strip()
server_ip = config.get("SERVER_IP", "").strip()
# Build playlist configs: [{name, stations}]
playlists_raw = config.get("PLAYLISTS")
if playlists_raw:
try:
playlists = _json.loads(playlists_raw) if isinstance(playlists_raw, str) else playlists_raw
except Exception:
playlists = []
else:
playlists = []
if not playlists:
# Fall back to flat PLAYLIST_NAMES / PLAYLIST_NAME + SELECTED_STATIONS
raw_names = config.get("PLAYLIST_NAMES") or config.get("PLAYLIST_NAME", "Journey FM Recently Played")
if isinstance(raw_names, list):
names = raw_names
else:
names = [n.strip() for n in str(raw_names).split(",") if n.strip()]
stations = config.get("SELECTED_STATIONS", ["journey_fm"])
if isinstance(stations, str):
stations = [s.strip() for s in stations.split(",") if s.strip()]
playlists = [{"name": n, "stations": list(stations)} for n in (names or ["Journey FM Recently Played"])]
if not token or not server_ip:
result["status"] = "error"
result["error_message"] = "Missing configuration: set Plex token and server IP before running updates"
return result
if persist_history:
init_history_db()
# Scrape the union of all stations across all playlists (no duplicate scrapes)
all_station_keys = list({s for pl in playlists for s in pl.get("stations", [])})
scrape_result = scrape_recently_played(all_station_keys)
all_songs = scrape_result["songs"]
result["station_breakdown"] = scrape_result["station_results"]
result["scraped_count"] = len(all_songs)
result["scraped_songs"] = all_songs
try:
plex = connect_to_plex_server(token, server_ip)
merged = {}
for pl in playlists:
pl_sources = {STATION_SOURCE.get(s, s) for s in pl.get("stations", [])}
pl_songs = [s for s in all_songs if s.get("source", "") in pl_sources] if pl_sources else all_songs
pr = create_or_update_playlist(plex, pl_songs, pl["name"], dry_run=dry_run)
if not merged:
merged = pr
else:
merged["added_count"] = merged.get("added_count", 0) + pr.get("added_count", 0)
merged["added_songs"] = merged.get("added_songs", []) + pr.get("added_songs", [])
merged["duplicate_count"] = merged.get("duplicate_count", 0) + pr.get("duplicate_count", 0)
result.update(merged)
result["missing_count"] = len(result.get("missing_songs", []))
result["skipped_count"] = len(result.get("skipped_songs", []))
if write_buy_list and not dry_run:
update_buy_list(result.get("missing_songs", []))
except PlexConnectionError as exc:
result["status"] = "error"
result["error_message"] = str(exc)
except Exception as exc:
result["status"] = "error"
result["error_message"] = f"Unexpected update failure: {exc}"
if persist_history:
save_history_entry(result)
return result