mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-01-23 02:35:14 +00:00
310 lines
9.9 KiB
Python
310 lines
9.9 KiB
Python
from django.db.models import Count
|
|
from rest_framework import serializers
|
|
|
|
from apps.media_library.models import (
|
|
ArtworkAsset,
|
|
Library,
|
|
LibraryLocation,
|
|
LibraryScan,
|
|
MediaFile,
|
|
MediaItem,
|
|
WatchProgress,
|
|
)
|
|
|
|
|
|
class LibraryLocationSerializer(serializers.ModelSerializer):
|
|
id = serializers.IntegerField(required=False)
|
|
|
|
class Meta:
|
|
model = LibraryLocation
|
|
fields = ["id", "path", "include_subdirectories", "is_primary"]
|
|
|
|
|
|
class LibrarySerializer(serializers.ModelSerializer):
|
|
locations = LibraryLocationSerializer(many=True)
|
|
movie_count = serializers.IntegerField(read_only=True)
|
|
show_count = serializers.IntegerField(read_only=True)
|
|
|
|
class Meta:
|
|
model = Library
|
|
fields = [
|
|
"id",
|
|
"name",
|
|
"description",
|
|
"library_type",
|
|
"metadata_language",
|
|
"metadata_country",
|
|
"metadata_options",
|
|
"scan_interval_minutes",
|
|
"auto_scan_enabled",
|
|
"add_to_vod",
|
|
"last_scan_at",
|
|
"last_successful_scan_at",
|
|
"movie_count",
|
|
"show_count",
|
|
"locations",
|
|
"created_at",
|
|
"updated_at",
|
|
]
|
|
|
|
def create(self, validated_data):
|
|
locations = validated_data.pop("locations", [])
|
|
library = Library.objects.create(**validated_data)
|
|
for index, location in enumerate(locations):
|
|
LibraryLocation.objects.create(
|
|
library=library,
|
|
path=location["path"],
|
|
include_subdirectories=location.get("include_subdirectories", True),
|
|
is_primary=location.get("is_primary", index == 0),
|
|
)
|
|
return library
|
|
|
|
def update(self, instance, validated_data):
|
|
locations = validated_data.pop("locations", None)
|
|
for attr, value in validated_data.items():
|
|
setattr(instance, attr, value)
|
|
instance.save()
|
|
|
|
if locations is not None:
|
|
existing_ids = {loc.id for loc in instance.locations.all()}
|
|
seen_ids: set[int] = set()
|
|
for index, location in enumerate(locations):
|
|
location_id = location.get("id")
|
|
if location_id and location_id in existing_ids:
|
|
LibraryLocation.objects.filter(id=location_id).update(
|
|
path=location.get("path", ""),
|
|
include_subdirectories=location.get("include_subdirectories", True),
|
|
is_primary=location.get("is_primary", index == 0),
|
|
)
|
|
seen_ids.add(location_id)
|
|
else:
|
|
created = LibraryLocation.objects.create(
|
|
library=instance,
|
|
path=location.get("path", ""),
|
|
include_subdirectories=location.get("include_subdirectories", True),
|
|
is_primary=location.get("is_primary", index == 0),
|
|
)
|
|
seen_ids.add(created.id)
|
|
|
|
missing_ids = existing_ids - seen_ids
|
|
if missing_ids:
|
|
LibraryLocation.objects.filter(id__in=missing_ids).delete()
|
|
|
|
return instance
|
|
|
|
|
|
class MediaFileSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = MediaFile
|
|
fields = [
|
|
"id",
|
|
"path",
|
|
"relative_path",
|
|
"file_name",
|
|
"size_bytes",
|
|
"modified_at",
|
|
"duration_ms",
|
|
]
|
|
|
|
|
|
class MediaItemSerializer(serializers.ModelSerializer):
|
|
watch_progress = serializers.SerializerMethodField()
|
|
watch_summary = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = MediaItem
|
|
fields = [
|
|
"id",
|
|
"library",
|
|
"parent",
|
|
"item_type",
|
|
"title",
|
|
"sort_title",
|
|
"normalized_title",
|
|
"synopsis",
|
|
"tagline",
|
|
"release_year",
|
|
"rating",
|
|
"runtime_ms",
|
|
"season_number",
|
|
"episode_number",
|
|
"genres",
|
|
"tags",
|
|
"studios",
|
|
"cast",
|
|
"crew",
|
|
"poster_url",
|
|
"backdrop_url",
|
|
"movie_db_id",
|
|
"imdb_id",
|
|
"youtube_trailer",
|
|
"metadata_source",
|
|
"metadata_last_synced_at",
|
|
"first_imported_at",
|
|
"updated_at",
|
|
"watch_progress",
|
|
"watch_summary",
|
|
]
|
|
|
|
def _progress_payload(self, progress: WatchProgress | None, item: MediaItem):
|
|
if not progress:
|
|
return None
|
|
duration = progress.duration_ms or item.runtime_ms
|
|
percentage = None
|
|
completed = progress.completed
|
|
if duration:
|
|
percentage = min(1, max(0, progress.position_ms / duration))
|
|
if percentage >= 0.95:
|
|
completed = True
|
|
return {
|
|
"id": progress.id,
|
|
"position_ms": progress.position_ms,
|
|
"duration_ms": duration,
|
|
"percentage": percentage,
|
|
"completed": completed,
|
|
"last_watched_at": progress.last_watched_at,
|
|
}
|
|
|
|
def _summary_for_progress(self, progress: WatchProgress | None, item: MediaItem):
|
|
if not progress:
|
|
return {"status": "unwatched"}
|
|
duration = progress.duration_ms or item.runtime_ms or 0
|
|
completed = progress.completed
|
|
if duration and progress.position_ms / max(duration, 1) >= 0.95:
|
|
completed = True
|
|
if completed:
|
|
status = "watched"
|
|
elif progress.position_ms > 0:
|
|
status = "in_progress"
|
|
else:
|
|
status = "unwatched"
|
|
return {
|
|
"status": status,
|
|
"position_ms": progress.position_ms,
|
|
"duration_ms": duration,
|
|
"completed": completed,
|
|
}
|
|
|
|
def _summary_for_show(self, item: MediaItem, user):
|
|
episodes = MediaItem.objects.filter(
|
|
parent=item, item_type=MediaItem.TYPE_EPISODE
|
|
).order_by("season_number", "episode_number", "id")
|
|
total = episodes.count()
|
|
if total == 0:
|
|
return {
|
|
"status": "unwatched",
|
|
"total_episodes": 0,
|
|
"completed_episodes": 0,
|
|
}
|
|
|
|
progress_map = {
|
|
entry.media_item_id: entry
|
|
for entry in WatchProgress.objects.filter(user=user, media_item__in=episodes)
|
|
}
|
|
|
|
completed_episodes = 0
|
|
resume_episode_id = None
|
|
next_episode_id = None
|
|
|
|
for episode in episodes:
|
|
progress = progress_map.get(episode.id)
|
|
if progress:
|
|
duration = progress.duration_ms or episode.runtime_ms or 0
|
|
percent = progress.position_ms / max(duration, 1) if duration else 0
|
|
completed = progress.completed or percent >= 0.95
|
|
if completed:
|
|
completed_episodes += 1
|
|
elif progress.position_ms > 0 and resume_episode_id is None:
|
|
resume_episode_id = episode.id
|
|
if next_episode_id is None:
|
|
if not progress:
|
|
next_episode_id = episode.id
|
|
else:
|
|
duration = progress.duration_ms or episode.runtime_ms or 0
|
|
percent = progress.position_ms / max(duration, 1) if duration else 0
|
|
completed = progress.completed or percent >= 0.95
|
|
if not completed:
|
|
next_episode_id = episode.id
|
|
|
|
if completed_episodes == total:
|
|
status = "watched"
|
|
elif completed_episodes > 0 or resume_episode_id:
|
|
status = "in_progress"
|
|
else:
|
|
status = "unwatched"
|
|
|
|
return {
|
|
"status": status,
|
|
"total_episodes": total,
|
|
"completed_episodes": completed_episodes,
|
|
"resume_episode_id": resume_episode_id,
|
|
"next_episode_id": next_episode_id,
|
|
}
|
|
|
|
def get_watch_progress(self, obj: MediaItem):
|
|
request = self.context.get("request")
|
|
user = getattr(request, "user", None)
|
|
if not user or not user.is_authenticated:
|
|
return None
|
|
progress = WatchProgress.objects.filter(user=user, media_item=obj).first()
|
|
return self._progress_payload(progress, obj)
|
|
|
|
def get_watch_summary(self, obj: MediaItem):
|
|
request = self.context.get("request")
|
|
user = getattr(request, "user", None)
|
|
if not user or not user.is_authenticated:
|
|
return None
|
|
if obj.item_type == MediaItem.TYPE_SHOW:
|
|
return self._summary_for_show(obj, user)
|
|
progress = WatchProgress.objects.filter(user=user, media_item=obj).first()
|
|
return self._summary_for_progress(progress, obj)
|
|
|
|
|
|
class MediaItemDetailSerializer(MediaItemSerializer):
|
|
files = MediaFileSerializer(many=True, read_only=True)
|
|
|
|
class Meta(MediaItemSerializer.Meta):
|
|
fields = MediaItemSerializer.Meta.fields + ["files", "metadata"]
|
|
|
|
|
|
class LibraryScanSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = LibraryScan
|
|
fields = [
|
|
"id",
|
|
"library",
|
|
"scan_type",
|
|
"status",
|
|
"summary",
|
|
"stages",
|
|
"processed_files",
|
|
"total_files",
|
|
"new_files",
|
|
"updated_files",
|
|
"removed_files",
|
|
"unmatched_files",
|
|
"log",
|
|
"extra",
|
|
"task_id",
|
|
"created_at",
|
|
"started_at",
|
|
"finished_at",
|
|
]
|
|
|
|
|
|
class MediaItemUpdateSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = MediaItem
|
|
fields = [
|
|
"title",
|
|
"synopsis",
|
|
"release_year",
|
|
"rating",
|
|
"genres",
|
|
"tags",
|
|
"studios",
|
|
"movie_db_id",
|
|
"imdb_id",
|
|
"poster_url",
|
|
"backdrop_url",
|
|
]
|