diff --git a/modules/app_video.py b/modules/app_video.py index 4ced921..6c29d85 100644 --- a/modules/app_video.py +++ b/modules/app_video.py @@ -17,7 +17,7 @@ CONVERT_SETTING = "video_convert_enabled" _thumbnail_lock = Semaphore() _generating = set() _generating_lock = Semaphore() -_converting = set() +_converting = {} # path -> {"started_at": float, "source_size": int, "tmp_path": str} _converting_lock = Semaphore() @@ -36,7 +36,7 @@ def queue_convert(relative_path, force=False): with _converting_lock: if relative_path in _converting: return False - _converting.add(relative_path) + _converting[relative_path] = {"started_at": time.time(), "source_size": 0, "tmp_path": ""} t = threading.Thread(target=_convert_bg, args=(relative_path,), daemon=True) t.start() return True @@ -53,6 +53,11 @@ def _convert_bg(relative_path): try: tmp_path = mp4_path + ".converting.mp4" + source_size = os.path.getsize(source_path) if os.path.exists(source_path) else 0 + with _converting_lock: + if relative_path in _converting: + _converting[relative_path]["source_size"] = source_size + _converting[relative_path]["tmp_path"] = tmp_path cmd = [ "ffmpeg", "-nostdin", "-loglevel", "error", "-y", "-i", source_path, @@ -84,7 +89,7 @@ def _convert_bg(relative_path): app_dash.end_local_video_use(relative_path) finally: with _converting_lock: - _converting.discard(relative_path) + _converting.pop(relative_path, None) def _local_path(relative_path): @@ -117,7 +122,24 @@ def list_local_videos(): errors.append(f"{relative_path}: {e}") continue with _converting_lock: - converting = relative_path in _converting + conv_info = _converting.get(relative_path) + converting = conv_info is not None + convert_progress = None + if converting and conv_info: + elapsed = time.time() - conv_info["started_at"] + source_size = conv_info["source_size"] + tmp_path = conv_info["tmp_path"] + tmp_size = 0 + if tmp_path and os.path.exists(tmp_path): + try: + tmp_size = os.path.getsize(tmp_path) + except OSError: + pass + pct = round(tmp_size / source_size * 100) if source_size > 0 else 0 + convert_progress = { + "elapsed_s": int(elapsed), + "pct": min(pct, 99), # never show 100% until done + } files.append( { "path": relative_path, @@ -129,6 +151,7 @@ def list_local_videos(): stat.st_mtime, ) in uploaded_stats, "converting": converting, + "convert_progress": convert_progress, } ) total_size += stat.st_size diff --git a/static/js/local-videos.js b/static/js/local-videos.js index a1f9ad8..e8e7cca 100644 --- a/static/js/local-videos.js +++ b/static/js/local-videos.js @@ -119,9 +119,12 @@ preview.appendChild(mobileMeta); // Converting badge shown on mobile inside thumbnail cell if (file.converting) { + const p = file.convert_progress; + const elapsed = p ? `${Math.floor(p.elapsed_s / 60)}m${p.elapsed_s % 60}s` : '…'; + const pct = p ? ` ${p.pct}%` : ''; const mobileBadge = document.createElement("span"); mobileBadge.className = "badge bg-info text-dark dash-thumb-converting mt-1"; - mobileBadge.innerHTML = ''; + mobileBadge.innerHTML = `${pct || elapsed}`; preview.appendChild(mobileBadge); } row.appendChild(preview); @@ -143,13 +146,16 @@ dateCell.className = "dash-col-date"; row.appendChild(dateCell); - // Upload / convert badge + // Upload / convert badge (desktop) const upload = document.createElement("td"); upload.className = "dash-col-upload"; const badge = document.createElement("span"); if (file.converting) { + const p = file.convert_progress; + const pct = p && p.pct > 0 ? ` ${p.pct}%` : ''; + const elapsed = p ? ` ${Math.floor(p.elapsed_s / 60)}m${p.elapsed_s % 60}s` : ''; badge.className = "badge bg-info text-dark"; - badge.innerHTML = 'Converting'; + badge.innerHTML = `Converting${pct}${elapsed}`; convertingCount++; } else { badge.className = file.uploaded ? "badge bg-success" : "badge bg-warning text-dark";