See #170: API for OPML export
This commit is contained in:
parent
831b6e1a44
commit
37885ada0b
|
@ -792,7 +792,7 @@ class RssFeedItemSerializer(serializers.Serializer):
|
||||||
return upload
|
return upload
|
||||||
|
|
||||||
|
|
||||||
def rss_date(dt):
|
def rfc822_date(dt):
|
||||||
return dt.strftime("%a, %d %b %Y %H:%M:%S %z")
|
return dt.strftime("%a, %d %b %Y %H:%M:%S %z")
|
||||||
|
|
||||||
|
|
||||||
|
@ -814,7 +814,7 @@ def rss_serialize_item(upload):
|
||||||
"title": [{"value": upload.track.title}],
|
"title": [{"value": upload.track.title}],
|
||||||
"itunes:title": [{"value": upload.track.title}],
|
"itunes:title": [{"value": upload.track.title}],
|
||||||
"guid": [{"cdata_value": str(upload.uuid), "isPermalink": "false"}],
|
"guid": [{"cdata_value": str(upload.uuid), "isPermalink": "false"}],
|
||||||
"pubDate": [{"value": rss_date(upload.creation_date)}],
|
"pubDate": [{"value": rfc822_date(upload.creation_date)}],
|
||||||
"itunes:duration": [{"value": rss_duration(upload.duration)}],
|
"itunes:duration": [{"value": rss_duration(upload.duration)}],
|
||||||
"itunes:explicit": [{"value": "no"}],
|
"itunes:explicit": [{"value": "no"}],
|
||||||
"itunes:episodeType": [{"value": "full"}],
|
"itunes:episodeType": [{"value": "full"}],
|
||||||
|
@ -921,3 +921,22 @@ def rss_serialize_channel_full(channel, uploads):
|
||||||
channel_data = rss_serialize_channel(channel)
|
channel_data = rss_serialize_channel(channel)
|
||||||
channel_data["item"] = [rss_serialize_item(upload) for upload in uploads]
|
channel_data["item"] = [rss_serialize_item(upload) for upload in uploads]
|
||||||
return {"channel": channel_data}
|
return {"channel": channel_data}
|
||||||
|
|
||||||
|
|
||||||
|
# OPML stuff
|
||||||
|
def get_opml_outline(channel):
|
||||||
|
return {
|
||||||
|
"title": channel.artist.name,
|
||||||
|
"text": channel.artist.name,
|
||||||
|
"type": "rss",
|
||||||
|
"xmlUrl": channel.get_rss_url(),
|
||||||
|
"htmlUrl": channel.actor.url,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_opml(channels, date, title):
|
||||||
|
return {
|
||||||
|
"version": "2.0",
|
||||||
|
"head": [{"date": [{"value": rfc822_date(date)}], "title": [{"value": title}]}],
|
||||||
|
"body": [{"outline": [get_opml_outline(channel) for channel in channels]}],
|
||||||
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ from rest_framework import viewsets
|
||||||
from django import http
|
from django import http
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Count, Prefetch, Q
|
from django.db.models import Count, Prefetch, Q
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from funkwhale_api.common import locales
|
from funkwhale_api.common import locales
|
||||||
from funkwhale_api.common import permissions
|
from funkwhale_api.common import permissions
|
||||||
|
@ -93,6 +94,20 @@ class ChannelViewSet(
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
return serializer.save(attributed_to=self.request.user.actor)
|
return serializer.save(attributed_to=self.request.user.actor)
|
||||||
|
|
||||||
|
def list(self, request, *args, **kwargs):
|
||||||
|
if self.request.GET.get("output") == "opml":
|
||||||
|
queryset = self.filter_queryset(self.get_queryset())[:500]
|
||||||
|
opml = serializers.get_opml(
|
||||||
|
channels=queryset,
|
||||||
|
date=timezone.now(),
|
||||||
|
title="Funkwhale channels OPML export",
|
||||||
|
)
|
||||||
|
xml_body = renderers.render_xml(renderers.dict_to_xml_tree("opml", opml))
|
||||||
|
return http.HttpResponse(xml_body, content_type="application/xml")
|
||||||
|
|
||||||
|
else:
|
||||||
|
return super().list(request, *args, **kwargs)
|
||||||
|
|
||||||
@decorators.action(
|
@decorators.action(
|
||||||
detail=True,
|
detail=True,
|
||||||
methods=["post"],
|
methods=["post"],
|
||||||
|
|
|
@ -303,7 +303,7 @@ def test_rss_item_serializer(factories):
|
||||||
"itunes:summary": [{"cdata_value": description.rendered}],
|
"itunes:summary": [{"cdata_value": description.rendered}],
|
||||||
"description": [{"value": description.as_plain_text}],
|
"description": [{"value": description.as_plain_text}],
|
||||||
"guid": [{"cdata_value": str(upload.uuid), "isPermalink": "false"}],
|
"guid": [{"cdata_value": str(upload.uuid), "isPermalink": "false"}],
|
||||||
"pubDate": [{"value": serializers.rss_date(upload.creation_date)}],
|
"pubDate": [{"value": serializers.rfc822_date(upload.creation_date)}],
|
||||||
"itunes:duration": [{"value": serializers.rss_duration(upload.duration)}],
|
"itunes:duration": [{"value": serializers.rss_duration(upload.duration)}],
|
||||||
"itunes:keywords": [{"value": "pop rock"}],
|
"itunes:keywords": [{"value": "pop rock"}],
|
||||||
"itunes:explicit": [{"value": "no"}],
|
"itunes:explicit": [{"value": "no"}],
|
||||||
|
@ -456,8 +456,8 @@ def test_rss_duration(seconds, expected):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_rss_date(dt, expected):
|
def test_rfc822_date(dt, expected):
|
||||||
assert serializers.rss_date(dt) == expected
|
assert serializers.rfc822_date(dt) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_channel_metadata_serializer_validation():
|
def test_channel_metadata_serializer_validation():
|
||||||
|
@ -915,3 +915,47 @@ def test_get_channel_from_rss_honor_mrf_inbox_after_http(
|
||||||
apply.assert_any_call({"id": rss_url})
|
apply.assert_any_call({"id": rss_url})
|
||||||
apply.assert_any_call({"id": final_rss_url})
|
apply.assert_any_call({"id": final_rss_url})
|
||||||
apply.assert_any_call({"id": public_url})
|
apply.assert_any_call({"id": public_url})
|
||||||
|
|
||||||
|
|
||||||
|
def test_opml_serializer(factories, now):
|
||||||
|
channels = [
|
||||||
|
factories["audio.Channel"](),
|
||||||
|
factories["audio.Channel"](),
|
||||||
|
factories["audio.Channel"](),
|
||||||
|
]
|
||||||
|
|
||||||
|
title = "Hello world"
|
||||||
|
expected = {
|
||||||
|
"version": "2.0",
|
||||||
|
"head": [
|
||||||
|
{
|
||||||
|
"date": [{"value": serializers.rfc822_date(now)}],
|
||||||
|
"title": [{"value": title}],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"outline": [
|
||||||
|
serializers.get_opml_outline(channels[0]),
|
||||||
|
serializers.get_opml_outline(channels[1]),
|
||||||
|
serializers.get_opml_outline(channels[2]),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
assert serializers.get_opml(channels=channels, date=now, title=title) == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_opml_outline_serializer(factories, now):
|
||||||
|
channel = factories["audio.Channel"]()
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"title": channel.artist.name,
|
||||||
|
"text": channel.artist.name,
|
||||||
|
"type": "rss",
|
||||||
|
"xmlUrl": channel.get_rss_url(),
|
||||||
|
"htmlUrl": channel.actor.url,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert serializers.get_opml_outline(channel) == expected
|
||||||
|
|
|
@ -4,6 +4,7 @@ import pytest
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from funkwhale_api.audio import categories
|
from funkwhale_api.audio import categories
|
||||||
|
from funkwhale_api.audio import renderers
|
||||||
from funkwhale_api.audio import serializers
|
from funkwhale_api.audio import serializers
|
||||||
from funkwhale_api.audio import views
|
from funkwhale_api.audio import views
|
||||||
from funkwhale_api.common import locales
|
from funkwhale_api.common import locales
|
||||||
|
@ -89,6 +90,23 @@ def test_channel_list(factories, logged_in_api_client):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_list_opml(factories, logged_in_api_client, now):
|
||||||
|
channel1 = factories["audio.Channel"]()
|
||||||
|
channel2 = factories["audio.Channel"]()
|
||||||
|
expected_xml = serializers.get_opml(
|
||||||
|
channels=[channel2, channel1], title="Funkwhale channels OPML export", date=now
|
||||||
|
)
|
||||||
|
expected_content = renderers.render_xml(
|
||||||
|
renderers.dict_to_xml_tree("opml", expected_xml)
|
||||||
|
)
|
||||||
|
url = reverse("api:v1:channels-list")
|
||||||
|
response = logged_in_api_client.get(url, {"output": "opml"})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.content == expected_content
|
||||||
|
assert response["content-type"] == "application/xml"
|
||||||
|
|
||||||
|
|
||||||
def test_channel_update(logged_in_api_client, factories):
|
def test_channel_update(logged_in_api_client, factories):
|
||||||
actor = logged_in_api_client.user.create_actor()
|
actor = logged_in_api_client.user.create_actor()
|
||||||
channel = factories["audio.Channel"](attributed_to=actor)
|
channel = factories["audio.Channel"](attributed_to=actor)
|
||||||
|
|
Loading…
Reference in New Issue