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>
177 lines
7.0 KiB
Python
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
|