From 586ffc0c3f869e154e793ec14cfdcd8430ffb71e Mon Sep 17 00:00:00 2001
From: uhensler <urs.hensler@frentix.com>
Date: Tue, 11 Aug 2020 08:34:19 +0200
Subject: [PATCH] OO-4821: BBB Opencast Recording Handler

---
 .../olat/modules/_spring/modulesContext.xml   |  22 ++-
 .../bigbluebutton/BigBlueButtonManager.java   |   3 +-
 .../BigBlueButtonRecordingsHandler.java       |   3 +-
 .../manager/BigBlueButtonManagerImpl.java     |   5 +-
 .../BigBlueButtonNativeRecordingsHandler.java |   3 +-
 ...igBlueButtonOpenCastRecordingsHandler.java |  70 ++++++-
 .../model/BigBlueButtonErrorCodes.java        |   1 +
 .../ui/BigBlueButtonMeetingController.java    |   6 +-
 .../opencast/OpencastBBBRecordingContext.java | 126 ++++++++++++
 .../olat/modules/opencast/OpencastEvent.java  |  40 ++++
 .../olat/modules/opencast/OpencastModule.java | 180 +++++++++++++++++
 .../modules/opencast/OpencastService.java     |  55 ++++++
 .../opencast/manager/OpencastServiceImpl.java |  75 ++++++++
 .../modules/opencast/manager/client/Api.java  |  52 +++++
 .../opencast/manager/client/Event.java        |  72 +++++++
 .../manager/client/GetEventsParams.java       |  67 +++++++
 .../manager/client/OpencastRestClient.java    | 142 ++++++++++++++
 .../opencast/model/OpencastEventImpl.java     |  75 ++++++++
 .../opencast/ui/OpencastAdminController.java  | 182 ++++++++++++++++++
 .../ui/_i18n/LocalStrings_de.properties       |  13 ++
 .../ui/_i18n/LocalStrings_en.properties       |  13 ++
 .../resources/serviceconfig/olat.properties   |  14 ++
 .../client/OpencastRestClientTest.java        |  78 ++++++++
 23 files changed, 1282 insertions(+), 15 deletions(-)
 create mode 100644 src/main/java/org/olat/modules/opencast/OpencastBBBRecordingContext.java
 create mode 100644 src/main/java/org/olat/modules/opencast/OpencastEvent.java
 create mode 100644 src/main/java/org/olat/modules/opencast/OpencastModule.java
 create mode 100644 src/main/java/org/olat/modules/opencast/OpencastService.java
 create mode 100644 src/main/java/org/olat/modules/opencast/manager/OpencastServiceImpl.java
 create mode 100644 src/main/java/org/olat/modules/opencast/manager/client/Api.java
 create mode 100644 src/main/java/org/olat/modules/opencast/manager/client/Event.java
 create mode 100644 src/main/java/org/olat/modules/opencast/manager/client/GetEventsParams.java
 create mode 100644 src/main/java/org/olat/modules/opencast/manager/client/OpencastRestClient.java
 create mode 100644 src/main/java/org/olat/modules/opencast/model/OpencastEventImpl.java
 create mode 100644 src/main/java/org/olat/modules/opencast/ui/OpencastAdminController.java
 create mode 100644 src/main/java/org/olat/modules/opencast/ui/_i18n/LocalStrings_de.properties
 create mode 100644 src/main/java/org/olat/modules/opencast/ui/_i18n/LocalStrings_en.properties
 create mode 100644 src/test/java/org/olat/modules/opencast/manager/client/OpencastRestClientTest.java

diff --git a/src/main/java/org/olat/modules/_spring/modulesContext.xml b/src/main/java/org/olat/modules/_spring/modulesContext.xml
index efabffe627b..51e34670314 100644
--- a/src/main/java/org/olat/modules/_spring/modulesContext.xml
+++ b/src/main/java/org/olat/modules/_spring/modulesContext.xml
@@ -103,7 +103,27 @@
 				<value>org.olat.admin.SystemAdminMainController</value>		
 			</list>
 		</property>
-	</bean>	
+	</bean>
+	
+	<!-- Opencast admin. panel -->
+	<bean class="org.olat.core.extensions.action.GenericActionExtension" init-method="initExtensionPoints">
+		<property name="order" value="8250" />
+		<property name="actionController">
+			<bean class="org.olat.core.gui.control.creator.AutoCreator" scope="prototype">
+				<property name="className" value="org.olat.modules.opencast.ui.OpencastAdminController"/>
+			</bean>
+		</property>
+		<property name="navigationKey" value="opencast" />
+		<property name="i18nActionKey" value="admin.menu.title"/>
+		<property name="i18nDescriptionKey" value="admin.menu.title.alt"/>
+		<property name="translationPackage" value="org.olat.modules.opencast.ui"/>
+		<property name="parentTreeNodeIdentifier" value="externalToolsParent" />
+		<property name="extensionPoints">
+			<list>
+				<value>org.olat.admin.SystemAdminMainController</value>
+			</list>
+		</property>
+	</bean>
 	
 	<!-- Goto admin. panel -->
 	<bean class="org.olat.core.extensions.action.GenericActionExtension" init-method="initExtensionPoints">
diff --git a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java
index d37129c0892..d78caba58de 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonManager.java
@@ -24,6 +24,7 @@ import java.util.List;
 
 import org.olat.core.id.Identity;
 import org.olat.core.id.Roles;
+import org.olat.core.util.UserSession;
 import org.olat.group.BusinessGroup;
 import org.olat.modules.bigbluebutton.manager.BigBlueButtonUriBuilder;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors;
@@ -155,7 +156,7 @@ public interface BigBlueButtonManager {
 	
 	public BigBlueButtonRecordingReference updateRecordingReference(BigBlueButtonRecordingReference reference);
 	
-	public String getRecordingUrl(BigBlueButtonRecording record);
+	public String getRecordingUrl(UserSession usess, BigBlueButtonRecording record);
 	
 	public void deleteRecording(BigBlueButtonRecording record, BigBlueButtonMeeting meeting, BigBlueButtonErrors errors);
 	
diff --git a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonRecordingsHandler.java b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonRecordingsHandler.java
index 52a224306b9..5483cd4b9ca 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonRecordingsHandler.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/BigBlueButtonRecordingsHandler.java
@@ -22,6 +22,7 @@ package org.olat.modules.bigbluebutton;
 import java.util.List;
 import java.util.Locale;
 
+import org.olat.core.util.UserSession;
 import org.olat.modules.bigbluebutton.manager.BigBlueButtonUriBuilder;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors;
 
@@ -41,7 +42,7 @@ public interface BigBlueButtonRecordingsHandler {
 	
 	public List<BigBlueButtonRecording> getRecordings(BigBlueButtonMeeting meeting, BigBlueButtonErrors errors);
 	
-	public String getRecordingURL(BigBlueButtonRecording recording);
+	public String getRecordingURL(UserSession usess, BigBlueButtonRecording recording);
 	
 	public void appendMetadata(BigBlueButtonUriBuilder uriBuilder, BigBlueButtonMeeting meeting);
 	
diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java
index ae5e6fe68fd..43cd249ba18 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonManagerImpl.java
@@ -53,6 +53,7 @@ import org.olat.core.id.context.BusinessControlFactory;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.CodeHelper;
 import org.olat.core.util.StringHelper;
+import org.olat.core.util.UserSession;
 import org.olat.core.util.WebappHelper;
 import org.olat.course.CourseFactory;
 import org.olat.course.ICourse;
@@ -921,8 +922,8 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager,
 	}
 	
 	@Override
-	public String getRecordingUrl(BigBlueButtonRecording recording) {
-		return getRecordingsHandler().getRecordingURL(recording);
+	public String getRecordingUrl(UserSession usess, BigBlueButtonRecording recording) {
+		return getRecordingsHandler().getRecordingURL(usess, recording);
 	}
 
 	@Override
diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonNativeRecordingsHandler.java b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonNativeRecordingsHandler.java
index c818c0dd9aa..34ffc1384b0 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonNativeRecordingsHandler.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonNativeRecordingsHandler.java
@@ -31,6 +31,7 @@ import org.olat.core.id.User;
 import org.olat.core.logging.Tracing;
 import org.olat.core.util.Formatter;
 import org.olat.core.util.StringHelper;
+import org.olat.core.util.UserSession;
 import org.olat.core.util.Util;
 import org.olat.group.BusinessGroup;
 import org.olat.modules.bigbluebutton.BigBlueButtonManager;
@@ -99,7 +100,7 @@ public class BigBlueButtonNativeRecordingsHandler implements BigBlueButtonRecord
 	}
 
 	@Override
-	public String getRecordingURL(BigBlueButtonRecording recording) {
+	public String getRecordingURL(UserSession usess, BigBlueButtonRecording recording) {
 		return recording.getUrl();
 	}
 
diff --git a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonOpenCastRecordingsHandler.java b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonOpenCastRecordingsHandler.java
index a9398c18e5d..ae94c72eec0 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonOpenCastRecordingsHandler.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/manager/BigBlueButtonOpenCastRecordingsHandler.java
@@ -19,27 +19,46 @@
  */
 package org.olat.modules.bigbluebutton.manager;
 
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 
+import org.apache.logging.log4j.Logger;
+import org.olat.core.dispatcher.mapper.Mapper;
+import org.olat.core.dispatcher.mapper.MapperService;
+import org.olat.core.dispatcher.mapper.manager.MapperKey;
 import org.olat.core.gui.translator.Translator;
 import org.olat.core.id.User;
 import org.olat.core.id.UserConstants;
+import org.olat.core.logging.Tracing;
 import org.olat.core.util.Formatter;
 import org.olat.core.util.StringHelper;
+import org.olat.core.util.UserSession;
 import org.olat.core.util.Util;
 import org.olat.course.CourseFactory;
 import org.olat.course.ICourse;
 import org.olat.course.nodes.CourseNode;
 import org.olat.group.BusinessGroup;
+import org.olat.ims.lti.LTIContext;
+import org.olat.ims.lti.LTIManager;
+import org.olat.ims.lti.ui.PostDataMapper;
 import org.olat.modules.bigbluebutton.BigBlueButtonMeeting;
 import org.olat.modules.bigbluebutton.BigBlueButtonRecording;
 import org.olat.modules.bigbluebutton.BigBlueButtonRecordingsHandler;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonError;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonErrorCodes;
 import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors;
+import org.olat.modules.bigbluebutton.model.BigBlueButtonRecordingImpl;
 import org.olat.modules.bigbluebutton.ui.BigBlueButtonAdminController;
+import org.olat.modules.opencast.OpencastBBBRecordingContext;
+import org.olat.modules.opencast.OpencastEvent;
+import org.olat.modules.opencast.OpencastModule;
+import org.olat.modules.opencast.OpencastService;
 import org.olat.repository.RepositoryEntry;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 
@@ -51,10 +70,21 @@ import org.springframework.stereotype.Service;
  */
 @Service
 @Qualifier("opencast")
-public class BigBlueButtonOpenCastRecordingsHandler implements BigBlueButtonRecordingsHandler  {
+public class BigBlueButtonOpenCastRecordingsHandler implements BigBlueButtonRecordingsHandler {
+	
+	private static final Logger log = Tracing.createLoggerFor(BigBlueButtonOpenCastRecordingsHandler.class);
 	
 	public static final String OPENCAST_RECORDING_HANDLER_ID = "opencast";
 	
+	@Autowired
+	private OpencastModule opencastModule;
+	@Autowired
+	private OpencastService opencastService;
+	@Autowired
+	private LTIManager ltiManager;
+	@Autowired
+	private MapperService mapperService;
+	
 	@Override
 	public String getId() {
 		return OPENCAST_RECORDING_HANDLER_ID;
@@ -73,12 +103,34 @@ public class BigBlueButtonOpenCastRecordingsHandler implements BigBlueButtonReco
 
 	@Override
 	public List<BigBlueButtonRecording> getRecordings(BigBlueButtonMeeting meeting, BigBlueButtonErrors errors) {
-		return Collections.emptyList();
+		if(!opencastModule.isEnabled()) {
+			log.error("Try getting recordings of disabled Opencast: {}", opencastModule.getApiUrl());
+			errors.append(new BigBlueButtonError(BigBlueButtonErrorCodes.opencastDisabled));
+			return Collections.emptyList();
+		}
+		
+		List<OpencastEvent> events = opencastService.getEvents(meeting.getIdentifier());
+		List<BigBlueButtonRecording> recordings = new ArrayList<>(events.size());
+		for (OpencastEvent event : events) {
+			String recordId = event.getIdentifier();
+			String name = event.getTitle();
+			String meetingId = meeting.getIdentifier();
+			Date startTime = event.getStart();
+			Date endTime = event.getEnd();
+			String url = null;
+			String type = null;
+			recordings.add(BigBlueButtonRecordingImpl.valueOf(recordId, name, meetingId, startTime, endTime, url, type));
+		}
+		return recordings;
 	}
 
 	@Override
-	public String getRecordingURL(BigBlueButtonRecording recording) {
-		return recording.getUrl();
+	public String getRecordingURL(UserSession usess, BigBlueButtonRecording recording) {
+		LTIContext context = new OpencastBBBRecordingContext(recording.getRecordId());
+		Map<String,String> unsignedProps = ltiManager.forgeLTIProperties(usess.getIdentity(), usess.getLocale(), context, false, false, true);
+		Mapper contentMapper = new PostDataMapper(unsignedProps, opencastModule.getLtiUrl(), opencastModule.getLtiKey(), opencastModule.getLtiSecret(), false);
+		MapperKey mapperKey = mapperService.register(usess, contentMapper);
+		return mapperKey.getUrl();
 	}
 
 	@Override
@@ -139,7 +191,7 @@ public class BigBlueButtonOpenCastRecordingsHandler implements BigBlueButtonReco
 		// Location of the event
 		uriBuilder.optionalParameter("meta_dc-spatial", "Olat-BigBlueButton");
 		// Date of the event
-		uriBuilder.optionalParameter("meta_dc-created", Formatter.formatDatetime(meetingCreation));							
+		uriBuilder.optionalParameter("meta_dc-created", Formatter.formatDatetime(meetingCreation));
 
 		uriBuilder.optionalParameter("meta_opencast-series-dc-title", seriesTitle);
 	}
@@ -158,6 +210,12 @@ public class BigBlueButtonOpenCastRecordingsHandler implements BigBlueButtonReco
 
 	@Override
 	public boolean deleteRecordings(List<BigBlueButtonRecording> recordings, BigBlueButtonMeeting meeting, BigBlueButtonErrors errors) {
-		return false;
+		if (!opencastModule.isEnabled()) {
+			log.error("Try deleting a recording of disabled Opencast: {}", opencastModule.getApiUrl());
+			errors.append(new BigBlueButtonError(BigBlueButtonErrorCodes.opencastDisabled));
+			return false;
+		}
+		
+		return opencastService.deleteEvents(meeting.getIdentifier());
 	}
 }
diff --git a/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonErrorCodes.java b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonErrorCodes.java
index f40b7471488..919d7cdf675 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonErrorCodes.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/model/BigBlueButtonErrorCodes.java
@@ -31,6 +31,7 @@ public enum BigBlueButtonErrorCodes {
 	deletedObject,
 	serverNotAvailable,
 	serverDisabled,
+	opencastDisabled,
 	unkown
 	;
 
diff --git a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonMeetingController.java b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonMeetingController.java
index cb3061d1bc7..df1e6d4def0 100644
--- a/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonMeetingController.java
+++ b/src/main/java/org/olat/modules/bigbluebutton/ui/BigBlueButtonMeetingController.java
@@ -381,7 +381,7 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen
 				if("delete".equals(se.getCommand())) {
 					doConfirmDeleteRecording(ureq, recordingTableModel.getObject(se.getIndex()).getRecording());
 				} else if("open-recording".equals(se.getCommand())) {
-					doOpenRecording(recordingTableModel.getObject(se.getIndex()).getRecording());
+					doOpenRecording(ureq, recordingTableModel.getObject(se.getIndex()).getRecording());
 				}
 			}
 		} else if(source instanceof FormLink) {
@@ -442,8 +442,8 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen
 		}
 	}
 	
-	private void doOpenRecording(BigBlueButtonRecording recording) {
-		String url = bigBlueButtonManager.getRecordingUrl(recording);
+	private void doOpenRecording(UserRequest ureq, BigBlueButtonRecording recording) {
+		String url = bigBlueButtonManager.getRecordingUrl(ureq.getUserSession(), recording);
 		if(StringHelper.containsNonWhitespace(url)) {
 			getWindowControl().getWindowBackOffice().sendCommandTo(CommandFactory.createNewWindowRedirectTo(url));
 		} else {
diff --git a/src/main/java/org/olat/modules/opencast/OpencastBBBRecordingContext.java b/src/main/java/org/olat/modules/opencast/OpencastBBBRecordingContext.java
new file mode 100644
index 00000000000..e6d53e3fc98
--- /dev/null
+++ b/src/main/java/org/olat/modules/opencast/OpencastBBBRecordingContext.java
@@ -0,0 +1,126 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.opencast;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.olat.basesecurity.BaseSecurityManager;
+import org.olat.core.CoreSpringFactory;
+import org.olat.core.id.Identity;
+import org.olat.ims.lti.LTIContext;
+import org.olat.ims.lti.LTIDisplayOptions;
+import org.olat.ims.lti.LTIManager;
+
+/**
+ * 
+ * Initial date: 10.09.2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class OpencastBBBRecordingContext implements LTIContext {
+	
+	private static final String LTI_ROLE = "Learner";
+	private static final String HEIGTH = "auto";
+	private static final String WIDTH = "auto";
+	private static final String TARGET = LTIDisplayOptions.fullscreen.name();
+	private static final String CUSTOM_TOOL = "tool";
+	
+	private final String identifier;
+
+	public OpencastBBBRecordingContext(String identifier) {
+		this.identifier = identifier;
+	}
+
+	@Override
+	public String getSourcedId() {
+		return null;
+	}
+
+	@Override
+	public String getTalkBackMapperUri() {
+		return null;
+	}
+	
+	@Override
+	public String getOutcomeMapperUri() {
+		return null;
+	}
+
+	@Override
+	public String getResourceId() {
+		return null;
+	}
+
+	@Override
+	public String getResourceTitle() {
+		return null;
+	}
+
+	@Override
+	public String getResourceDescription() {
+		return null;
+	}
+
+	@Override
+	public String getContextId() {
+		return null;
+	}
+
+	@Override
+	public String getContextTitle() {
+		return null;
+	}
+
+	@Override
+	public String getRoles(Identity identity) {
+		return LTI_ROLE;
+	}
+
+	@Override
+	public String getCustomProperties() {
+		Map<String, String> customProps = new HashMap<>();
+		
+		customProps.put(CUSTOM_TOOL, "play/" + identifier);
+		
+		return CoreSpringFactory.getImpl(LTIManager.class).joinCustomProps(customProps);
+	}
+
+	@Override
+	public String getTarget() {
+		return TARGET;
+	}
+
+	@Override
+	public String getPreferredWidth() {
+		return WIDTH;
+	}
+
+	@Override
+	public String getPreferredHeight() {
+		return HEIGTH;
+	}
+
+	@Override
+	public String getUserId(Identity identity) {
+		return CoreSpringFactory.getImpl(BaseSecurityManager.class).findAuthenticationName(identity);
+	}
+
+}
diff --git a/src/main/java/org/olat/modules/opencast/OpencastEvent.java b/src/main/java/org/olat/modules/opencast/OpencastEvent.java
new file mode 100644
index 00000000000..4348e57421c
--- /dev/null
+++ b/src/main/java/org/olat/modules/opencast/OpencastEvent.java
@@ -0,0 +1,40 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.opencast;
+
+import java.util.Date;
+
+/**
+ * 
+ * Initial date: 10 Aug 2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public interface OpencastEvent {
+	
+	String getIdentifier();
+	
+	String getTitle();
+	
+	Date getStart();
+	
+	Date getEnd();
+
+}
diff --git a/src/main/java/org/olat/modules/opencast/OpencastModule.java b/src/main/java/org/olat/modules/opencast/OpencastModule.java
new file mode 100644
index 00000000000..4f6bf53795b
--- /dev/null
+++ b/src/main/java/org/olat/modules/opencast/OpencastModule.java
@@ -0,0 +1,180 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.opencast;
+
+import java.nio.charset.StandardCharsets;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.logging.log4j.Logger;
+import org.olat.core.configuration.AbstractSpringModule;
+import org.olat.core.configuration.ConfigOnOff;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.StringHelper;
+import org.olat.core.util.coordinate.CoordinatorManager;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+/**
+ * 
+ * Initial date: 4 Aug 2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+@Service
+public class OpencastModule extends AbstractSpringModule implements ConfigOnOff {
+
+	private static final Logger log = Tracing.createLoggerFor(OpencastModule.class);
+
+	private static final String ENABLED = "opencast.enabled";
+	private static final String API_URL = "api.url";
+	private static final String API_USERNAME = "api.username";
+	private static final String API_PASSOWRD = "api.password";
+	private static final String LTI_URL = "lti.url";
+	private static final String LTI_KEY = "lti.key";
+	private static final String LTI_SECRET = "lti.secret";
+	
+	@Value("${opencast.enabled}")
+	private boolean enabled;
+	@Value("${opencast.api.url}")
+	private String apiUrl;
+	@Value("${opencast.api.username}")
+	private String apiUsername;
+	@Value("${opencast.api.password}")
+	private String apiPassword;
+	@Value("${opencast.lti.url}")
+	private String ltiUrl;
+	@Value("${opencast.lti.key}")
+	private String ltiKey;
+	@Value("${opencast.lti.secret}")
+	private String ltiSecret;
+
+	private String apiAuthorizationHeader;
+	
+	@Autowired
+	public OpencastModule(CoordinatorManager coordinatorManager) {
+		super(coordinatorManager);
+	}
+
+	@Override
+	public void init() {
+		String enabledObj = getStringPropertyValue(ENABLED, true);
+		if(StringHelper.containsNonWhitespace(enabledObj)) {
+			enabled = "true".equals(enabledObj);
+		}
+		
+		apiUrl = getStringPropertyValue(API_URL, apiUrl);
+		apiUsername = getStringPropertyValue(API_USERNAME, apiUsername);
+		apiPassword = getStringPropertyValue(API_PASSOWRD, apiPassword);
+		refreshApiAutorization();
+
+		ltiUrl = getStringPropertyValue(LTI_URL, ltiUrl);
+		ltiKey = getStringPropertyValue(LTI_KEY, ltiKey);
+		ltiSecret = getStringPropertyValue(LTI_SECRET, ltiSecret);
+	}
+
+	@Override
+	protected void initFromChangedProperties() {
+		init();
+	}
+	
+	@Override
+	public boolean isEnabled() {
+		return enabled;
+	}
+	
+	public void setEnabled(boolean enabled) {
+		this.enabled = enabled;
+		setStringProperty(ENABLED, Boolean.toString(enabled), true);
+	}
+
+	public void setApiUrl(String apiUrl) {
+		this.apiUrl = apiUrl;
+		setStringProperty(API_URL, apiUrl, true);
+	}
+
+	public String getApiUrl() {
+		return apiUrl;
+	}
+
+	public String getApiUsername() {
+		return apiUsername;
+	}
+
+	public String getApiPassword() {
+		return apiPassword;
+	}
+
+	public void setApiCredentials(String apiUsername, String apiPassword) {
+		this.apiUsername = apiUsername;
+		setStringProperty(API_USERNAME, apiUsername, true);
+		
+		this.apiPassword = apiPassword;
+		setSecretStringProperty(API_PASSOWRD, apiPassword, true);
+		
+		refreshApiAutorization();
+	}
+	
+	/*
+	 * Did not work with BasicCredentialsProvider!?
+	 * So let's create the AUTHORIZATION header by ourself.
+	 */
+	private void refreshApiAutorization() {
+		try {
+			String auth = apiUsername + ":" + apiPassword;
+			byte[] encodedAuth = Base64.encodeBase64(auth.getBytes(StandardCharsets.ISO_8859_1));
+			apiAuthorizationHeader = "Basic " + new String(encodedAuth);
+		} catch (Exception e) {
+			log.error("Opencast AUTHORIZATION header not created", e);
+		}
+	}
+
+	public String getApiAuthorizationHeader() {
+		return apiAuthorizationHeader;
+	}
+	
+	public String getLtiUrl() {
+		return ltiUrl;
+	}
+	
+	public void setLtiUrl(String ltiUrl) {
+		this.ltiUrl = ltiUrl;
+		setStringProperty(LTI_URL, ltiUrl, true);
+	}
+
+	public String getLtiKey() {
+		return ltiKey;
+	}
+
+	public void setLtiKey(String ltiKey) {
+		this.ltiKey = ltiKey;
+		setStringProperty(LTI_KEY, ltiKey, true);
+	}
+
+	public String getLtiSecret() {
+		return ltiSecret;
+	}
+
+	public void setLtiSecret(String ltiSecret) {
+		this.ltiSecret = ltiSecret;
+		setStringProperty(LTI_SECRET, ltiSecret, true);
+	}
+
+}
diff --git a/src/main/java/org/olat/modules/opencast/OpencastService.java b/src/main/java/org/olat/modules/opencast/OpencastService.java
new file mode 100644
index 00000000000..5f0cbca8595
--- /dev/null
+++ b/src/main/java/org/olat/modules/opencast/OpencastService.java
@@ -0,0 +1,55 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.opencast;
+
+import java.util.List;
+
+/**
+ * 
+ * Initial date: 4 Aug 2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public interface OpencastService {
+
+	/**
+	 * Check if the connection to Opencast can be established with the url and credentials of the OpencastModul.
+	 *
+	 * @return true if the connection was successfully established.
+	 */
+	boolean checkApiConnection();
+
+	/**
+	 * Get the events with the identifier
+	 *
+	 * @param identifier
+	 * @return
+	 */
+	List<OpencastEvent> getEvents(String identifier);
+	
+	/**
+	 * Delete all events with the identifier.
+	 *
+	 * @param identifier
+	 * @return true if some event was deleted
+	 */
+	boolean deleteEvents(String identifier);
+
+}
diff --git a/src/main/java/org/olat/modules/opencast/manager/OpencastServiceImpl.java b/src/main/java/org/olat/modules/opencast/manager/OpencastServiceImpl.java
new file mode 100644
index 00000000000..3ff89d32959
--- /dev/null
+++ b/src/main/java/org/olat/modules/opencast/manager/OpencastServiceImpl.java
@@ -0,0 +1,75 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.opencast.manager;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.olat.modules.opencast.OpencastEvent;
+import org.olat.modules.opencast.OpencastService;
+import org.olat.modules.opencast.manager.client.Api;
+import org.olat.modules.opencast.manager.client.Event;
+import org.olat.modules.opencast.manager.client.GetEventsParams;
+import org.olat.modules.opencast.manager.client.OpencastRestClient;
+import org.olat.modules.opencast.model.OpencastEventImpl;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+/**
+ * 
+ * Initial date: 4 Aug 2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+@Service
+public class OpencastServiceImpl implements OpencastService {
+	
+	@Autowired
+	private OpencastRestClient opencastRestClient;
+
+	@Override
+	public boolean checkApiConnection() {
+		Api api = opencastRestClient.getApi();
+		return api != null && api.getVersion() != null;
+	}
+
+	@Override
+	public List<OpencastEvent> getEvents(String identifier) {
+		GetEventsParams params = new GetEventsParams();
+		params.getFilter().setTextFilter(identifier);
+		Event[] events = opencastRestClient.getEvents(params);
+		List<OpencastEvent> opencastEvents = new ArrayList<>(events.length);
+		for (Event event : events) {
+			OpencastEventImpl opencastEvent = new OpencastEventImpl();
+			opencastEvent.setIdentifier(event.getIdentifier());
+			opencastEvent.setTitle(event.getTitle());
+			opencastEvent.setStart(event.getStart());
+			// End has to be calculated with the duration, but the duration of the event is always 0.
+			// Only the duration of the metadata would be the right value. We skip that for now.
+			opencastEvents.add(opencastEvent);
+		}
+		return opencastEvents;
+	}
+
+	@Override
+	public boolean deleteEvents(String identifier) {
+		return opencastRestClient.deleteEvent(identifier);
+	}
+}
diff --git a/src/main/java/org/olat/modules/opencast/manager/client/Api.java b/src/main/java/org/olat/modules/opencast/manager/client/Api.java
new file mode 100644
index 00000000000..660f3af1cdb
--- /dev/null
+++ b/src/main/java/org/olat/modules/opencast/manager/client/Api.java
@@ -0,0 +1,52 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.opencast.manager.client;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * 
+ * Initial date: 4 Aug 2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+@JsonIgnoreProperties(ignoreUnknown=true)
+public class Api {
+	
+	private String version;
+	private String url;
+	
+	public String getVersion() {
+		return version;
+	}
+	
+	public void setVersion(String version) {
+		this.version = version;
+	}
+	
+	public String getUrl() {
+		return url;
+	}
+	
+	public void setUrl(String url) {
+		this.url = url;
+	}
+	
+}
diff --git a/src/main/java/org/olat/modules/opencast/manager/client/Event.java b/src/main/java/org/olat/modules/opencast/manager/client/Event.java
new file mode 100644
index 00000000000..62e83a213df
--- /dev/null
+++ b/src/main/java/org/olat/modules/opencast/manager/client/Event.java
@@ -0,0 +1,72 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.opencast.manager.client;
+
+import java.util.Date;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+/**
+ * 
+ * Initial date: 10 Aug 2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+@JsonIgnoreProperties(ignoreUnknown=true)
+public class Event {
+	
+	private String identifier;
+	private String title;
+	private Date start;
+	private String duration;
+	
+	public String getIdentifier() {
+		return identifier;
+	}
+
+	public void setIdentifier(String identifier) {
+		this.identifier = identifier;
+	}
+
+	public String getTitle() {
+		return title;
+	}
+
+	public void setTitle(String title) {
+		this.title = title;
+	}
+
+	public Date getStart() {
+		return start;
+	}
+
+	public void setStart(Date start) {
+		this.start = start;
+	}
+
+	public String getDuration() {
+		return duration;
+	}
+
+	public void setDuration(String duration) {
+		this.duration = duration;
+	}
+
+}
diff --git a/src/main/java/org/olat/modules/opencast/manager/client/GetEventsParams.java b/src/main/java/org/olat/modules/opencast/manager/client/GetEventsParams.java
new file mode 100644
index 00000000000..97d35453d3d
--- /dev/null
+++ b/src/main/java/org/olat/modules/opencast/manager/client/GetEventsParams.java
@@ -0,0 +1,67 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.opencast.manager.client;
+
+import org.olat.core.util.StringHelper;
+
+/**
+ * 
+ * Initial date: 10 Aug 2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class GetEventsParams {
+	
+	private Filter filter;
+	
+	public Filter getFilter() {
+		if (filter == null) {
+			filter = new Filter();
+		}
+		return filter;
+	}
+
+	public String getFilterParam() {
+		if (filter != null) {
+			StringBuilder sb = new StringBuilder();
+			String textFilter = filter.getTextFilter();
+			if (StringHelper.containsNonWhitespace(textFilter)) {
+				sb.append("textFilter:").append(textFilter);
+			}
+			return sb.toString();
+		}
+		return null;
+	}
+
+	public static final class Filter {
+		
+		private String textFilter;
+
+		private String getTextFilter() {
+			return textFilter;
+		}
+
+		public void setTextFilter(String textFilter) {
+			this.textFilter = textFilter;
+		}
+		
+	}
+
+}
diff --git a/src/main/java/org/olat/modules/opencast/manager/client/OpencastRestClient.java b/src/main/java/org/olat/modules/opencast/manager/client/OpencastRestClient.java
new file mode 100644
index 00000000000..b28dff3dbb5
--- /dev/null
+++ b/src/main/java/org/olat/modules/opencast/manager/client/OpencastRestClient.java
@@ -0,0 +1,142 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.opencast.manager.client;
+
+import java.net.URI;
+
+import org.apache.http.HttpHeaders;
+import org.apache.http.HttpStatus;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpRequestBase;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+import org.apache.logging.log4j.Logger;
+import org.olat.core.logging.Tracing;
+import org.olat.core.util.StringHelper;
+import org.olat.modules.opencast.OpencastModule;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * 
+ * Initial date: 4 Aug 2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+@Service
+public class OpencastRestClient {
+
+	private static final Logger log = Tracing.createLoggerFor(OpencastRestClient.class);
+	
+	private static final Event[] NO_EVENTS = new Event[]{};
+	private static final int TIMEOUT_5000_MILLIS = 5000;
+	private static final RequestConfig REQUEST_CONFIG = RequestConfig.copy(RequestConfig.DEFAULT)
+			.setSocketTimeout(TIMEOUT_5000_MILLIS)
+			.setConnectTimeout(TIMEOUT_5000_MILLIS)
+			.setConnectionRequestTimeout(TIMEOUT_5000_MILLIS)
+			.build();
+	
+	@Autowired
+	private OpencastModule opencastModule;
+	
+	private final ObjectMapper objectMapper = new ObjectMapper();
+
+	public Api getApi() {
+		URI uri = URI.create(opencastModule.getApiUrl());
+		HttpGet request = new HttpGet(uri);
+		decorateRequest(request);
+		
+		try(CloseableHttpClient client = HttpClientBuilder.create().build();
+				CloseableHttpResponse response = client.execute(request)) {
+			int statusCode = response.getStatusLine().getStatusCode();
+			log.debug("Status code of: {} {}", uri, statusCode);
+			
+			if (statusCode == HttpStatus.SC_OK) {
+				String json = EntityUtils.toString(response.getEntity(), "UTF-8");
+				return objectMapper.readValue(json, Api.class);
+			}
+		} catch(Exception e) {
+			log.error("Cannot send: {}", uri, e);
+		}
+		return null;
+	}
+	
+	public Event[] getEvents(GetEventsParams params) {
+		URI uri;
+		try {
+			URIBuilder builder = new URIBuilder(opencastModule.getApiUrl() + "/events");
+			String filterParam = params.getFilterParam();
+			if (StringHelper.containsNonWhitespace(filterParam)) {
+				builder.addParameter("filter", filterParam);
+			}
+			uri = builder.build();
+		} catch (Exception e) {
+			log.error("Cannot get Opencast events.", e);
+			return NO_EVENTS;
+		}
+
+		HttpGet request = new HttpGet(uri);
+		decorateRequest(request);
+		
+		try(CloseableHttpClient client = HttpClientBuilder.create().build();
+				CloseableHttpResponse response = client.execute(request)) {
+			int statusCode = response.getStatusLine().getStatusCode();
+			log.debug("Status code of: {} {}", uri, statusCode);
+			if (statusCode == HttpStatus.SC_OK) {
+				String json = EntityUtils.toString(response.getEntity(), "UTF-8");
+				return objectMapper.readValue(json, Event[].class);
+			}
+		} catch(Exception e) {
+			log.error("Cannot send: {}", uri, e);
+		}
+		return NO_EVENTS;
+	}
+	
+	public boolean deleteEvent(String identifier) {
+		URI uri = URI.create(opencastModule.getApiUrl() + "/events/" + identifier);
+		HttpDelete request = new HttpDelete(uri);
+		decorateRequest(request);
+		
+		try(CloseableHttpClient client = HttpClientBuilder.create().build();
+				CloseableHttpResponse response = client.execute(request)) {
+			int statusCode = response.getStatusLine().getStatusCode();
+			log.debug("Status code of: {} {}", uri, statusCode);
+			if (statusCode == HttpStatus.SC_NO_CONTENT || statusCode == HttpStatus.SC_OK) {
+				return true;
+			}
+		} catch(Exception e) {
+			log.error("Cannot send: {}", uri, e);
+		}
+		return false;
+	}	
+
+	private void decorateRequest(HttpRequestBase request) {
+		request.setConfig(REQUEST_CONFIG);
+		request.setHeader(HttpHeaders.AUTHORIZATION, opencastModule.getApiAuthorizationHeader());
+	}
+
+}
diff --git a/src/main/java/org/olat/modules/opencast/model/OpencastEventImpl.java b/src/main/java/org/olat/modules/opencast/model/OpencastEventImpl.java
new file mode 100644
index 00000000000..e6fb476a404
--- /dev/null
+++ b/src/main/java/org/olat/modules/opencast/model/OpencastEventImpl.java
@@ -0,0 +1,75 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.opencast.model;
+
+import java.util.Date;
+
+import org.olat.modules.opencast.OpencastEvent;
+
+/**
+ * 
+ * Initial date: 10 Aug 2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class OpencastEventImpl implements OpencastEvent {
+	
+	private String identifier;
+	private String title;
+	private Date start;
+	private Date end;
+
+	@Override
+	public String getIdentifier() {
+		return identifier;
+	}
+
+	public void setIdentifier(String identifier) {
+		this.identifier = identifier;
+	}
+
+	@Override
+	public String getTitle() {
+		return title;
+	}
+
+	public void setTitle(String title) {
+		this.title = title;
+	}
+
+	@Override
+	public Date getStart() {
+		return start;
+	}
+
+	public void setStart(Date start) {
+		this.start = start;
+	}
+
+	@Override
+	public Date getEnd() {
+		return end;
+	}
+
+	public void setEnd(Date end) {
+		this.end = end;
+	}
+
+}
diff --git a/src/main/java/org/olat/modules/opencast/ui/OpencastAdminController.java b/src/main/java/org/olat/modules/opencast/ui/OpencastAdminController.java
new file mode 100644
index 00000000000..eb1a4a91927
--- /dev/null
+++ b/src/main/java/org/olat/modules/opencast/ui/OpencastAdminController.java
@@ -0,0 +1,182 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.opencast.ui;
+
+import org.olat.core.gui.UserRequest;
+import org.olat.core.gui.components.form.flexible.FormItem;
+import org.olat.core.gui.components.form.flexible.FormItemContainer;
+import org.olat.core.gui.components.form.flexible.elements.FormLink;
+import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement;
+import org.olat.core.gui.components.form.flexible.elements.TextElement;
+import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
+import org.olat.core.gui.components.form.flexible.impl.FormEvent;
+import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
+import org.olat.core.gui.components.link.Link;
+import org.olat.core.gui.control.Controller;
+import org.olat.core.gui.control.WindowControl;
+import org.olat.core.util.StringHelper;
+import org.olat.modules.opencast.OpencastModule;
+import org.olat.modules.opencast.OpencastService;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * 
+ * Initial date: 4 Aug 2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class OpencastAdminController extends FormBasicController {
+	
+	private static final String[] ENABLED_KEYS = new String[]{ "on" };
+	
+	private MultipleSelectionElement enabledEl;
+	private TextElement apiUrlEl;
+	private TextElement apiUsernameEl;
+	private TextElement apiPasswordEl;
+	private TextElement ltiUrlEl;
+	private TextElement ltiKeyEl;
+	private TextElement ltiSectretEl;
+	private FormLink checkApiConnectionButton;
+	
+	@Autowired
+	private OpencastModule opencastModule;
+	@Autowired
+	private OpencastService opencastService;
+
+	public OpencastAdminController(UserRequest ureq, WindowControl wControl) {
+		super(ureq, wControl);
+		initForm(ureq);
+	}
+
+	@Override
+	protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
+		setFormTitle("admin.title");
+		
+		String[] enableValues = new String[]{ translate("on") };
+		enabledEl = uifactory.addCheckboxesHorizontal("admin.enabled", formLayout, ENABLED_KEYS, enableValues);
+		enabledEl.select(ENABLED_KEYS[0], opencastModule.isEnabled());
+		
+		String apiUrl = opencastModule.getApiUrl();
+		apiUrlEl = uifactory.addTextElement("admin.api.url", "admin.api.url", 128, apiUrl, formLayout);
+		apiUrlEl.setMandatory(true);
+		
+		String apiUsername = opencastModule.getApiUsername();
+		apiUsernameEl = uifactory.addTextElement("admin.api.username", 128, apiUsername, formLayout);
+		apiUsernameEl.setMandatory(true);
+		
+		String apiPassword = opencastModule.getApiPassword();
+		apiPasswordEl = uifactory.addPasswordElement("admin.api.password", "admin.api.password", 128, apiPassword, formLayout);
+		apiPasswordEl.setAutocomplete("new-password");
+		apiPasswordEl.setMandatory(true);
+		
+		String ltiUrl = opencastModule.getApiUrl();
+		ltiUrlEl = uifactory.addTextElement("admin.lti.url", "admin.lti.url", 128, ltiUrl, formLayout);
+		ltiUrlEl.setMandatory(true);
+		
+		String ltiKey = opencastModule.getLtiKey();
+		ltiKeyEl = uifactory.addTextElement("admin.lti.key", 123, ltiKey, formLayout);
+		ltiKeyEl.setMandatory(true);
+		
+		String ltiSecret = opencastModule.getLtiSecret();
+		ltiSectretEl = uifactory.addPasswordElement("admin.lti.secret", "admin.lti.secret", 128, ltiSecret, formLayout);
+		ltiSectretEl.setAutocomplete("new-password");
+		ltiSectretEl.setMandatory(true);
+		
+		FormLayoutContainer buttonLayout = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
+		formLayout.add("buttons", buttonLayout);
+		uifactory.addFormSubmitButton("save", buttonLayout);
+		checkApiConnectionButton = uifactory.addFormLink("admin.check.api.connection", buttonLayout, Link.BUTTON);
+	}
+	
+	@Override
+	protected boolean validateFormLogic(UserRequest ureq) {
+		boolean allOk = true;
+		
+		//validate only if the module is enabled
+		if(enabledEl.isAtLeastSelected(1)) {
+			allOk &= validateIsMandatory(apiUrlEl);
+			allOk &= validateIsMandatory(apiUsernameEl);
+			allOk &= validateIsMandatory(apiPasswordEl);
+			allOk &= validateIsMandatory(ltiUrlEl);
+			allOk &= validateIsMandatory(ltiKeyEl);
+			allOk &= validateIsMandatory(ltiSectretEl);
+		}
+		
+		return allOk & super.validateFormLogic(ureq);
+	}
+
+	private boolean validateIsMandatory(TextElement textElement) {
+		boolean allOk = true;
+		
+		if (!StringHelper.containsNonWhitespace(textElement.getValue())) {
+			textElement.setErrorKey("form.legende.mandatory", null);
+			allOk &= false;
+		}
+		
+		return allOk;
+	}
+
+	@Override
+	protected void formInnerEvent(UserRequest ureq, FormItem source, FormEvent event) {
+		if (source == checkApiConnectionButton) {
+			doCheckApiConnection();
+		}
+		super.formInnerEvent(ureq, source, event);
+	}
+
+	@Override
+	protected void formOK(UserRequest ureq) {
+		boolean enabled = enabledEl.isAtLeastSelected(1);
+		opencastModule.setEnabled(enabled);
+		
+		String apiUrl = apiUrlEl.getValue();
+		apiUrl = apiUrl.endsWith("/")? apiUrl.substring(0, apiUrl.length() - 1): apiUrl;
+		opencastModule.setApiUrl(apiUrl);
+		
+		String apiUsername = apiUsernameEl.getValue();
+		String apiPassword = apiPasswordEl.getValue();
+		opencastModule.setApiCredentials(apiUsername, apiPassword);
+		
+		String ltiUrl = ltiUrlEl.getValue();
+		ltiUrl = ltiUrl.endsWith("/")? ltiUrl.substring(0, ltiUrl.length() - 1): ltiUrl;
+		opencastModule.setLtiUrl(ltiUrl);
+		
+		String ltiKey = ltiKeyEl.getValue();
+		opencastModule.setLtiKey(ltiKey);
+		
+		String ltiSecret = ltiSectretEl.getValue();
+		opencastModule.setLtiSecret(ltiSecret);
+	}
+
+	private void doCheckApiConnection() {
+		boolean connectionOk = opencastService.checkApiConnection();
+		if (connectionOk) {
+			showInfo("check.api.connection.ok");
+		} else {
+			showError("check.api.connection.nok");
+		}
+	}
+
+	@Override
+	protected void doDispose() {
+		//
+	}
+
+}
diff --git a/src/main/java/org/olat/modules/opencast/ui/_i18n/LocalStrings_de.properties b/src/main/java/org/olat/modules/opencast/ui/_i18n/LocalStrings_de.properties
new file mode 100644
index 00000000000..b4a3e84df20
--- /dev/null
+++ b/src/main/java/org/olat/modules/opencast/ui/_i18n/LocalStrings_de.properties
@@ -0,0 +1,13 @@
+admin.api.password=API Passwort
+admin.api.url=API URL
+admin.api.username=API Benutzername
+admin.check.api.connection=API Verbindung testen
+admin.enabled=Modul "Opencast"
+admin.lti.key=LTI Key
+admin.lti.secret=LTI Secret
+admin.api.url=LTI URL
+admin.menu.title=Opencast
+admin.menu.title.alt=Opencast
+admin.title=Konfiguration
+check.api.connection.ok=Die Verbindung konnte erfolgreich hergestellt werden!
+check.api.connection.nok=Die Verbindung konnte nicht hergestellt werden!
\ No newline at end of file
diff --git a/src/main/java/org/olat/modules/opencast/ui/_i18n/LocalStrings_en.properties b/src/main/java/org/olat/modules/opencast/ui/_i18n/LocalStrings_en.properties
new file mode 100644
index 00000000000..a319be2c0ce
--- /dev/null
+++ b/src/main/java/org/olat/modules/opencast/ui/_i18n/LocalStrings_en.properties
@@ -0,0 +1,13 @@
+admin.api.password=API Password
+admin.api.username=API Username
+admin.api.url=API URL
+admin.check.api.connection=Check API connection
+admin.enabled=Module "Opencast"
+admin.lti.url=LTI URL
+admin.lti.key=LTI Key
+admin.lti.secret=LTI Secret
+admin.menu.title=Opencast
+admin.menu.title.alt=Opencast
+admin.title=Configuration
+check.api.connection.ok=The connection was successfully established.
+check.api.connection.nok=The connection could not be established!
\ No newline at end of file
diff --git a/src/main/resources/serviceconfig/olat.properties b/src/main/resources/serviceconfig/olat.properties
index 76e6f2bb0e1..3611f88bd37 100644
--- a/src/main/resources/serviceconfig/olat.properties
+++ b/src/main/resources/serviceconfig/olat.properties
@@ -1757,6 +1757,20 @@ video.transcoding.dir.values=${folder.root}/transcodedVideos, /mount/cheap/disk/
 # allow to retrieve metadata for youtube video
 youtube.api.key=
 
+###############################################################################
+# Opencast
+###############################################################################
+opencast.enabled=false
+# API
+opencast.api.url=https://opencast.example.com/api
+# Username and password of the technical opencast user
+opencast.api.username=
+opencast.api.password=
+# LTI
+opencast.lti.url=https://opencast.example.com/lti
+opencast.lti.key=
+opencast.lti.secret=
+
 ###############################################################################
 # Options for the live stream course node
 ###############################################################################
diff --git a/src/test/java/org/olat/modules/opencast/manager/client/OpencastRestClientTest.java b/src/test/java/org/olat/modules/opencast/manager/client/OpencastRestClientTest.java
new file mode 100644
index 00000000000..d7ec2939617
--- /dev/null
+++ b/src/test/java/org/olat/modules/opencast/manager/client/OpencastRestClientTest.java
@@ -0,0 +1,78 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.modules.opencast.manager.client;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.assertj.core.api.SoftAssertions;
+import org.junit.Before;
+import org.junit.Test;
+import org.olat.modules.opencast.OpencastModule;
+import org.olat.test.OlatTestCase;
+import org.springframework.beans.factory.annotation.Autowired;
+
+/**
+ * This tests are run again a real Opencast instance.
+ * CAUTION: Do not run the thest agians a productive Opencast instance!
+ * Do not add this file to AllTestsJUnit4. You should only run it manually.
+ * 
+ * Initial date: 5 Aug 2020<br>
+ * @author uhensler, urs.hensler@frentix.com, http://www.frentix.com
+ *
+ */
+public class OpencastRestClientTest extends OlatTestCase {
+	
+	@Autowired
+	private OpencastModule opencastModule;
+	
+	@Autowired
+	private OpencastRestClient sut;
+	
+	@Before
+	public void setUp() {
+		opencastModule.setApiUrl("http://localhost:8480");
+		opencastModule.setApiCredentials("admin", "opencast");
+	}
+
+	@Test
+	public void shouldGetApi() {
+		Api api = sut.getApi();
+		
+		assertThat(api).isNotNull();
+	}
+	
+	@Test
+	public void shouldGetEvents() {
+		GetEventsParams params = new GetEventsParams();
+		params.getFilter().setTextFilter("8678c09a-9f76-4d60-b178-fb84d2b9c494");
+		
+		Event[] events = sut.getEvents(params);
+		
+		SoftAssertions softly = new SoftAssertions();
+		softly.assertThat(events.length).isEqualTo(1);
+		Event event = events[0];
+		softly.assertThat(event.getIdentifier()).isNotNull();
+		softly.assertThat(event.getTitle()).isNotNull();
+		softly.assertThat(event.getStart()).isNotNull();
+		softly.assertThat(event.getDuration()).isNotNull();
+		softly.assertAll();
+	}
+	
+}
-- 
GitLab