Skip to content
Open
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
21 changes: 21 additions & 0 deletions docs/api/restapi.rst
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,11 @@ This route will return all releases to display inside Nextcloud's apps admin are
"smallThumbnail": ""
}
],
"videos": [
{
"url": "https://peertube.tv/videos/embed/dMWVlMwd9ecp5UVAOUhTDt"
}
],
"translations": {
"en": {
"name": "News",
Expand All @@ -416,6 +421,11 @@ screenshots
smallThumbnail
Small thumbnail which can be used as preview image. Guaranteed to be HTTPS. Not required, so if not present or an empty string, use the screenshot url instead.

videos
PeerTube videos declared in the app's info.xml. Guaranteed to be HTTPS. Each entry has:

* **url**: Normalized PeerTube embed URL suitable for iframes

download
Download archive location, guaranteed to be HTTPS

Expand Down Expand Up @@ -603,6 +613,11 @@ This route will return all releases to display inside Nextcloud's apps admin are
"smallThumbnail": ""
}
],
"videos": [
{
"url": "https://peertube.tv/videos/embed/dMWVlMwd9ecp5UVAOUhTDt"
}
],
"translations": {
"en": {
"name": "News",
Expand All @@ -629,6 +644,11 @@ screenshots
smallThumbnail
Small thumbnail which can be used as preview image. Guaranteed to be HTTPS. Not required, so if not present or an empty string, use the screenshot url instead.

videos
PeerTube videos declared in the app's info.xml. Guaranteed to be HTTPS. Each entry has:

* **url**: Normalized PeerTube embed URL suitable for iframes

download
Download archive location, guaranteed to be HTTPS

Expand Down Expand Up @@ -789,6 +809,7 @@ If the app release version is the latest version, everything is updated. If it's
* bugs
* website
* screenshot
* video


For more information about validation and which **info.xml** fields are parsed, see :ref:`app-metadata`
Expand Down
13 changes: 13 additions & 0 deletions docs/developer.rst
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ A full blown example would look like this (needs to be utf-8 encoded):
<discussion>https://your.forum.com</discussion>
<bugs>https://github.com/nextcloud/news/issues</bugs>
<repository>https://github.com/nextcloud/news</repository>
<video>https://peertube.tv/w/dMWVlMwd9ecp5UVAOUhTDt</video>
<video>https://peertube.tv/videos/embed/TpUpEIu3PkYqljmQw7T0jR</video>
<screenshot small-thumbnail="https://example.com/1-small.png">https://example.com/1.png</screenshot>
<screenshot>https://example.com/2.jpg</screenshot>
<donation type="paypal" title="Donate via PayPal">https://paypal.com/example-link</donation>
Expand Down Expand Up @@ -435,6 +437,17 @@ repository
* must contain an URL to the project's repository
* can contain a **type** attribute, **git**, **mercurial**, **subversion** and **bzr** are allowed values, defaults to **git**
* currently not used
video
* optional
* can occur multiple times (up to 10)
* must contain an HTTPS URL to a PeerTube video in one of these forms:

* ``https://host/w/<id>``
* ``https://host/videos/watch/<id>``
* ``https://host/videos/embed/<id>``

* the store normalizes the URL to an embed URL and shows it in the app detail page gallery (before screenshots), in the given order
* other video hosts (for example YouTube) are not supported
screenshot
* optional
* must contain an HTTPS URL to an image
Expand Down
18 changes: 18 additions & 0 deletions nextcloudappstore/api/v1/release/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from django.utils import timezone
from semantic_version import Version # type: ignore

from nextcloudappstore.api.v1.release.peertube import peertube_embed_url
from nextcloudappstore.core.facades import all_match
from nextcloudappstore.core.models import (
App,
Expand All @@ -28,6 +29,7 @@
PhpExtensionDependency,
Screenshot,
ShellCommand,
Video,
)
from nextcloudappstore.core.versioning import to_raw_spec, to_spec

Expand Down Expand Up @@ -156,6 +158,19 @@ def create_screenshot(img: dict[str, str]) -> Screenshot:
obj.screenshots.set(list(shots))


class VideosImporter(ScalarImporter):
def import_data(self, key: str, value: Any, obj: Any) -> None:
def create_video(data: dict[str, str]) -> Video:
return Video.objects.create(
url=peertube_embed_url(data["url"].strip()),
app=obj,
ordering=data["ordering"],
)

videos = map(lambda val: create_video(val["video"]), value)
obj.videos.set(list(videos))


class DonationsImporter(ScalarImporter):
def import_data(self, key: str, value: Any, obj: Any) -> None:
def create_donation(dnt: dict[str, str]) -> Donation:
Expand Down Expand Up @@ -287,6 +302,7 @@ def __init__(
self,
release_importer: AppReleaseImporter,
screenshots_importer: ScreenshotsImporter,
videos_importer: VideosImporter,
donations_importer: DonationsImporter,
attribute_importer: StringAttributeImporter,
l10n_importer: L10NImporter,
Expand All @@ -298,6 +314,7 @@ def __init__(
{
"release": release_importer,
"screenshots": screenshots_importer,
"videos": videos_importer,
"donations": donations_importer,
"user_docs": attribute_importer,
"admin_docs": attribute_importer,
Expand Down Expand Up @@ -332,6 +349,7 @@ def _before_import(self, key: str, value: Any, obj: Any) -> tuple[Any, Any]:
if self._should_update_everything(value):
# clear all relations
obj.screenshots.all().delete()
obj.videos.all().delete()
obj.donations.all().delete()
obj.authors.all().delete()
obj.categories.clear()
Expand Down
2 changes: 2 additions & 0 deletions nextcloudappstore/api/v1/release/info.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
maxOccurs="1"/>
<xs:element name="repository" type="repository" minOccurs="0"
maxOccurs="1"/>
<xs:element name="video" type="secure-url" minOccurs="0"
maxOccurs="10"/>
<xs:element name="screenshot" type="screenshot" minOccurs="0"
maxOccurs="10"/>
<xs:element name="donation" type="donation" minOccurs="0"
Expand Down
13 changes: 13 additions & 0 deletions nextcloudappstore/api/v1/release/info.xslt
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,19 @@
</xsl:for-each>
</authors>

<videos type="list">
<xsl:for-each select="video">
<video>
<url>
<xsl:value-of select="."/>
</url>
<ordering type="int">
<xsl:value-of select="position()"/>
</ordering>
</video>
</xsl:for-each>
</videos>

<screenshots type="list">
<xsl:for-each select="screenshot">
<screenshot>
Expand Down
37 changes: 37 additions & 0 deletions nextcloudappstore/api/v1/release/peertube.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""
SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later
"""

import re
from urllib.parse import urlparse, urlunparse

from rest_framework.exceptions import ValidationError

PEERTUBE_PATH = re.compile(
r"^/(w|videos/watch|videos/embed)/(?P<video_id>[A-Za-z0-9_-]+)/?$",
)


class InvalidPeerTubeUrl(ValidationError):
"""Raised when a URL is not a recognized PeerTube watch or embed URL."""


def peertube_embed_url(url: str) -> str:
"""Normalize a PeerTube watch/short/embed URL to an embed URL.

Accepts:
- https://host/w/<id>
- https://host/videos/watch/<id>
- https://host/videos/embed/<id>
"""
parsed = urlparse(url.strip())
if parsed.scheme != "https" or not parsed.netloc:
raise InvalidPeerTubeUrl(f"PeerTube URL must be HTTPS with a host: {url}")

match = PEERTUBE_PATH.match(parsed.path)
if not match:
raise InvalidPeerTubeUrl(f"PeerTube URL path must be /w/<id>, /videos/watch/<id>, or /videos/embed/<id>: {url}")

video_id = match.group("video_id")
return urlunparse(("https", parsed.netloc, f"/videos/embed/{video_id}", "", "", ""))
1 change: 1 addition & 0 deletions nextcloudappstore/api/v1/release/pre-info.xslt
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<xsl:copy-of select="discussion"/>
<xsl:copy-of select="bugs"/>
<xsl:copy-of select="repository"/>
<xsl:copy-of select="video"/>
<xsl:copy-of select="screenshot"/>
<xsl:copy-of select="donation"/>
<xsl:apply-templates select="dependencies"/>
Expand Down
9 changes: 9 additions & 0 deletions nextcloudappstore/api/v1/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
NextcloudRelease,
PhpExtensionDependency,
Screenshot,
Video,
)
from nextcloudappstore.core.validators import HttpsUrlValidator

Expand Down Expand Up @@ -178,10 +179,17 @@ class Meta:
fields = ("url", "small_thumbnail")


class VideoSerializer(serializers.ModelSerializer):
class Meta:
model = Video
fields = ("url",)


class AppSerializer(serializers.ModelSerializer):
releases = AppReleaseSerializer(many=True, read_only=True)
discussion = SerializerMethodField()
screenshots = ScreenshotSerializer(many=True, read_only=True)
videos = VideoSerializer(many=True, read_only=True)
authors = AuthorSerializer(many=True, read_only=True)
translations = TranslatedFieldsField(shared_model=App)
last_modified = DateTimeField(source="last_release")
Expand All @@ -200,6 +208,7 @@ class Meta:
"last_modified",
"releases",
"screenshots",
"videos",
"translations",
"is_featured",
"authors",
Expand Down
3 changes: 3 additions & 0 deletions nextcloudappstore/api/v1/tests/data/infoxmls/fullimport.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
<!-- issue tracker -->
<bugs>https://github.com/owncloud/news/issues </bugs>

<video>https://peertube.tv/w/dMWVlMwd9ecp5UVAOUhTDt</video>
<video>https://peertube.tv/videos/watch/TpUpEIu3PkYqljmQw7T0jR</video>

<!-- screenshots, can be multiple and will be displayed in order -->
<!-- need to be served with https -->
<screenshot small-thumbnail="https://example.com/1-thumb.png">https://example.com/1.png </screenshot>
Expand Down
22 changes: 22 additions & 0 deletions nextcloudappstore/api/v1/tests/data/infoxmls/videos.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0"?>
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<info xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../release/info.xsd">
<id>news</id>
<name>News</name>
<summary>An RSS/Atom feed reader</summary>
<description>An RSS/Atom feed reader</description>
<version>8.8.2</version>
<licence>AGPL-3.0-or-later</licence>
<author>Bernhard Posselt</author>
<category>multimedia</category>
<bugs>https://github.com/nextcloud/news/issues</bugs>
<video>https://peertube.tv/w/dMWVlMwd9ecp5UVAOUhTDt</video>
<video>https://peertube.tv/videos/embed/TpUpEIu3PkYqljmQw7T0jR</video>
<screenshot>https://example.com/1.png</screenshot>
<dependencies>
<nextcloud min-version="31" max-version="32"/>
</dependencies>
</info>
23 changes: 23 additions & 0 deletions nextcloudappstore/api/v1/tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def test_parse_minimal(self):
"discussion": None,
"website": None,
"issue_tracker": "https://github.com/nextcloud/news/issues",
"videos": [],
"screenshots": [],
"categories": [{"category": {"id": "multimedia"}}],
"donations": [],
Expand Down Expand Up @@ -515,11 +516,33 @@ def test_map_data(self):
{"screenshot": {"url": "https://example.com/1.png", "small_thumbnail": None, "ordering": 1}},
{"screenshot": {"url": "https://example.com/2.jpg", "small_thumbnail": None, "ordering": 2}},
],
"videos": [],
"donations": [],
}
}
self.assertDictEqual(expected, result)

def test_parse_videos(self):
xml = self._get_contents("data/infoxmls/videos.xml")
result = parse_app_metadata(xml, self.config.info_schema, self.config.pre_info_xslt, self.config.info_xslt)
self.assertEqual(
[
{
"video": {
"url": "https://peertube.tv/w/dMWVlMwd9ecp5UVAOUhTDt",
"ordering": 1,
}
},
{
"video": {
"url": "https://peertube.tv/videos/embed/TpUpEIu3PkYqljmQw7T0jR",
"ordering": 2,
}
},
],
result["app"]["videos"],
)

def test_parse_changelog_empty(self):
changelog = parse_changelog("", "9.0")
self.assertEqual("", changelog)
Expand Down
34 changes: 34 additions & 0 deletions nextcloudappstore/api/v1/tests/test_peertube.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""
SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later
"""

from django.test import TestCase

from nextcloudappstore.api.v1.release.peertube import (
InvalidPeerTubeUrl,
peertube_embed_url,
)


class PeerTubeEmbedUrlTest(TestCase):
def test_short_watch_url(self):
self.assertEqual(
peertube_embed_url("https://peertube.tv/w/dMWVlMwd9ecp5UVAOUhTDt"),
"https://peertube.tv/videos/embed/dMWVlMwd9ecp5UVAOUhTDt",
)

def test_watch_url(self):
self.assertEqual(
peertube_embed_url("https://peertube.tv/videos/watch/TpUpEIu3PkYqljmQw7T0jR"),
"https://peertube.tv/videos/embed/TpUpEIu3PkYqljmQw7T0jR",
)

def test_embed_url_passthrough(self):
url = "https://peertube.tv/videos/embed/TpUpEIu3PkYqljmQw7T0jR"
self.assertEqual(peertube_embed_url(url), url)

def test_rejects_non_peertube_path(self):
with self.assertRaises(InvalidPeerTubeUrl) as ctx:
peertube_embed_url("https://peertube.tv/about")
self.assertIn("/w/", str(ctx.exception.detail[0]))
4 changes: 4 additions & 0 deletions nextcloudappstore/api/v1/tests/test_release_importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,16 @@ def test_full(self):
)
release = app.releases.all()[0]
screenshots = app.screenshots.all()
videos = app.videos.all()
extensions = release.php_extensions.all()
databases = release.databases.all()

self.assertEqual(2, screenshots.count())
self.assertEqual("https://example.com/1-thumb.png", screenshots[0].small_thumbnail)
self.assertEqual("", screenshots[1].small_thumbnail)
self.assertEqual(2, videos.count())
self.assertEqual("https://peertube.tv/videos/embed/dMWVlMwd9ecp5UVAOUhTDt", videos[0].url)
self.assertEqual("https://peertube.tv/videos/embed/TpUpEIu3PkYqljmQw7T0jR", videos[1].url)
self.assertEqual(3, databases.count())
self.assertEqual(4, extensions.count())

Expand Down
1 change: 1 addition & 0 deletions nextcloudappstore/api/v1/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
BASIC_PREFETCH_LIST = [
"authors",
"screenshots",
"videos",
"categories",
"translations",
]
Expand Down
Loading
Loading