From 86fc70941b3245c6298c5a3df28119b855640491 Mon Sep 17 00:00:00 2001 From: zloy_linux Date: Tue, 25 Nov 2025 18:36:08 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=BC?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=20=D1=82=D1=80=D0=B5=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/rmpc/config.ron | 6 +- config/rmpc/notify.py | 135 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 3 deletions(-) create mode 100755 config/rmpc/notify.py diff --git a/config/rmpc/config.ron b/config/rmpc/config.ron index ea021e2..ab5025d 100644 --- a/config/rmpc/config.ron +++ b/config/rmpc/config.ron @@ -11,7 +11,7 @@ - + on_song_change: ["/usr/bin/python3", "/home/crud/.config/rmpc/notify.py"], volume_step: 5, max_fps: 30, scrolloff: 0, @@ -69,7 +69,7 @@ "": PaneDown, "": PaneLeft, "": PaneRight, - "": UpHalf, + "": UpHalf, "N": PreviousResult, "a": Add, "A": AddAll, @@ -82,7 +82,7 @@ "": Confirm, "i": FocusInput, "J": MoveDown, - "": DownHalf, + "": DownHalf, "/": EnterSearch, "": Close, "": Close, diff --git a/config/rmpc/notify.py b/config/rmpc/notify.py new file mode 100755 index 0000000..60b430e --- /dev/null +++ b/config/rmpc/notify.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +import fcntl +import os +import time +import eyed3 +import tempfile +import subprocess +from mpd import MPDClient +from pathlib import Path +from typing import Optional, Union +import hashlib + +# lock file to prevent parallel runs / duplicate notifications +LOCK_FN = "/tmp/rmpc-notify.lock" +DUPLICATE_WINDOW = 5 # seconds to consider same file a duplicate + +exception_buff = [] + +# Acquire non-blocking exclusive lock; if fails, another instance is running -> exit +_lock_fd = open(LOCK_FN, "a+") +try: + fcntl.flock(_lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + _lock_fd.seek(0) + _lock_fd.truncate() + _lock_fd.write(f"{os.getpid()} {time.time()}\n") + _lock_fd.flush() +except BlockingIOError: + # Another instance is running — exit silently + raise SystemExit(0) + + +_last_shown = {"id": None, "ts": 0} + +def file_id(path: Path) -> str: + try: + st = path.stat() + return f"{path}:{st.st_mtime_ns}" + except Exception: + return hashlib.sha1(str(path).encode()).hexdigest() + +def extract(track: Path) -> Optional[Path]: + try: + audio = eyed3.load(track) + except Exception as e: + exception_buff.append(f"eyed3.load error: {e}") + return None + + if not audio or not audio.tag: + exception_buff.append("track data unavailable") + return None + + # Защита от проблем с парсингом дат (например "0") + try: + rd = getattr(audio.tag, "recording_date", None) + if rd: + raw = getattr(rd, "text", None) or str(rd) + if raw == "0" or raw.strip() == "": + audio.tag.recording_date = None + except Exception: + pass + + images = getattr(audio.tag, "images", None) + if not images: + return None + + try: + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp: + temp.write(images[0].image_data) + return temp.name + except Exception as e: + exception_buff.append(f"image extract error: {e}") + return None + +def notify(icon: Union[str, Path], title: str, body: str) -> None: + try: + subprocess.Popen(["notify-send", "-i", str(icon), "-a", "rmpc", "-t", "2500", title, body]) + except Exception as e: + exception_buff.append(e) + +def main(): + global _last_shown + + try: + client = MPDClient() + client.connect("localhost", 6600) + song = client.currentsong() or {} + except Exception as e: + exception_buff.append(f"mpd connection error: {e}") + song = {} + + music_path = Path("/home/zloy_linux/Музыка/iTunes/") + track_rel = song.get('file') + if not track_rel: + exception_buff.append("no track file in mpd response") + track_path = None + else: + track_path = music_path / track_rel + + cover_path = None + if not track_path or not track_path.exists(): + exception_buff.append("track file not found") + else: + ident = file_id(track_path) + now = time.time() + # дедупликация по файлу+mtime в пределах окна + if _last_shown["id"] == ident and (now - _last_shown["ts"]) < DUPLICATE_WINDOW: + return # пропускаем повторный показ + cover_path = extract(track_path) + _last_shown["id"] = ident + _last_shown["ts"] = now + + artist = song.get('artist', 'Unknown artist') + title = song.get('title', 'Unknown title') + body = f"[E] {', '.join(str(e) for e in exception_buff)}" if exception_buff else artist + + notify(cover_path or "audio-x-generic", title, body) + + if cover_path: + time.sleep(1) + try: + os.remove(cover_path) + except Exception as e: + # не критично — логим в exception_buff и выводим в stdout + print(f"[E] cleanup error: {e}") + +if __name__ == "__main__": + try: + main() + finally: + try: + # release lock by closing descriptor (file remains but unlocked) + _lock_fd.close() + except Exception: + pass +