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