initial commit

This commit is contained in:
teraflops 2025-05-29 18:57:13 +02:00
commit b25ebf7ca2
Signed by: teraflops
GPG Key ID: 2B77D97AF6F8968C
5 changed files with 452 additions and 0 deletions

22
LICENSE Normal file
View File

@ -0,0 +1,22 @@
MIT License
Copyright (c) 2025 Teraflops
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

65
README.md Normal file
View File

@ -0,0 +1,65 @@
# mpvcovergrabber
`mpvcovergrabber` is a script for [MPV](https://mpv.io) that shows rich notifications with album covers or movie posters when playing audio or video files. It uses local inference via language models such as `gemma2:2b` through [Ollama](https://ollama.com) and APIs like Last.fm and TMDB.
## Features
- Metadata inference using LLMs (via Ollama)
- Cover retrieval from:
- Local files
- Last.fm (for music)
- TMDB (for movies)
- Metadata cleanup using customizable regex patterns
- Support for audio streams
## Installation
You can install it form the AUR:
```bash
yay -S mpvcovergrabber-git
```
or
```bash
git clone https://aur.archlinux.org/mpvcovergrabber-git.git
cd mpvcovergrabber-git
makepkg -si
```
This will install the following files:
- `/etc/mpv/scripts/notify.lua`
- `/etc/mpv/filters/patterns_artist.txt`
- `/etc/mpv/filters/patterns_common.txt`
## Dependencies
- [mpv](https://mpv.io)
- `curl`
- `coreutils`
### Optional dependencies
- [Ollama](https://ollama.com): for LLM-based metadata inference (e.g., gemma2)
- `ollama-cuda`: for GPU-accelerated inference
## Configuration
Edit the pattern files to customize metadata cleanup:
- `/etc/mpv/filters/patterns_artist.txt`
- `/etc/mpv/filters/patterns_common.txt`
Store your API keys in:
- `~/.config/mpv/apikeys/lastfm`
- `~/.config/mpv/apikeys/tmdb`
## Author
Developed by [Teraflops](https://gitlab.com/teraflops)
---
## License
This project is licensed under the MIT License. See the `LICENSE` file for more details.

View File

@ -0,0 +1,5 @@
[Oo]fficial
vevo
topic
auto%-generated

View File

@ -0,0 +1,80 @@
(?i)%s*cover$
[wW][wW][wW]%.%a+%.com
^[ABCDEF]%d+%s+
^%d+[%s%.%-_]*
%b()
%b[]
%b{}
[Uu]ltimate [Mm]ix
[Rr]emaster(ed)?
[Ll]yric [Vv]ideo
[Oo]fficial [Mm]usic [Vv]ideo
[Ff]ull [Aa]lbum
[Aa]udio [Oo]nly
[Hh][Dd]
[Mm][Vv]
[Uu][Hh][Dd]
4K
[xX]264
[xX]265
[Hh][Ee][Vv][Cc]
[Ww][Ee][Bb].?Rip
[Bb][Rr][Rr]ip
[Dd][Vv][Dd][Rr]ip
%.%w+$
[–—−]
^%d+%.%s*
Official Audio
Official Video
Official Promo
OFFICIAL PROMO
IMAX
WebDL
60fps
dual
Promo
Version %d+
Full Album
Lyrics
Video
MusicVideo
WEL
#%w+
%-
\\
%.mkv
%.mp4
%.avi
%.www
%..-[%w%-]+%.com
(%d)%.(%d)
(%D)%.(%D)
DOT
[bB][dD][rR]?[iI]?[pP]?
2160p
2160
1080p
720p
480p
360p
30fps
HDR
[Ww][Ee][Bb][%-%.]?[Dd][Ll]
[Ww][Ee][Bb][Rr][Ii][Pp]
HEVC
H264
H265
AC3
remux
UHD
WeL
DTS
FLAC
multi
rip
reescalado
sub
:
wwwnewpct1
1080wwwpctnewcom
M1080wwwnewpct1com

280
etc/mpv/scripts/notify.lua Normal file
View File

@ -0,0 +1,280 @@
-- ~/.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))
local res = utils.subprocess({ args = { "curl", "-s", url } })
if res.status == 0 then
local data = utils.parse_json(res.stdout)
if data and 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
end
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"
Return only a JSON: { "title": "..." }
do NOT explain anything. do NOT return anything else.
]], 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
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)