From f20aa9b0d810c2986899280bc3074b228a770156 Mon Sep 17 00:00:00 2001 From: Johan Hjorth Date: Mon, 23 Jun 2025 10:31:25 +0200 Subject: [PATCH] Iniital Commit --- README.md | 17 ++++++++++++++- config.py | 15 +++++++++++++ discovery_sync.py | 48 ++++++++++++++++++++++++++++++++++++++++++ lastfm_helpers.py | 43 +++++++++++++++++++++++++++++++++++++ lidarr_helpers.py | 28 ++++++++++++++++++++++++ musicbrainz_helpers.py | 18 ++++++++++++++++ 6 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 config.py create mode 100644 discovery_sync.py create mode 100644 lastfm_helpers.py create mode 100644 lidarr_helpers.py create mode 100644 musicbrainz_helpers.py diff --git a/README.md b/README.md index 5643ad1..895da55 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,17 @@ -# discoverylidarr +# DiscoveryLidarr 🎧 +Upptäck nya artister baserat på din Last.fm-historik och lägg till dem i Lidarr automatiskt. + +## Instruktioner + +1. Fyll i `config.py` med dina egna API-nycklar och inställningar. +2. Installera beroenden: + ``` + pip install requests + ``` +3. Kör scriptet: + ``` + python3 discovery_sync.py + ``` + +Vill du automatisera? Lägg till i cron eller som systemd-timer. diff --git a/config.py b/config.py new file mode 100644 index 0000000..6559200 --- /dev/null +++ b/config.py @@ -0,0 +1,15 @@ +LASTFM_USERNAME = "ditt_lastfm_namn" +LASTFM_API_KEY = "din_lastfm_api_nyckel" + +LIDARR_URL = "http://localhost:8686" +LIDARR_API_KEY = "din_lidarr_api_nyckel" + +ROOT_FOLDER = "/media/music2" +QUALITY_PROFILE_ID = 1 + +MIN_PLAYS = 15 +RECENT_MONTHS = 3 +MAX_SIMILAR_PER_ART = 20 +SIMILAR_MATCH_MIN = 0.5 +CACHE_TTL_HOURS = 24 +DEBUG_PRINT = True diff --git a/discovery_sync.py b/discovery_sync.py new file mode 100644 index 0000000..600aff5 --- /dev/null +++ b/discovery_sync.py @@ -0,0 +1,48 @@ +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 +import time, logging + +logging.basicConfig(level=logging.INFO) +log = logging.getLogger("DiscoveryLidarr") + +def sync(): + start = time.time() + cache = load_cache() + added_artists = set(cache.get("added_artists", [])) + similar_cache = cache.setdefault("similar_cache", {}) + + recent = recent_artists() + log.info(f"🎧 Analyserar {len(recent)} senaste artister från Last.fm") + + new = 0 + for name, mbid in recent: + if not mbid or mbid in added_artists: + continue + + 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 [] + 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: + continue + + log.info(f"✨ Ny artist: {sim.get('name')} (match {match:.2f})") + if lidarr_api_add_artist(sid): + added_artists.add(sid) + new += 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") + +if __name__ == "__main__": + sync() diff --git a/lastfm_helpers.py b/lastfm_helpers.py new file mode 100644 index 0000000..fbe55e3 --- /dev/null +++ b/lastfm_helpers.py @@ -0,0 +1,43 @@ +from config import * +import requests, time, urllib.parse +from collections import defaultdict +from datetime import datetime, timedelta + +def lf_request(method, **params): + 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 + +def recent_artists(): + since = 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 + + for t in js.get("recenttracks", {}).get("track", []): + a = t["artist"] + counts[(a["#text"], a.get("mbid", ""))] += 1 + + attr = js.get("recenttracks", {}).get("@attr", {}) + if page >= int(attr.get("totalPages", 1)): + break + page += 1 + + return [(n, m) for (n, m), c in counts.items() if c >= MIN_PLAYS] diff --git a/lidarr_helpers.py b/lidarr_helpers.py new file mode 100644 index 0000000..f6a5b39 --- /dev/null +++ b/lidarr_helpers.py @@ -0,0 +1,28 @@ +from config import * +import requests + +def lidarr_api_add_artist(mbid): + lookup_url = f"{LIDARR_URL}/api/v1/artist/lookup?term=mbid:{mbid}" + headers = {"X-Api-Key": LIDARR_API_KEY, "Content-Type": "application/json"} + + res = requests.get(lookup_url, headers=headers) + if not res.ok or not res.json(): + return False + + artist = res.json()[0] + payload = { + "foreignArtistId": artist['foreignArtistId'], + "artistName": artist['artistName'], + "monitored": True, + "qualityProfileId": QUALITY_PROFILE_ID, + "metadataProfileId": 1, + "rootFolderPath": ROOT_FOLDER, + "addOptions": { + "monitor": "all", + "searchForMissingAlbums": True + } + } + + add_url = f"{LIDARR_URL}/api/v1/artist" + post_res = requests.post(add_url, headers=headers, json=payload) + return post_res.ok diff --git a/musicbrainz_helpers.py b/musicbrainz_helpers.py new file mode 100644 index 0000000..650d752 --- /dev/null +++ b/musicbrainz_helpers.py @@ -0,0 +1,18 @@ +import json +from pathlib import Path + +CACHE_FILE = Path(__file__).resolve().parent / "cache.json" + +def load_cache(): + try: + if not CACHE_FILE.exists(): + with open(CACHE_FILE, "w") as f: + json.dump({"added_artists": [], "similar_cache": {}}, f) + with open(CACHE_FILE, "r") as f: + return json.load(f) + except Exception: + return {"added_artists": [], "similar_cache": {}} + +def save_cache(cache): + with open(CACHE_FILE, "w") as f: + json.dump(cache, f, indent=2)