Iniital Commit
This commit is contained in:
parent
91245007e6
commit
f20aa9b0d8
6 changed files with 168 additions and 1 deletions
17
README.md
17
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.
|
||||||
|
|
|
||||||
15
config.py
Normal file
15
config.py
Normal file
|
|
@ -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
|
||||||
48
discovery_sync.py
Normal file
48
discovery_sync.py
Normal file
|
|
@ -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()
|
||||||
43
lastfm_helpers.py
Normal file
43
lastfm_helpers.py
Normal file
|
|
@ -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]
|
||||||
28
lidarr_helpers.py
Normal file
28
lidarr_helpers.py
Normal file
|
|
@ -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
|
||||||
18
musicbrainz_helpers.py
Normal file
18
musicbrainz_helpers.py
Normal file
|
|
@ -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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue