diff --git a/api/bridge.py b/api/bridge.py index af97aa8..2188c70 100644 --- a/api/bridge.py +++ b/api/bridge.py @@ -887,6 +887,7 @@ class Api: directory = (payload.get("directory") or "").strip() archive_dir = (payload.get("archive_dir") or "").strip() or None + archive_auto_pick = bool(payload.get("archive_auto_pick", False)) recursive = bool(payload.get("recursive", False)) try: threshold = int(payload.get("threshold", 310)) @@ -912,7 +913,7 @@ class Api: self._upgrade_thread = threading.Thread( target=self._upgrade_worker, - args=(directory, threshold, cookies_path, recursive, archive_dir), + args=(directory, threshold, cookies_path, recursive, archive_dir, archive_auto_pick), daemon=True, ) self._upgrade_thread.start() @@ -929,6 +930,32 @@ class Api: self._log("upgrade", "[INFO] Interruzione richiesta...") return {"ok": True} + def read_audio_data_url(self, path: str) -> dict: + """Legge un file audio locale e lo ritorna come data URL base64 + per il preview HTML5 nel modal upgrade. WKWebView (macOS pywebview) + blocca `file://` da HTML servito via file://, quindi passiamo dal + bridge. Limite: 60MB per non esplodere la memoria JS.""" + import base64 as _b64 + MAX = 60 * 1024 * 1024 + try: + p = Path(path or "") + if not p.exists() or not p.is_file(): + return {"ok": False, "error": "File non trovato"} + size = p.stat().st_size + if size > MAX: + return {"ok": False, "error": f"File troppo grande ({size // 1024 // 1024}MB, max 60MB)"} + ext = p.suffix.lower().lstrip(".") + mime_map = { + "mp3": "audio/mpeg", "m4a": "audio/mp4", "aac": "audio/aac", + "wav": "audio/wav", "flac": "audio/flac", "ogg": "audio/ogg", + "opus": "audio/opus", "webm": "audio/webm", + } + mime = mime_map.get(ext, "audio/mpeg") + b64 = _b64.b64encode(p.read_bytes()).decode("ascii") + return {"ok": True, "data_url": f"data:{mime};base64,{b64}"} + except Exception as e: + return {"ok": False, "error": str(e)} + def upgrade_resolve_candidates(self, choice: dict) -> dict: """Riceve la scelta utente dal modal 'match locali multipli' e la deposita nella queue attesa dal resolve_callback del worker. @@ -949,7 +976,8 @@ class Api: return {"ok": False, "error": str(e)} def _upgrade_worker(self, directory, threshold, cookies_path, recursive, - archive_dir: Optional[str] = None): + archive_dir: Optional[str] = None, + archive_auto_pick: bool = False): view = "upgrade" def progress_cb(idx, total, filename, status, old_kbps, new_kbps): @@ -999,10 +1027,21 @@ class Api: self._emit("upgrade:progress", payload_evt) - def resolve_cb(filename: str, candidates: list) -> dict: - """Bloccante: emette evento e attende scelta utente via queue.""" + def resolve_cb(source_path: str, candidates: list) -> dict: + """Bloccante: emette evento e attende scelta utente via queue. + Se archive_auto_pick=True, salta il modal e sceglie il primo + candidato (già ordinato per bitrate DESC nel core). + `source_path` è il path assoluto del file sorgente (per il preview + audio nel modal).""" + src_name = Path(source_path).name if source_path else "" + if archive_auto_pick and candidates: + top = candidates[0] + path = top.get("path") if isinstance(top, dict) else str(top) + self._log(view, f"[AUTO] {src_name}: match multipli, uso {Path(path).name}") + return {"action": "use_local", "path": path} self._emit("upgrade:candidates_needed", { - "file": filename, + "file": src_name, + "source_path": source_path, "candidates": candidates, }) try: diff --git a/core/beatport.py b/core/beatport.py index 5ce9196..2468e64 100644 --- a/core/beatport.py +++ b/core/beatport.py @@ -24,6 +24,7 @@ class BeatportTrack: duration_sec: int beatport_id: int image_url: str = "" # URL cover art (default vuoto per retrocompatibilità test) + release_date: str = "" # ISO YYYY-MM-DD, vuoto se mancante @property def display(self) -> str: @@ -177,6 +178,7 @@ def _parse_tracks(data: dict) -> list: duration_sec=length_ms // 1000, beatport_id=int(item.get("id") or 0), image_url=_extract_image_url(item.get("image")), + release_date=str(item.get("publish_date") or item.get("new_release_date") or "").strip(), ) except (TypeError, ValueError) as e: raise BeatportParseError(f"track[{i}] shape inattesa: {e}") from e diff --git a/core/config.py b/core/config.py index 842c7ff..782c055 100644 --- a/core/config.py +++ b/core/config.py @@ -5,7 +5,7 @@ import os import sys from pathlib import Path -VERSION = "v1.9.3" +VERSION = "v1.9.4" APP_NAME = "MusicTools" diff --git a/core/upgrader.py b/core/upgrader.py index da8c4b4..45522ac 100644 --- a/core/upgrader.py +++ b/core/upgrader.py @@ -188,7 +188,10 @@ def _copy_or_convert_to_mp3( """ try: if src.suffix.lower() == ".mp3": - shutil.copy2(str(src), str(dst)) + # shutil.copy (non copy2): mtime del target = ora, non quello + # del sorgente. Così è chiaro nel Finder che il file è stato + # aggiornato dall'upgrade. + shutil.copy(str(src), str(dst)) return dst.exists() # Convert non-mp3 -> mp3 320k ffmpeg_bin = find_ffmpeg() or "ffmpeg" @@ -431,7 +434,7 @@ def upgrade_folder( progress_callback(processed, total, filepath.name, "resolve_wait", current_kbps, 0) try: - choice = resolve_callback(filepath.name, cand_payload) or {} + choice = resolve_callback(str(filepath), cand_payload) or {} except Exception: choice = {} action = (choice.get("action") or "").strip() diff --git a/core/youtube_search.py b/core/youtube_search.py index b5b7b40..885ecfa 100644 --- a/core/youtube_search.py +++ b/core/youtube_search.py @@ -63,6 +63,14 @@ def search_youtube(query: str, limit: int = 50) -> list: video_id = e.get("id") or "" url = e.get("url") or (f"https://www.youtube.com/watch?v={video_id}" if video_id else "") image_url = f"https://i.ytimg.com/vi/{video_id}/mqdefault.jpg" if video_id else "" + # yt-dlp `upload_date` è YYYYMMDD (stringa). Lo converto in ISO YYYY-MM-DD. + # Con --flat-playlist può essere assente; in tal caso resta stringa vuota. + raw_date = str(e.get("upload_date") or "").strip() + release_date = "" + if len(raw_date) == 8 and raw_date.isdigit(): + release_date = f"{raw_date[0:4]}-{raw_date[4:6]}-{raw_date[6:8]}" + elif raw_date: + release_date = raw_date # forma sconosciuta, passa così result.append({ "id": video_id, "url": url, @@ -70,5 +78,6 @@ def search_youtube(query: str, limit: int = 50) -> list: "channel": e.get("uploader") or e.get("channel") or "", "duration_sec": int(e.get("duration") or 0), "image_url": image_url, + "release_date": release_date, }) return result diff --git a/webui/css/style.css b/webui/css/style.css index f381aa4..33579e7 100644 --- a/webui/css/style.css +++ b/webui/css/style.css @@ -1680,8 +1680,21 @@ input[type="number"]::-webkit-inner-spin-button { } .beatport-table .col-pos { width: 40px; color: var(--text-3); font-variant-numeric: tabular-nums; } .beatport-table .col-dur { width: 70px; color: var(--text-2); font-variant-numeric: tabular-nums; text-align: right; } +.beatport-table .col-date { width: 100px; color: var(--text-2); font-variant-numeric: tabular-nums; font-size: 12px; white-space: nowrap; } .beatport-table .col-state { width: 130px; font-size: 12px; } +.beatport-table thead th.sortable { + cursor: pointer; + user-select: none; + transition: color 0.15s ease; +} +.beatport-table thead th.sortable:hover { color: var(--text); } +.beatport-table thead th .sort-arrow { + font-size: 10px; + margin-left: 4px; + color: var(--indigo-text); +} + .beatport-table input[type="checkbox"] { width: 16px; height: 16px; @@ -1720,6 +1733,17 @@ input[type="number"]::-webkit-inner-spin-button { white-space: normal; user-select: none; } + +/* Modal picker: più largo dei modal standard + footer con wrap + per far entrare i 4 bottoni senza troncare */ +#upgradePickerModal .modal { + min-width: 620px; + max-width: 780px; +} +#upgradePickerModal .modal-foot { + flex-wrap: wrap; + justify-content: flex-end; +} .modal-sub { font-size: 12px; color: var(--text-2); @@ -1757,6 +1781,33 @@ input[type="number"]::-webkit-inner-spin-button { flex: 1; min-width: 0; } +.upgrade-picker-play { + display: flex; + align-items: center; + flex-shrink: 0; +} +.upgrade-preview-btn { + width: 34px; + height: 34px; + border-radius: 50%; + border: 1px solid var(--border); + background: var(--bg-input, transparent); + color: var(--text); + font-size: 13px; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0; + margin-left: 6px; + transition: background 0.15s ease, transform 0.1s ease; +} +.upgrade-preview-btn:hover { background: var(--bg-input-hover, rgba(255,255,255,0.05)); } +.upgrade-preview-btn.playing { + background: var(--green, #1DB954); + color: #000; + border-color: var(--green, #1DB954); +} .upgrade-picker-title { font-weight: 600; color: var(--text); diff --git a/webui/index.html b/webui/index.html index 06139e7..63c0526 100644 --- a/webui/index.html +++ b/webui/index.html @@ -280,9 +280,13 @@ +