Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 59 additions & 22 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ This document describes the available endpoints and their usage. All endpoints r
- [GET /api/player/subtitle/{file:.\*}.vtt](#get-apiplayersubtitlefilevtt)
- [GET /api/player/subtitles/manifest/{file:.\*}](#get-apiplayersubtitlesmanifestfile)
- [GET /api/player/subtitles/{source\_format}/{file:.\*}](#get-apiplayersubtitlessource_formatfile)
- [GET /api/thumbnail](#get-apithumbnail)
- [GET /api/file/ffprobe/{file:.\*}](#get-apifileffprobefile)
- [GET /api/file/info/{file:.\*}](#get-apifileinfofile)
- [GET /api/file/browser/{path:.\*}](#get-apifilebrowserpath)
Expand Down Expand Up @@ -1655,17 +1654,6 @@ Binary TS data (`Content-Type: video/mpegts`).

---

### GET /api/thumbnail
**Purpose**: Proxy/fetch a remote thumbnail image.

**Query Parameter**:
- `?url=<remote-thumbnail-url>`

**Response**:
Binary image data with the appropriate `Content-Type`.

---

### GET /api/file/ffprobe/{file:.*}
**Purpose**: Return the `ffprobe` data for a local file.

Expand Down Expand Up @@ -2325,12 +2313,36 @@ Binary image data with appropriate headers
{
"logs": [
{
"timestamp": "2023-01-01T12:00:00Z",
"level": "INFO",
"message": "...",
...
},
...
"id": "<uuid>",
"datetime": "2026-05-18T12:00:00.000+00:00",
"level": "error",
"levelno": 40,
"logger": "downloads.queue",
"message": "Download failed",
"exception": {
"type": "ValueError",
"message": "bad",
"file": "/app/library/downloads/queue_manager.py",
"line": 123,
"stack": [
{
"path": "/app/library/downloads/queue_manager.py",
"file": "queue_manager.py",
"module": "queue_manager",
"function": "start",
"line": 123
}
]
},
"source": {
"path": "/app/library/downloads/queue_manager.py",
"file": "queue_manager.py",
"module": "queue_manager",
"function": "start",
"line": 123
},
"fields": {}
}
],
"offset": 0,
"limit": 100,
Expand All @@ -2352,9 +2364,35 @@ Binary image data with appropriate headers
**Event Payload**:
```json
{
"id": "<sha256>",
"line": "<log line>",
"datetime": "2024-01-01T12:00:00.000000+00:00"
"id": "<uuid>",
"datetime": "2026-05-18T12:00:00.000+00:00",
"level": "error",
"levelno": 40,
"logger": "downloads.queue",
"message": "Download failed",
"exception": {
"type": "ValueError",
"message": "bad",
"file": "/app/library/downloads/queue_manager.py",
"line": 123,
"stack": [
{
"path": "/app/library/downloads/queue_manager.py",
"file": "queue_manager.py",
"module": "queue_manager",
"function": "start",
"line": 123
}
]
},
"source": {
"path": "/app/library/downloads/queue_manager.py",
"file": "queue_manager.py",
"module": "queue_manager",
"function": "start",
"line": 123
},
"fields": {}
}
```

Expand Down Expand Up @@ -2602,7 +2640,6 @@ or an error:
{
"downloads": {
"paused": false,
"live_bypasses_limits": true,
"global": {
"limit": 20,
"active": 3,
Expand Down
34 changes: 24 additions & 10 deletions app/features/ytdlp/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,32 @@


def _ytdlp_logger(target: logging.Logger):
def _log(level: int, msg: str, *args: Any, **kwargs: Any) -> None:
return _YTDLPLogger(target)


class _YTDLPLogger:
def __init__(self, target: logging.Logger) -> None:
self.target = target

def __call__(self, level: int, msg: str, *args: Any, **kwargs: Any) -> None:
kwargs.setdefault("stacklevel", 4)
if level <= logging.DEBUG and isinstance(msg, str) and msg.startswith("[debug] "):
target.debug(msg.removeprefix("[debug] "), *args, **kwargs)
self.target.debug(msg.removeprefix("[debug] "), *args, **kwargs)
return

if level <= logging.DEBUG:
target.info(msg, *args, **kwargs)
self.target.info(msg, *args, **kwargs)
return

target.log(level, msg, *args, **kwargs)
self.target.log(level, msg, *args, **kwargs)


class _LogCapture:
def __init__(self, logs: list[str]) -> None:
self.logs = logs

return _log
def __call__(self, _: int, msg: str, *args: Any, **__: Any) -> None:
self.logs.append(msg % args if args else msg)


def _get_process_pool_kwargs() -> dict[str, Any]:
Expand Down Expand Up @@ -242,7 +256,7 @@ def extract_info_sync(
sanitize_info: bool = False,
capture_logs: int | None = None,
**kwargs,
) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
) -> tuple[dict[str, Any] | None, list[str]]:
"""
Extract video information from a URL.

Expand All @@ -257,7 +271,7 @@ def extract_info_sync(
**kwargs: Additional arguments

Returns:
tuple[dict | None, list[dict]]: Extracted information and captured logs.
tuple[dict | None, list[str]]: Extracted information and captured logs.

"""
params: dict[str, Any] = {**config, **_DATA.YTDLP_PARAMS, "simulate": True}
Expand Down Expand Up @@ -286,7 +300,7 @@ def extract_info_sync(
captured_logs: list[str] = kwargs.get("captured_logs", [])
if capture_logs is not None:
log_wrapper.add_target(
target=lambda _, msg: captured_logs.append(msg),
target=_LogCapture(captured_logs),
level=capture_logs,
name="log-capture",
)
Expand Down Expand Up @@ -350,7 +364,7 @@ async def fetch_info(
extractor_config: ExtractorConfig | None = None,
budget_sleep: bool = False,
**kwargs,
) -> tuple[dict[str, Any] | None, list[dict[str, Any]]]:
) -> tuple[dict[str, Any] | None, list[str]]:
"""
Extract video information from a URL.

Expand All @@ -370,7 +384,7 @@ async def fetch_info(
**kwargs: Additional arguments

Returns:
tuple[dict | None, list[dict]]: Extracted information and captured logs.
tuple[dict | None, list[str]]: Extracted information and captured logs.

"""
if extractor_config is None:
Expand Down
22 changes: 20 additions & 2 deletions app/features/ytdlp/tests/test_ytdlp_extractor.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import logging
import pickle
from unittest.mock import MagicMock, patch

from app.features.ytdlp.extractor import (
ExtractorConfig,
ExtractorPool,
_LogCapture,
_get_process_pool_kwargs,
_ytdlp_logger,
extract_info_sync,
)
from app.features.ytdlp.utils import LogWrapper


class TestProcessPoolConfiguration:
Expand Down Expand Up @@ -162,11 +165,26 @@ def test_debug_prefix_uses_debug(self) -> None:

_ytdlp_logger(logger)(logging.DEBUG, "[debug] hello")

logger.debug.assert_called_once_with("hello")
logger.debug.assert_called_once_with("hello", stacklevel=4)

def test_screen_style_debug_uses_info(self) -> None:
logger = MagicMock()

_ytdlp_logger(logger)(logging.DEBUG, "screen line")

logger.info.assert_called_once_with("screen line")
logger.info.assert_called_once_with("screen line", stacklevel=4)

def test_targets_are_picklable(self) -> None:
logs: list[str] = []
wrapper = LogWrapper()
wrapper.add_target(_ytdlp_logger(logging.getLogger("yt-dlp.test")), level=logging.DEBUG, name="yt-dlp.test")
wrapper.add_target(_LogCapture(logs), level=logging.WARNING, name="log-capture")

pickle.dumps(wrapper)

def test_capture_formats_args(self) -> None:
logs: list[str] = []

_LogCapture(logs)(logging.WARNING, "hello %s", "world")

assert logs == ["hello world"]
1 change: 1 addition & 0 deletions app/features/ytdlp/tests/test_ytdlp_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def sink(level: int, msg: str, *args: Any, **kwargs: Any) -> None:
assert len(cap.records) == 1
assert cap.records[0].levelno == logging.INFO
assert cap.records[0].getMessage() == "hello X"
assert cap.records[0].funcName == "test_level_filtering_and_dispatch"
assert len(calls) == 0

# WARNING hits both
Expand Down
4 changes: 3 additions & 1 deletion app/features/ytdlp/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ def _log(self, level, msg, *args, **kwargs):
continue

if target.logger:
target.target.log(level, msg, *args, **kwargs)
log_kwargs = {**kwargs}
log_kwargs.setdefault("stacklevel", 3)
target.target.log(level, msg, *args, **log_kwargs)
else:
target.target(level, msg, *args, **kwargs)

Expand Down
Loading
Loading