From 88d1f947483abae8bad70bb53995708e6a89255f Mon Sep 17 00:00:00 2001 From: Johan Hjorth Date: Wed, 25 Jun 2025 13:53:24 +0200 Subject: [PATCH] working on plex_playlists based on gemini --- discovery_sync.py | 32 +++++------ lastfm_helpers.py | 120 ++++++++++++++++++++++++++++-------------- plex_playlist_sync.py | 86 +++++++++++++++--------------- 3 files changed, 142 insertions(+), 96 deletions(-) diff --git a/discovery_sync.py b/discovery_sync.py index 600aff5..4507136 100644 --- a/discovery_sync.py +++ b/discovery_sync.py @@ -1,7 +1,9 @@ +# discovery_sync.py + from config import * -from lastfm_helpers import lf_request, recent_artists -from lidarr_helpers import lidarr_api_add_artist -from musicbrainz_helpers import load_cache, save_cache +from lastfm_helpers import recent_artists, get_similar_artists_pylast # Korrekt +from lidarr_helpers import lidarr_api_add_artist # Antar att lidarr_helpers bara har denna funktion att exportera +from musicbrainz_helpers import load_cache, save_cache # Antar att musicbrainz_helpers bara har dessa funktioner att exportera import time, logging logging.basicConfig(level=logging.INFO) @@ -14,9 +16,9 @@ def sync(): similar_cache = cache.setdefault("similar_cache", {}) recent = recent_artists() - log.info(f"🎧 Analyserar {len(recent)} senaste artister frĂ„n Last.fm") + log.info(f"🎧 Analyzing {len(recent)} recent artists from Last.fm") - new = 0 + new_artists_added_count = 0 for name, mbid in recent: if not mbid or mbid in added_artists: continue @@ -24,25 +26,25 @@ def sync(): if mbid in similar_cache: sims = similar_cache[mbid]["data"] else: - js = lf_request("artist.getSimilar", mbid=mbid, limit=MAX_SIMILAR_PER_ART) - sims = js.get("similarartists", {}).get("artist", []) if js else [] + sims = get_similar_artists_pylast(mbid, MAX_SIMILAR_PER_ART) similar_cache[mbid] = {"ts": time.time(), "data": sims} save_cache(cache) - for sim in sims: - sid = sim.get("mbid") - match = float(sim.get("match", 0)) - if not sid or sid in added_artists or match < SIMILAR_MATCH_MIN: + for sim_artist_obj, match in sims: + sid = sim_artist_obj.mbid + match_float = float(match) + + if not sid or sid in added_artists or match_float < SIMILAR_MATCH_MIN: continue - log.info(f"✹ Ny artist: {sim.get('name')} (match {match:.2f})") + log.info(f"✹ New artist: {sim_artist_obj.name} (match {match_float:.2f})") if lidarr_api_add_artist(sid): added_artists.add(sid) - new += 1 + new_artists_added_count += 1 cache["added_artists"] = list(added_artists) save_cache(cache) - log.info(f"✅ Klar! {new} nya artister tillagda pĂ„ {((time.time()-start)/60):.1f} min") + log.info(f"✅ Done! {new_artists_added_count} new artists added in {((time.time()-start)/60):.1f} min") if __name__ == "__main__": - sync() + sync() \ No newline at end of file diff --git a/lastfm_helpers.py b/lastfm_helpers.py index e177ca5..e5188b5 100644 --- a/lastfm_helpers.py +++ b/lastfm_helpers.py @@ -1,47 +1,89 @@ -from config import * -import requests, time, urllib.parse -from collections import defaultdict -from datetime import datetime, timedelta +# lastfm_helpers.py -def lf_request(method, **params): - # Fixa 'from_' → 'from' och 'to_' → 'to' (Last.fm accepterar inte Python-sĂ€kra namn) - for alt, real in (("from_", "from"), ("to_", "to")): - if alt in params: - params[real] = params.pop(alt) - url = "https://ws.audioscrobbler.com/2.0/" - params.update({ - "method": method, - "api_key": LASTFM_API_KEY, - "format": "json" - }) - try: - r = requests.get(url, params=params) - r.raise_for_status() - return r.json() - except Exception as e: - print(f"[LFM ERR] {e}") - return None +import pylast +from config import ( + LASTFM_API_KEY, LASTFM_API_SECRET, LASTFM_USERNAME, + MIN_PLAYS, RECENT_MONTHS +) +import logging +from datetime import datetime, timedelta +from collections import defaultdict + +log = logging.getLogger("LastFmHelpers") + +LASTFM_NETWORK = None +try: + LASTFM_NETWORK = pylast.LastFMNetwork( + api_key=LASTFM_API_KEY, + api_secret=LASTFM_API_SECRET, + username=LASTFM_USERNAME + ) + log.info("✅ Connected to Last.fm API.") +except Exception as e: + log.error(f"❌ Failed to initialize Last.fm network. Check API keys and username: {e}") def recent_artists(): - since = int((datetime.utcnow() - timedelta(days=30 * RECENT_MONTHS)).timestamp()) + """Fetches recently played artists from Last.fm, filtered by play count and recency.""" + if not LASTFM_NETWORK: + log.error("Last.fm network not initialized. Cannot fetch recent artists.") + return [] + + user = LASTFM_NETWORK.get_user(LASTFM_USERNAME) + since_timestamp = int((datetime.utcnow() - timedelta(days=30 * RECENT_MONTHS)).timestamp()) counts = defaultdict(int) - page = 1 - while True: - js = lf_request( - "user.getRecentTracks", user=LASTFM_USERNAME, - limit=200, page=page, from_=since - ) - if not js: - break + log.info(f"Fetching recent tracks for {LASTFM_USERNAME} since {datetime.fromtimestamp(since_timestamp).strftime('%Y-%m-%d %H:%M:%S')}") + try: + for track in user.get_recent_tracks(time_from=since_timestamp, limit=None): + artist = track.artist + counts[(artist.name, artist.mbid)] += 1 + except pylast.NetworkError as ne: + log.error(f"Network error fetching recent tracks: {ne}") + return [] + except pylast.WSError as wse: + log.error(f"Last.fm API error fetching recent tracks: {wse}") + return [] + except Exception as e: + log.error(f"Unexpected error fetching recent tracks: {e}") + return [] - for t in js.get("recenttracks", {}).get("track", []): - a = t["artist"] - counts[(a["#text"], a.get("mbid", ""))] += 1 + return [(name, mbid) for (name, mbid), count in counts.items() if count >= MIN_PLAYS] - attr = js.get("recenttracks", {}).get("@attr", {}) - if page >= int(attr.get("totalPages", 1)): - break - page += 1 +def get_similar_artists_pylast(artist_mbid, limit): + """Fetches artists similar to a given artist from Last.fm.""" + if not LASTFM_NETWORK: + log.error("Last.fm network not initialized. Cannot fetch similar artists.") + return [] + + try: + artist = LASTFM_NETWORK.get_artist_by_mbid(artist_mbid) + similar_artists_tuples = artist.get_similar(limit=limit) + return similar_artists_tuples + except pylast.WSError as wse: + log.warning(f"Last.fm API error fetching similar artists for MBID {artist_mbid}: {wse}. Check MBID.") + return [] + except pylast.NetworkError as ne: + log.warning(f"Network error fetching similar artists for MBID {artist_mbid}: {ne}") + return [] + except Exception as e: + log.warning(f"Could not fetch similar artists for MBID {artist_mbid}: {e}") + return [] - return [(n, m) for (n, m), c in counts.items() if c >= MIN_PLAYS] +def get_artist_top_tracks(artist_name, limit=5): + """Fetches top tracks for a specific artist from Last.fm.""" + if not LASTFM_NETWORK: + log.error("Last.fm network not initialized. Cannot fetch top tracks.") + return [] + try: + artist = LASTFM_NETWORK.get_artist(artist_name) + top_tracks = artist.get_top_tracks(limit=limit) + return [{"artist": track.item.artist.name, "title": track.item.title} for track in top_tracks] + except pylast.WSError as wse: + log.warning(f"Last.fm API error fetching top tracks for '{artist_name}': {wse}. Check artist name.") + return [] + except pylast.NetworkError as ne: + log.warning(f"Network error fetching top tracks for '{artist_name}': {ne}") + return [] + except Exception as e: + log.warning(f"Unexpected error fetching top tracks for '{artist_name}': {e}") + return [] \ No newline at end of file diff --git a/plex_playlist_sync.py b/plex_playlist_sync.py index 37442d8..fa6afc9 100644 --- a/plex_playlist_sync.py +++ b/plex_playlist_sync.py @@ -1,16 +1,16 @@ -# plex_playlist_sync.py (Den fullstĂ€ndiga koden frĂ„n mitt tidigare svar) +# plex_playlist_sync.py -# Importera konfiguration och hjĂ€lpfunktioner frĂ„n dina befintliga filer from config import ( LASTFM_API_KEY, LASTFM_API_SECRET, LASTFM_USERNAME, PLEX_BASEURL, PLEX_TOKEN, MIN_PLAYS, RECENT_MONTHS, MAX_SIMILAR_PER_ART, SIMILAR_MATCH_MIN, CACHE_TTL_HOURS, DEBUG_PRINT ) -from lastfm_helpers import lf_request, recent_artists +# Importera relevanta funktioner frĂ„n lastfm_helpers +from lastfm_helpers import recent_artists, get_artist_top_tracks from musicbrainz_helpers import load_cache, save_cache # AnvĂ€nds för cache -import pylast +import pylast # Behövs för pylast.LastFMNetwork initiering i denna fil from plexapi.server import PlexServer import datetime import time @@ -19,6 +19,11 @@ import logging logging.basicConfig(level=logging.INFO) log = logging.getLogger("PlexPlaylistSync") +# Initialisera Last.fm-nĂ€tverket i denna fil ocksĂ„, om det behövs hĂ€r. +# OBS: lastfm_helpers initialiserar det globalt, sĂ„ du kan potentiellt ta bort detta block +# om du Ă€r sĂ€ker pĂ„ att lastfm_helpers alltid laddas först och initierar nĂ€tverket korrekt. +# Men för att vara sĂ€ker, kan du behĂ„lla det eller flytta initieringen till en central punkt. +LASTFM_NETWORK = None try: LASTFM_NETWORK = pylast.LastFMNetwork( api_key=LASTFM_API_KEY, @@ -26,74 +31,71 @@ try: username=LASTFM_USERNAME ) except Exception as e: - log.error(f"Kunde inte initiera Last.fm-nĂ€tverket. Kontrollera nycklar/anvĂ€ndarnamn: {e}") + log.error(f"Failed to initialize Last.fm network. Check keys/username: {e}") LASTFM_NETWORK = None -def get_artist_top_tracks(artist_name, limit=5): - if not LASTFM_NETWORK: - return [] - try: - artist = LASTFM_NETWORK.get_artist(artist_name) - top_tracks = artist.get_top_tracks(limit=limit) - return [{"artist": track.item.artist.name, "title": track.item.title} for track in top_tracks] - except Exception as e: - log.warning(f"Kunde inte hĂ€mta top-spĂ„r för '{artist_name}' frĂ„n Last.fm: {e}") - return [] +# get_artist_top_tracks funktionen behöver INTE definieras hĂ€r +# om den importeras frĂ„n lastfm_helpers.py +# Jag tar bort den hĂ€rifrĂ„n, sĂ„ den anvĂ€nds korrekt importerad. def create_lastfm_recommended_playlist(): start_time = time.time() - log.info("🚀 Börjar processen för att skapa Last.fm-baserade spellistor i Plex.") + log.info("🚀 Starting process to create Last.fm-based playlists in Plex.") try: plex = PlexServer(PLEX_BASEURL, PLEX_TOKEN) - log.info(f"✅ Ansluten till Plex Media Server: {plex.baseurl}") + log.info(f"✅ Connected to Plex Media Server: {plex.baseurl}") except Exception as e: - log.error(f"❌ Kunde inte ansluta till Plex Server: {e}") - log.error("Kontrollera PLEX_BASEURL och PLEX_TOKEN i config_local.py.") + log.error(f"❌ Could not connect to Plex Server: {e}") + log.error("Check PLEX_BASEURL and PLEX_TOKEN in config_local.py.") return music_library = None try: music_library = plex.library.section('Music') - log.info(f"AnvĂ€nder Plex-biblioteket: '{music_library.title}'") + log.info(f"Using Plex library: '{music_library.title}'") except Exception as e: - log.error(f"❌ Kunde inte hitta musikbiblioteket 'Music' i Plex: {e}") - log.error("Kontrollera namnet pĂ„ ditt musikbibliotek i Plex.") + log.error(f"❌ Could not find 'Music' library in Plex: {e}") + log.error("Check your music library name in Plex.") return artists_for_playlist = [] cache = load_cache() added_artists_mbids = cache.get("added_artists", []) - log.info("Samlar in artister för spellistan...") + log.info("Gathering artists for the playlist...") + # Get names for artists added by Lidarr sync for mbid in added_artists_mbids: try: - artist_info = LASTFM_NETWORK.get_artist_by_mbid(mbid) + # Re-use the LASTFM_NETWORK initialized above or in lastfm_helpers + artist_info = LASTFM_NETWORK.get_artist_by_mbid(mbid) # Needs LASTFM_NETWORK from this file or lastfm_helpers artists_for_playlist.append(artist_info.name) except Exception: - log.debug(f"Kunde inte hitta namn för MBID: {mbid} via pylast.") + log.debug(f"Could not find name for MBID: {mbid} via pylast.") pass - recent_played_artists = recent_artists() + # Add some of the most recently played artists as well + recent_played_artists = recent_artists() # Uses recent_artists from lastfm_helpers for name, mbid in recent_played_artists[:10]: if name not in artists_for_playlist: artists_for_playlist.append(name) if not artists_for_playlist: - log.info("Inga artister hittades för att skapa en spellista. Se till att Last.fm-synkroniseringen har kört.") + log.info("No artists found to create a playlist. Ensure Last.fm sync has run.") return - log.info(f"Hittade {len(artists_for_playlist)} unika artister för att bygga spellistan.") + log.info(f"Found {len(artists_for_playlist)} unique artists to build the playlist.") all_recommended_tracks_info = [] for artist_name in artists_for_playlist: + # Use the imported get_artist_top_tracks top_tracks = get_artist_top_tracks(artist_name, limit=3) all_recommended_tracks_info.extend(top_tracks) unique_tracks_to_add = [] seen_track_identifiers = set() - log.info(f"Söker efter {len(all_recommended_tracks_info)} potentiella spĂ„r i Plex-biblioteket...") + log.info(f"Searching for {len(all_recommended_tracks_info)} potential tracks in Plex library...") for track_info in all_recommended_tracks_info: artist_name = track_info["artist"] track_title = track_info["title"] @@ -112,28 +114,28 @@ def create_lastfm_recommended_playlist(): break except Exception as e: - log.warning(f"Fel vid Plex-sökning efter '{track_title}' av '{artist_name}': {e}") + log.warning(f"Error searching Plex for '{track_title}' by '{artist_name}': {e}") if found_plex_track: unique_tracks_to_add.append(found_plex_track) seen_track_identifiers.add(identifier) - log.info(f" ✅ Hittade: {found_plex_track.artist().title} - {found_plex_track.title}") + log.info(f" ✅ Found: {found_plex_track.artist().title} - {found_plex_track.title}") else: - log.info(f" ❌ Hittade inte: '{track_title}' av '{artist_name}' i Plex.") + log.info(f" ❌ Not found: '{track_title}' by '{artist_name}' in Plex.") if not unique_tracks_to_add: - log.info("Inga nya matchande lĂ„tar hittades i Plex för att skapa en spellista.") + log.info("No new matching songs found in Plex to create a playlist.") return current_date = datetime.datetime.now().strftime("%Y-%m") - playlist_name = f"Last.fm Rekommendationer {current_date}" + playlist_name = f"Last.fm Recommendations {current_date}" playlist = None try: playlist = plex.playlist(playlist_name) - log.info(f"Spellistan '{playlist_name}' finns redan.") + log.info(f"Playlist '{playlist_name}' already exists.") except Exception: - log.info(f"Skapar ny spellista: '{playlist_name}'") + log.info(f"Creating new playlist: '{playlist_name}'") if playlist: existing_playlist_items = playlist.items() @@ -142,19 +144,19 @@ def create_lastfm_recommended_playlist(): if new_items_to_add: try: playlist.addItems(new_items_to_add) - log.info(f"Lade till {len(new_items_to_add)} nya lĂ„tar i befintlig spellista '{playlist_name}'.") + log.info(f"Added {len(new_items_to_add)} new songs to existing playlist '{playlist_name}'.") except Exception as e: - log.error(f"Kunde inte lĂ€gga till lĂ„tar i befintlig spellista: {e}") + log.error(f"Could not add songs to existing playlist: {e}") else: - log.info(f"Inga nya lĂ„tar att lĂ€gga till i befintlig spellista '{playlist_name}'.") + log.info(f"No new songs to add to existing playlist '{playlist_name}'.") else: try: new_playlist = plex.createPlaylist(playlist_name, items=unique_tracks_to_add) - log.info(f"✅ Skapade ny spellista '{new_playlist.title}' med {len(unique_tracks_to_add)} lĂ„tar.") + log.info(f"✅ Created new playlist '{new_playlist.title}' with {len(unique_tracks_to_add)} songs.") except Exception as e: - log.error(f"❌ Kunde inte skapa spellista i Plex: {e}") + log.error(f"❌ Could not create playlist in Plex: {e}") - log.info(f"🎉 Klar! Processen tog {((time.time()-start_time)/60):.1f} minuter.") + log.info(f"🎉 Done! Process took {((time.time()-start_time)/60):.1f} minutes.") if __name__ == "__main__": create_lastfm_recommended_playlist() \ No newline at end of file