mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-06-02 06:00:38 +08:00
feat: add video transcoding URL retrieval and enhance thumbnail generation logic
This commit is contained in:
@@ -290,6 +290,11 @@ class QuarkAdapter:
|
||||
return None
|
||||
return None
|
||||
|
||||
async def get_video_transcoding_url(self, fid: str) -> Optional[str]:
|
||||
if not self.use_transcoding_address:
|
||||
return None
|
||||
return await self._get_transcoding_url(fid)
|
||||
|
||||
def _is_video_name(self, name: str) -> bool:
|
||||
mime, _ = mimetypes.guess_type(name)
|
||||
return bool(mime and mime.startswith("video/"))
|
||||
@@ -316,6 +321,29 @@ class QuarkAdapter:
|
||||
resp.raise_for_status()
|
||||
return resp.content
|
||||
|
||||
async def read_file_range(self, root: str, rel: str, start: int, end: Optional[int] = None) -> bytes:
|
||||
if not rel or rel.endswith("/"):
|
||||
raise IsADirectoryError("Path is a directory")
|
||||
parent = rel.rsplit("/", 1)[0] if "/" in rel else ""
|
||||
name = rel.rsplit("/", 1)[-1]
|
||||
base_fid = root or self.root_fid
|
||||
parent_fid = await self._resolve_dir_fid_from(base_fid, parent)
|
||||
it = await self._find_child(parent_fid, name)
|
||||
if not it or it["is_dir"]:
|
||||
raise FileNotFoundError(rel)
|
||||
|
||||
url = await self._get_download_url(it["fid"])
|
||||
headers = dict(self._download_headers())
|
||||
headers["Range"] = f"bytes={start}-" if end is None else f"bytes={start}-{end}"
|
||||
async with httpx.AsyncClient(timeout=self._timeout, follow_redirects=True) as client:
|
||||
resp = await client.get(url, headers=headers)
|
||||
if resp.status_code == 404:
|
||||
raise FileNotFoundError(rel)
|
||||
if resp.status_code == 416:
|
||||
raise HTTPException(416, detail="Requested Range Not Satisfiable")
|
||||
resp.raise_for_status()
|
||||
return resp.content
|
||||
|
||||
async def stream_file(self, root: str, rel: str, range_header: str | None):
|
||||
if not rel or rel.endswith("/"):
|
||||
raise IsADirectoryError("Path is a directory")
|
||||
|
||||
@@ -17,7 +17,7 @@ VIDEO_TAIL_LIMIT = 2 * 1024 * 1024 # 2MB
|
||||
VIDEO_TAIL_FALLBACK_LIMIT = 4 * 1024 * 1024 # 4MB
|
||||
VIDEO_HEAD_LIMIT = 2 * 1024 * 1024 # 2MB
|
||||
VIDEO_HEAD_FALLBACK_LIMIT = 4 * 1024 * 1024 # 4MB
|
||||
VIDEO_THUMB_SEEK_SECONDS = (20, 10, 5, 3, 1, 0)
|
||||
VIDEO_THUMB_SEEK_SECONDS = (15, 10, 5, 3, 1, 0)
|
||||
VIDEO_BLACK_FRAME_MEAN_THRESHOLD = 12.0
|
||||
CACHE_ROOT = Path('data/.thumb_cache')
|
||||
|
||||
@@ -223,10 +223,14 @@ async def _run_ffmpeg_extract_frame(src_path: str, dst_path: str, *, seek_second
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-loglevel", "error",
|
||||
"-i", src_path,
|
||||
]
|
||||
if seek_seconds is not None:
|
||||
cmd += ["-ss", str(seek_seconds)]
|
||||
is_http_input = src_path.startswith(("http://", "https://"))
|
||||
if is_http_input and seek_seconds is not None:
|
||||
cmd += ["-ss", str(seek_seconds), "-i", src_path]
|
||||
else:
|
||||
cmd += ["-i", src_path]
|
||||
if seek_seconds is not None:
|
||||
cmd += ["-ss", str(seek_seconds)]
|
||||
cmd += [
|
||||
"-frames:v", "1",
|
||||
dst_path,
|
||||
@@ -350,6 +354,28 @@ async def get_or_create_thumb(adapter, adapter_id: int, root: str, rel: str, w:
|
||||
|
||||
if not thumb_bytes:
|
||||
if is_video:
|
||||
async def _maybe_transcoding_thumb() -> Tuple[bytes, str] | None:
|
||||
fid = (stat or {}).get("fid") if isinstance(stat, dict) else None
|
||||
get_url = getattr(adapter, "get_video_transcoding_url", None)
|
||||
if not fid or not callable(get_url):
|
||||
return None
|
||||
try:
|
||||
url = await get_url(str(fid))
|
||||
except Exception as e:
|
||||
print(f"Video transcoding url fetch failed: {e}")
|
||||
return None
|
||||
if not url:
|
||||
return None
|
||||
try:
|
||||
return await _generate_video_thumb_from_src_path(url, w, h, fit)
|
||||
except Exception as e:
|
||||
print(f"Video transcoding thumbnail generation failed: {e}")
|
||||
return None
|
||||
|
||||
def _is_hevc_decoder_missing(exc: Exception) -> bool:
|
||||
msg = str(exc).lower()
|
||||
return ("no decoder found" in msg) and ("hevc" in msg or "h265" in msg)
|
||||
|
||||
async def _read_head(limit: int) -> bytes:
|
||||
try:
|
||||
return await _read_video_head(adapter, root, rel, size, limit=limit)
|
||||
@@ -377,17 +403,22 @@ async def get_or_create_thumb(adapter, adapter_id: int, root: str, rel: str, w:
|
||||
thumb_bytes, mime = await _generate_video_thumb_from_segments(
|
||||
head_bytes, tail_bytes, tail_offset, rel, w, h, fit
|
||||
)
|
||||
except Exception:
|
||||
try:
|
||||
tail_bytes, tail_offset = await _read_tail(VIDEO_TAIL_FALLBACK_LIMIT)
|
||||
thumb_bytes, mime = await _generate_video_thumb_from_segments(
|
||||
head_bytes, tail_bytes, tail_offset, rel, w, h, fit
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e2:
|
||||
print(f"Video thumbnail generation failed: {e2}")
|
||||
raise HTTPException(500, detail=f"Video thumbnail generation failed: {e2}")
|
||||
except Exception as e1:
|
||||
if _is_hevc_decoder_missing(e1):
|
||||
got = await _maybe_transcoding_thumb()
|
||||
if got is not None:
|
||||
thumb_bytes, mime = got
|
||||
if not thumb_bytes:
|
||||
try:
|
||||
tail_bytes, tail_offset = await _read_tail(VIDEO_TAIL_FALLBACK_LIMIT)
|
||||
thumb_bytes, mime = await _generate_video_thumb_from_segments(
|
||||
head_bytes, tail_bytes, tail_offset, rel, w, h, fit
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e2:
|
||||
print(f"Video thumbnail generation failed: {e2}")
|
||||
raise HTTPException(500, detail=f"Video thumbnail generation failed: {e2}")
|
||||
|
||||
if thumb_bytes and _is_black_image_bytes(thumb_bytes):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user