add serializers for video

This commit is contained in:
Simon 2025-02-12 18:59:56 +07:00
parent 5772ab3e68
commit 88132fef3c
No known key found for this signature in database
GPG Key ID: 2C15AA5E89985DD4
4 changed files with 345 additions and 60 deletions

View File

@ -201,7 +201,7 @@ class YoutubePlaylist(YouTubeItem):
current_idx = all_entries.index(current[0])
if current_idx == 0:
previous_item = False
previous_item = None
else:
previous_item = all_entries[current_idx - 1]
prev_id = previous_item["youtube_id"]
@ -209,7 +209,7 @@ class YoutubePlaylist(YouTubeItem):
previous_item["vid_thumb"] = f"{cache_root}/{prev_thumb_path}"
if current_idx == len(all_entries) - 1:
next_item = False
next_item = None
else:
next_item = all_entries[current_idx + 1]
next_id = next_item["youtube_id"]

View File

@ -0,0 +1,198 @@
"""video serializers"""
# pylint: disable=abstract-method
from channel.serializers import ChannelSerializer
from common.serializers import PaginationSerializer
from rest_framework import serializers
class PlayerSerializer(serializers.Serializer):
"""serialize player"""
watched = serializers.BooleanField()
duration = serializers.IntegerField()
duration_str = serializers.CharField()
progress = serializers.FloatField(required=False)
position = serializers.FloatField(required=False)
class SponsorBlockSegmentSerializer(serializers.Serializer):
"""serialize sponsorblock segment"""
actionType = serializers.CharField()
videoDuration = serializers.FloatField()
segment = serializers.ListField(child=serializers.FloatField())
votes = serializers.IntegerField()
category = serializers.CharField()
UUID = serializers.CharField()
locked = serializers.IntegerField()
class SponsorBlockSerializer(serializers.Serializer):
"""serialize sponsorblock"""
is_enabled = serializers.BooleanField()
last_refresh = serializers.IntegerField()
has_unlocked = serializers.BooleanField(required=False)
segments = SponsorBlockSegmentSerializer(many=True)
class StatsSerializer(serializers.Serializer):
"""serialize stats"""
like_count = serializers.IntegerField(required=False)
average_rating = serializers.FloatField(required=False)
view_count = serializers.IntegerField(required=False)
dislike_count = serializers.IntegerField(required=False)
class StreamItemSerializer(serializers.Serializer):
"""serialize stream item"""
index = serializers.IntegerField()
codec = serializers.CharField()
bitrate = serializers.IntegerField()
type = serializers.ChoiceField(choices=["video", "audio"])
width = serializers.IntegerField(required=False)
height = serializers.IntegerField(required=False)
class SubtitleItemSerializer(serializers.Serializer):
"""serialize subtitle item"""
ext = serializers.ChoiceField(choices=["json3"])
name = serializers.CharField()
source = serializers.ChoiceField(choices=["user", "auto"])
lang = serializers.CharField()
media_url = serializers.CharField()
url = serializers.URLField()
class VideoSerializer(serializers.Serializer):
"""serialize video item"""
active = serializers.BooleanField()
category = serializers.ListField(child=serializers.CharField())
channel = ChannelSerializer()
comment_count = serializers.IntegerField(allow_null=True)
date_downloaded = serializers.IntegerField()
description = serializers.CharField()
media_size = serializers.IntegerField()
media_url = serializers.CharField()
player = PlayerSerializer()
playlist = serializers.ListField(
child=serializers.CharField(), allow_null=True
)
published = serializers.CharField()
sponsorblock = SponsorBlockSerializer(allow_null=True)
stats = StatsSerializer()
streams = StreamItemSerializer(many=True)
subtitles = SubtitleItemSerializer(many=True)
tags = serializers.ListField(child=serializers.CharField())
title = serializers.CharField()
vid_last_refresh = serializers.CharField()
vid_thumb_url = serializers.CharField()
vid_type = serializers.ChoiceField(choices=["video", "stream", "short"])
youtube_id = serializers.CharField()
_index = serializers.CharField(required=False)
_score = serializers.FloatField(required=False)
class VideoListSerializer(serializers.Serializer):
"""serialize video list"""
data = VideoSerializer(many=True)
paginate = PaginationSerializer()
class VideoListQuerySerializer(serializers.Serializer):
"""serialize query for video list"""
playlist = serializers.CharField(required=False)
channel = serializers.CharField(required=False)
watch = serializers.ChoiceField(
choices=["watched", "unwatched", "continue"], required=False
)
sort = serializers.ChoiceField(
choices=[
"published",
"downloaded",
"views",
"likes",
"duration",
"filesize",
],
required=False,
)
order = serializers.ChoiceField(choices=["asc", "desc"], required=False)
type = serializers.ChoiceField(
choices=["video", "stream", "short"], required=False
)
class CommentThreadItemSerializer(serializers.Serializer):
"""serialize comment thread item"""
comment_id = serializers.CharField()
comment_text = serializers.CharField()
comment_timestamp = serializers.IntegerField()
comment_time_text = serializers.CharField()
comment_likecount = serializers.IntegerField()
comment_is_favorited = serializers.BooleanField()
comment_author = serializers.CharField()
comment_author_id = serializers.CharField()
comment_author_thumbnail = serializers.URLField()
comment_author_is_uploader = serializers.BooleanField()
comment_parent = serializers.CharField()
class CommentItemSerializer(serializers.Serializer):
"""serialize comment item"""
comment_id = serializers.CharField()
comment_text = serializers.CharField()
comment_timestamp = serializers.IntegerField()
comment_time_text = serializers.CharField()
comment_likecount = serializers.IntegerField()
comment_is_favorited = serializers.BooleanField()
comment_author = serializers.CharField()
comment_author_id = serializers.CharField()
comment_author_thumbnail = serializers.URLField()
comment_author_is_uploader = serializers.BooleanField()
comment_parent = serializers.CharField()
comment_replies = CommentThreadItemSerializer(many=True)
class PlaylistNavMetaSerializer(serializers.Serializer):
"""serialize playlist nav meta"""
current_idx = serializers.IntegerField()
playlist_id = serializers.CharField()
playlist_name = serializers.CharField()
playlist_channel = serializers.CharField()
class PlaylistNavVideoSerializer(serializers.Serializer):
"""serialize video item on playlist nav"""
youtube_id = serializers.CharField()
title = serializers.CharField()
uploader = serializers.CharField()
idx = serializers.IntegerField()
downloaded = serializers.BooleanField()
vid_thumb = serializers.CharField()
class PlaylistNavItemSerializer(serializers.Serializer):
"""serialize nav on playlist"""
playlist_meta = PlaylistNavMetaSerializer()
playlist_previous = PlaylistNavVideoSerializer(allow_null=True)
playlist_next = PlaylistNavVideoSerializer(allow_null=True)
class VideoProgressUpdateSerializer(serializers.Serializer):
"""serialize progress update data"""
position = serializers.FloatField(default=0)

View File

@ -27,20 +27,20 @@ class QueryBuilder:
must_list = []
channel = self.request_params.get("channel")
if channel:
must_list.append({"match": {"channel.channel_id": channel[0]}})
must_list.append({"match": {"channel.channel_id": channel}})
playlist = self.request_params.get("playlist")
if playlist:
must_list.append({"match": {"playlist.keyword": playlist[0]}})
must_list.append({"match": {"playlist.keyword": playlist}})
watch = self.request_params.get("watch")
if watch:
watch_must_list = self.parse_watch(watch[0])
watch_must_list = self.parse_watch(watch)
must_list.append(watch_must_list)
video_type = self.request_params.get("type")
if video_type:
type_list_list = self.parse_type(video_type[0])
type_list_list = self.parse_type(video_type)
must_list.append(type_list_list)
query = {"bool": {"must": must_list}}
@ -88,14 +88,12 @@ class QueryBuilder:
if not sort:
return None
sort = sort[0]
if not hasattr(SortEnum, sort.upper()):
raise ValueError(f"'{sort}' not in SortEnum")
sort_field = getattr(SortEnum, sort.upper()).value
order = self.request_params.get("order", ["desc"])
order = order[0]
if not hasattr(OrderEnum, order.upper()):
raise ValueError(f"'{order}' not in OrderEnum")

View File

@ -1,11 +1,22 @@
"""all API views for video endpoints"""
from common.serializers import ErrorResponseSerializer
from common.src.helper import calc_is_watched
from common.src.ta_redis import RedisArchivist
from common.src.watched import WatchState
from common.views_base import AdminWriteOnly, ApiBaseView
from drf_spectacular.utils import OpenApiResponse, extend_schema
from playlist.src.index import YoutubePlaylist
from rest_framework.response import Response
from video.serializers import (
CommentItemSerializer,
PlayerSerializer,
PlaylistNavItemSerializer,
VideoListQuerySerializer,
VideoListSerializer,
VideoProgressUpdateSerializer,
VideoSerializer,
)
from video.src.index import YoutubeVideo
from video.src.query_building import QueryBuilder
@ -24,13 +35,22 @@ class VideoApiListView(ApiBaseView):
search_base = "ta_video/_search/"
@extend_schema(
parameters=[VideoListQuerySerializer()],
responses={
200: VideoListSerializer(),
400: OpenApiResponse(
ErrorResponseSerializer(), description="bad request"
),
},
)
def get(self, request):
"""get request"""
try:
data = QueryBuilder(request.user.id, **request.GET).build_data()
except ValueError as err:
return Response({"error": str(err)}, status=400)
"""get video list"""
query_serializer = VideoListQuerySerializer(data=request.query_params)
query_serializer.is_valid(raise_exception=True)
validated_query = query_serializer.validated_data
data = QueryBuilder(request.user.id, **validated_query).build_data()
if data == {"query": {"bool": {"must": [None]}}}:
# skip empty lookup
return Response([])
@ -38,7 +58,9 @@ class VideoApiListView(ApiBaseView):
self.data = data
self.get_document_list(request, progress_match=request.user.id)
return Response(self.response)
response_serializer = VideoListSerializer(self.response)
return Response(response_serializer.data)
class VideoApiView(ApiBaseView):
@ -49,25 +71,71 @@ class VideoApiView(ApiBaseView):
search_base = "ta_video/_doc/"
permission_classes = [AdminWriteOnly]
@extend_schema(
responses={
200: VideoSerializer(),
404: OpenApiResponse(
ErrorResponseSerializer(), description="video not found"
),
},
)
def get(self, request, video_id):
# pylint: disable=unused-argument
"""get request"""
"""get video"""
self.get_document(video_id, progress_match=request.user.id)
return Response(self.response, status=self.status_code)
if not self.response:
error = ErrorResponseSerializer({"error": "video not found"})
return Response(error.data, status=404)
serializer = VideoSerializer(self.response)
return Response(serializer.data)
@extend_schema(
responses={
204: OpenApiResponse(description="video deleted"),
404: OpenApiResponse(
ErrorResponseSerializer(), description="video not found"
),
}
)
def delete(self, request, video_id):
# pylint: disable=unused-argument
"""delete single video"""
message = {"video": video_id}
"""delete video"""
try:
YoutubeVideo(video_id).delete_media_file()
status_code = 200
message.update({"state": "delete"})
except FileNotFoundError:
status_code = 404
message.update({"state": "not found"})
error = ErrorResponseSerializer({"error": "video not found"})
return Response(error.data, status=404)
return Response(message, status=status_code)
return Response(status=204)
class VideoCommentView(ApiBaseView):
"""resolves to /api/video/<video_id>/comment/
handle video comments
GET: return all comments from video with reply threads
"""
search_base = "ta_comment/_doc/"
@extend_schema(
responses={
200: CommentItemSerializer(),
404: OpenApiResponse(
ErrorResponseSerializer(), description="video not found"
),
}
)
def get(self, request, video_id):
"""get video comments"""
# pylint: disable=unused-argument
self.get_document(video_id)
if self.status_code == 404:
error = ErrorResponseSerializer({"error": "video not found"})
return Response(error.data, status=404)
serializer = CommentItemSerializer(self.response, many=True)
return Response(serializer.data)
class VideoApiNavView(ApiBaseView):
@ -77,26 +145,39 @@ class VideoApiNavView(ApiBaseView):
search_base = "ta_video/_doc/"
@extend_schema(
responses={
200: PlaylistNavItemSerializer(),
404: OpenApiResponse(
ErrorResponseSerializer(), description="video not found"
),
}
)
def get(self, request, video_id):
# pylint: disable=unused-argument
"""get request"""
"""get video playlist nav"""
self.get_document(video_id)
if self.status_code != 200:
return Response(status=self.status_code)
if self.status_code == 404:
error = ErrorResponseSerializer({"error": "video not found"})
return Response(error.data, status=404)
playlist_nav = []
if not self.response["data"].get("playlist"):
if not self.response.get("playlist"):
return Response(playlist_nav)
for playlist_id in self.response["data"]["playlist"]:
for playlist_id in self.response["playlist"]:
playlist = YoutubePlaylist(playlist_id)
playlist.get_from_es()
playlist.build_nav(video_id)
if playlist.nav:
playlist_nav.append(playlist.nav)
return Response(playlist_nav, status=self.status_code)
response_serializer = PlaylistNavItemSerializer(
playlist_nav, many=True
)
return Response(response_serializer.data)
class VideoProgressView(ApiBaseView):
@ -111,19 +192,32 @@ class VideoProgressView(ApiBaseView):
"""redis key"""
return f"{user_id}:progress:{video_id}"
@extend_schema(
request=VideoProgressUpdateSerializer(),
responses={
200: PlayerSerializer(),
404: OpenApiResponse(
ErrorResponseSerializer(), description="video not found"
),
},
)
def post(self, request, video_id):
"""set progress position in redis"""
position = request.data.get("position", 0)
"""set video progress position in redis"""
data_serializer = VideoProgressUpdateSerializer(data=request.data)
data_serializer.is_valid(raise_exception=True)
validated_data = data_serializer.validated_data
self.get_document(video_id)
if self.status_code == 404:
error = ErrorResponseSerializer({"error": "video not found"})
return Response(error.data, status=404)
position = validated_data["position"]
key = self._get_key(request.user.id, video_id)
redis_con = RedisArchivist()
current_progress = redis_con.get_message_dict(key)
if not current_progress:
self.get_document(video_id)
if self.status_code != 200:
return Response(status=self.status_code)
current_progress = self.response["data"]["player"]
current_progress = (
redis_con.get_message_dict(key) or self.response["player"]
)
current_progress.update({"position": position, "youtube_id": video_id})
watched = self._check_watched(request, video_id, current_progress)
@ -134,8 +228,11 @@ class VideoProgressView(ApiBaseView):
current_progress.update({"watched": watched})
redis_con.set_message(key, current_progress, expire=expire)
print(current_progress)
return Response(current_progress)
response_serializer = PlayerSerializer(current_progress)
return Response(response_serializer.data)
def _check_watched(self, request, video_id, current_progress) -> bool:
"""check watched state"""
@ -150,29 +247,17 @@ class VideoProgressView(ApiBaseView):
return watched
@extend_schema(
responses={
204: OpenApiResponse(description="video progress deleted"),
}
)
def delete(self, request, video_id):
"""delete progress position"""
key = self._get_key(request.user.id, video_id)
RedisArchivist().del_message(key)
self.response = {"progress-reset": video_id}
return Response(self.response)
class VideoCommentView(ApiBaseView):
"""resolves to /api/video/<video_id>/comment/
handle video comments
GET: return all comments from video with reply threads
"""
search_base = "ta_comment/_doc/"
def get(self, request, video_id):
"""get video comments"""
# pylint: disable=unused-argument
self.get_document(video_id)
return Response(self.response, status=200)
return Response(status=204)
class VideoSimilarView(ApiBaseView):
@ -182,6 +267,9 @@ class VideoSimilarView(ApiBaseView):
search_base = "ta_video/_search/"
@extend_schema(
responses=VideoSerializer(many=True),
)
def get(self, request, video_id):
"""get similar videos"""
self.data = {
@ -196,4 +284,5 @@ class VideoSimilarView(ApiBaseView):
},
}
self.get_document_list(request, pagination=False)
return Response(self.response, status=200)
serializer = VideoSerializer(self.response["data"], many=True)
return Response(serializer.data)