diff --git a/cds/config.py b/cds/config.py index 66b337d84..31e83afab 100644 --- a/cds/config.py +++ b/cds/config.py @@ -1127,7 +1127,7 @@ def _parse_env_bool(var_name, default=None): "'unsafe-inline'", ], "img-src": ["'self'", "https://*.theoplayer.com", "data:"], - "connect-src": ["'self'", "https://*.theoplayer.com", "https://*.cern.ch"], + "connect-src": ["'self'", "https://*.theoplayer.com", "https://*.cern.ch", "blob:"], "object-src": ["'self'"], "media-src": ["'self'", "blob:"], "frame-src": ["'self'", "https://*.theoplayer.com"], diff --git a/cds/modules/previewer/extensions/video.py b/cds/modules/previewer/extensions/video.py index 52ef2c31f..3f01305fb 100644 --- a/cds/modules/previewer/extensions/video.py +++ b/cds/modules/previewer/extensions/video.py @@ -25,6 +25,7 @@ """Previews video files.""" +from cds.modules.records.utils import parse_video_chapters from flask import render_template @@ -63,6 +64,12 @@ def preview(self, file, embed_config=None): if "report_number" in record and len(record["report_number"]) else "" ) + + description = record.get('description', '') + if description: + record['chapters'] = parse_video_chapters(description) + else: + record['chapters'] = [] return render_template( self.template, diff --git a/cds/modules/previewer/templates/cds_previewer/macros/player.html b/cds/modules/previewer/templates/cds_previewer/macros/player.html index b5b3a3902..8144c430f 100644 --- a/cds/modules/previewer/templates/cds_previewer/macros/player.html +++ b/cds/modules/previewer/templates/cds_previewer/macros/player.html @@ -66,77 +66,27 @@ } return null; } - function cleanHtmlFromTitle(title) { - if (!title) return title; - - // Decode HTML entities by using a temporary DOM element - const temp = document.createElement("textarea"); - temp.innerHTML = title; - let decoded = temp.value; - decoded = decoded.replace(/<[^>]+>/g, " "); - decoded = decoded.replace(/\s+/g, " ").trim(); - - return decoded; - } - function parseChapters(description) { - if (!description) return []; - - const pattern = /(?:^|\n)\s*(\d{1,2}:(?:\d{1,2}:)?\d{1,2})\s*[-\s]*(.+?)(?=\n|$)/gm; - const chapters = []; - let match; - - while ((match = pattern.exec(description)) !== null) { - const [, timestampStr, title] = match; - - const timeParts = timestampStr.split(":"); - let totalSeconds; - - if (timeParts.length === 2) { - const [minutes, seconds] = timeParts.map(Number); - totalSeconds = minutes * 60 + seconds; - } else if (timeParts.length === 3) { - const [hours, minutes, seconds] = timeParts.map(Number); - totalSeconds = hours * 3600 + minutes * 60 + seconds; - } else { - continue; - } - - const cleanTitle = cleanHtmlFromTitle(title); - if (cleanTitle) { - chapters.push({ - startTime: totalSeconds, - text: cleanTitle - }); - } - } - - chapters.sort((a, b) => a.startTime - b.startTime); - - // Add endTime for VTT - const videoDuration = durationToSeconds({{ (record.duration if record and record.duration else "") | tojson }}); - for (let i = 0; i < chapters.length; i++) { - if (i + 1 < chapters.length) { - chapters[i].endTime = chapters[i + 1].startTime; - } else { - chapters[i].endTime = videoDuration || (chapters[i].startTime + 1); - } - } - - return chapters; - } function formatVttTime(total) { const h = Math.floor(total / 3600); const m = Math.floor((total % 3600) / 60); const s = Math.floor(total % 60); return String(h).padStart(2,'0') + ':' + String(m).padStart(2,'0') + ':' + String(s).padStart(2,'0') + '.000'; } - function buildChaptersVtt(description) { - const chapters = parseChapters(description); - if (!chapters.length) return null; + function buildChaptersVttFromArray(chaptersArr, videoDurationStr) { + if (!chaptersArr || !chaptersArr.length) return null; + + chaptersArr.sort((a,b) => a.seconds - b.seconds); + const videoDuration = durationToSeconds(videoDurationStr); + if (videoDuration !== null && videoDuration <= 0) return null; let vtt = 'WEBVTT\n\n'; - for (let i = 0; i < chapters.length; i++) { - const c = chapters[i]; - vtt += `${i+1}\n${formatVttTime(c.startTime)} --> ${formatVttTime(c.endTime)}\n${c.text}\n\n`; + for (let i = 0; i < chaptersArr.length; i++) { + const c = chaptersArr[i]; + const start = c.seconds; + const end = (i + 1 < chaptersArr.length) + ? chaptersArr[i+1].seconds + : (videoDuration || start + 1); + + vtt += `${i+1}\n${formatVttTime(start)} --> ${formatVttTime(end)}\n${c.title}\n\n`; } return vtt; } @@ -144,8 +94,9 @@ // Build textTracks var textTracksArr = []; // Chapters - var descStr = {{ (record.description if record and record.description else "") | tojson }}; - var vttStr = buildChaptersVtt(descStr); + var chaptersArr = {{ (record.chapters if record and record.chapters else []) | tojson }}; + var videoDurationStr = {{ (record.duration if record and record.duration else "") | tojson }}; + var vttStr = buildChaptersVttFromArray(chaptersArr, videoDurationStr); if (vttStr) { var chapUrl = URL.createObjectURL(new Blob([vttStr], { type: 'text/vtt' })); textTracksArr.push({ kind: 'chapters', src: chapUrl, label: 'Chapters' }); diff --git a/cds/modules/records/serializers/json.py b/cds/modules/records/serializers/json.py index 9524d15f9..0a05d85de 100644 --- a/cds/modules/records/serializers/json.py +++ b/cds/modules/records/serializers/json.py @@ -31,7 +31,7 @@ has_read_record_eos_path_permission, has_read_record_permission, ) -from ..utils import HTMLTagRemover, remove_html_tags +from ..utils import HTMLTagRemover, parse_video_chapters, remove_html_tags class CDSJSONSerializer(JSONSerializer): @@ -81,6 +81,12 @@ def preprocess_record(self, pid, record, links_factory=None): except KeyError: # ignore error if keys are missing in the metadata pass + + description = metadata.get('description', '') + if description: + metadata['chapters'] = parse_video_chapters(description) + else: + metadata['chapters'] = [] return result diff --git a/cds/modules/records/serializers/schemas/video.py b/cds/modules/records/serializers/schemas/video.py index 13b34ac89..66e9f763f 100644 --- a/cds/modules/records/serializers/schemas/video.py +++ b/cds/modules/records/serializers/schemas/video.py @@ -167,7 +167,6 @@ class VideoSchema(StrictKeysSchema): ) collections = fields.List(fields.Str, many=True) additional_languages = fields.List(fields.Str, many=True) - chapters = fields.List(fields.Dict, dump_only=True) # Preservation fields location = fields.Str() @@ -180,13 +179,3 @@ def post_load(self, data, **kwargs): data["$schema"] = current_jsonschemas.path_to_url(Video._schema) return data - @post_dump(pass_many=False) - def post_dump(self, data, **kwargs): - """Post dump - add parsed chapters.""" - description = data.get('description', '') - if description: - data['chapters'] = parse_video_chapters(description) - else: - data['chapters'] = [] - - return data diff --git a/cds/modules/records/static/templates/cds_records/video/detail.html b/cds/modules/records/static/templates/cds_records/video/detail.html index f28126fec..76b7ddf9d 100644 --- a/cds/modules/records/static/templates/cds_records/video/detail.html +++ b/cds/modules/records/static/templates/cds_records/video/detail.html @@ -4,7 +4,7 @@