Metadati: campo 'Origine (macOS · kMDItemWhereFroms)'
kMDItemWhereFroms non e un tag ID3/MP4/Vorbis: e' un extended attribute filesystem di macOS che Safari/Mail/AirDrop e altri tool impostano per ricordare l'origine di un file scaricato. - core/metadata.py: _read_where_from / _write_where_from via subprocess xattr + plistlib (lettura/scrittura del plist binario embedded nell'attributo) - read_metadata espone 'where_from' (lista URL) e 'is_macos' - write_metadata applica l'xattr se data['where_from'] e fornito; lista vuota -> rimuove l'attributo - webui: textarea 'Origine' visibile solo su macOS (is_macos=true), una URL per riga Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
1 parent
61879c42d3
commit
1d7832e246
3 files changed
+79
-1
No files matched your search
+59
-1
@@ -3,7 +3,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import plistlib
|
||||
import subprocess
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
@@ -19,6 +22,53 @@ from mutagen.wave import WAVE
|
||||
|
||||
SUPPORTED_EXTS = (".mp3", ".m4a", ".mp4", ".aac", ".flac", ".wav")
|
||||
|
||||
_WHERE_FROM_ATTR = "com.apple.metadata:kMDItemWhereFroms"
|
||||
|
||||
|
||||
def _read_where_from(path: str) -> list:
|
||||
"""Legge l'xattr macOS 'kMDItemWhereFroms' (lista di URL/origini).
|
||||
Ritorna [] se non e macOS o se l'attributo non esiste."""
|
||||
if sys.platform != "darwin":
|
||||
return []
|
||||
try:
|
||||
res = subprocess.run(
|
||||
["xattr", "-px", _WHERE_FROM_ATTR, path],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
if res.returncode != 0:
|
||||
return []
|
||||
hex_str = "".join(res.stdout.split())
|
||||
if not hex_str:
|
||||
return []
|
||||
raw = bytes.fromhex(hex_str)
|
||||
items = plistlib.loads(raw)
|
||||
if isinstance(items, list):
|
||||
return [str(x) for x in items]
|
||||
return [str(items)]
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
|
||||
def _write_where_from(path: str, urls: list) -> None:
|
||||
"""Scrive l'xattr macOS 'kMDItemWhereFroms'. Lista vuota -> rimuove."""
|
||||
if sys.platform != "darwin":
|
||||
return
|
||||
try:
|
||||
clean = [u.strip() for u in (urls or []) if u and u.strip()]
|
||||
if not clean:
|
||||
subprocess.run(
|
||||
["xattr", "-d", _WHERE_FROM_ATTR, path],
|
||||
capture_output=True, timeout=5,
|
||||
)
|
||||
return
|
||||
data = plistlib.dumps(clean, fmt=plistlib.FMT_BINARY)
|
||||
subprocess.run(
|
||||
["xattr", "-wx", _WHERE_FROM_ATTR, data.hex(), path],
|
||||
capture_output=True, timeout=5, check=True,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _text(value) -> str:
|
||||
"""Estrai testo da un frame mutagen."""
|
||||
@@ -242,6 +292,8 @@ def read_metadata(path: str) -> dict:
|
||||
"bpm": "", "key": "",
|
||||
"duration": 0, "bitrate": 0,
|
||||
"cover_base64": "", "cover_mime": "",
|
||||
"where_from": _read_where_from(str(p)),
|
||||
"is_macos": sys.platform == "darwin",
|
||||
}
|
||||
|
||||
audio = MutagenFile(str(p))
|
||||
@@ -448,7 +500,9 @@ def _write_wav(path: str, data: dict, cover_path: Optional[str], remove_cover: b
|
||||
def write_metadata(path: str, data: dict, cover_path: Optional[str] = None,
|
||||
remove_cover: bool = False) -> None:
|
||||
"""Salva i metadati. Se remove_cover=True rimuove la copertina esistente
|
||||
senza sostituirla. Se cover_path e fornito, la sostituisce."""
|
||||
senza sostituirla. Se cover_path e fornito, la sostituisce.
|
||||
Su macOS aggiorna anche l'xattr kMDItemWhereFroms se data['where_from']
|
||||
e fornito (lista di stringhe)."""
|
||||
ext = Path(path).suffix.lower()
|
||||
if ext == ".mp3":
|
||||
_write_mp3(path, data, cover_path, remove_cover)
|
||||
@@ -460,3 +514,7 @@ def write_metadata(path: str, data: dict, cover_path: Optional[str] = None,
|
||||
_write_wav(path, data, cover_path, remove_cover)
|
||||
else:
|
||||
raise ValueError(f"Formato non supportato: {ext}")
|
||||
|
||||
# macOS extended attribute: kMDItemWhereFroms (lista URL/origini)
|
||||
if "where_from" in data:
|
||||
_write_where_from(path, data.get("where_from") or [])
|
||||
@@ -305,6 +305,13 @@
|
||||
<label class="field-label">Commento</label>
|
||||
<textarea id="metaComment" class="input textarea" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="field full" id="metaWhereFromField" hidden>
|
||||
<label class="field-label">
|
||||
Origine (macOS · kMDItemWhereFroms)
|
||||
</label>
|
||||
<textarea id="metaWhereFrom" class="input textarea" rows="2" placeholder="https://... (uno per riga)"></textarea>
|
||||
<div class="cover-hint">Extended attribute macOS — una URL per riga. Vuoto = rimuove l'attributo.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -414,6 +414,15 @@ function populateMetaForm(d) {
|
||||
$("#metaKey").value = d.key || "";
|
||||
$("#metaComment").value = d.comment || "";
|
||||
|
||||
// WhereFroms (macOS only)
|
||||
if (d.is_macos) {
|
||||
$("#metaWhereFromField").hidden = false;
|
||||
$("#metaWhereFrom").value = (d.where_from || []).join("\n");
|
||||
} else {
|
||||
$("#metaWhereFromField").hidden = true;
|
||||
$("#metaWhereFrom").value = "";
|
||||
}
|
||||
|
||||
// Info riga sotto il path
|
||||
const info = $("#metaInfo");
|
||||
info.hidden = false;
|
||||
@@ -511,6 +520,10 @@ $("#metaSaveBtn").addEventListener("click", async () => {
|
||||
bpm: $("#metaBpm").value,
|
||||
key: $("#metaKey").value,
|
||||
comment: $("#metaComment").value,
|
||||
where_from: $("#metaWhereFrom").value
|
||||
.split("\n")
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean),
|
||||
},
|
||||
cover_path: state.meta.newCoverPath,
|
||||
remove_cover: state.meta.removeCover,
|
||||
|
||||
Reference in new issue
Block a user