1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.editor;
23
24 import static java.util.Collections.emptyList;
25 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
26 import static org.opencastproject.util.data.Tuple.tuple;
27
28 import org.opencastproject.assetmanager.api.AssetManager;
29 import org.opencastproject.assetmanager.api.AssetManagerException;
30 import org.opencastproject.assetmanager.util.WorkflowPropertiesUtil;
31 import org.opencastproject.assetmanager.util.Workflows;
32 import org.opencastproject.editor.api.EditingData;
33 import org.opencastproject.editor.api.EditorService;
34 import org.opencastproject.editor.api.EditorServiceException;
35 import org.opencastproject.editor.api.ErrorStatus;
36 import org.opencastproject.editor.api.LockData;
37 import org.opencastproject.editor.api.SegmentData;
38 import org.opencastproject.editor.api.TrackData;
39 import org.opencastproject.editor.api.TrackSubData;
40 import org.opencastproject.editor.api.WorkflowData;
41 import org.opencastproject.elasticsearch.api.SearchIndexException;
42 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
43 import org.opencastproject.elasticsearch.index.objects.event.Event;
44 import org.opencastproject.index.service.api.IndexService;
45 import org.opencastproject.index.service.exception.IndexServiceException;
46 import org.opencastproject.index.service.impl.util.EventUtils;
47 import org.opencastproject.mediapackage.Attachment;
48 import org.opencastproject.mediapackage.Catalog;
49 import org.opencastproject.mediapackage.MediaPackage;
50 import org.opencastproject.mediapackage.MediaPackageElement;
51 import org.opencastproject.mediapackage.MediaPackageElementBuilder;
52 import org.opencastproject.mediapackage.MediaPackageElementBuilderFactory;
53 import org.opencastproject.mediapackage.MediaPackageElementFlavor;
54 import org.opencastproject.mediapackage.Publication;
55 import org.opencastproject.mediapackage.Track;
56 import org.opencastproject.mediapackage.attachment.AttachmentImpl;
57 import org.opencastproject.metadata.dublincore.DublinCoreMetadataCollection;
58 import org.opencastproject.metadata.dublincore.EventCatalogUIAdapter;
59 import org.opencastproject.metadata.dublincore.MetadataJson;
60 import org.opencastproject.metadata.dublincore.MetadataList;
61 import org.opencastproject.security.api.AuthorizationService;
62 import org.opencastproject.security.api.Organization;
63 import org.opencastproject.security.api.SecurityConstants;
64 import org.opencastproject.security.api.SecurityService;
65 import org.opencastproject.security.api.UnauthorizedException;
66 import org.opencastproject.security.api.User;
67 import org.opencastproject.security.urlsigning.exception.UrlSigningException;
68 import org.opencastproject.security.urlsigning.service.UrlSigningService;
69 import org.opencastproject.security.urlsigning.utils.UrlSigningServiceOsgiUtil;
70 import org.opencastproject.smil.api.SmilException;
71 import org.opencastproject.smil.api.SmilResponse;
72 import org.opencastproject.smil.api.SmilService;
73 import org.opencastproject.smil.entity.api.Smil;
74 import org.opencastproject.smil.entity.media.api.SmilMediaObject;
75 import org.opencastproject.smil.entity.media.container.api.SmilMediaContainer;
76 import org.opencastproject.smil.entity.media.element.api.SmilMediaElement;
77 import org.opencastproject.util.MimeType;
78 import org.opencastproject.util.MimeTypes;
79 import org.opencastproject.util.NotFoundException;
80 import org.opencastproject.util.data.Tuple;
81 import org.opencastproject.workflow.api.ConfiguredWorkflow;
82 import org.opencastproject.workflow.api.WorkflowDatabaseException;
83 import org.opencastproject.workflow.api.WorkflowDefinition;
84 import org.opencastproject.workflow.api.WorkflowInstance;
85 import org.opencastproject.workflow.api.WorkflowService;
86 import org.opencastproject.workflow.api.WorkflowUtil;
87 import org.opencastproject.workflow.handler.distribution.InternalPublicationChannel;
88 import org.opencastproject.workspace.api.Workspace;
89
90 import com.entwinemedia.fn.data.Opt;
91
92 import org.apache.commons.io.FileUtils;
93 import org.apache.commons.io.IOUtils;
94 import org.apache.commons.lang3.BooleanUtils;
95 import org.apache.commons.lang3.StringUtils;
96 import org.osgi.service.component.ComponentContext;
97 import org.osgi.service.component.annotations.Activate;
98 import org.osgi.service.component.annotations.Component;
99 import org.osgi.service.component.annotations.Modified;
100 import org.osgi.service.component.annotations.Reference;
101 import org.slf4j.Logger;
102 import org.slf4j.LoggerFactory;
103 import org.xml.sax.SAXException;
104
105 import java.awt.datatransfer.MimeTypeParseException;
106 import java.io.ByteArrayInputStream;
107 import java.io.File;
108 import java.io.IOException;
109 import java.io.InputStream;
110 import java.net.URI;
111 import java.net.URISyntaxException;
112 import java.nio.charset.StandardCharsets;
113 import java.text.MessageFormat;
114 import java.util.ArrayList;
115 import java.util.Arrays;
116 import java.util.Base64;
117 import java.util.Collection;
118 import java.util.Collections;
119 import java.util.Comparator;
120 import java.util.Dictionary;
121 import java.util.HashMap;
122 import java.util.HashSet;
123 import java.util.Iterator;
124 import java.util.List;
125 import java.util.Map;
126 import java.util.Objects;
127 import java.util.Optional;
128 import java.util.Set;
129 import java.util.UUID;
130 import java.util.stream.Collectors;
131
132 import javax.ws.rs.WebApplicationException;
133 import javax.xml.bind.JAXBException;
134
135
136 @Component(
137 property = {
138 "service.description=Editor Service"
139 },
140 immediate = true,
141 service = EditorService.class
142 )
143 public class EditorServiceImpl implements EditorService {
144
145
146 private static final Logger logger = LoggerFactory.getLogger(EditorServiceImpl.class);
147
148
149 private static final String EDITOR_WORKFLOW_TAG = "editor";
150
151 private static EditorLock editorLock;
152
153 private long expireSeconds = UrlSigningServiceOsgiUtil.DEFAULT_URL_SIGNING_EXPIRE_DURATION;
154
155 private Boolean signWithClientIP = UrlSigningServiceOsgiUtil.DEFAULT_SIGN_WITH_CLIENT_IP;
156
157
158 private IndexService index;
159 private AssetManager assetManager;
160 private SecurityService securityService;
161 private SmilService smilService;
162 private UrlSigningService urlSigningService;
163 private WorkflowService workflowService;
164 private Workspace workspace;
165 private AuthorizationService authorizationService;
166
167
168 private MediaPackageElementFlavor smilCatalogFlavor;
169 private String previewVideoSubtype;
170 private String previewTag;
171 private String previewSubtype;
172 private String waveformSubtype;
173 private String thumbnailSubType;
174 private MediaPackageElementFlavor smilSilenceFlavor;
175 private ElasticsearchIndex searchIndex;
176 private MediaPackageElementFlavor captionsFlavor;
177 private String thumbnailWfProperty;
178 private List<MediaPackageElementFlavor> thumbnailSourcePrimary;
179 private String distributionDirectory;
180 private Boolean localPublication = null;
181
182 private static final String DEFAULT_PREVIEW_SUBTYPE = "source";
183 private static final String DEFAULT_PREVIEW_TAG = "editor";
184 private static final String DEFAULT_WAVEFORM_SUBTYPE = "waveform";
185 private static final String DEFAULT_SMIL_CATALOG_FLAVOR = "smil/cutting";
186 private static final String DEFAULT_SMIL_CATALOG_TAGS = "archive";
187 private static final String DEFAULT_SMIL_SILENCE_FLAVOR = "*/silence";
188 private static final String DEFAULT_PREVIEW_VIDEO_SUBTYPE = "video+preview";
189 private static final String DEFAULT_CAPTIONS_FLAVOR = "captions/*";
190 private static final String DEFAULT_THUMBNAIL_SUBTYPE = "player+preview";
191 private static final String DEFAULT_THUMBNAIL_WF_PROPERTY = "thumbnail_edited";
192 private static final List<MediaPackageElementFlavor> DEFAULT_THUMBNAIL_PRIORITY_FLAVOR = new ArrayList<>();
193 private static final int DEFAULT_LOCK_TIMEOUT_SECONDS = 300;
194 private static final int DEFAULT_LOCK_REFRESH_SECONDS = 60;
195
196 public static final String OPT_PREVIEW_SUBTYPE = "preview.subtype";
197 public static final String OPT_PREVIEW_TAG = "preview.tag";
198 public static final String OPT_WAVEFORM_SUBTYPE = "waveform.subtype";
199 public static final String OPT_SMIL_CATALOG_FLAVOR = "smil.catalog.flavor";
200 public static final String OPT_SMIL_CATALOG_TAGS = "smil.catalog.tags";
201 public static final String OPT_SMIL_SILENCE_FLAVOR = "smil.silence.flavor";
202 public static final String OPT_PREVIEW_VIDEO_SUBTYPE = "preview.video.subtype";
203 public static final String OPT_CAPTIONS_FLAVOR = "captions.flavor";
204 public static final String OPT_THUMBNAILSUBTYPE = "thumbnail.subtype";
205 public static final String OPT_THUMBNAIL_WF_PROPERTY = "thumbnail.workflow.property";
206 public static final String OPT_THUMBNAIL_PRIORITY_FLAVOR = "thumbnail.priority.flavor";
207 public static final String OPT_LOCAL_PUBLICATION = "publication.local";
208 public static final String OPT_LOCK_ENABLED = "lock.enable";
209 public static final String OPT_LOCK_TIMEOUT = "lock.release.after.seconds";
210 public static final String OPT_LOCK_REFRESH = "lock.refresh.after.seconds";
211
212 private Boolean lockingActive;
213 private int lockRefresh = DEFAULT_LOCK_REFRESH_SECONDS;
214 private int lockTimeout = DEFAULT_LOCK_TIMEOUT_SECONDS;
215
216 private final Set<String> smilCatalogTagSet = new HashSet<>();
217
218 @Reference
219 void setSecurityService(SecurityService securityService) {
220 this.securityService = securityService;
221 }
222
223 @Reference
224 void setSmilService(SmilService smilService) {
225 this.smilService = smilService;
226 }
227
228 @Reference
229 void setWorkflowService(WorkflowService workflowService) {
230 this.workflowService = workflowService;
231 }
232
233 @Reference
234 void setWorkspace(Workspace workspace) {
235 this.workspace = workspace;
236 }
237
238 @Reference
239 void setUrlSigningService(UrlSigningService urlSigningService) {
240 this.urlSigningService = urlSigningService;
241 }
242
243 @Reference
244 void setAssetManager(AssetManager assetManager) {
245 this.assetManager = assetManager;
246 }
247
248 @Reference
249 public void setElasticsearchIndex(ElasticsearchIndex elasticsearchIndex) {
250 this.searchIndex = elasticsearchIndex;
251 }
252
253 @Reference
254 public void setIndexService(IndexService index) {
255 this.index = index;
256 }
257
258 @Reference
259 public void setAuthorizationService(AuthorizationService authorizationService) {
260 this.authorizationService = authorizationService;
261 }
262
263 public MediaPackageElementFlavor getSmilCatalogFlavor() {
264 return smilCatalogFlavor;
265 }
266
267 public Set<String> getSmilCatalogTags() {
268 return smilCatalogTagSet;
269 }
270
271 public String getPreviewVideoSubtype() {
272 return previewVideoSubtype;
273 }
274
275 public MediaPackageElementFlavor getSmilSilenceFlavor() {
276 return smilSilenceFlavor;
277 }
278
279 private String getPreviewSubtype() {
280 return previewSubtype;
281 }
282
283 public String getPreviewTag() {
284 return previewTag;
285 }
286
287 private String getWaveformSubtype() {
288 return waveformSubtype;
289 }
290
291 private String getThumbnailSubtype() {
292 return thumbnailSubType;
293 }
294
295 @Activate
296 @Modified
297 public void activate(ComponentContext cc) {
298 Dictionary<String, Object> properties = cc.getProperties();
299 if (properties == null) {
300 return;
301 }
302
303 expireSeconds = UrlSigningServiceOsgiUtil.getUpdatedSigningExpiration(properties, this.getClass().getSimpleName());
304 signWithClientIP = UrlSigningServiceOsgiUtil.getUpdatedSignWithClientIP(properties,this.getClass().getSimpleName());
305
306 previewTag = Objects.toString(properties.get(OPT_PREVIEW_TAG), DEFAULT_PREVIEW_TAG);
307 logger.debug("Preview tag configuration set to '{}'", previewTag);
308
309
310 previewSubtype = Objects.toString(properties.get(OPT_PREVIEW_SUBTYPE), DEFAULT_PREVIEW_SUBTYPE);
311 logger.debug("Preview subtype configuration set to '{}'", previewSubtype);
312
313
314 waveformSubtype = Objects.toString(properties.get(OPT_WAVEFORM_SUBTYPE), DEFAULT_WAVEFORM_SUBTYPE);
315 logger.debug("Waveform subtype configuration set to '{}'", waveformSubtype);
316
317
318 smilCatalogFlavor = MediaPackageElementFlavor.parseFlavor(
319 StringUtils.defaultString((String) properties.get(OPT_SMIL_CATALOG_FLAVOR), DEFAULT_SMIL_CATALOG_FLAVOR));
320 logger.debug("Smil catalog flavor configuration set to '{}'", smilCatalogFlavor);
321
322
323 String tags = Objects.toString(properties.get(OPT_SMIL_CATALOG_TAGS), DEFAULT_SMIL_CATALOG_TAGS);
324 String[] smilCatalogTags = StringUtils.split(tags, ",");
325 smilCatalogTagSet.clear();
326 if (smilCatalogTags != null) {
327 smilCatalogTagSet.addAll(Arrays.asList(smilCatalogTags));
328 }
329
330
331 smilSilenceFlavor = MediaPackageElementFlavor.parseFlavor(
332 StringUtils.defaultString((String) properties.get(OPT_SMIL_SILENCE_FLAVOR), DEFAULT_SMIL_SILENCE_FLAVOR));
333 logger.debug("Smil silence flavor configuration set to '{}'", smilSilenceFlavor);
334
335
336 previewVideoSubtype = Objects.toString(properties.get(OPT_PREVIEW_VIDEO_SUBTYPE), DEFAULT_PREVIEW_VIDEO_SUBTYPE);
337
338 logger.debug("Preview video subtype set to '{}'", previewVideoSubtype);
339
340
341 captionsFlavor = MediaPackageElementFlavor.parseFlavor(
342 StringUtils.defaultString((String) properties.get(OPT_CAPTIONS_FLAVOR), DEFAULT_CAPTIONS_FLAVOR));
343 logger.debug("Caption flavor set to '{}'", captionsFlavor);
344
345 thumbnailSubType = Objects.toString(properties.get(OPT_THUMBNAILSUBTYPE), DEFAULT_THUMBNAIL_SUBTYPE);
346 logger.debug("Thumbnail subtype set to '{}'", thumbnailSubType);
347
348 thumbnailWfProperty = Objects.toString(properties.get(OPT_THUMBNAIL_WF_PROPERTY), DEFAULT_THUMBNAIL_WF_PROPERTY);
349 logger.debug("Thumbnail workflow property set to '{}'", thumbnailWfProperty);
350
351 String thumbnailPriorities = Objects.toString(properties.get(OPT_THUMBNAIL_PRIORITY_FLAVOR));
352 if ("null".equals(thumbnailPriorities) || thumbnailPriorities.isEmpty()) {
353 thumbnailSourcePrimary = DEFAULT_THUMBNAIL_PRIORITY_FLAVOR;
354 } else {
355 thumbnailSourcePrimary = Arrays.stream(thumbnailPriorities.split(",", -1))
356 .map(MediaPackageElementFlavor::parseFlavor)
357 .collect(Collectors.toList());
358 }
359
360 String localPublicationConfig = Objects.toString(properties.get(OPT_LOCAL_PUBLICATION), "auto");
361 if (!"auto".equals(localPublicationConfig)) {
362
363 localPublication = BooleanUtils.toBoolean(localPublicationConfig);
364 }
365
366 distributionDirectory = cc.getBundleContext().getProperty("org.opencastproject.download.directory");
367 if (StringUtils.isEmpty(distributionDirectory)) {
368 final String storageDir = cc.getBundleContext().getProperty("org.opencastproject.storage.dir");
369 if (StringUtils.isNotEmpty(storageDir)) {
370 distributionDirectory = new File(storageDir, "downloads").getPath();
371 }
372 }
373 logger.debug("Thumbnail track priority set to '{}'", thumbnailSourcePrimary);
374
375 lockingActive = Boolean.parseBoolean(StringUtils.trimToEmpty((String) properties.get(OPT_LOCK_ENABLED)));
376
377 try {
378 lockTimeout = Integer.parseUnsignedInt(
379 Objects.toString(properties.get(OPT_LOCK_TIMEOUT)));
380 } catch (NumberFormatException e) {
381 logger.info("Configuration {} contains invalid value, defaulting to {}", OPT_LOCK_TIMEOUT, lockTimeout);
382 }
383
384 try {
385 lockRefresh = Integer.parseUnsignedInt(
386 Objects.toString(properties.get(OPT_LOCK_REFRESH)));
387 } catch (NumberFormatException e) {
388 logger.info("Configuration {} contains invalid value, defaulting to {}", OPT_LOCK_REFRESH, lockRefresh);
389 }
390
391 editorLock = new EditorLock(lockTimeout);
392
393 }
394
395
396
397
398
399
400
401
402
403 private boolean isLocal(URI uri) {
404 var path = uri.normalize().getPath();
405 if (!path.startsWith("/static/")) {
406 return false;
407 }
408 var localFile = new File(distributionDirectory, path.substring("/static".length()));
409 return localFile.exists();
410 }
411
412 private Boolean elementHasPreviewTag(MediaPackageElement element) {
413 return element.getTags() != null
414 && Arrays.asList(element.getTags()).contains(getPreviewTag());
415 }
416
417 private Boolean elementHasPreviewFlavor(MediaPackageElement element) {
418 return element.getFlavor() != null
419 && getPreviewSubtype().equals(element.getFlavor().getSubtype());
420 }
421
422 private Boolean elementHasWaveformFlavor(MediaPackageElement element) {
423 return element.getFlavor() != null
424 && getWaveformSubtype().equals(element.getFlavor().getSubtype());
425 }
426
427 private String signIfNecessary(final URI uri) {
428 if (!urlSigningService.accepts(uri.toString())) {
429 return uri.toString();
430 }
431 String clientIP = signWithClientIP ? securityService.getUserIP() : null;
432 try {
433 return new URI(urlSigningService.sign(uri.toString(), expireSeconds, null, clientIP)).toString();
434 } catch (URISyntaxException | UrlSigningException e) {
435 throw new WebApplicationException(e, SC_INTERNAL_SERVER_ERROR);
436 }
437 }
438
439
440
441
442
443
444
445
446
447
448
449
450 Smil createSmilCuttingCatalog(final EditingData editingInfo, final MediaPackage mediaPackage) throws SmilException {
451
452 SmilResponse smilResponse = smilService.createNewSmil(mediaPackage);
453
454
455 ArrayList<Track> tracks = new ArrayList<>();
456
457 for (final TrackData trackdata : editingInfo.getTracks()) {
458 String trackId = trackdata.getId();
459 Track track = mediaPackage.getTrack(trackId);
460 if (track == null) {
461 track = Arrays.stream(getInternalPublication(mediaPackage)
462 .orElseThrow(() -> new IllegalStateException("Event has no internal publication"))
463 .getTracks())
464 .filter(t -> trackId.equals(t.getIdentifier()))
465 .findFirst()
466 .orElseThrow(() -> new IllegalStateException(
467 String.format("The track '%s' doesn't exist in media package '%s'", trackId, mediaPackage)));
468 }
469 tracks.add(track);
470 }
471
472 for (SegmentData segment : editingInfo.getSegments()) {
473 smilResponse = smilService.addParallel(smilResponse.getSmil());
474 final String parentId = smilResponse.getEntity().getId();
475
476 final long duration = segment.getEnd() - segment.getStart();
477 if (!segment.isDeleted()) {
478 smilResponse = smilService.addClips(smilResponse.getSmil(), parentId, tracks.toArray(new Track[0]),
479 segment.getStart(), duration);
480 }
481 }
482
483 return smilResponse.getSmil();
484 }
485
486
487
488
489
490
491
492
493
494
495
496
497 MediaPackage addSmilToArchive(MediaPackage mediaPackage, final Smil smil) throws IOException {
498 MediaPackageElementFlavor mediaPackageElementFlavor = getSmilCatalogFlavor();
499
500 String catalogId = smil.getId();
501 Catalog[] catalogs = mediaPackage.getCatalogs();
502
503
504 for (Catalog p: catalogs) {
505 if (p.getFlavor().matches(mediaPackageElementFlavor)) {
506 logger.debug("Set Identifier for Smil-Catalog to: {}", p.getIdentifier());
507 catalogId = p.getIdentifier();
508 break;
509 }
510 }
511 Catalog catalog = mediaPackage.getCatalog(catalogId);
512
513 URI smilURI;
514 try (InputStream is = IOUtils.toInputStream(smil.toXML(), "UTF-8")) {
515 smilURI = workspace.put(mediaPackage.getIdentifier().toString(), catalogId, EditorService.TARGET_FILE_NAME, is);
516 } catch (SAXException | JAXBException e) {
517 throw new IOException("Error while serializing the SMIL catalog to XML" ,e);
518 }
519
520 if (catalog == null) {
521 MediaPackageElementBuilder mpeBuilder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();
522 catalog = (Catalog) mpeBuilder.elementFromURI(smilURI, MediaPackageElement.Type.Catalog, getSmilCatalogFlavor());
523 mediaPackage.add(catalog);
524 }
525 catalog.setURI(smilURI);
526 catalog.setIdentifier(catalogId);
527 catalog.setMimeType(MimeTypes.XML);
528 for (String tag : getSmilCatalogTags()) {
529 catalog.addTag(tag);
530 }
531
532 catalog.setChecksum(null);
533 return mediaPackage;
534 }
535
536
537
538
539
540
541
542
543
544
545
546 private MediaPackage processSubtitleTrack(MediaPackage mediaPackage, List<EditingData.Subtitle> subtitles)
547 throws IOException, IllegalArgumentException {
548 for (EditingData.Subtitle subtitle : subtitles) {
549
550 String subtitleId = UUID.randomUUID().toString();
551 String trackId = null;
552
553
554 for (Track t : mediaPackage.getTracks()) {
555 if (t.getIdentifier().matches(subtitle.getId())) {
556 logger.debug("Set Identifier for Subtitle-Track to: {}", t.getIdentifier());
557 subtitleId = t.getIdentifier();
558 trackId = t.getIdentifier();
559 break;
560 }
561 }
562
563 Track track = mediaPackage.getTrack(trackId);
564
565 if (subtitle.isDeleted()) {
566
567 if (trackId != null) {
568 mediaPackage.remove(track);
569 }
570 continue;
571 }
572
573
574 URI oldTrackURI = null;
575 if (track != null) {
576 oldTrackURI = track.getURI();
577 }
578
579
580 try (InputStream is = IOUtils.toInputStream(subtitle.getSubtitle(), "UTF-8")) {
581 URI subtitleUri = workspace.put(mediaPackage.getIdentifier().toString(), subtitleId, "subtitle.vtt", is);
582
583
584 if (track == null) {
585 MediaPackageElementBuilder mpeBuilder = MediaPackageElementBuilderFactory.newInstance().newElementBuilder();
586
587 track = (Track) mpeBuilder.elementFromURI(subtitleUri, MediaPackageElement.Type.Track,
588 new MediaPackageElementFlavor(captionsFlavor.getType(),"source"));
589 mediaPackage.add(track);
590 logger.info("Creating new subtitle track " + track.getIdentifier() + " with tags "
591 + track.getTags().toString());
592 }
593
594 track.setURI(subtitleUri);
595 track.setIdentifier(subtitleId);
596 track.setChecksum(null);
597 for (String tag : subtitle.getTags()) {
598 track.addTag(tag);
599 }
600
601 if (oldTrackURI != null && oldTrackURI != subtitleUri) {
602
603 logger.info("Removing old track file {}", oldTrackURI);
604 try {
605 workspace.delete(oldTrackURI);
606 } catch (NotFoundException | IOException e) {
607 logger.info("Could not remove track from workspace. Could be it was never there.");
608 }
609 }
610 }
611 }
612
613 return mediaPackage;
614 }
615
616
617
618
619
620
621
622
623
624
625
626 private MediaPackage addThumbnailsToArchive(EditingData editingData, MediaPackage mediaPackage)
627 throws MimeTypeParseException, IOException {
628 for (TrackData track : editingData.getTracks()) {
629 String id = track.getId();
630 MediaPackageElementFlavor flavor = new MediaPackageElementFlavor(track.getFlavor().getType(),
631 getThumbnailSubtype());
632 String uri = track.getThumbnailURI();
633
634
635 if (uri == null || uri.isEmpty()) {
636 continue;
637 }
638
639 if (!uri.startsWith("data")) {
640 continue;
641 }
642
643
644 uri = uri.substring(uri.indexOf(",") + 1);
645 byte[] byteArray = Base64.getMimeDecoder().decode(uri);
646 InputStream inputStream = new ByteArrayInputStream(byteArray);
647
648
649 String stringMimeType = detectMimeType(uri);
650 MimeType mimeType = MimeType.mimeType(stringMimeType.split("/")[0], stringMimeType.split("/")[1]);
651
652
653 final String filename = "thumbnail_" + id + "." + mimeType.getSubtype();
654 final String originalThumbnailId = UUID.randomUUID().toString();
655 URI tempThumbnail = null;
656 try {
657 tempThumbnail = workspace
658 .put(mediaPackage.getIdentifier().toString(), originalThumbnailId, filename, inputStream);
659 } catch (IOException e) {
660 throw new IOException("Could not add thumbnail to workspace", e);
661 }
662
663
664 final Attachment attachment = AttachmentImpl.fromURI(tempThumbnail);
665 attachment.setFlavor(flavor);
666 attachment.setMimeType(mimeType);
667 Arrays.stream(mediaPackage.getElementsByFlavor(flavor))
668 .map(MediaPackageElement::getTags)
669 .flatMap(Arrays::stream)
670 .distinct()
671 .forEach(attachment::addTag);
672
673
674 Arrays.stream(mediaPackage.getElementsByFlavor(flavor)).forEach(mediaPackage::remove);
675
676
677 mediaPackage.add(attachment);
678
679
680
681
682 WorkflowPropertiesUtil
683 .storeProperty(assetManager, mediaPackage,
684 flavor.getType() + "/" + thumbnailWfProperty, "true");
685 }
686
687 return mediaPackage;
688 }
689
690
691
692
693
694
695
696
697
698
699 private String detectMimeType(String b64) throws MimeTypeParseException {
700 var signatures = new HashMap<String, String>();
701 signatures.put("R0lGODdh", "image/gif");
702 signatures.put("iVBORw0KGgo", "image/png");
703 signatures.put("/9j/", "image/jpg");
704
705 for (var s : signatures.entrySet()) {
706 if (b64.indexOf(s.getKey()) == 0) {
707 return s.getValue();
708 }
709 }
710 throw new MimeTypeParseException("No image mimetype found");
711 }
712
713 private Optional<Publication> getInternalPublication(MediaPackage mp) {
714 return Arrays.stream(mp.getPublications())
715 .filter(publication -> InternalPublicationChannel.CHANNEL_ID.equals(publication.getChannel()))
716 .findFirst();
717 }
718
719
720
721
722
723
724
725
726 private Event getEvent(final String mediaPackageId) throws EditorServiceException {
727 try {
728 Opt<Event> optEvent = index.getEvent(mediaPackageId, searchIndex);
729 if (optEvent.isNone()) {
730 errorExit("Event not found", mediaPackageId,
731 ErrorStatus.MEDIAPACKAGE_NOT_FOUND);
732 } else {
733 return optEvent.get();
734 }
735 } catch (SearchIndexException e) {
736 errorExit("Error while reading event from search index:", mediaPackageId,
737 ErrorStatus.MEDIAPACKAGE_NOT_FOUND, e);
738 }
739 return null;
740 }
741
742
743
744
745
746
747
748 private List<WorkflowDefinition> getEditingWorkflows() {
749 try {
750 return workflowService.listAvailableWorkflowDefinitions().stream()
751 .filter(workflow -> workflow.containsTag(EDITOR_WORKFLOW_TAG))
752 .collect(Collectors.toList());
753 } catch (WorkflowDatabaseException e) {
754 logger.warn("Error while retrieving list of workflow definitions:", e);
755 }
756 return emptyList();
757 }
758
759
760
761
762
763
764
765
766 private List<SegmentData> getSegments(final MediaPackage mediaPackage) {
767 List<SegmentData> segments = new ArrayList<>();
768 for (Catalog smilCatalog : mediaPackage.getCatalogs(getSmilCatalogFlavor())) {
769 try {
770 Smil smil = smilService.fromXml(workspace.get(smilCatalog.getURI())).getSmil();
771 segments = mergeSegments(segments, getSegmentsFromSmil(smil));
772
773 } catch (NotFoundException e) {
774 logger.warn("File '{}' could not be loaded by workspace service:", smilCatalog.getURI(), e);
775 } catch (IOException e) {
776 logger.warn("Reading file '{}' from workspace service failed:", smilCatalog.getURI(), e);
777 } catch (SmilException e) {
778 logger.warn("Error while parsing SMIL catalog '{}':", smilCatalog.getURI(), e);
779 }
780 }
781
782 if (!segments.isEmpty()) {
783 return segments;
784 }
785
786
787 for (Catalog smilCatalog : mediaPackage.getCatalogs(getSmilSilenceFlavor())) {
788 try {
789 Smil smil = smilService.fromXml(workspace.get(smilCatalog.getURI())).getSmil();
790 segments = getSegmentsFromSmil(smil);
791 } catch (NotFoundException e) {
792 logger.warn("File '{}' could not be loaded by workspace service:", smilCatalog.getURI(), e);
793 } catch (IOException e) {
794 logger.warn("Reading file '{}' from workspace service failed:", smilCatalog.getURI(), e);
795 } catch (SmilException e) {
796 logger.warn("Error while parsing SMIL catalog '{}':", smilCatalog.getURI(), e);
797 }
798 }
799
800
801 if (segments.size() == 1) {
802 SegmentData singleSegment = segments.get(0);
803 if (singleSegment.getStart() == 0 && singleSegment.getEnd() >= mediaPackage.getDuration()) {
804 segments.remove(0);
805 }
806 }
807
808 return segments;
809 }
810
811 protected List<SegmentData> getDeletedSegments(MediaPackage mediaPackage, List<SegmentData> segments) {
812
813 long lastTime = 0;
814 List<SegmentData> deletedElements = new ArrayList<>();
815 for (int i = 0; i < segments.size(); i++) {
816 SegmentData segmentData = segments.get(i);
817 if (segmentData.getStart() != lastTime) {
818 SegmentData deleted = new SegmentData(lastTime, segmentData.getStart(), true);
819 deletedElements.add(deleted);
820 }
821 lastTime = segmentData.getEnd();
822
823 if (segments.size() - 1 == i) {
824 if (mediaPackage.getDuration() != null && lastTime < mediaPackage.getDuration()) {
825 deletedElements.add(new SegmentData(lastTime, mediaPackage.getDuration(), true));
826 }
827 }
828 }
829 return deletedElements;
830 }
831
832 protected List<SegmentData> mergeSegments(List<SegmentData> segments, List<SegmentData> segments2) {
833
834 List<SegmentData> mergedSegments = mergeInternal(segments, segments2);
835
836
837 sortSegments(mergedSegments);
838
839 return mergedSegments;
840 }
841
842 private void sortSegments(List<SegmentData> mergedSegments) {
843 mergedSegments.sort(Comparator.comparing(SegmentData::getStart));
844 }
845
846
847
848
849
850
851
852
853
854
855
856 private List<SegmentData> mergeInternal(List<SegmentData> segments, List<SegmentData> segments2) {
857 for (Iterator<SegmentData> it = segments.iterator(); it.hasNext();) {
858 SegmentData seg = it.next();
859 for (Iterator<SegmentData> it2 = segments2.iterator(); it2.hasNext();) {
860 SegmentData seg2 = it2.next();
861 long combinedStart = Math.max(seg.getStart(), seg2.getStart());
862 long combinedEnd = Math.min(seg.getEnd(), seg2.getEnd());
863 if (combinedEnd > combinedStart) {
864 it.remove();
865 it2.remove();
866 List<SegmentData> newSegments = new ArrayList<>(segments);
867 newSegments.add(new SegmentData(combinedStart, combinedEnd));
868 return mergeInternal(newSegments, segments2);
869 }
870 }
871 }
872 segments.addAll(segments2);
873 return segments;
874 }
875
876
877
878
879
880
881
882
883 List<SegmentData> getSegmentsFromSmil(Smil smil) {
884 List<SegmentData> segments = new ArrayList<>();
885 for (SmilMediaObject elem : smil.getBody().getMediaElements()) {
886 if (elem instanceof SmilMediaContainer) {
887 SmilMediaContainer mediaContainer = (SmilMediaContainer) elem;
888
889 SegmentData tuple = null;
890 for (SmilMediaObject video : mediaContainer.getElements()) {
891 if (video instanceof SmilMediaElement) {
892 SmilMediaElement videoElem = (SmilMediaElement) video;
893 try {
894
895 if (tuple == null || (videoElem.getClipEndMS()
896 - videoElem.getClipBeginMS()) > tuple.getEnd() - tuple.getStart()) {
897 tuple = new SegmentData(videoElem.getClipBeginMS(), videoElem.getClipEndMS());
898 }
899 } catch (SmilException e) {
900 logger.warn("Media element '{}' of SMIL catalog '{}' seems to be invalid",
901 videoElem, smil, e);
902 }
903 }
904 }
905 if (tuple != null) {
906 segments.add(tuple);
907 }
908 }
909 }
910 return segments;
911 }
912
913 @Override
914 public void lockMediaPackage(final String mediaPackageId, LockData lockRequest) throws EditorServiceException {
915
916 getEvent(mediaPackageId);
917
918
919 editorLock.lock(mediaPackageId, lockRequest);
920 }
921
922 @Override
923 public void unlockMediaPackage(final String mediaPackageId, LockData lockRequest) throws EditorServiceException {
924
925 getEvent(mediaPackageId);
926
927
928 editorLock.unlock(mediaPackageId, lockRequest);
929 }
930
931 @Override
932 public EditingData getEditData(final String mediaPackageId) throws EditorServiceException, UnauthorizedException {
933
934 Event event = getEvent(mediaPackageId);
935 MediaPackage mp = getMediaPackage(event);
936
937 if (!isAdmin() && !authorizationService.hasPermission(mp, "write")) {
938 throw new UnauthorizedException("User has no write access to this event");
939 }
940
941 boolean workflowActive = WorkflowUtil.isActive(event.getWorkflowState());
942
943 final Optional<Publication> internalPubOpt = getInternalPublication(mp);
944 if (internalPubOpt.isEmpty()) {
945 errorExit("No internal publication", mediaPackageId, ErrorStatus.NO_INTERNAL_PUBLICATION);
946 }
947 Publication internalPub = internalPubOpt.get();
948
949
950 List<SegmentData> segments = getSegments(mp);
951 segments.addAll(getDeletedSegments(mp, segments));
952 sortSegments(segments);
953
954
955
956 List<WorkflowData> workflows = new ArrayList<>();
957 for (WorkflowDefinition workflow : getEditingWorkflows()) {
958 workflows.add(new WorkflowData(workflow.getId(), workflow.getTitle(), workflow.getDisplayOrder(),
959 workflow.getDescription()));
960 }
961
962 final Map<String, String> latestWfProperties = WorkflowPropertiesUtil
963 .getLatestWorkflowProperties(assetManager, mediaPackageId);
964
965
966 final Collection<Tuple<String, String>> hiddens = latestWfProperties.entrySet()
967 .stream()
968 .map(property -> tuple(property.getKey().split("_"), property.getValue()))
969 .filter(property -> property.getA().length == 3)
970 .filter(property -> property.getA()[0].equals("hide"))
971 .filter(property -> property.getB().equals("true"))
972 .map(property -> tuple(property.getA()[1], property.getA()[2]))
973 .collect(Collectors.toSet());
974
975 List<Track> trackList = Arrays.stream(internalPub.getTracks()).filter(this::elementHasPreviewTag)
976 .collect(Collectors.toList());
977 if (trackList.isEmpty()) {
978 trackList = Arrays.stream(internalPub.getTracks()).filter(this::elementHasPreviewFlavor)
979 .collect(Collectors.toList());
980 if (trackList.isEmpty()) {
981 trackList = Arrays.asList(internalPub.getTracks());
982 }
983 }
984
985
986 Track[] subtitleTracks = mp.getTracks(captionsFlavor);
987 List<EditingData.Subtitle> subtitles = new ArrayList<>();
988 for (Track t: subtitleTracks) {
989 try {
990 File subtitleFile = workspace.get(t.getURI());
991 String subtitleString = FileUtils.readFileToString(subtitleFile, StandardCharsets.UTF_8);
992 subtitles.add(new EditingData.Subtitle(t.getIdentifier(), subtitleString, t.getTags()));
993 } catch (NotFoundException | IOException e) {
994 errorExit("Could not read subtitle from file", mediaPackageId, ErrorStatus.UNKNOWN);
995 }
996 }
997
998
999
1000 final List<TrackData> tracks = trackList.stream().map(track -> {
1001 final String uri = signIfNecessary(track.getURI());
1002 final boolean audioEnabled = !hiddens.contains(tuple(track.getFlavor().getType(), "audio"));
1003 final TrackSubData audio = new TrackSubData(track.hasAudio(), null,
1004 audioEnabled);
1005 final boolean videoEnable = !hiddens.contains(tuple(track.getFlavor().getType(), "video"));
1006 final String videoPreview = Arrays.stream(internalPub.getAttachments())
1007 .filter(attachment -> attachment.getFlavor().getType().equals(track.getFlavor().getType()))
1008 .filter(attachment -> attachment.getFlavor().getSubtype().equals(getPreviewVideoSubtype()))
1009 .map(MediaPackageElement::getURI).map(this::signIfNecessary)
1010 .findAny()
1011 .orElse(null);
1012 final TrackSubData video = new TrackSubData(track.hasVideo(), videoPreview,
1013 videoEnable);
1014
1015
1016
1017
1018 String thumbnailURI = Arrays.stream(mp.getAttachments())
1019 .filter(attachment -> attachment.getFlavor().getType().equals(track.getFlavor().getType()))
1020 .filter(attachment -> attachment.getFlavor().getSubtype().equals(getThumbnailSubtype()))
1021 .map(MediaPackageElement::getURI).map(this::signIfNecessary)
1022 .findAny()
1023 .orElse(null);
1024
1025
1026
1027 if (thumbnailURI == null) {
1028 thumbnailURI = Arrays.stream(internalPub.getAttachments())
1029 .filter(attachment -> attachment.getFlavor().getType().equals(track.getFlavor().getType()))
1030 .filter(attachment -> attachment.getFlavor().getSubtype().equals(getThumbnailSubtype()))
1031 .map(MediaPackageElement::getURI).map(this::signIfNecessary)
1032 .findAny()
1033 .orElse(null);
1034 }
1035
1036 final int priority = thumbnailSourcePrimary.indexOf(track.getFlavor());
1037
1038 if (localPublication == null) {
1039 localPublication = isLocal(track.getURI());
1040 }
1041
1042 return new TrackData(track.getFlavor().getType(), track.getFlavor().getSubtype(), audio, video, uri,
1043 track.getIdentifier(), thumbnailURI, priority);
1044 }).collect(Collectors.toList());
1045
1046 List<String> waveformList = Arrays.stream(internalPub.getAttachments())
1047 .filter(this::elementHasWaveformFlavor)
1048 .map(Attachment::getURI).map(this::signIfNecessary)
1049 .collect(Collectors.toList());
1050
1051 User user = securityService.getUser();
1052
1053 return new EditingData(segments, tracks, workflows, mp.getDuration(), mp.getTitle(), event.getRecordingStartDate(),
1054 event.getSeriesId(), event.getSeriesName(), workflowActive, waveformList, subtitles, localPublication,
1055 lockingActive, lockRefresh, user, "");
1056 }
1057
1058
1059 private boolean isAdmin() {
1060 final User currentUser = securityService.getUser();
1061
1062
1063 if (currentUser.hasRole(SecurityConstants.GLOBAL_ADMIN_ROLE)) {
1064 return true;
1065 }
1066
1067
1068 final Organization currentOrg = securityService.getOrganization();
1069 return currentUser.getOrganization().getId().equals(currentOrg.getId())
1070 && currentUser.hasRole(currentOrg.getAdminRole());
1071 }
1072
1073 private MediaPackage getMediaPackage(Event event) throws EditorServiceException {
1074 if (event == null) {
1075 errorExit("No Event provided", "", ErrorStatus.UNKNOWN);
1076 return null;
1077 }
1078 try {
1079 return index.getEventMediapackage(event);
1080 } catch (IndexServiceException e) {
1081 errorExit("Not Found", event.getIdentifier(), ErrorStatus.MEDIAPACKAGE_NOT_FOUND);
1082 return null;
1083 }
1084 }
1085
1086 private void errorExit(final String message, final String mediaPackageId, ErrorStatus status)
1087 throws EditorServiceException {
1088 errorExit(message, mediaPackageId, status, null);
1089 }
1090
1091 private void errorExit(final String message, final String mediaPackageId, ErrorStatus status, Exception e)
1092 throws EditorServiceException {
1093 String errorMessage = MessageFormat.format("{0}. Event ID: {1}", message, mediaPackageId);
1094 throw new EditorServiceException(errorMessage, status, e);
1095 }
1096
1097 @Override
1098 public void setEditData(String mediaPackageId, EditingData editingData) throws EditorServiceException,
1099 IOException {
1100 final Event event = getEvent(mediaPackageId);
1101
1102 if (WorkflowUtil.isActive(event.getWorkflowState())) {
1103 errorExit("Workflow is running", mediaPackageId, ErrorStatus.WORKFLOW_ACTIVE);
1104 }
1105
1106 MediaPackage mediaPackage = getMediaPackage(event);
1107 Smil smil = null;
1108 try {
1109 smil = createSmilCuttingCatalog(editingData, mediaPackage);
1110 } catch (Exception e) {
1111 errorExit("Unable to create SMIL cutting catalog", mediaPackageId, ErrorStatus.UNABLE_TO_CREATE_CATALOG, e);
1112 }
1113
1114 final Map<String, String> workflowProperties = new HashMap<String, String>();
1115 for (TrackData track : editingData.getTracks()) {
1116 MediaPackageElementFlavor flavor = track.getFlavor();
1117 String type = null;
1118 if (flavor != null) {
1119 type = flavor.getType();
1120 } else {
1121 Track mpTrack = mediaPackage.getTrack(track.getId());
1122 if (mpTrack != null) {
1123 type = mpTrack.getFlavor().getType();
1124 } else {
1125 errorExit("Unable to determine track type", mediaPackageId, ErrorStatus.UNKNOWN);
1126 }
1127 }
1128 workflowProperties.put("hide_" + type + "_audio", Boolean.toString(!track.getAudio().isEnabled()));
1129 workflowProperties.put("hide_" + type + "_video", Boolean.toString(!track.getVideo().isEnabled()));
1130 }
1131 WorkflowPropertiesUtil.storeProperties(assetManager, mediaPackage, workflowProperties);
1132
1133 try {
1134 mediaPackage = addSmilToArchive(mediaPackage, smil);
1135 } catch (IOException e) {
1136 errorExit("Unable to add SMIL cutting catalog to archive", mediaPackageId, ErrorStatus.UNKNOWN, e);
1137 }
1138
1139 try {
1140 if (editingData.getSubtitles() != null) {
1141 mediaPackage = processSubtitleTrack(mediaPackage, editingData.getSubtitles());
1142 }
1143 } catch (IOException e) {
1144 errorExit("Unable to add subtitle track to archive", mediaPackageId, ErrorStatus.UNKNOWN, e);
1145 } catch (IllegalArgumentException e) {
1146 errorExit("Illegal subtitle given", mediaPackageId, ErrorStatus.UNKNOWN, e);
1147 }
1148
1149 try {
1150 mediaPackage = addThumbnailsToArchive(editingData, mediaPackage);
1151 } catch (MimeTypeParseException e) {
1152 errorExit("Thumbnail had an illegal MimeType", mediaPackageId, ErrorStatus.UNKNOWN, e);
1153 } catch (IOException e) {
1154 errorExit("Unable to add thumbnail to archive", mediaPackageId, ErrorStatus.UNKNOWN, e);
1155 }
1156
1157 try {
1158 assetManager.takeSnapshot(mediaPackage);
1159 } catch (AssetManagerException e) {
1160 logger.error("Error while adding the updated media package ({}) to the archive",
1161 mediaPackage.getIdentifier(), e);
1162 throw new IOException(e);
1163 }
1164
1165
1166 try {
1167 index.updateAllEventMetadata(mediaPackageId, editingData.getMetadataJSON(), searchIndex);
1168 } catch (SearchIndexException | IndexServiceException | IllegalArgumentException e) {
1169 errorExit("Event metadata can't be updated.", mediaPackageId, ErrorStatus.METADATA_UPDATE_FAIL, e);
1170 } catch (NotFoundException e) {
1171 errorExit("Event not found.", mediaPackageId, ErrorStatus.MEDIAPACKAGE_NOT_FOUND, e);
1172 } catch (UnauthorizedException e) {
1173 errorExit("Not authorized to update event metadata .", mediaPackageId, ErrorStatus.NOT_AUTHORIZED, e);
1174 }
1175
1176 if (editingData.getPostProcessingWorkflow() != null) {
1177 final String workflowId = editingData.getPostProcessingWorkflow();
1178 try {
1179 final Map<String, String> workflowParameters = WorkflowPropertiesUtil
1180 .getLatestWorkflowProperties(assetManager, mediaPackage.getIdentifier().toString());
1181 final Workflows workflows = new Workflows(assetManager, workflowService);
1182 workflows.applyWorkflowToLatestVersion(Collections.singletonList(mediaPackage.getIdentifier().toString()),
1183 ConfiguredWorkflow.workflow(workflowService.getWorkflowDefinitionById(workflowId), workflowParameters))
1184 .run();
1185 } catch (AssetManagerException e) {
1186 errorExit("Unable to start workflow" + workflowId, mediaPackageId, ErrorStatus.WORKFLOW_ERROR, e);
1187 } catch (WorkflowDatabaseException e) {
1188 errorExit("Unable to load workflow" + workflowId, mediaPackageId, ErrorStatus.WORKFLOW_ERROR, e);
1189 } catch (NotFoundException e) {
1190 errorExit("Unable to load workflow" + workflowId, mediaPackageId, ErrorStatus.WORKFLOW_NOT_FOUND, e);
1191 }
1192 }
1193 }
1194
1195 @Override
1196 public String getMetadata(String mediaPackageId) throws EditorServiceException {
1197 final Event event = getEvent(mediaPackageId);
1198 MediaPackage mediaPackage = getMediaPackage(event);
1199 MetadataList metadataList = new MetadataList();
1200 List<EventCatalogUIAdapter> catalogUIAdapters = index.getEventCatalogUIAdapters();
1201 catalogUIAdapters.remove(index.getCommonEventCatalogUIAdapter());
1202 for (EventCatalogUIAdapter catalogUIAdapter : catalogUIAdapters) {
1203 metadataList.add(catalogUIAdapter, catalogUIAdapter.getFields(mediaPackage));
1204 }
1205
1206 DublinCoreMetadataCollection metadataCollection = null;
1207 try {
1208 metadataCollection = EventUtils.getEventMetadata(event,
1209 index.getCommonEventCatalogUIAdapter());
1210 } catch (Exception e) {
1211 errorExit("Unable to retrieve event metadata", mediaPackageId, ErrorStatus.UNKNOWN);
1212 }
1213 metadataList.add(index.getCommonEventCatalogUIAdapter(), metadataCollection);
1214
1215 final String wfState = event.getWorkflowState();
1216 if (wfState != null && WorkflowUtil.isActive(WorkflowInstance.WorkflowState.valueOf(wfState))) {
1217 metadataList.setLocked(MetadataList.Locked.WORKFLOW_RUNNING);
1218 }
1219
1220 return MetadataJson.listToJson(metadataList, true).toString();
1221 }
1222 }