Skip to content

Commit 73d607d

Browse files
zzacharoclaude
andcommitted
claude: add video chapters, filters, and UI improvements
Major enhancements to video platform functionality: - Add chapters functionality via deposit description with frame extraction - Implement video list filters and related_identifiers field - Remove toggleable layout and keep new filtered list design - Fix delete video timing issues for better UX This squashed commit combines multiple feature additions while dropping the iOS fullscreen fix that wasn't working properly. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 3345587 commit 73d607d

File tree

17 files changed

+1848
-250
lines changed

17 files changed

+1848
-250
lines changed

cds/modules/deposit/api.py

Lines changed: 94 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@
7676
)
7777
from ..records.minters import cds_doi_generator, is_local_doi, report_number_minter
7878
from ..records.resolver import record_resolver
79-
from ..records.utils import is_record, lowercase_value
79+
from ..records.utils import is_record, lowercase_value, parse_video_chapters
8080
from ..records.validators import PartialDraft4Validator
8181
from ..records.permissions import is_public
8282
from .errors import DiscardConflict
@@ -504,7 +504,7 @@ def create(cls, data, id_=None, **kwargs):
504504
data.setdefault("_access", {})
505505
access_update = data["_access"].setdefault("update", [])
506506
try:
507-
if current_user.email not in access_update:
507+
if current_user.email not in access_update:
508508
# Add the current user to the ``_access.update`` list
509509
access_update.append(current_user.email)
510510
except AttributeError:
@@ -905,11 +905,95 @@ def _publish_edited(self):
905905

906906
return super(Video, self)._publish_edited()
907907

908+
def _has_chapters_changed(self, old_record=None):
909+
"""Check if chapters in description have changed."""
910+
current_description = self.get("description", "")
911+
current_chapters = parse_video_chapters(current_description)
912+
913+
if old_record is None:
914+
# First publish - trigger if chapters exist
915+
return len(current_chapters) > 0
916+
917+
old_description = old_record.get("description", "")
918+
old_chapters = parse_video_chapters(old_description)
919+
920+
# Compare chapter timestamps and titles
921+
if len(current_chapters) != len(old_chapters):
922+
return True
923+
924+
for curr, old in zip(current_chapters, old_chapters):
925+
if curr["seconds"] != old["seconds"] or curr["title"] != old["title"]:
926+
return True
927+
928+
return False
929+
930+
def _trigger_chapter_frame_extraction(self):
931+
"""Trigger chapter frame extraction asynchronously for existing video files."""
932+
try:
933+
from ..flows.tasks import ExtractChapterFramesTask
934+
from ..flows.models import FlowMetadata
935+
936+
# Find the master video file
937+
master_file = CDSVideosFilesIterator.get_master_video_file(self)
938+
939+
if master_file is None:
940+
current_app.logger.warning(
941+
f"No master video file found for video {self.id}"
942+
)
943+
return
944+
945+
# Get the current flow for this deposit
946+
current_flow = FlowMetadata.get_by_deposit(self["_deposit"]["id"])
947+
if current_flow is None:
948+
current_app.logger.warning(
949+
f"No current flow found for video {self.id}. Cannot trigger chapter frame extraction."
950+
)
951+
return
952+
953+
current_app.logger.info(
954+
f"Triggering asynchronous ExtractChapterFramesTask for video {self.id} with flow {current_flow.id}"
955+
)
956+
957+
# Prepare the payload for the async task with correct parameter names
958+
payload = {
959+
"deposit_id": str(self["_deposit"]["id"]),
960+
"version_id": master_file["version_id"], # Keep as UUID, don't convert to string
961+
"flow_id": str(current_flow.id),
962+
"key": master_file["key"],
963+
}
964+
965+
current_app.logger.info(f"Submitting ExtractChapterFramesTask with payload: {payload}")
966+
967+
# Submit the chapter frame extraction task asynchronously
968+
ExtractChapterFramesTask.create_flow_tasks(payload)
969+
task_result = ExtractChapterFramesTask().s(**payload).apply_async()
970+
971+
current_app.logger.info(
972+
f"ExtractChapterFramesTask submitted asynchronously for video {self.id}, flow_id: {current_flow.id}, task_id: {task_result.id}"
973+
)
974+
975+
except Exception as e:
976+
current_app.logger.error(
977+
f"Failed to trigger async chapter frame extraction for video {self.id}: {e}"
978+
)
979+
import traceback
980+
981+
current_app.logger.error(f"Traceback: {traceback.format_exc()}")
982+
908983
@mark_as_action
909984
def publish(self, pid=None, id_=None, **kwargs):
910985
"""Publish a video and update the related project."""
911986
# save a copy of the old PID
912987
video_old_id = self["_deposit"]["id"]
988+
989+
# Check if this is a republish and get the old record
990+
old_record = None
991+
try:
992+
_, old_record = self.fetch_published()
993+
except:
994+
# First publish
995+
pass
996+
913997
try:
914998
self["category"] = self.project["category"]
915999
self["type"] = self.project["type"]
@@ -926,6 +1010,13 @@ def publish(self, pid=None, id_=None, **kwargs):
9261010
# generate extra tags for files
9271011
self._create_tags()
9281012

1013+
# Check if chapters have changed and trigger frame extraction if needed
1014+
if self._has_chapters_changed(old_record):
1015+
current_app.logger.info(
1016+
f"Chapters changed for video {self.id}, triggering frame extraction"
1017+
)
1018+
self._trigger_chapter_frame_extraction()
1019+
9291020
# publish the video
9301021
video_published = super(Video, self).publish(pid=pid, id_=id_, **kwargs)
9311022
_, record_new = self.fetch_published()
@@ -1088,7 +1179,6 @@ def _create_tags(self):
10881179
except IndexError:
10891180
return
10901181

1091-
10921182
def mint_doi(self):
10931183
"""Mint DOI."""
10941184
assert self.has_record()
@@ -1109,7 +1199,7 @@ def mint_doi(self):
11091199
status=PIDStatus.RESERVED,
11101200
)
11111201
return self
1112-
1202+
11131203

11141204
project_resolver = Resolver(
11151205
pid_type="depid",

cds/modules/deposit/receivers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from cds.modules.flows.tasks import (
3434
DownloadTask,
3535
ExtractFramesTask,
36+
ExtractChapterFramesTask,
3637
ExtractMetadataTask,
3738
TranscodeVideoTask,
3839
)
@@ -87,4 +88,5 @@ def register_celery_class_based_tasks(sender, app=None):
8788
celery.register_task(ExtractMetadataTask())
8889
celery.register_task(DownloadTask())
8990
celery.register_task(ExtractFramesTask())
91+
celery.register_task(ExtractChapterFramesTask())
9092
celery.register_task(TranscodeVideoTask())

cds/modules/deposit/static/json/cds_deposit/forms/video.json

Lines changed: 116 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -567,17 +567,127 @@
567567
],
568568
"related_links": [
569569
{
570-
"key": "related_links",
570+
"key": "related_identifiers",
571571
"type": "array",
572-
"add": "Add related links",
572+
"add": "Add related identifiers",
573+
"title": "Related Identifiers",
574+
"description": "Add identifiers for related resources such as DOIs, URLs, or Indico event IDs.",
573575
"items": [
574576
{
575-
"title": "Name",
576-
"key": "related_links[].name"
577+
"title": "Identifier",
578+
"key": "related_identifiers[].identifier",
579+
"required": true,
580+
"placeholder": "e.g., 10.1234/example.doi, https://example.com, 12345",
581+
"description": "The identifier value (DOI, URL, or Indico event ID)"
577582
},
578583
{
579-
"title": "URL",
580-
"key": "related_links[].url"
584+
"title": "Scheme",
585+
"key": "related_identifiers[].scheme",
586+
"type": "select",
587+
"required": true,
588+
"placeholder": "Select identifier scheme",
589+
"description": "The type of identifier scheme",
590+
"titleMap": [
591+
{
592+
"value": "URL",
593+
"name": "URL (Uniform Resource Locator)"
594+
},
595+
{
596+
"value": "DOI",
597+
"name": "DOI (Digital Object Identifier)"
598+
},
599+
{
600+
"value": "Indico",
601+
"name": "Indico (Event ID)"
602+
}
603+
]
604+
},
605+
{
606+
"title": "Relation Type",
607+
"key": "related_identifiers[].relation_type",
608+
"type": "select",
609+
"required": true,
610+
"placeholder": "Select relation type",
611+
"description": "How this resource relates to the identified resource",
612+
"titleMap": [
613+
{
614+
"value": "IsPartOf",
615+
"name": "Is part of"
616+
},
617+
{
618+
"value": "IsVariantFormOf",
619+
"name": "Is variant form of"
620+
}
621+
]
622+
},
623+
{
624+
"title": "Resource Type",
625+
"key": "related_identifiers[].resource_type",
626+
"type": "select",
627+
"placeholder": "Select resource type (optional)",
628+
"description": "The type of the related resource (optional)",
629+
"titleMap": [
630+
{
631+
"value": "Audiovisual",
632+
"name": "Audiovisual"
633+
},
634+
{
635+
"value": "Collection",
636+
"name": "Collection"
637+
},
638+
{
639+
"value": "DataPaper",
640+
"name": "Data Paper"
641+
},
642+
{
643+
"value": "Dataset",
644+
"name": "Dataset"
645+
},
646+
{
647+
"value": "Event",
648+
"name": "Event"
649+
},
650+
{
651+
"value": "Image",
652+
"name": "Image"
653+
},
654+
{
655+
"value": "InteractiveResource",
656+
"name": "Interactive Resource"
657+
},
658+
{
659+
"value": "Model",
660+
"name": "Model"
661+
},
662+
{
663+
"value": "PhysicalObject",
664+
"name": "Physical Object"
665+
},
666+
{
667+
"value": "Service",
668+
"name": "Service"
669+
},
670+
{
671+
"value": "Software",
672+
"name": "Software"
673+
},
674+
{
675+
"value": "Sound",
676+
"name": "Sound"
677+
},
678+
{
679+
"value": "Text",
680+
"name": "Text"
681+
},
682+
{
683+
"value": "Workflow",
684+
"name": "Workflow"
685+
},
686+
{
687+
"value": "Other",
688+
"name": "Other"
689+
}
690+
]
581691
}
582692
]
583693
}

cds/modules/deposit/static/templates/cds_deposit/types/video/form.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ <h4 class="mb-5 mt-0">
206206
</li>
207207
<li role="presentation" ng-class="{active: active=='related_links'}">
208208
<a ng-click="active='related_links'" role="tab" data-toggle="tab">
209-
Related links
209+
Related information
210210
</a>
211211
</li>
212212
<li role="presentation" ng-class="{active: active=='admin'}" ng-show="$ctrl.cdsDepositCtrl.cdsDepositsCtrl.isSuperAdmin">

0 commit comments

Comments
 (0)