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:
luzadevandClaude Opus 4.7 committed 2026-06-08 10:36:18 +02:00
1 parent 61879c42d3
commit 1d7832e246
3 files changed
+79 -1

No files matched your search

+59 -1
View File
@@ -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 [])
+7
View File
@@ -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>
+13
View File
@@ -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,