Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,6 @@ tmp

# local python version/venv
.python-version

# AI artifact files
CLAUDE.md
3 changes: 1 addition & 2 deletions cds/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1297,7 +1297,6 @@ def _parse_env_bool(var_name, default=None):
# Licence key and base URL for THEO player
THEOPLAYER_LIBRARY_LOCATION = None
THEOPLAYER_LICENSE = None

# Wowza server URL for m3u8 playlist generation
WOWZA_PLAYLIST_URL = (
"https://wowza.cern.ch/cds/_definist_/smil:" "{filepath}/playlist.m3u8"
Expand Down Expand Up @@ -1569,7 +1568,7 @@ def _parse_env_bool(var_name, default=None):

# The number of max videos per project. It blocks the upload of new videos in a
# project only client side
DEPOSIT_PROJECT_MAX_N_VIDEOS = 10
DEPOSIT_PROJECT_MAX_N_VIDEOS = 20

###############################################################################
# Keywords
Expand Down
98 changes: 94 additions & 4 deletions cds/modules/deposit/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
)
from ..records.minters import cds_doi_generator, is_local_doi, report_number_minter
from ..records.resolver import record_resolver
from ..records.utils import is_record, lowercase_value
from ..records.utils import is_record, lowercase_value, parse_video_chapters
from ..records.validators import PartialDraft4Validator
from ..records.permissions import is_public
from .errors import DiscardConflict
Expand Down Expand Up @@ -504,7 +504,7 @@ def create(cls, data, id_=None, **kwargs):
data.setdefault("_access", {})
access_update = data["_access"].setdefault("update", [])
try:
if current_user.email not in access_update:
if current_user.email not in access_update:
# Add the current user to the ``_access.update`` list
access_update.append(current_user.email)
except AttributeError:
Expand Down Expand Up @@ -905,11 +905,95 @@ def _publish_edited(self):

return super(Video, self)._publish_edited()

def _has_chapters_changed(self, old_record=None):
"""Check if chapters in description have changed."""
current_description = self.get("description", "")
current_chapters = parse_video_chapters(current_description)

if old_record is None:
# First publish - trigger if chapters exist
return len(current_chapters) > 0

old_description = old_record.get("description", "")
old_chapters = parse_video_chapters(old_description)

# Compare chapter timestamps and titles
if len(current_chapters) != len(old_chapters):
return True

for curr, old in zip(current_chapters, old_chapters):
if curr["seconds"] != old["seconds"] or curr["title"] != old["title"]:
return True

return False

def _trigger_chapter_frame_extraction(self):
"""Trigger chapter frame extraction asynchronously for existing video files."""
try:
from ..flows.tasks import ExtractChapterFramesTask
from ..flows.models import FlowMetadata

# Find the master video file
master_file = CDSVideosFilesIterator.get_master_video_file(self)

if master_file is None:
current_app.logger.warning(
f"No master video file found for video {self.id}"
)
return

# Get the current flow for this deposit
current_flow = FlowMetadata.get_by_deposit(self["_deposit"]["id"])
if current_flow is None:
current_app.logger.warning(
f"No current flow found for video {self.id}. Cannot trigger chapter frame extraction."
)
return

current_app.logger.info(
f"Triggering asynchronous ExtractChapterFramesTask for video {self.id} with flow {current_flow.id}"
)

# Prepare the payload for the async task with correct parameter names
payload = {
"deposit_id": str(self["_deposit"]["id"]),
"version_id": master_file["version_id"], # Keep as UUID, don't convert to string
"flow_id": str(current_flow.id),
"key": master_file["key"],
}

current_app.logger.info(f"Submitting ExtractChapterFramesTask with payload: {payload}")

# Submit the chapter frame extraction task asynchronously
ExtractChapterFramesTask.create_flow_tasks(payload)
task_result = ExtractChapterFramesTask().s(**payload).apply_async()

current_app.logger.info(
f"ExtractChapterFramesTask submitted asynchronously for video {self.id}, flow_id: {current_flow.id}, task_id: {task_result.id}"
)

except Exception as e:
current_app.logger.error(
f"Failed to trigger async chapter frame extraction for video {self.id}: {e}"
)
import traceback

current_app.logger.error(f"Traceback: {traceback.format_exc()}")

@mark_as_action
def publish(self, pid=None, id_=None, **kwargs):
"""Publish a video and update the related project."""
# save a copy of the old PID
video_old_id = self["_deposit"]["id"]

# Check if this is a republish and get the old record
old_record = None
try:
_, old_record = self.fetch_published()
except:
# First publish
pass

try:
self["category"] = self.project["category"]
self["type"] = self.project["type"]
Expand All @@ -926,6 +1010,13 @@ def publish(self, pid=None, id_=None, **kwargs):
# generate extra tags for files
self._create_tags()

# Check if chapters have changed and trigger frame extraction if needed
if self._has_chapters_changed(old_record):
current_app.logger.info(
f"Chapters changed for video {self.id}, triggering frame extraction"
)
self._trigger_chapter_frame_extraction()

# publish the video
video_published = super(Video, self).publish(pid=pid, id_=id_, **kwargs)
_, record_new = self.fetch_published()
Expand Down Expand Up @@ -1088,7 +1179,6 @@ def _create_tags(self):
except IndexError:
return


def mint_doi(self):
"""Mint DOI."""
assert self.has_record()
Expand All @@ -1109,7 +1199,7 @@ def mint_doi(self):
status=PIDStatus.RESERVED,
)
return self


project_resolver = Resolver(
pid_type="depid",
Expand Down
2 changes: 2 additions & 0 deletions cds/modules/deposit/receivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from cds.modules.flows.tasks import (
DownloadTask,
ExtractFramesTask,
ExtractChapterFramesTask,
ExtractMetadataTask,
TranscodeVideoTask,
)
Expand Down Expand Up @@ -87,4 +88,5 @@ def register_celery_class_based_tasks(sender, app=None):
celery.register_task(ExtractMetadataTask())
celery.register_task(DownloadTask())
celery.register_task(ExtractFramesTask())
celery.register_task(ExtractChapterFramesTask())
celery.register_task(TranscodeVideoTask())
122 changes: 116 additions & 6 deletions cds/modules/deposit/static/json/cds_deposit/forms/video.json
Original file line number Diff line number Diff line change
Expand Up @@ -567,17 +567,127 @@
],
"related_links": [
{
"key": "related_links",
"key": "related_identifiers",
"type": "array",
"add": "Add related links",
"add": "Add related identifiers",
"title": "Related Identifiers",
"description": "Add identifiers for related resources such as DOIs, URLs, or Indico event IDs.",
"items": [
{
"title": "Name",
"key": "related_links[].name"
"title": "Identifier",
"key": "related_identifiers[].identifier",
"required": true,
"placeholder": "e.g., 10.1234/example.doi, https://example.com, 12345",
"description": "The identifier value (DOI, URL, or Indico event ID)"
},
{
"title": "URL",
"key": "related_links[].url"
"title": "Scheme",
"key": "related_identifiers[].scheme",
"type": "select",
"required": true,
"placeholder": "Select identifier scheme",
"description": "The type of identifier scheme",
"titleMap": [
{
"value": "URL",
"name": "URL (Uniform Resource Locator)"
},
{
"value": "DOI",
"name": "DOI (Digital Object Identifier)"
},
{
"value": "Indico",
"name": "Indico (Event ID)"
}
]
},
{
"title": "Relation Type",
"key": "related_identifiers[].relation_type",
"type": "select",
"required": true,
"placeholder": "Select relation type",
"description": "How this resource relates to the identified resource",
"titleMap": [
{
"value": "IsPartOf",
"name": "Is part of"
},
{
"value": "IsVariantFormOf",
"name": "Is variant form of"
}
]
},
{
"title": "Resource Type",
"key": "related_identifiers[].resource_type",
"type": "select",
"placeholder": "Select resource type (optional)",
"description": "The type of the related resource (optional)",
"titleMap": [
{
"value": "Audiovisual",
"name": "Audiovisual"
},
{
"value": "Collection",
"name": "Collection"
},
{
"value": "DataPaper",
"name": "Data Paper"
},
{
"value": "Dataset",
"name": "Dataset"
},
{
"value": "Event",
"name": "Event"
},
{
"value": "Image",
"name": "Image"
},
{
"value": "InteractiveResource",
"name": "Interactive Resource"
},
{
"value": "Model",
"name": "Model"
},
{
"value": "PhysicalObject",
"name": "Physical Object"
},
{
"value": "Service",
"name": "Service"
},
{
"value": "Software",
"name": "Software"
},
{
"value": "Sound",
"name": "Sound"
},
{
"value": "Text",
"name": "Text"
},
{
"value": "Workflow",
"name": "Workflow"
},
{
"value": "Other",
"name": "Other"
}
]
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
<li
class="{{ form.fieldHtmlClass }} list-group-item"
ng-class="{'deposit-inline': form.inline, 'bg-warn': form.firstItemMessage && $first}"
ng-repeat="item in modelArray track by $index">
ng-repeat="item in modelArray track by (item.id || item.uuid || $index)">
<div
ng-hide="(form.firstItemMessage && $first)"
class="close-container pull-right"
Expand All @@ -32,8 +32,8 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="pb-20" ng-if="form.firstItemMessage && $first">
<span class="label label-default">{{form.firstItemMessage}}</span>
<div class="pb-20" ng-if="::form.firstItemMessage && $first">
<span class="label label-default">{{::form.firstItemMessage}}</span>
</div>
<div class="clearfix"></div>
<sf-decorator
Expand All @@ -48,7 +48,7 @@
<li
class="{{ form.fieldHtmlClass }} list-group-item"
ng-class="{'deposit-inline': form.inline, 'bg-warn': form.firstItemMessage && $first}"
ng-repeat="item in modelArray track by $index">
ng-repeat="item in modelArray track by (item.id || item.uuid || $index)">
<div
ng-hide="(form.firstItemMessage && $first)"
class="close-container pull-right"
Expand All @@ -64,8 +64,8 @@
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="pb-20" ng-if="form.firstItemMessage && $first">
<span class="label label-default">{{form.firstItemMessage}}</span>
<div class="pb-20" ng-if="::form.firstItemMessage && $first">
<span class="label label-default">{{::form.firstItemMessage}}</span>
</div>
<div class="clearfix"></div>
<sf-decorator
Expand Down
Loading