/
domahes
/
audio_visualizer_improved
Обзор
Документация
Войти
/
domahes
/
audio_visualizer_improved
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
utils.py
219 строк
8 KB
Евгений Харченко
Integrate i18n localization support and dynamic language config into visualizer GUI
02 июн 2026, 18:37
02 июн 2026, 18:37
907260f
Код
Авторство
О чём код?
""" Utility helpers: metadata extraction, text formatting, path escaping, etc. Extracted in Phase 1. """ from __future__ import annotations import json import os import subprocess from typing import Dict, List, Optional, Tuple from availability import ( MUTAGEN_AVAILABLE, MutagenFile, clamp, ) def _normalize_tag_value(v) -> str: if v is None: return "" if isinstance(v, (list, tuple)): parts = [str(x).strip() for x in v if str(x).strip()] return "; ".join(parts) if isinstance(v, bytes): try: return v.decode("utf-8", errors="ignore").strip() except Exception: return "" return str(v).strip() def _extract_track_metadata(path: str) -> Dict[str, str]: """ Best-effort metadata extraction. Priority: mutagen -> ffprobe. Returns normalized keys. """ out: Dict[str, str] = {} if not path or not os.path.isfile(path): return out # 1) mutagen (if available) if MUTAGEN_AVAILABLE: try: mf = MutagenFile(path, easy=True) if mf is not None and getattr(mf, "tags", None): tags = dict(mf.tags) key_aliases = { "title": ["title"], "artist": ["artist", "albumartist", "performer"], "album": ["album"], "genre": ["genre"], "year": ["date", "year", "originaldate"], "track": ["tracknumber", "track"], "composer": ["composer"], "comment": ["comment", "description"], } for dst, src_keys in key_aliases.items(): for sk in src_keys: if sk in tags: v = _normalize_tag_value(tags.get(sk)) if v: out[dst] = v break try: length = float(getattr(getattr(mf, "info", None), "length", 0.0) or 0.0) if length > 0: out["duration"] = _format_hms_full(length) except Exception: pass except Exception: pass # Raw mutagen tags fallback (helps when easy tags miss TITLE on some files) if not out.get("title"): try: mf_raw = MutagenFile(path, easy=False) raw_tags = getattr(mf_raw, "tags", None) if raw_tags: key_map = { "title": ["TIT2", "TITLE", "\xa9nam"], "artist": ["TPE1", "ARTIST", "\xa9ART"], "album": ["TALB", "ALBUM", "\xa9alb"], "genre": ["TCON", "GENRE", "\xa9gen"], "year": ["TDRC", "TYER", "DATE", "\xa9day"], "track": ["TRCK", "TRACKNUMBER", "TRACK"], "composer": ["TCOM", "COMPOSER", "\xa9wrt"], } def _read_raw_value(tag_obj): if tag_obj is None: return "" if hasattr(tag_obj, "text"): return _normalize_tag_value(getattr(tag_obj, "text")) return _normalize_tag_value(tag_obj) for dst, candidates in key_map.items(): if out.get(dst): continue val = "" for rk in candidates: tag_obj = None try: if hasattr(raw_tags, "get"): tag_obj = raw_tags.get(rk) if tag_obj is None and hasattr(raw_tags, "__contains__") and rk in raw_tags: tag_obj = raw_tags[rk] except Exception: tag_obj = None val = _read_raw_value(tag_obj) if val: break if val: out[dst] = val except Exception: pass # 2) ffprobe fallback/augmentation try: cmd = [ "ffprobe", "-v", "error", "-show_entries", "format=duration:format_tags", "-of", "json", path ] proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False) if proc.returncode == 0 and proc.stdout.strip(): data = json.loads(proc.stdout) fmt = data.get("format", {}) if isinstance(data, dict) else {} tags = fmt.get("tags", {}) if isinstance(fmt, dict) else {} if isinstance(tags, dict): lower = {str(k).lower(): _normalize_tag_value(v) for k, v in tags.items()} for dst, keys in { "title": ["title"], "artist": ["artist", "album_artist", "albumartist"], "album": ["album"], "genre": ["genre"], "year": ["date", "year"], "track": ["track", "tracknumber"], "composer": ["composer"], "comment": ["comment", "description"], }.items(): if dst in out and out[dst]: continue for sk in keys: val = lower.get(sk, "") if val: out[dst] = val break if not out.get("duration"): try: dur = float(fmt.get("duration", 0.0)) if dur > 0: out["duration"] = _format_hms_full(dur) except Exception: pass except Exception: pass return out def _build_track_metadata_text(meta: Dict[str, str]) -> str: if not meta: return "" from i18n import _ labels = [ ("title", _("metadata_title")), ("artist", _("metadata_artist")), ("album", _("metadata_album")), ("genre", _("metadata_genre")), ("year", _("metadata_year")), ("track", _("metadata_track")), ("composer", _("metadata_composer")), ("duration", _("metadata_duration")), ] lines: List[str] = [] for key, label in labels: v = _normalize_tag_value(meta.get(key, "")) if v: lines.append(f"{label}: {v}") return "\n".join(lines) def _escape_ffconcat_path(p: str) -> str: """Escape single quotes for ffmpeg concat demuxer line: file '<path>'""" return p.replace("'", "'\\''") def _unique_out_path(dir_: str, base_name: str, suffix: str) -> str: """Ensure no overwrites when inputs share the same basename. Returns a path like dir_/f"{base}_{suffix}.mp4"; if exists, adds _1, _2... """ base, ext = os.path.splitext(base_name) candidate = os.path.join(dir_, f"{base}_{suffix}{ext}") if not os.path.exists(candidate): return candidate i = 1 while True: cand = os.path.join(dir_, f"{base}_{suffix}_{i}{ext}") if not os.path.exists(cand): return cand i += 1 def _format_hms(seconds: float) -> str: seconds = max(0, int(seconds)) h = seconds // 3600 m = (seconds % 3600) // 60 s = seconds % 60 return f"{h:02d}:{m:02d}:{s:02d}" if h else f"{m:02d}:{s:02d}" def _format_hms_full(seconds: float) -> str: seconds = max(0, int(seconds)) h = seconds // 3600 m = (seconds % 3600) // 60 s = seconds % 60 return f"{h:02d}:{m:02d}:{s:02d}"