See #170: subsonic API for podcasts
This commit is contained in:
parent
f8baae53fd
commit
23d3893f01
|
@ -135,7 +135,7 @@ test_api:
|
||||||
only:
|
only:
|
||||||
- branches
|
- branches
|
||||||
before_script:
|
before_script:
|
||||||
- apk add make git gcc python3-dev
|
- apk add make git gcc python3-dev musl-dev
|
||||||
- cd api
|
- cd api
|
||||||
- pip3 install -r requirements/base.txt
|
- pip3 install -r requirements/base.txt
|
||||||
- pip3 install -r requirements/local.txt
|
- pip3 install -r requirements/local.txt
|
||||||
|
|
|
@ -31,6 +31,15 @@ class ChannelQuerySet(models.QuerySet):
|
||||||
return self.filter(query)
|
return self.filter(query)
|
||||||
return self.exclude(query)
|
return self.exclude(query)
|
||||||
|
|
||||||
|
def subscribed(self, actor):
|
||||||
|
if not actor:
|
||||||
|
return self.none()
|
||||||
|
|
||||||
|
subscriptions = actor.emitted_follows.filter(
|
||||||
|
approved=True, target__channel__isnull=False
|
||||||
|
)
|
||||||
|
return self.filter(actor__in=subscriptions.values_list("target", flat=True))
|
||||||
|
|
||||||
|
|
||||||
class Channel(models.Model):
|
class Channel(models.Model):
|
||||||
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
uuid = models.UUIDField(default=uuid.uuid4, unique=True)
|
||||||
|
|
|
@ -295,6 +295,8 @@ def clean_html(html, permissive=False):
|
||||||
|
|
||||||
|
|
||||||
def render_html(text, content_type, permissive=False):
|
def render_html(text, content_type, permissive=False):
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
rendered = render_markdown(text)
|
rendered = render_markdown(text)
|
||||||
if content_type == "text/html":
|
if content_type == "text/html":
|
||||||
rendered = text
|
rendered = text
|
||||||
|
@ -307,6 +309,8 @@ def render_html(text, content_type, permissive=False):
|
||||||
|
|
||||||
|
|
||||||
def render_plain_text(html):
|
def render_plain_text(html):
|
||||||
|
if not html:
|
||||||
|
return ""
|
||||||
return bleach.clean(html, tags=[], strip=True)
|
return bleach.clean(html, tags=[], strip=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -102,7 +102,7 @@ def get_track_data(album, track, upload):
|
||||||
"id": track.pk,
|
"id": track.pk,
|
||||||
"isDir": "false",
|
"isDir": "false",
|
||||||
"title": track.title,
|
"title": track.title,
|
||||||
"album": album.title,
|
"album": album.title if album else "",
|
||||||
"artist": album.artist.name,
|
"artist": album.artist.name,
|
||||||
"track": track.position or 1,
|
"track": track.position or 1,
|
||||||
"discNumber": track.disc_number or 1,
|
"discNumber": track.disc_number or 1,
|
||||||
|
@ -118,18 +118,20 @@ def get_track_data(album, track, upload):
|
||||||
"path": get_track_path(track, upload.extension or "mp3"),
|
"path": get_track_path(track, upload.extension or "mp3"),
|
||||||
"duration": upload.duration or 0,
|
"duration": upload.duration or 0,
|
||||||
"created": to_subsonic_date(track.creation_date),
|
"created": to_subsonic_date(track.creation_date),
|
||||||
"albumId": album.pk,
|
"albumId": album.pk if album else "",
|
||||||
"artistId": album.artist.pk,
|
"artistId": album.artist.pk if album else track.artist.pk,
|
||||||
"type": "music",
|
"type": "music",
|
||||||
}
|
}
|
||||||
if track.album.attachment_cover_id:
|
if album and album.attachment_cover_id:
|
||||||
data["coverArt"] = "al-{}".format(track.album.id)
|
data["coverArt"] = "al-{}".format(album.id)
|
||||||
if upload.bitrate:
|
if upload.bitrate:
|
||||||
data["bitrate"] = int(upload.bitrate / 1000)
|
data["bitrate"] = int(upload.bitrate / 1000)
|
||||||
if upload.size:
|
if upload.size:
|
||||||
data["size"] = upload.size
|
data["size"] = upload.size
|
||||||
if album.release_date:
|
if album.release_date:
|
||||||
data["year"] = album.release_date.year
|
data["year"] = album.release_date.year
|
||||||
|
else:
|
||||||
|
data["year"] = track.creation_date.year
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@ -287,7 +289,7 @@ def get_user_detail_data(user):
|
||||||
"adminRole": "false",
|
"adminRole": "false",
|
||||||
"settingsRole": "false",
|
"settingsRole": "false",
|
||||||
"commentRole": "false",
|
"commentRole": "false",
|
||||||
"podcastRole": "false",
|
"podcastRole": "true",
|
||||||
"coverArtRole": "false",
|
"coverArtRole": "false",
|
||||||
"shareRole": "false",
|
"shareRole": "false",
|
||||||
"uploadRole": "true",
|
"uploadRole": "true",
|
||||||
|
@ -319,3 +321,53 @@ def get_genre_data(tag):
|
||||||
"albumCount": getattr(tag, "_albums_count", 0),
|
"albumCount": getattr(tag, "_albums_count", 0),
|
||||||
"value": tag.name,
|
"value": tag.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_channel_data(channel, uploads):
|
||||||
|
data = {
|
||||||
|
"id": str(channel.uuid),
|
||||||
|
"url": channel.get_rss_url(),
|
||||||
|
"title": channel.artist.name,
|
||||||
|
"description": channel.artist.description.as_plain_text
|
||||||
|
if channel.artist.description
|
||||||
|
else "",
|
||||||
|
"coverArt": "at-{}".format(channel.artist.attachment_cover.uuid)
|
||||||
|
if channel.artist.attachment_cover
|
||||||
|
else "",
|
||||||
|
"originalImageUrl": channel.artist.attachment_cover.url
|
||||||
|
if channel.artist.attachment_cover
|
||||||
|
else "",
|
||||||
|
"status": "completed",
|
||||||
|
}
|
||||||
|
if uploads:
|
||||||
|
data["episode"] = [
|
||||||
|
get_channel_episode_data(upload, channel.uuid) for upload in uploads
|
||||||
|
]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def get_channel_episode_data(upload, channel_id):
|
||||||
|
return {
|
||||||
|
"id": str(upload.uuid),
|
||||||
|
"channelId": str(channel_id),
|
||||||
|
"streamId": upload.track.id,
|
||||||
|
"title": upload.track.title,
|
||||||
|
"description": upload.track.description.as_plain_text
|
||||||
|
if upload.track.description
|
||||||
|
else "",
|
||||||
|
"coverArt": "at-{}".format(upload.track.attachment_cover.uuid)
|
||||||
|
if upload.track.attachment_cover
|
||||||
|
else "",
|
||||||
|
"isDir": "false",
|
||||||
|
"year": upload.track.creation_date.year,
|
||||||
|
"publishDate": upload.track.creation_date.isoformat(),
|
||||||
|
"created": upload.track.creation_date.isoformat(),
|
||||||
|
"genre": "Podcast",
|
||||||
|
"size": upload.size if upload.size else "",
|
||||||
|
"duration": upload.duration if upload.duration else "",
|
||||||
|
"bitrate": upload.bitrate / 1000 if upload.bitrate else "",
|
||||||
|
"contentType": upload.mimetype or "audio/mpeg",
|
||||||
|
"suffix": upload.extension or "mp3",
|
||||||
|
"status": "completed",
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ import functools
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db.models import Count, Q
|
from django.db import transaction
|
||||||
|
from django.db.models import Count, Prefetch, Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import exceptions
|
from rest_framework import exceptions
|
||||||
from rest_framework import permissions as rest_permissions
|
from rest_framework import permissions as rest_permissions
|
||||||
|
@ -16,12 +17,17 @@ from rest_framework.serializers import ValidationError
|
||||||
|
|
||||||
import funkwhale_api
|
import funkwhale_api
|
||||||
from funkwhale_api.activity import record
|
from funkwhale_api.activity import record
|
||||||
|
from funkwhale_api.audio import models as audio_models
|
||||||
|
from funkwhale_api.audio import serializers as audio_serializers
|
||||||
|
from funkwhale_api.audio import views as audio_views
|
||||||
from funkwhale_api.common import (
|
from funkwhale_api.common import (
|
||||||
fields,
|
fields,
|
||||||
preferences,
|
preferences,
|
||||||
|
models as common_models,
|
||||||
utils as common_utils,
|
utils as common_utils,
|
||||||
tasks as common_tasks,
|
tasks as common_tasks,
|
||||||
)
|
)
|
||||||
|
from funkwhale_api.federation import models as federation_models
|
||||||
from funkwhale_api.favorites.models import TrackFavorite
|
from funkwhale_api.favorites.models import TrackFavorite
|
||||||
from funkwhale_api.moderation import filters as moderation_filters
|
from funkwhale_api.moderation import filters as moderation_filters
|
||||||
from funkwhale_api.music import models as music_models
|
from funkwhale_api.music import models as music_models
|
||||||
|
@ -101,6 +107,22 @@ def get_playlist_qs(request):
|
||||||
return qs.order_by("-creation_date")
|
return qs.order_by("-creation_date")
|
||||||
|
|
||||||
|
|
||||||
|
def requires_channels(f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def inner(*args, **kwargs):
|
||||||
|
if not preferences.get("audio__channels_enabled"):
|
||||||
|
payload = {
|
||||||
|
"error": {
|
||||||
|
"code": 0,
|
||||||
|
"message": "Channels / podcasts are disabled on this pod",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response.Response(payload, status=405)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return inner
|
||||||
|
|
||||||
|
|
||||||
class SubsonicViewSet(viewsets.GenericViewSet):
|
class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
content_negotiation_class = negotiation.SubsonicContentNegociation
|
content_negotiation_class = negotiation.SubsonicContentNegociation
|
||||||
authentication_classes = [authentication.SubsonicAuthentication]
|
authentication_classes = [authentication.SubsonicAuthentication]
|
||||||
|
@ -752,6 +774,14 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
{"error": {"code": 70, "message": "cover art not found."}}
|
{"error": {"code": 70, "message": "cover art not found."}}
|
||||||
)
|
)
|
||||||
attachment = album.attachment_cover
|
attachment = album.attachment_cover
|
||||||
|
elif id.startswith("at-"):
|
||||||
|
try:
|
||||||
|
attachment_id = id.replace("at-", "")
|
||||||
|
attachment = common_models.Attachment.objects.get(uuid=attachment_id)
|
||||||
|
except (TypeError, ValueError, music_models.Album.DoesNotExist):
|
||||||
|
return response.Response(
|
||||||
|
{"error": {"code": 70, "message": "cover art not found."}}
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return response.Response(
|
return response.Response(
|
||||||
{"error": {"code": 70, "message": "cover art not found."}}
|
{"error": {"code": 70, "message": "cover art not found."}}
|
||||||
|
@ -810,3 +840,149 @@ class SubsonicViewSet(viewsets.GenericViewSet):
|
||||||
"genres": {"genre": [serializers.get_genre_data(tag) for tag in queryset]}
|
"genres": {"genre": [serializers.get_genre_data(tag) for tag in queryset]}
|
||||||
}
|
}
|
||||||
return response.Response(data)
|
return response.Response(data)
|
||||||
|
|
||||||
|
# podcast related views
|
||||||
|
@action(
|
||||||
|
detail=False,
|
||||||
|
methods=["get", "post"],
|
||||||
|
url_name="create_podcast_channel",
|
||||||
|
url_path="createPodcastChannel",
|
||||||
|
)
|
||||||
|
@requires_channels
|
||||||
|
@transaction.atomic
|
||||||
|
def create_podcast_channel(self, request, *args, **kwargs):
|
||||||
|
data = request.GET or request.POST
|
||||||
|
serializer = audio_serializers.RssSubscribeSerializer(data=data)
|
||||||
|
if not serializer.is_valid():
|
||||||
|
return response.Response({"error": {"code": 0, "message": "invalid url"}})
|
||||||
|
channel = (
|
||||||
|
audio_models.Channel.objects.filter(
|
||||||
|
rss_url=serializer.validated_data["url"],
|
||||||
|
)
|
||||||
|
.order_by("id")
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
if not channel:
|
||||||
|
# try to retrieve the channel via its URL and create it
|
||||||
|
try:
|
||||||
|
channel, uploads = audio_serializers.get_channel_from_rss_url(
|
||||||
|
serializer.validated_data["url"]
|
||||||
|
)
|
||||||
|
except audio_serializers.FeedFetchException as e:
|
||||||
|
return response.Response(
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": 0,
|
||||||
|
"message": "Error while fetching url: {}".format(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
subscription = federation_models.Follow(actor=request.user.actor)
|
||||||
|
subscription.fid = subscription.get_federation_id()
|
||||||
|
audio_views.SubscriptionsViewSet.queryset.get_or_create(
|
||||||
|
target=channel.actor,
|
||||||
|
actor=request.user.actor,
|
||||||
|
defaults={
|
||||||
|
"approved": True,
|
||||||
|
"fid": subscription.fid,
|
||||||
|
"uuid": subscription.uuid,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return response.Response({"status": "ok"})
|
||||||
|
|
||||||
|
@action(
|
||||||
|
detail=False,
|
||||||
|
methods=["get", "post"],
|
||||||
|
url_name="delete_podcast_channel",
|
||||||
|
url_path="deletePodcastChannel",
|
||||||
|
)
|
||||||
|
@requires_channels
|
||||||
|
@find_object(
|
||||||
|
audio_models.Channel.objects.all().select_related("actor"),
|
||||||
|
model_field="uuid",
|
||||||
|
field="id",
|
||||||
|
cast=str,
|
||||||
|
)
|
||||||
|
def delete_podcast_channel(self, request, *args, **kwargs):
|
||||||
|
channel = kwargs.pop("obj")
|
||||||
|
actor = request.user.actor
|
||||||
|
actor.emitted_follows.filter(target=channel.actor).delete()
|
||||||
|
return response.Response({"status": "ok"})
|
||||||
|
|
||||||
|
@action(
|
||||||
|
detail=False,
|
||||||
|
methods=["get", "post"],
|
||||||
|
url_name="get_podcasts",
|
||||||
|
url_path="getPodcasts",
|
||||||
|
)
|
||||||
|
@requires_channels
|
||||||
|
def get_podcasts(self, request, *args, **kwargs):
|
||||||
|
data = request.GET or request.POST
|
||||||
|
id = data.get("id")
|
||||||
|
channels = audio_models.Channel.objects.subscribed(request.user.actor)
|
||||||
|
if id:
|
||||||
|
channels = channels.filter(uuid=id)
|
||||||
|
channels = channels.select_related(
|
||||||
|
"artist__attachment_cover", "artist__description", "library", "actor"
|
||||||
|
)
|
||||||
|
uploads_qs = (
|
||||||
|
music_models.Upload.objects.playable_by(request.user.actor)
|
||||||
|
.select_related("track__attachment_cover", "track__description",)
|
||||||
|
.order_by("-track__creation_date")
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.get("includeEpisodes", "true") == "true":
|
||||||
|
channels = channels.prefetch_related(
|
||||||
|
Prefetch(
|
||||||
|
"library__uploads",
|
||||||
|
queryset=uploads_qs,
|
||||||
|
to_attr="_prefetched_uploads",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"podcasts": {
|
||||||
|
"channel": [
|
||||||
|
serializers.get_channel_data(
|
||||||
|
channel, getattr(channel.library, "_prefetched_uploads", [])
|
||||||
|
)
|
||||||
|
for channel in channels
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return response.Response(data)
|
||||||
|
|
||||||
|
@action(
|
||||||
|
detail=False,
|
||||||
|
methods=["get", "post"],
|
||||||
|
url_name="get_newest_podcasts",
|
||||||
|
url_path="getNewestPodcasts",
|
||||||
|
)
|
||||||
|
@requires_channels
|
||||||
|
def get_newest_podcasts(self, request, *args, **kwargs):
|
||||||
|
data = request.GET or request.POST
|
||||||
|
try:
|
||||||
|
count = int(data["count"])
|
||||||
|
except (TypeError, KeyError, ValueError):
|
||||||
|
count = 20
|
||||||
|
channels = audio_models.Channel.objects.subscribed(request.user.actor)
|
||||||
|
uploads = (
|
||||||
|
music_models.Upload.objects.playable_by(request.user.actor)
|
||||||
|
.filter(library__channel__in=channels)
|
||||||
|
.select_related(
|
||||||
|
"track__attachment_cover", "track__description", "library__channel"
|
||||||
|
)
|
||||||
|
.order_by("-track__creation_date")
|
||||||
|
)
|
||||||
|
data = {
|
||||||
|
"newestPodcasts": {
|
||||||
|
"episode": [
|
||||||
|
serializers.get_channel_episode_data(
|
||||||
|
upload, upload.library.channel.uuid
|
||||||
|
)
|
||||||
|
for upload in uploads[:count]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response.Response(data)
|
||||||
|
|
|
@ -33,3 +33,5 @@ env =
|
||||||
PROXY_MEDIA=true
|
PROXY_MEDIA=true
|
||||||
MUSIC_USE_DENORMALIZATION=true
|
MUSIC_USE_DENORMALIZATION=true
|
||||||
EXTERNAL_MEDIA_PROXY_ENABLED=true
|
EXTERNAL_MEDIA_PROXY_ENABLED=true
|
||||||
|
DISABLE_PASSWORD_VALIDATORS=false
|
||||||
|
DISABLE_PASSWORD_VALIDATORS=false
|
||||||
|
|
|
@ -302,3 +302,55 @@ def test_scrobble_serializer(factories):
|
||||||
|
|
||||||
assert listening.user == user
|
assert listening.user == user
|
||||||
assert listening.track == track
|
assert listening.track == track
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_serializer(factories):
|
||||||
|
description = factories["common.Content"]()
|
||||||
|
channel = factories["audio.Channel"](external=True, artist__description=description)
|
||||||
|
upload = factories["music.Upload"](
|
||||||
|
playable=True, library=channel.library, duration=42
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"id": str(channel.uuid),
|
||||||
|
"url": channel.rss_url,
|
||||||
|
"title": channel.artist.name,
|
||||||
|
"description": description.as_plain_text,
|
||||||
|
"coverArt": "at-{}".format(channel.artist.attachment_cover.uuid),
|
||||||
|
"originalImageUrl": channel.artist.attachment_cover.url,
|
||||||
|
"status": "completed",
|
||||||
|
"episode": [serializers.get_channel_episode_data(upload, channel.uuid)],
|
||||||
|
}
|
||||||
|
data = serializers.get_channel_data(channel, [upload])
|
||||||
|
assert data == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_channel_episode_serializer(factories):
|
||||||
|
description = factories["common.Content"]()
|
||||||
|
channel = factories["audio.Channel"]()
|
||||||
|
track = factories["music.Track"](description=description, artist=channel.artist)
|
||||||
|
upload = factories["music.Upload"](
|
||||||
|
playable=True, track=track, bitrate=128000, duration=42
|
||||||
|
)
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"id": str(upload.uuid),
|
||||||
|
"channelId": str(channel.uuid),
|
||||||
|
"streamId": upload.track.id,
|
||||||
|
"title": track.title,
|
||||||
|
"description": description.as_plain_text,
|
||||||
|
"coverArt": "at-{}".format(track.attachment_cover.uuid),
|
||||||
|
"isDir": "false",
|
||||||
|
"year": track.creation_date.year,
|
||||||
|
"created": track.creation_date.isoformat(),
|
||||||
|
"publishDate": track.creation_date.isoformat(),
|
||||||
|
"genre": "Podcast",
|
||||||
|
"size": upload.size,
|
||||||
|
"duration": upload.duration,
|
||||||
|
"bitrate": upload.bitrate / 1000,
|
||||||
|
"contentType": upload.mimetype,
|
||||||
|
"suffix": upload.extension,
|
||||||
|
"status": "completed",
|
||||||
|
}
|
||||||
|
data = serializers.get_channel_episode_data(upload, channel.uuid)
|
||||||
|
assert data == expected
|
||||||
|
|
|
@ -731,6 +731,19 @@ def test_get_cover_art_album(factories, logged_in_api_client):
|
||||||
).decode("utf-8")
|
).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_cover_art_attachment(factories, logged_in_api_client):
|
||||||
|
attachment = factories["common.Attachment"]()
|
||||||
|
url = reverse("api:subsonic-get_cover_art")
|
||||||
|
assert url.endswith("getCoverArt") is True
|
||||||
|
response = logged_in_api_client.get(url, {"id": "at-{}".format(attachment.uuid)})
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response["Content-Type"] == ""
|
||||||
|
assert response["X-Accel-Redirect"] == music_views.get_file_path(
|
||||||
|
attachment.file
|
||||||
|
).decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
def test_get_avatar(factories, logged_in_api_client):
|
def test_get_avatar(factories, logged_in_api_client):
|
||||||
user = factories["users.User"]()
|
user = factories["users.User"]()
|
||||||
url = reverse("api:subsonic-get_avatar")
|
url = reverse("api:subsonic-get_avatar")
|
||||||
|
@ -776,7 +789,7 @@ def test_get_user(f, db, logged_in_api_client, factories):
|
||||||
"settingsRole": "false",
|
"settingsRole": "false",
|
||||||
"playlistRole": "true",
|
"playlistRole": "true",
|
||||||
"commentRole": "false",
|
"commentRole": "false",
|
||||||
"podcastRole": "false",
|
"podcastRole": "true",
|
||||||
"streamRole": "true",
|
"streamRole": "true",
|
||||||
"jukeboxRole": "true",
|
"jukeboxRole": "true",
|
||||||
"coverArtRole": "false",
|
"coverArtRole": "false",
|
||||||
|
@ -787,3 +800,138 @@ def test_get_user(f, db, logged_in_api_client, factories):
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_podcast_channel(logged_in_api_client, factories, mocker):
|
||||||
|
channel = factories["audio.Channel"](external=True)
|
||||||
|
rss_url = "https://rss.url/"
|
||||||
|
get_channel_from_rss_url = mocker.patch(
|
||||||
|
"funkwhale_api.audio.serializers.get_channel_from_rss_url",
|
||||||
|
return_value=(channel, []),
|
||||||
|
)
|
||||||
|
actor = logged_in_api_client.user.create_actor()
|
||||||
|
url = reverse("api:subsonic-create_podcast_channel")
|
||||||
|
assert url.endswith("createPodcastChannel") is True
|
||||||
|
response = logged_in_api_client.get(url, {"f": "json", "url": rss_url})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {"status": "ok"}
|
||||||
|
|
||||||
|
subscription = actor.emitted_follows.get(target=channel.actor)
|
||||||
|
assert subscription.approved is True
|
||||||
|
get_channel_from_rss_url.assert_called_once_with(rss_url)
|
||||||
|
|
||||||
|
|
||||||
|
def test_delete_podcast_channel(logged_in_api_client, factories, mocker):
|
||||||
|
actor = logged_in_api_client.user.create_actor()
|
||||||
|
channel = factories["audio.Channel"](external=True)
|
||||||
|
subscription = factories["federation.Follow"](actor=actor, target=channel.actor)
|
||||||
|
other_subscription = factories["federation.Follow"](target=channel.actor)
|
||||||
|
url = reverse("api:subsonic-delete_podcast_channel")
|
||||||
|
assert url.endswith("deletePodcastChannel") is True
|
||||||
|
response = logged_in_api_client.get(url, {"f": "json", "id": channel.uuid})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {"status": "ok"}
|
||||||
|
other_subscription.refresh_from_db()
|
||||||
|
with pytest.raises(subscription.DoesNotExist):
|
||||||
|
subscription.refresh_from_db()
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_podcasts(logged_in_api_client, factories, mocker):
|
||||||
|
actor = logged_in_api_client.user.create_actor()
|
||||||
|
channel = factories["audio.Channel"](
|
||||||
|
external=True, library__privacy_level="everyone"
|
||||||
|
)
|
||||||
|
upload1 = factories["music.Upload"](
|
||||||
|
playable=True,
|
||||||
|
track__artist=channel.artist,
|
||||||
|
library=channel.library,
|
||||||
|
bitrate=128000,
|
||||||
|
duration=42,
|
||||||
|
)
|
||||||
|
upload2 = factories["music.Upload"](
|
||||||
|
playable=True,
|
||||||
|
track__artist=channel.artist,
|
||||||
|
library=channel.library,
|
||||||
|
bitrate=256000,
|
||||||
|
duration=43,
|
||||||
|
)
|
||||||
|
factories["federation.Follow"](actor=actor, target=channel.actor, approved=True)
|
||||||
|
factories["music.Upload"](import_status="pending", track__artist=channel.artist)
|
||||||
|
factories["audio.Channel"](external=True)
|
||||||
|
factories["federation.Follow"]()
|
||||||
|
url = reverse("api:subsonic-get_podcasts")
|
||||||
|
assert url.endswith("getPodcasts") is True
|
||||||
|
response = logged_in_api_client.get(url, {"f": "json"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {
|
||||||
|
"podcasts": {
|
||||||
|
"channel": [serializers.get_channel_data(channel, [upload2, upload1])],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_podcasts_by_id(logged_in_api_client, factories, mocker):
|
||||||
|
actor = logged_in_api_client.user.create_actor()
|
||||||
|
channel1 = factories["audio.Channel"](
|
||||||
|
external=True, library__privacy_level="everyone"
|
||||||
|
)
|
||||||
|
channel2 = factories["audio.Channel"](
|
||||||
|
external=True, library__privacy_level="everyone"
|
||||||
|
)
|
||||||
|
upload1 = factories["music.Upload"](
|
||||||
|
playable=True,
|
||||||
|
track__artist=channel1.artist,
|
||||||
|
library=channel1.library,
|
||||||
|
bitrate=128000,
|
||||||
|
duration=42,
|
||||||
|
)
|
||||||
|
factories["music.Upload"](
|
||||||
|
playable=True,
|
||||||
|
track__artist=channel2.artist,
|
||||||
|
library=channel2.library,
|
||||||
|
bitrate=256000,
|
||||||
|
duration=43,
|
||||||
|
)
|
||||||
|
factories["federation.Follow"](actor=actor, target=channel1.actor, approved=True)
|
||||||
|
factories["federation.Follow"](actor=actor, target=channel2.actor, approved=True)
|
||||||
|
url = reverse("api:subsonic-get_podcasts")
|
||||||
|
assert url.endswith("getPodcasts") is True
|
||||||
|
response = logged_in_api_client.get(url, {"f": "json", "id": channel1.uuid})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {
|
||||||
|
"podcasts": {"channel": [serializers.get_channel_data(channel1, [upload1])]}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_newest_podcasts(logged_in_api_client, factories, mocker):
|
||||||
|
actor = logged_in_api_client.user.create_actor()
|
||||||
|
channel = factories["audio.Channel"](
|
||||||
|
external=True, library__privacy_level="everyone"
|
||||||
|
)
|
||||||
|
upload1 = factories["music.Upload"](
|
||||||
|
playable=True,
|
||||||
|
track__artist=channel.artist,
|
||||||
|
library=channel.library,
|
||||||
|
bitrate=128000,
|
||||||
|
duration=42,
|
||||||
|
)
|
||||||
|
upload2 = factories["music.Upload"](
|
||||||
|
playable=True,
|
||||||
|
track__artist=channel.artist,
|
||||||
|
library=channel.library,
|
||||||
|
bitrate=256000,
|
||||||
|
duration=43,
|
||||||
|
)
|
||||||
|
factories["federation.Follow"](actor=actor, target=channel.actor, approved=True)
|
||||||
|
url = reverse("api:subsonic-get_newest_podcasts")
|
||||||
|
assert url.endswith("getNewestPodcasts") is True
|
||||||
|
response = logged_in_api_client.get(url, {"f": "json"})
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.data == {
|
||||||
|
"newestPodcasts": {
|
||||||
|
"episode": [
|
||||||
|
serializers.get_channel_episode_data(upload, channel.uuid)
|
||||||
|
for upload in [upload2, upload1]
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue