#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-only
"""Melody lyrics provider (ADR-0293): lyrics from LRCLIB (lrclib.net).

Asked when the files have no lyrics. LRCLIB is looked up by artist, title,
album and length; failing that, searched by artist and title for lyrics of
the same length, give or take two seconds -- synced ones first. No account
is needed. What is found is kept a month in the cache folder, what is not a
day; a busy LRCLIB (429, 503) is asked again a few times after a wait.
"""
import hashlib
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

AGENT = "Melody-lrclib/1 (https://github.com/carnager/melody-next)"
API = "https://lrclib.net/api/"
FOUND_KEPT = 30 * 24 * 3600
MISSING_KEPT = 24 * 3600
TRIES = 4
CACHE = os.path.join(os.environ.get("XDG_CACHE_HOME") or os.path.expanduser("~/.cache"),
                     "melody", "plugins", "lrclib")


def fail(message):
    print(message, file=sys.stderr)
    sys.exit(1)


def ask(path, query):
    url = API + path + "?" + urllib.parse.urlencode(query)
    for attempt in range(TRIES):
        request = urllib.request.Request(url, headers={"User-Agent": AGENT})
        try:
            with urllib.request.urlopen(request, timeout=15) as response:
                return json.load(response)
        except urllib.error.HTTPError as error:
            if error.code == 404:
                return None
            if error.code in (429, 503) and attempt + 1 < TRIES:
                try:
                    wait = float(error.headers.get("Retry-After") or 0)
                except ValueError:
                    wait = 0
                time.sleep(min(max(wait, 1.5 * (attempt + 1)), 8.0))
                continue
            fail("LRCLIB answered %s%s" % (error.code, ", busy; try again in a while"
                                          if error.code in (429, 503) else ""))
        except Exception as error:
            fail("LRCLIB: %s" % error)
    return None


def lyrics_of(record):
    if not record or record.get("instrumental"):
        return None
    return record.get("syncedLyrics") or record.get("plainLyrics") or None


def find(artist, title, album, seconds):
    query = {"artist_name": artist, "track_name": title}
    if album:
        query["album_name"] = album
    if seconds:
        query["duration"] = seconds
    exact = lyrics_of(ask("get", query)) if seconds else None
    if exact:
        return exact
    found = ask("search", {"artist_name": artist, "track_name": title}) or []
    if not isinstance(found, list):
        return None
    fitting = [r for r in found if lyrics_of(r)
               and (not seconds or abs(float(r.get("duration") or 0) - seconds) <= 2)]
    fitting.sort(key=lambda r: (not r.get("syncedLyrics"),
                                abs(float(r.get("duration") or 0) - (seconds or 0))))
    return lyrics_of(fitting[0]) if fitting else None


def main():
    given = json.load(sys.stdin)
    playing = given.get("playing") or {}
    artist = (playing.get("artist") or playing.get("album_artist") or "").strip()
    title = (playing.get("title") or "").strip()
    album = (playing.get("album") or "").strip()
    duration = playing.get("duration_ms")
    seconds = round(duration / 1000) if isinstance(duration, (int, float)) and duration > 0 else 0
    if not artist or not title:
        print(json.dumps({"lyrics": None}))
        return
    key = hashlib.sha1(("%s\x1f%s\x1f%s\x1f%d" % (artist.casefold(), title.casefold(),
                                                   album.casefold(), seconds)).encode("utf-8"))
    path = os.path.join(CACHE, key.hexdigest())
    try:
        with open(path, encoding="utf-8") as kept:
            known = json.load(kept)
        age = time.time() - os.path.getmtime(path)
        if age < (FOUND_KEPT if known.get("lyrics") else MISSING_KEPT):
            print(json.dumps({"lyrics": known.get("lyrics"), "source": "LRCLIB"}))
            return
    except (OSError, ValueError):
        pass
    text = find(artist, title, album, seconds)
    os.makedirs(CACHE, exist_ok=True)
    with open(path + ".partial", "w", encoding="utf-8") as kept:
        json.dump({"lyrics": text}, kept)
    os.replace(path + ".partial", path)
    print(json.dumps({"lyrics": text, "source": "LRCLIB"}))


if __name__ == "__main__":
    main()
