2025-06-01 22:17:12 +02:00

309 lines
10 KiB
Lua
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- ~/.config/mpv/scripts/notify.lua
local mp = require 'mp'
local utils = require 'mp.utils'
local HOME = os.getenv("HOME") or ""
local function read_api_key(name)
local user_path = HOME .. "/.config/mpv/apikeys/" .. name
local system_path = "/etc/mpv/apikeys/" .. name
local file = io.open(user_path, "r") or io.open(system_path, "r")
if file then
local key = file:read("*l")
file:close()
return key
end
return nil
end
local LASTFM_API_KEY = read_api_key("lastfm")
local TMDB_API_KEY = read_api_key("tmdb")
local OLLAMA_URL = "http://localhost:11434/api/generate"
local HOME = os.getenv("HOME")
local pattern_cache, pattern_mtimes = {}, {}
local function log(tag, msg)
mp.msg.info("[" .. tag .. "] " .. tostring(msg))
end
local function url_encode(str)
return str:gsub("([^%w _%%%-%.~])", function(c)
return string.format("%%%02X", string.byte(c))
end):gsub(" ", "%%20")
end
local function file_mtime(path)
local stat = utils.file_info(path)
return stat and stat.modify or 0
end
local function watch_patterns(file_path)
local current_mtime = file_mtime(file_path)
if not pattern_mtimes[file_path] or pattern_mtimes[file_path] < current_mtime then
local file = io.open(file_path, "r")
if not file then return {} end
local patterns = {}
for line in file:lines() do
line = line:match("^%s*(.-)%s*$")
if line ~= "" and not line:match("^#") then
table.insert(patterns, line)
-- log(" Pattern loaded", line)
end
end
file:close()
pattern_cache[file_path] = patterns
pattern_mtimes[file_path] = current_mtime
end
return pattern_cache[file_path] or {}
end
local function apply_patterns(text, patterns)
for _, pat in ipairs(patterns) do
local lower = text:lower()
local low_pat = pat:lower()
local new = lower:gsub(low_pat, "")
if lower ~= new then
log(" Pattern applied", pat)
text = text:gsub(pat, "")
end
end
return text:gsub("[%.:_%-]", " "):gsub("%s+", " "):gsub("^%s*(.-)%s*$", "%1")
end
local function get_pattern_path(filename)
local user_path = HOME .. "/.config/mpv/filters/" .. filename
if utils.file_info(user_path) then
return user_path
end
return "/usr/share/mpv/scripts/notify/" .. filename
end
local function clean_artist_name(artist)
return apply_patterns(artist or "", watch_patterns(get_pattern_path("patterns_artist.txt")))
end
local function clean_title_common(title)
return apply_patterns(title or "", watch_patterns(get_pattern_path("patterns_common.txt")))
end
local function classify_media()
local tracks = mp.get_property_native("track-list") or {}
local has_audio, has_real_video = false, false
for _, track in ipairs(tracks) do
if track.type == "audio" and track.selected then
has_audio = true
elseif track.type == "video" and track.selected and not (track.codec and track.codec:match("mjpeg")) then
has_real_video = true
end
end
if has_real_video then
return "video"
elseif has_audio then
return "music"
end
return "unknown"
end
local function find_local_cover()
local path = mp.get_property("path")
if not path then return nil end
local dir, file = utils.split_path(path)
local base = file:match("^(.-)%.%w+$") or file
local suffixes = { "-poster.jpg", "-landscape.jpg", "-logo.png" }
for _, suffix in ipairs(suffixes) do
local candidate = utils.join_path(dir, base .. suffix)
if utils.file_info(candidate) then return candidate end
end
local files = utils.readdir(dir, "files") or {}
for _, f in ipairs(files) do
if f:lower():match("cover%.jpe?g") or f:lower():match("folder%.jpe?g") or f:lower():match("front%.jpe?g") then
return dir .. "/" .. f
end
end
return nil
end
local function notify_with_image(title, msg, image_path)
local tmp_path = "/tmp/mpv_art.jpg"
if image_path:match("^https?://") then
local dl = utils.subprocess({ args = { "curl", "-s", "-L", "-o", tmp_path, image_path } })
if dl.status == 0 then image_path = tmp_path end
end
utils.subprocess({ args = { "notify-send", title, msg, "--icon=" .. image_path } })
end
local function fetch_tmdb_poster(title)
title = clean_title_common(title)
local url = string.format("https://api.themoviedb.org/3/search/movie?api_key=%s&query=%s", TMDB_API_KEY,
url_encode(title))
log("TMDB Query", url)
local res = utils.subprocess({ args = { "curl", "-s", url } })
if res.status ~= 0 then
log("TMDB Error", "curl failed with status " .. tostring(res.status))
return nil
end
log("TMDB Response", res.stdout)
local data = utils.parse_json(res.stdout)
if not data then
log("TMDB Error", "parse_json returned nil")
return nil
end
if data.results and data.results[1] and data.results[1].poster_path then
return "https://image.tmdb.org/t/p/w500" .. data.results[1].poster_path
else
log("TMDB Error", "No poster found in results")
end
return nil
end
local function fetch_lastfm_cover(title, artist)
if not artist or artist == "" then return nil end
title = clean_title_common(title)
local url = string.format(
"https://ws.audioscrobbler.com/2.0/?method=track.getInfo&api_key=%s&artist=%s&track=%s&format=json", LASTFM_API_KEY,
url_encode(artist), url_encode(title))
log("Last.fm getInfo URL", url)
local res = utils.subprocess({ args = { "curl", "-s", url } })
if res.status == 0 then
local data = utils.parse_json(res.stdout)
if data and data.track and data.track.album and data.track.album.image then
for _, img in ipairs(data.track.album.image) do
if img.size == "extralarge" and img["#text"] ~= "" then
return img["#text"]
end
end
end
end
return nil
end
local function infer_music_metadata(title, artist, metadata)
local prompt = string.format([[File: "%s"
Metadata detected:
Artist: %s
Album: %s
Based on this, return a JSON: { "artist": "...", "title": "..." }
do NOT explain anything. do NOT return quotation nor additional text.
]], title, artist or "", metadata.album or "")
log(" Prompt sent to LLM (music)", prompt)
local payload = utils.format_json({ model = "gemma2:2b", prompt = prompt, stream = false })
local res = utils.subprocess({ args = { "curl", "-s", "-X", "POST", OLLAMA_URL, "-H", "Content-Type: application/json", "-d", payload } })
if not res or res.status ~= 0 then return "", title end
local outer = utils.parse_json(res.stdout)
if outer and outer.response then
local clean = outer.response:gsub("```json", ""):gsub("```", ""):gsub("\n", ""):gsub("\\", "")
local parsed = utils.parse_json(clean)
if parsed then return parsed.artist or "", parsed.title or title end
end
return "", title
end
local function safe_infer_metadata(title, artist, metadata)
local inferred_artist, inferred_title = infer_music_metadata(title, artist, metadata)
if not inferred_title or inferred_title:lower():match("unknown") then inferred_title = title end
local trusted_artist = artist
if artist == "" or artist:lower():match("semal") or artist:lower():match("official") then
trusted_artist = inferred_artist
elseif inferred_artist:lower() == artist:lower() then
trusted_artist = inferred_artist
elseif artist:lower():find(inferred_artist:lower(), 1, true) then
trusted_artist = inferred_artist
else
log(" Inference ignored: artist not confiable", inferred_artist)
end
trusted_artist = clean_artist_name(trusted_artist)
return trusted_artist, inferred_title
end
local function infer_title_with_ollama(title)
if not title or title == "" then return title end
local prompt = string.format([[Archivo: "%s"
Devuelve un JSON válido con el título real de la obra. NO incluyas:
- Número de capítulo o lista (como "01.", "16.")
- Nombre del grupo release (como "newpct", "rarbg", "YTS", etc)
- Resolución, calidad o formato ("4K", "HDR", "1080p", etc)
Ejemplos válidos:
Archivo: "16. Licencia para Matar 1985 [newpct]"
→ { "title": "Licencia para Matar" }
Archivo: "300.mkv"
→ { "title": "300" }
NO EXPLIQUES NADA. SOLO devuélvelo como JSON exacto: { "title": "..." }
]], title)
log(" Prompt enviado a Ollama (video)", prompt)
local payload = utils.format_json({ model = "gemma2:2b", prompt = prompt, stream = false })
local res = utils.subprocess({ args = { "curl", "-s", "-X", "POST", OLLAMA_URL, "-H", "Content-Type: application/json", "-d", payload } })
if res.status ~= 0 then return title end
print("\27[1;34m[OLLAMA VIDEO RAW]\27[0m " .. (res.stdout or "")) -- Color azul, puedes quitarlo
local outer = utils.parse_json(res.stdout)
if outer and outer.response then
local clean = outer.response:gsub("```json", ""):gsub("```", ""):gsub("\n", ""):gsub("\\", "")
local parsed = utils.parse_json(clean)
if parsed and parsed.title then return parsed.title end
end
return title
end
local function on_file_loaded()
local media_type = classify_media()
local metadata = mp.get_property_native("metadata") or {}
local raw_title = metadata.title or metadata.album or mp.get_property("media-title") or mp.get_property("filename") or
"Unknown"
local raw_artist = mp.get_property("metadata/by-key/artist") or ""
if raw_artist:find(",") then raw_artist = raw_artist:match("([^,]+)") end
local artist = clean_artist_name(raw_artist)
log(" Type of media", media_type)
log(" Artist detected", artist)
log(" raw title", raw_title)
local local_cover = find_local_cover()
if local_cover then
log(" Local cover detected", local_cover)
notify_with_image(media_type == "video" and " Video" or " " .. (artist ~= "" and artist or "Music"),
clean_title_common(raw_title), local_cover)
return
end
if media_type == "music" then
artist, raw_title = safe_infer_metadata(raw_title, raw_artist, metadata)
elseif media_type == "video" then
raw_title = infer_title_with_ollama(raw_title)
end
local clean_title = clean_title_common(raw_title)
if media_type == "music" then
local cover = fetch_lastfm_cover(clean_title, artist)
if cover then
notify_with_image(" " .. (artist ~= "" and artist or "Música"), clean_title, cover)
return
end
elseif media_type == "video" then
local poster = fetch_tmdb_poster(clean_title)
if poster then
log(" Poster TMDB detectado", poster)
notify_with_image(" Video", clean_title, poster)
return
end
end
notify_with_image(" " .. (artist ~= "" and artist or "Desconocido"), clean_title, "/tmp/default_cover.jpg")
end
mp.observe_property("metadata", "native", function(_, _) on_file_loaded() end)
mp.observe_property("metadata/by-key/icy-title", "string", on_stream_title_change)