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
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ repos:
- id: uv-export
name: uv-export
files: ^uv\.lock|requirements.txt$
entry: uv export --frozen --offline --quiet --no-default-groups -o requirements.txt
entry: uv export --frozen --offline --quiet --no-default-groups
-o="requirements.txt"
Comment on lines +52 to +53
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line continuation syntax is incorrect for YAML. Multi-line strings in YAML should use proper continuation markers or be written as a single line. This may cause parsing issues.

Suggested change
entry: uv export --frozen --offline --quiet --no-default-groups
-o="requirements.txt"
entry: uv export --frozen --offline --quiet --no-default-groups -o="requirements.txt"

Copilot uses AI. Check for mistakes.
additional_dependencies:
- uv==0.8.3
language: python
Expand Down Expand Up @@ -124,7 +125,6 @@ repos:
rev: v2.4.1
hooks:
- id: codespell
exclude: ^assets/
additional_dependencies:
- tomli
- repo: local
Expand All @@ -146,4 +146,4 @@ default_install_hook_types:
- post-rewrite
default_stages:
- pre-commit
- pre-push
- pre-push
10 changes: 9 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dynamic = [ "urls", "version" ]
dependencies = [
"attrs>=22.2.0",
"httpx>=0.23.0",
"pydantic>=2.11.7",
"python-dateutil>=2.8.0",
"typing-extensions>=4.0.0",
]
Expand Down Expand Up @@ -242,7 +243,7 @@ lint.flake8-tidy-imports.banned-api."pytz".msg = "'zoneinfo' should be preferred
lint.pydocstyle.convention = "google"

[tool.codespell]
skip = "assets"
skip = "assets/**"
ignore-regex = [ "[A-Za-z0-9+/]{100,}" ]
ignore-words-list = [ "...", "followings" ]

Expand Down Expand Up @@ -295,6 +296,13 @@ exclude_lines = [
[tool.coverage.xml]
output = "test-reports/coverage.xml"

[tool.pyright]
reportAttributeAccessIssue = false
reportMissingModuleSource = false
reportInvalidTypeVarUse = false
reportGeneralTypeIssues = false
reportCallIssue = false

[tool.uv]
default-groups = [ "dev", "test", "doc" ]
required-version = "==0.8.3" # sync with .pre-commit-config.yaml and release.yml
Expand Down
108 changes: 101 additions & 7 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# This file was autogenerated by uv via the following command:
# uv export --frozen --offline --no-default-groups -o requirements.txt
# uv export --frozen --offline --no-default-groups -o=requirements.txt
-e .
annotated-types==0.7.0 \
--hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \
--hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89
# via pydantic
anyio==4.10.0 \
--hash=sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6 \
--hash=sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1
Expand All @@ -9,9 +13,9 @@ attrs==25.3.0 \
--hash=sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3 \
--hash=sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b
# via peertube
certifi==2025.7.14 \
--hash=sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2 \
--hash=sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995
certifi==2025.8.3 \
--hash=sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407 \
--hash=sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5
# via
# httpcore
# httpx
Expand All @@ -37,6 +41,89 @@ idna==3.10 \
# via
# anyio
# httpx
pydantic==2.11.9 \
--hash=sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2 \
--hash=sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2
# via peertube
pydantic-core==2.33.2 \
--hash=sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d \
--hash=sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac \
--hash=sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02 \
--hash=sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56 \
--hash=sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22 \
--hash=sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef \
--hash=sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec \
--hash=sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d \
--hash=sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a \
--hash=sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f \
--hash=sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052 \
--hash=sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab \
--hash=sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916 \
--hash=sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c \
--hash=sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf \
--hash=sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a \
--hash=sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8 \
--hash=sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7 \
--hash=sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612 \
--hash=sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1 \
--hash=sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7 \
--hash=sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a \
--hash=sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b \
--hash=sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7 \
--hash=sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025 \
--hash=sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849 \
--hash=sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b \
--hash=sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa \
--hash=sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e \
--hash=sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea \
--hash=sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac \
--hash=sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51 \
--hash=sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e \
--hash=sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162 \
--hash=sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65 \
--hash=sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2 \
--hash=sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b \
--hash=sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de \
--hash=sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc \
--hash=sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb \
--hash=sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d \
--hash=sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef \
--hash=sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1 \
--hash=sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5 \
--hash=sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88 \
--hash=sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290 \
--hash=sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d \
--hash=sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808 \
--hash=sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc \
--hash=sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc \
--hash=sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e \
--hash=sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640 \
--hash=sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30 \
--hash=sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e \
--hash=sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9 \
--hash=sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9 \
--hash=sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f \
--hash=sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5 \
--hash=sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab \
--hash=sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572 \
--hash=sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593 \
--hash=sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29 \
--hash=sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1 \
--hash=sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f \
--hash=sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8 \
--hash=sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf \
--hash=sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246 \
--hash=sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9 \
--hash=sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011 \
--hash=sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a \
--hash=sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6 \
--hash=sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8 \
--hash=sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a \
--hash=sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2 \
--hash=sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c \
--hash=sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6 \
--hash=sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d
# via pydantic
python-dateutil==2.9.0.post0 \
--hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
--hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
Expand All @@ -49,10 +136,17 @@ sniffio==1.3.1 \
--hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \
--hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc
# via anyio
typing-extensions==4.14.1 \
--hash=sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36 \
--hash=sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76
typing-extensions==4.15.0 \
--hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
--hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
# via
# anyio
# exceptiongroup
# peertube
# pydantic
# pydantic-core
# typing-inspection
typing-inspection==0.4.1 \
--hash=sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51 \
--hash=sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28
# via pydantic
4 changes: 4 additions & 0 deletions src/peertube/api/video_captions/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
"""Contains endpoint functions for accessing the API"""

from .get_video_captions_content import get_video_captions_content

__all__ = ["get_video_captions_content"]
108 changes: 108 additions & 0 deletions src/peertube/api/video_captions/get_video_captions_content.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
"""Get video captions content as text."""

from __future__ import annotations

from typing import Any
from urllib.parse import urljoin
from uuid import UUID

from pydantic import BaseModel, ConfigDict, validate_call

from peertube.api.video_captions.get_video_captions import (
sync as get_video_captions_sync,
)
from peertube.client import AuthenticatedClient, Client
from peertube.types import UNSET


class CaptionNormalized(BaseModel):
"""Normalized caption data."""

lang: str | None = None
url: str

@classmethod
def from_raw(cls, cap: Any, base_url: str) -> CaptionNormalized | None:
"""Convert raw caption to normalized form."""
# Extract language safely, coalescing UNSET to None
lang_obj = getattr(cap, "language", UNSET)
lang = None if lang_obj is UNSET else getattr(lang_obj, "id", None)
if lang is UNSET:
lang = None

# Prefer explicit fileUrl, fallback to caption_path
file_url = getattr(cap, "additional_properties", {}).get("fileUrl")
if not file_url:
caption_path = getattr(cap, "caption_path", UNSET)
if caption_path is UNSET or caption_path is None:
return None
# Robustly join base URL and possibly-relative path
file_url = urljoin(str(base_url).rstrip("/") + "/", str(caption_path))

return cls(lang=lang, url=file_url)


@validate_call(config=ConfigDict(arbitrary_types_allowed=True))
def get_video_captions_content(
client: AuthenticatedClient | Client,
id: UUID | int | str,
language_filter: str | None = "en",
*,
x_peertube_video_password: str | None = None,
) -> str:
"""Get the content of video captions as a string.

This function wraps get_video_captions to retrieve caption metadata,
then downloads the VTT file content for the specified language.

Args:
client: PeerTube client instance
id: Video identifier
language_filter: Language code to filter captions (default: "en")
x_peertube_video_password: Video-related parameter

Returns:
The VTT file content as a string

Raises:
ValueError: If no captions are available or specified language not found
httpx.HTTPError: For network-related issues when downloading captions
UnicodeDecodeError: If VTT content cannot be decoded as UTF-8
"""
captions_response = get_video_captions_sync(
client=client,
id=id,
x_peertube_video_password=UNSET
if x_peertube_video_password is None
else x_peertube_video_password,
)

data = getattr(captions_response, "data", None) or []
normalized = [
c for c in (CaptionNormalized.from_raw(c, client.base_url) for c in data) if c
]
if not normalized:
msg = "No captions available for this video."
raise ValueError(msg)

if language_filter:
selected = next((c for c in normalized if c.lang == language_filter), None)
if not selected:
available = sorted({c.lang for c in normalized if c.lang})
msg = f"Caption language '{language_filter}' not found. Available: {available}"
raise ValueError(msg)
else:
selected = normalized[0]

r = client.get_httpx_client().get(selected.url)
r.raise_for_status()
try:
return r.content.decode("utf-8")
except UnicodeDecodeError as exc:
raise UnicodeDecodeError(
exc.encoding,
exc.object,
exc.start,
exc.end,
"Failed to decode caption content as UTF-8",
) from exc
Comment on lines +102 to +108
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-raising UnicodeDecodeError with the same arguments is redundant. The original exception already contains all the necessary information. Simply use raise to preserve the original traceback or create a more meaningful exception type.

Suggested change
raise UnicodeDecodeError(
exc.encoding,
exc.object,
exc.start,
exc.end,
"Failed to decode caption content as UTF-8",
) from exc
raise

Copilot uses AI. Check for mistakes.
5 changes: 5 additions & 0 deletions src/peertube/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ class Client:
_client: httpx.Client | None = field(default=None, init=False)
_async_client: httpx.AsyncClient | None = field(default=None, init=False)

@property
def base_url(self) -> str:
"""Get the base URL."""
return self._base_url

def with_headers(self, headers: dict[str, str]) -> "Client":
"""Get a new client matching this one with additional headers"""

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Tests for get_video_captions_content function."""

from peertube.api.video_captions.get_video_captions_content import (
get_video_captions_content,
)
from peertube.client import Client
from peertube.models.get_video_captions_response_200 import GetVideoCaptionsResponse200
from peertube.models.video_caption import VideoCaption
from peertube.models.video_constant_string_language import VideoConstantStringLanguage


def test_get_captions_content_success(httpx_mock):
"""Test successful caption content retrieval."""
# Create client
client = Client(base_url="https://peertube.example.com")

# Mock caption data
language = VideoConstantStringLanguage(id="en", label="English")
caption = VideoCaption(
language=language, caption_path="/api/v1/videos/test-video/captions/en"
)
caption.additional_properties = {
"fileUrl": "https://peertube.example.com/caption/en.vtt"
}
captions_response = GetVideoCaptionsResponse200(total=1, data=[caption])

# Mock HTTP response for captions list
httpx_mock.add_response(
method="GET",
url="https://peertube.example.com/api/v1/videos/test-video/captions",
json=captions_response.to_dict(),
status_code=200,
)

# Mock HTTP response for VTT content
sample_vtt_content = """WEBVTT

00:00:00.000 --> 00:00:02.000
Hello world!

00:00:02.000 --> 00:00:04.000
This is a test caption.
"""
httpx_mock.add_response(
method="GET",
url="https://peertube.example.com/caption/en.vtt",
content=sample_vtt_content.encode("utf-8"),
status_code=200,
)

# Test the function
result = get_video_captions_content(client=client, id="test-video")

assert result == sample_vtt_content
Loading
Loading