diff --git a/lang/de.slf b/lang/de.slf
index 9e333944cc0d124a9a14f0001622f0ce0858d93c..d977c90cd12d6ba74c7e040190d6f837e0f30093 100644
--- a/lang/de.slf
+++ b/lang/de.slf
@@ -185,8 +185,6 @@ ui.download_manager.quality = "Qualität"
 // ui.download_manager.close = #ui.generic.close
 
 ui.video_player.back_to_course = "Zur Veranstaltungsseite"
-ui.video_player.suggest_chapter = "Kapitelmarker vorschlagen"
-ui.video_player.add_chapter = "Neues Kapitel"
 ui.video_player.embed = "Einbetten"
 ui.video_player.login_required = "Anmeldung erforderlich"
 // ui.video_player.login = #ui.generic.login
@@ -197,10 +195,28 @@ ui.video_player.login_rwth.description = "Für RWTH-Angehörige und aus dem RWTH
 ui.video_player.login_moodle.description = "Für Teilnehmer der Veranstaltung verfügbar"
 ui.video_player.login_moodle.not_in_course = "Du bist kein Teilnehmer des Moodle-Kurses!"
 ui.video_player.login_moodle.refresh_course = "Kurse aktualisieren"
+ui.video_player.login_oauth.finished_no_access = "OAuth erfolgreich allerdings hast Du keine Berechtigung für diese Vorlesung"
+ui.video_player.login_oauth.unfinished_popup_closed = "Popup wurde geschlossen bevor der OAuth fertig war"
+ui.video_player.login_oauth.waiting_for_finish = """
+    Bitte schließe den OAuth in dem neuen Fenster ab, welches sich geöffnet hat, und schließe es dann.
+    """
+ui.video_player.login_oauth.error_start = "Fehler beim Starten des OAuth"
+ui.video_player.login_oauth.error_finish = "Fehler beim Beenden des OAuth"
+ui.video_player.login_oauth.error_no_popup = """
+    Popup Fenster kann nicht geöffnet werden, OAuth kann nicht gestartet werden. Bitte sende einen Bugreport an \
+    video@fsmpi.rwth-aachen.de
+    """
 ui.video_player.student_council_only = "Nur für Fachschaftler verfügbar."
 ui.video_player.next_lecture = "Nächste Vorlesung"
 ui.video_player.previous_lecture = "Vorherige Vorlesung"
 
+ui.chapter_suggestion.suggest = "Kapitel vorschlagen"
+ui.chapter_suggestion.create = "Kapitel erstellen"
+ui.chapter_suggestion.popup.send_suggestion = "Vorschlagen"
+ui.chapter_suggestion.popup.send_create = "Erstellen"
+ui.chapter_suggestion.popup.invalid_time_message = "Muss im Format 'hh:mm:ss' oder 'mm:ss' sein"
+ui.chapter_suggestion.popup.received_suggestion = "Wir haben deinen Kapitelvorschlag erfolgreich erhalten. Vielen Dank!"
+
 ui.feedback.title = "Feedback"
 ui.feedback.sent_successfully = "Nachricht wurde erfolgreich gesendet. Vielen Dank!"
 ui.feedback.info_text = """
diff --git a/lang/en.slf b/lang/en.slf
index a8ddcc87086456685f2525e93c50ad4a0db4306b..d3536438b354e2e2b3632476335902d2cb26e1f0 100644
--- a/lang/en.slf
+++ b/lang/en.slf
@@ -184,8 +184,6 @@ ui.download_manager.download_now = #ui.generic.download
 ui.download_manager.close = #ui.generic.close
 
 ui.video_player.back_to_course = "Back to course"
-ui.video_player.suggest_chapter = "Suggest chapter"
-ui.video_player.add_chapter = "Add chapter"
 ui.video_player.embed = "Embed"
 ui.video_player.login_required = "Login required"
 ui.video_player.login = #ui.generic.login
@@ -196,10 +194,25 @@ ui.video_player.login_rwth.description = "Available to RWTH members and in the R
 ui.video_player.login_moodle.description = "Available to participants of the Moodle course"
 ui.video_player.login_moodle.not_in_course = "You are not enrolled in the Moodle course"
 ui.video_player.login_moodle.refresh_course = "Refresh course"
+ui.video_player.login_oauth.finished_no_access = "OAuth finished but you don't have permissions to access this lecture"
+ui.video_player.login_oauth.unfinished_popup_closed = "Popup closed without finishing OAuth"
+ui.video_player.login_oauth.waiting_for_finish = "Please finish the OAuth in the new window which has opened, then close it."
+ui.video_player.login_oauth.error_start = "Error while starting OAuth"
+ui.video_player.login_oauth.error_finish = "Error while finishing OAuth"
+ui.video_player.login_oauth.error_no_popup = """
+    Cannot open popup window, cannot start OAuth. Please send a bug report to video@fsmpi.rwth-aachen.de
+    """
 ui.video_player.student_council_only = "Only available to student council members"
 ui.video_player.next_lecture = "Next lecture"
 ui.video_player.previous_lecture = "Previous lecture"
 
+ui.chapter_suggestion.suggest = "Suggest Chapter"
+ui.chapter_suggestion.create = "Create Chapter"
+ui.chapter_suggestion.popup.send_suggestion = "Suggest"
+ui.chapter_suggestion.popup.send_create = "Create"
+ui.chapter_suggestion.popup.invalid_time_message = "Must have format 'hh:mm:ss' or 'mm:ss'"
+ui.chapter_suggestion.popup.received_suggestion = "We successfully received your chapter suggestion. Thank You!"
+
 ui.feedback.title = "Feedback"
 ui.feedback.sent_successfully = "Message was sent successfully. Thank you!"
 ui.feedback.info_text = """
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index bc0550fef1f9ec7729645b20413b86de92ed2cd4..cb2267b678db2b9b46f6452fea8f32c2b68dc627 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -3,7 +3,7 @@ import { ToastContainer } from "react-toastify";
 import Head from "next/head";
 import "react-toastify/dist/ReactToastify.css";
 
-import { RealUserProvider } from "@/videoag/authentication/UserDataProvider";
+import { AuthStatusProvider } from "@/videoag/authentication/AuthStatus";
 import { showErrorToast } from "@/videoag/error/ErrorDisplay";
 import { LanguageProvider } from "@/videoag/localization/LanguageProvider";
 import { ReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary";
@@ -38,7 +38,7 @@ export default function App({ Component, pageProps }: AppProps) {
                 }}
             >
                 <RealBackendProvider>
-                    <RealUserProvider>
+                    <AuthStatusProvider>
                         <EditModeProvider>
                             <LanguageProvider>
                                 <ToastContainer />
@@ -47,7 +47,7 @@ export default function App({ Component, pageProps }: AppProps) {
                                 </DefaultLayout>
                             </LanguageProvider>
                         </EditModeProvider>
-                    </RealUserProvider>
+                    </AuthStatusProvider>
                 </RealBackendProvider>
             </ReloadBoundary>
         </>
diff --git a/src/pages/courses.tsx b/src/pages/courses.tsx
index 179866c0f960173969b99d5f8b7e9a12bef4617d..aa83c34142ab16507f264b516c19a940a5b8cbb8 100644
--- a/src/pages/courses.tsx
+++ b/src/pages/courses.tsx
@@ -5,7 +5,7 @@ import DropdownItem from "react-bootstrap/DropdownItem";
 
 import type { GetCoursesResponse, course } from "@/videoag/api/types";
 import { useApi } from "@/videoag/api/ApiProvider";
-import { useUserContext } from "@/videoag/authentication/UserDataProvider";
+import { useAuthStatus } from "@/videoag/authentication/AuthStatus";
 import { ErrorComponent } from "@/videoag/error/ErrorDisplay";
 import { FallbackErrorBoundary } from "@/videoag/error/FallbackErrorBoundary";
 import { useLanguage } from "@/videoag/localization/LanguageProvider";
@@ -182,8 +182,8 @@ function CourseList({
 
 export default function Courses() {
     const api = useApi();
-    const userContext = useUserContext();
-    const hasUserInfo = userContext.hasUserInfo();
+    const authStatus = useAuthStatus();
+    const hasUserInfo = authStatus.hasUserInfo();
     const { editMode } = useEditMode();
     const [courseData, setCourseData] = useState<ResourceType<GetCoursesResponse>>();
     const nextCoursesDataPromise = useRef<Promise<GetCoursesResponse> | null>(null);
diff --git a/src/pages/dynamic_course_listing.tsx b/src/pages/dynamic_course_listing.tsx
index 3fa6428fe4d3abb2ec6fc9031493e46cfda55355..e1955070fc84fbd0d894b742c6aba127cfe33ded 100644
--- a/src/pages/dynamic_course_listing.tsx
+++ b/src/pages/dynamic_course_listing.tsx
@@ -3,7 +3,7 @@ import { usePathname } from "next/navigation";
 
 import { GetCourseResponse } from "@/videoag/api/types";
 import { useApi } from "@/videoag/api/ApiProvider";
-import { useUserContext } from "@/videoag/authentication/UserDataProvider";
+import { useAuthStatus } from "@/videoag/authentication/AuthStatus";
 import CourseListing from "@/videoag/course/CourseListing";
 import { Four04, ErrorComponent } from "@/videoag/error/ErrorDisplay";
 import { FallbackErrorBoundary } from "@/videoag/error/FallbackErrorBoundary";
@@ -12,8 +12,8 @@ import { ResourceType, fetchDataWrapper } from "@/videoag/miscellaneous/PromiseH
 
 function ActualRedirector() {
     const api = useApi();
-    const userContext = useUserContext();
-    const hasUserInfo = userContext.hasUserInfo();
+    const authStatus = useAuthStatus();
+    const hasUserInfo = authStatus.hasUserInfo();
     const pathname = usePathname();
     const matchedParams = pathname.match(/^\/([a-z0-9_-]+)\/?$/);
     const [courseData, setCourseData] = useState<ResourceType<GetCourseResponse>>();
diff --git a/src/pages/dynamic_player.tsx b/src/pages/dynamic_player.tsx
index ee9181a2a6f63531766deab302847abc72f5148a..9d8a347abd862509ec5c67d7d8da85079f5ce886 100644
--- a/src/pages/dynamic_player.tsx
+++ b/src/pages/dynamic_player.tsx
@@ -1,10 +1,10 @@
 import { Suspense, useEffect, useRef, useState } from "react";
 import { usePathname } from "next/navigation";
 
-import { AuthenticationStatusResponse, GetCourseResponse } from "@/videoag/api/types";
+import { course, lecture } from "@/videoag/api/types";
 import { useApi } from "@/videoag/api/ApiProvider";
 import { Backend } from "@/videoag/api/Backend";
-import { useUserContext } from "@/videoag/authentication/UserDataProvider";
+import { useAuthStatus } from "@/videoag/authentication/AuthStatus";
 import Player, { EmbeddedPlayer } from "@/videoag/course/Player";
 import { Four04, ErrorComponent } from "@/videoag/error/ErrorDisplay";
 import { FallbackErrorBoundary } from "@/videoag/error/FallbackErrorBoundary";
@@ -12,49 +12,38 @@ import { ReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary";
 import { ResourceType, fetchDataWrapper } from "@/videoag/miscellaneous/PromiseHelpers";
 
 export interface PlayerData {
-    course: GetCourseResponse;
-    perms: AuthenticationStatusResponse;
-    loaderHadUserInfo: boolean;
-    lectureId: number;
+    course: course;
+    lecture: lecture;
 }
 
 async function dataLoader(
     api: Backend,
     course_handle: string,
     lecture_id: string,
-    hasUserInfo: boolean,
 ): Promise<PlayerData> {
-    let course: GetCourseResponse = await api.getCourse(course_handle, true);
+    let course: course = await api.getCourse(course_handle, true);
     let lecture = course.lectures!.find((l) => l.id === parseInt(lecture_id));
     if (lecture === undefined) {
         throw new Error("Lecture not found");
     }
 
-    let perms: AuthenticationStatusResponse = {
-        authenticated_methods: ["public"],
-        is_lecture_authenticated: true,
-    };
-    if (
-        lecture.authentication_methods.includes("public") === false &&
-        lecture.authentication_methods.length > 0
-    ) {
-        perms = await api.getAuthenticationStatus({ lecture_id: lecture.id });
-    }
-    return { course, perms, loaderHadUserInfo: hasUserInfo, lectureId: parseInt(lecture_id) };
+    return { course, lecture };
 }
 
 // This redirector handles both the regular player and the embedded player
 export function ActualRedirector() {
     const api = useApi();
-    const userContext = useUserContext();
-    const hasUserInfo = userContext.hasUserInfo();
+    const authStatus = useAuthStatus();
+    const hasUserInfo = authStatus.hasUserInfo();
+    const currentlyAuthenticatedLectureId = authStatus.getCurrentlyAuthenticatedLectureId();
     const pathname = usePathname();
-    const matchedParamsPlayer = pathname.match(/^\/([a-z0-9_-]+)\/([a-z0-9_-]+)$/);
-    const matchedParamsEmbed = pathname.match(/^\/([a-z0-9_-]+)\/([a-z0-9_-]+)\/embed$/);
     const [playerData, setPlayerData] = useState<ResourceType<PlayerData>>();
     const nextPlayerDataPromise = useRef<Promise<PlayerData> | null>(null);
     const [isReloading, setIsReloading] = useState(false);
 
+    const matchedParamsPlayer = pathname.match(/^\/([a-z0-9_-]+)\/([a-z0-9_-]+)$/);
+    const matchedParamsEmbed = pathname.match(/^\/([a-z0-9_-]+)\/([a-z0-9_-]+)\/embed$/);
+
     const reloadData = () => {
         if (!matchedParamsPlayer && !matchedParamsEmbed) return;
         if (isReloading) {
@@ -63,7 +52,7 @@ export function ActualRedirector() {
         const matched = (matchedParamsPlayer ?? matchedParamsEmbed)!;
         const courseId = matched[1];
         const lectureId = matched[2];
-        const prom = dataLoader(api, courseId, lectureId, userContext.hasUserInfo());
+        const prom = dataLoader(api, courseId, lectureId);
         nextPlayerDataPromise.current = prom;
         if (!playerData) {
             setPlayerData(fetchDataWrapper(prom));
@@ -81,7 +70,7 @@ export function ActualRedirector() {
     };
 
     // eslint-disable-next-line react-hooks/exhaustive-deps
-    useEffect(reloadData, [api, hasUserInfo, pathname]);
+    useEffect(reloadData, [api, hasUserInfo, currentlyAuthenticatedLectureId, pathname]);
 
     if (!matchedParamsPlayer && !matchedParamsEmbed)
         return <Four04 title="Unbekannter Pfad" text="dynamic_player" />;
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
index ba3ce608de70528ee27aaf32fd57c663bb9a2c18..bdaae65688f02ab1e75b6a02f916a4c851efffa5 100644
--- a/src/pages/index.tsx
+++ b/src/pages/index.tsx
@@ -5,7 +5,7 @@ import { useRouter } from "next/router";
 
 import type { GetHomepageResponse, featured, int } from "@/videoag/api/types";
 import { useApi } from "@/videoag/api/ApiProvider";
-import { useUserContext } from "@/videoag/authentication/UserDataProvider";
+import { useAuthStatus } from "@/videoag/authentication/AuthStatus";
 import { LectureCard } from "@/videoag/course/Lecture";
 import { LectureLiveLabel } from "@/videoag/course/LiveLabel";
 import { ErrorComponent, showError, showWarningToast } from "@/videoag/error/ErrorDisplay";
@@ -260,13 +260,13 @@ function ModButtons() {
 
 export default function Home() {
     const api = useApi();
-    const userContext = useUserContext();
+    const authStatus = useAuthStatus();
 
     const [homepageData, setHomepageData] = useState<ResourceType<GetHomepageResponse>>();
     const [query, setQuery] = useState("");
     const [updateQueryDeferred, _] = useDebounceWithArgument(setQuery, 500);
 
-    const hasUserInfo = userContext.hasUserInfo();
+    const hasUserInfo = authStatus.hasUserInfo();
     const { editMode } = useEditMode();
     const { language } = useLanguage();
 
diff --git a/src/videoag/authentication/AuthStatus.tsx b/src/videoag/authentication/AuthStatus.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d2ab43cce0b28d0bfaaae9d3e836c8c46694ac51
--- /dev/null
+++ b/src/videoag/authentication/AuthStatus.tsx
@@ -0,0 +1,140 @@
+import type React from "react";
+import { createContext, useContext, useState, useEffect, startTransition } from "react";
+
+import { int, user } from "@/videoag/api/types";
+import { useApi } from "@/videoag/api/ApiProvider";
+
+interface AuthStatusData {
+    user: user | null;
+    authenticatedMethods: string[];
+    currentlyAuthenticatedLectureId: int | null;
+}
+
+export class AuthStatus {
+    public _updateStatus: (fn: (old: AuthStatus) => AuthStatus) => void;
+    private _isLoading: boolean;
+    private data: AuthStatusData;
+
+    constructor(
+        updateStatus: (fn: (old: AuthStatus) => AuthStatus) => void,
+        isLoading: boolean,
+        data: AuthStatusData,
+    ) {
+        this._updateStatus = updateStatus;
+        this._isLoading = isLoading;
+        this.data = data;
+    }
+
+    _setInitialStatus(data: AuthStatusData) {
+        this._updateStatus((oldStatus) => new AuthStatus(oldStatus._updateStatus, false, data));
+    }
+
+    public isLoading() {
+        return this._isLoading;
+    }
+
+    public update(fn: (old: AuthStatusData) => AuthStatusData) {
+        this._updateStatus(
+            (oldStatus) =>
+                new AuthStatus(oldStatus._updateStatus, oldStatus.isLoading(), fn(oldStatus.data)),
+        );
+    }
+
+    public setUser(user: user | null) {
+        this.update((old) => ({
+            ...old,
+            user: user,
+        }));
+    }
+
+    public addAuthenticatedMethod(method: string) {
+        this.update((old) => ({
+            ...old,
+            authenticatedMethods: old.authenticatedMethods.concat(method),
+        }));
+    }
+
+    public getAuthenticatedMethods(): string[] {
+        return this.data.authenticatedMethods;
+    }
+
+    public isMethodAuthenticated(method: string) {
+        return this.getAuthenticatedMethods().includes(method);
+    }
+
+    public setCurrentlyAuthenticatedLectureId(lectureId: int) {
+        this.update((old) => ({
+            ...old,
+            currentlyAuthenticatedLectureId: lectureId,
+        }));
+    }
+
+    public getCurrentlyAuthenticatedLectureId() {
+        return this.data.currentlyAuthenticatedLectureId;
+    }
+
+    // TODO rename
+
+    public getUserInfo(): user | null {
+        return this.data.user;
+    }
+
+    public hasUserInfo(): boolean {
+        return this.getUserInfo() !== null;
+    }
+
+    public canEditStuff(): boolean {
+        return this.hasUserInfo(); // TODO
+    }
+}
+
+function newEmptyAuthStatus() {
+    return new AuthStatus((old) => old, true, {
+        user: null,
+        authenticatedMethods: [],
+        currentlyAuthenticatedLectureId: null,
+    });
+}
+
+const AuthStatusContext = createContext<AuthStatus>(newEmptyAuthStatus());
+
+export function AuthStatusProvider({ children }: { children: React.ReactNode }) {
+    const api = useApi();
+    const [status, setStatus] = useState<AuthStatus>(newEmptyAuthStatus());
+    status._updateStatus = setStatus;
+
+    const [loadedStatus, setLoadedStatus] = useState<boolean>(false);
+    useEffect(() => {
+        if (loadedStatus) return;
+        api.getAuthenticationStatus()
+            .then((resp) => {
+                startTransition(() => {
+                    setLoadedStatus(true);
+                    status._setInitialStatus({
+                        user: resp.user ?? null,
+                        authenticatedMethods: resp.authenticated_methods,
+                        currentlyAuthenticatedLectureId: null,
+                    });
+                });
+            })
+            .catch((e) => {
+                startTransition(() => {
+                    setLoadedStatus(true);
+                    status._setInitialStatus({
+                        user: null,
+                        authenticatedMethods: [],
+                        currentlyAuthenticatedLectureId: null,
+                    });
+                    console.error("Failed to get authentication status", e);
+                });
+            });
+    }, [api, status, loadedStatus]);
+
+    api.isPrivileged = status.canEditStuff();
+
+    return <AuthStatusContext.Provider value={status}>{children}</AuthStatusContext.Provider>;
+}
+
+export function useAuthStatus() {
+    return useContext(AuthStatusContext);
+}
diff --git a/src/videoag/authentication/ModeratorBarrier.tsx b/src/videoag/authentication/ModeratorBarrier.tsx
index 64e490da428876181bbe7bb4840660905007494e..83435b8428b3c2ae5ee4aa923791ba66b7a014a1 100644
--- a/src/videoag/authentication/ModeratorBarrier.tsx
+++ b/src/videoag/authentication/ModeratorBarrier.tsx
@@ -1,15 +1,13 @@
 import { Four04 } from "@/videoag/error/ErrorDisplay";
-import { useUserContext } from "./UserDataProvider";
+import { useAuthStatus } from "./AuthStatus";
 
 import type React from "react";
 
 export default function ModeratorBarrier({ children }: { children: React.ReactNode }) {
-    const userContext = useUserContext();
+    const authStatus = useAuthStatus();
 
-    if (userContext.canEditStuff() === true) {
+    if (authStatus.canEditStuff() === true) {
         return <>{children} </>;
-    } else if (userContext.isLoggingIn()) {
-        return <div className="spinner-border" role="status" />;
     } else {
         return (
             <Four04
diff --git a/src/videoag/authentication/UserData.tsx b/src/videoag/authentication/UserData.tsx
deleted file mode 100644
index 89a0c11e66c4800efe6e3b04d4384339ab7ff4c6..0000000000000000000000000000000000000000
--- a/src/videoag/authentication/UserData.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import type { user } from "@/videoag/api/types";
-
-export class UserData {
-    public setUserData: (data: UserData) => void;
-    private isLoggingIn_: boolean;
-    private userInfo: user | null;
-
-    constructor(set: (data: UserData) => void, userInfo: user | null, isLoggingIn: boolean) {
-        this.setUserData = set;
-        this.userInfo = userInfo;
-        this.isLoggingIn_ = isLoggingIn;
-    }
-
-    replace(data: user | null, isLoggingIn: boolean = false): void {
-        this.setUserData(new UserData(this.setUserData, data, isLoggingIn));
-    }
-
-    getUserInfo(): user | null {
-        return this.userInfo;
-    }
-
-    hasUserInfo(): boolean {
-        return this.getUserInfo() !== null;
-    }
-
-    canEditStuff(): boolean {
-        return this.hasUserInfo(); // TODO
-    }
-
-    isLoggingIn(): boolean {
-        return this.isLoggingIn_;
-    }
-}
diff --git a/src/videoag/authentication/UserDataProvider.tsx b/src/videoag/authentication/UserDataProvider.tsx
deleted file mode 100644
index 430bce9e9073d880e00b07163dee5e65049cb82d..0000000000000000000000000000000000000000
--- a/src/videoag/authentication/UserDataProvider.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type React from "react";
-import { createContext, useContext, useState } from "react";
-
-import { useApi } from "@/videoag/api/ApiProvider";
-
-import { UserData } from "./UserData";
-
-const UserContext = createContext<UserData>(new UserData(() => {}, null, true));
-
-export function RealUserProvider({ children }: { children: React.ReactNode }) {
-    const api = useApi();
-    let [user, setUser] = useState<UserData>(() => new UserData(() => {}, null, true));
-    user.setUserData = setUser;
-
-    api.isPrivileged = user.canEditStuff();
-
-    return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
-}
-export function useUserContext() {
-    return useContext(UserContext);
-}
diff --git a/src/videoag/authentication/UserField.tsx b/src/videoag/authentication/UserField.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7d92fbc3684b7d4fed0c700f267708388081a9b5
--- /dev/null
+++ b/src/videoag/authentication/UserField.tsx
@@ -0,0 +1,176 @@
+import Link from "next/link";
+import type React from "react";
+import { useState } from "react";
+import { Dropdown, Popover, OverlayTrigger } from "react-bootstrap";
+
+import { useApi } from "@/videoag/api/ApiProvider";
+import { ApiError } from "@/videoag/api/ApiError";
+import { useAuthStatus } from "@/videoag/authentication/AuthStatus";
+import { showError } from "@/videoag/error/ErrorDisplay";
+import { useLanguage } from "@/videoag/localization/LanguageProvider";
+import { TooltipButton } from "@/videoag/miscellaneous/Util";
+import { useEditMode } from "@/videoag/object_management/EditModeProvider";
+
+export default function UserField({ isUnavailable }: { isUnavailable: boolean }) {
+    const api = useApi();
+    const authStatus = useAuthStatus();
+
+    const [loginError, setLoginError] = useState("");
+    const [showPop, setShowPop] = useState(false);
+    const { editMode, setEditMode } = useEditMode();
+
+    const [isLoggingIn, setIsLoggingIn] = useState(false);
+    const { language } = useLanguage();
+
+    const doFsmpiLogin = (e: React.FormEvent<HTMLFormElement>) => {
+        e.preventDefault();
+        const form = e.currentTarget;
+        const name = form.username.value;
+        const password = form.password.value;
+        setIsLoggingIn(true);
+
+        api.authenticateFsmpi({ username: name, password })
+            .then((resp) => {
+                authStatus.setUser(resp.user!);
+                setShowPop(false);
+                setLoginError("");
+            })
+            .catch((err) => {
+                authStatus.setUser(null);
+                if (err instanceof ApiError) {
+                    setLoginError("Error: " + err.api_message);
+                } else {
+                    setLoginError("Login failed " + err.message);
+                }
+            })
+            .finally(() => {
+                setIsLoggingIn(false);
+            });
+    };
+
+    const doLogout = () => {
+        api.logout()
+            .then(() => {
+                authStatus.setUser(null);
+                setEditMode(false);
+            })
+            .catch((e) => {
+                console.error(e);
+                showError(e, "Unable to log out");
+            });
+    };
+
+    if (isUnavailable) {
+        return (
+            <TooltipButton iconClassName="bi-box-arrow-in-right">
+                {language.get("ui.login.unavailable_message")}
+            </TooltipButton>
+        );
+    }
+
+    if (authStatus.isLoading()) {
+        return <div className="spinner-border text-primary me-2" role="status" />;
+    }
+
+    if (!authStatus.hasUserInfo()) {
+        const loginPop = (
+            <Popover>
+                <Popover.Header as="h3">Login für FSMPI</Popover.Header>
+                <Popover.Body>
+                    <form onSubmit={doFsmpiLogin}>
+                        <input
+                            placeholder="User"
+                            name="username"
+                            type="text"
+                            className="form-control mb-2"
+                        />
+                        <input
+                            placeholder="Password"
+                            name="password"
+                            type="password"
+                            className="form-control mb-3"
+                        />
+                        {loginError !== "" ? (
+                            <div className="alert alert-danger" role="alert">
+                                {loginError}
+                            </div>
+                        ) : (
+                            <></>
+                        )}
+                        <div className="d-flex align-items-center">
+                            <input
+                                type="submit"
+                                value="Login"
+                                className="btn btn-primary"
+                                disabled={isLoggingIn}
+                            />
+                            {isLoggingIn && <div className="spinner-border ms-3" />}
+                        </div>
+                    </form>
+                </Popover.Body>
+            </Popover>
+        );
+
+        return (
+            <OverlayTrigger trigger="click" placement="bottom" overlay={loginPop}>
+                <button className="btn" type="button" onClick={() => setShowPop(!showPop)}>
+                    <span className="bi bi-box-arrow-in-right" />
+                </button>
+            </OverlayTrigger>
+        );
+    }
+    return (
+        <>
+            {authStatus.canEditStuff() && (
+                <>
+                    <div
+                        className="form-check form-switch me-2"
+                        title="Press 'e' to toggle edit mode"
+                    >
+                        <input
+                            className="form-check-input"
+                            type="checkbox"
+                            role="switch"
+                            id="flexSwitchCheckDefault"
+                            onChange={() => {
+                                setEditMode(!editMode);
+                            }}
+                            checked={editMode}
+                        />
+                        <label className="form-check-label" htmlFor="flexSwitchCheckDefault">
+                            Edit Mode
+                        </label>
+                    </div>
+                    <div className="vr d-none d-lg-inline-block" />
+                </>
+            )}
+            <Dropdown className="ms-1">
+                <Dropdown.Toggle variant="" style={{ padding: "10px 6px" }}>
+                    {authStatus.getUserInfo()!.display_name} <span className="caret" />
+                </Dropdown.Toggle>
+                <Dropdown.Menu className="me-2">
+                    <li>
+                        <Dropdown.Item as={Link} href="/internal/changelog">
+                            Changelog
+                        </Dropdown.Item>
+                    </li>
+                    <li>
+                        <Dropdown.Item as={Link} href="/internal/timetable">
+                            Drehplan
+                        </Dropdown.Item>
+                    </li>
+                    <li className="dropdown-divider" />
+                    <li>
+                        <Dropdown.Item as={Link} href="/internal/user">
+                            Settings
+                        </Dropdown.Item>
+                    </li>
+                    <li className="dropdown-divider" />
+                    <li>
+                        <Dropdown.Item onClick={doLogout}>Logout</Dropdown.Item>
+                    </li>
+                </Dropdown.Menu>
+            </Dropdown>
+        </>
+    );
+}
diff --git a/src/videoag/authentication/ViewPermissions.tsx b/src/videoag/authentication/ViewPermissions.tsx
index fa574e6df39223d3213a738176410035be6000c0..e572693864ab9a639847598ad0bc92e312c7ed9f 100644
--- a/src/videoag/authentication/ViewPermissions.tsx
+++ b/src/videoag/authentication/ViewPermissions.tsx
@@ -1,3 +1,16 @@
+import type React from "react";
+import { useEffect, useRef, useState } from "react";
+
+import { course, lecture } from "@/videoag/api/types";
+import { ApiError } from "@/videoag/api/ApiError";
+import { useApi } from "@/videoag/api/ApiProvider";
+import { useAuthStatus } from "@/videoag/authentication/AuthStatus";
+import { showError, showErrorToast } from "@/videoag/error/ErrorDisplay";
+import { useLanguage } from "@/videoag/localization/LanguageProvider";
+import { useReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary";
+import { StylizedText } from "@/videoag/miscellaneous/StylizedText";
+import { useDebounceUpdateData } from "@/videoag/miscellaneous/PromiseHelpers";
+
 export function AuthenticationMethodIcons({
     authentication_methods,
 }: {
@@ -61,3 +74,296 @@ export function AuthenticationMethodIcons({
         </span>
     );
 }
+
+function PasswordAuthComponent({ course, lecture }: { course: course; lecture: lecture }) {
+    const api = useApi();
+    const reloadFunc = useReloadBoundary();
+    const authStatus = useAuthStatus();
+    const { language } = useLanguage();
+
+    const [isLoggingIn, setIsLoggingIn] = useState(false);
+    const [loginException, setLoginException] = useState<any>();
+
+    const handleUserPass = (e: React.FormEvent<HTMLFormElement>) => {
+        e.preventDefault();
+        let form = e.currentTarget;
+        let user = form.username.value;
+        let pass = form.password.value;
+        setIsLoggingIn(true);
+        api.authenticatePassword({ username: user, password: pass, lecture_id: lecture.id })
+            .then(() => {
+                authStatus.addAuthenticatedMethod("password");
+                authStatus.setCurrentlyAuthenticatedLectureId(lecture.id);
+                reloadFunc();
+            })
+            .catch(setLoginException)
+            .finally(() => setIsLoggingIn(false));
+        return false;
+    };
+    let errorElement = <></>;
+    if (loginException) {
+        if (
+            loginException instanceof ApiError &&
+            loginException.error_code === "authentication_failed"
+        ) {
+            errorElement = (
+                <p className="alert alert-warning">
+                    {language.get("ui.video_player.login_user_password.user_or_pass_incorrect")}
+                </p>
+            );
+        } else {
+            errorElement = <p className="alert alert-warning">{loginException + ""}</p>;
+        }
+    } else if (authStatus.isMethodAuthenticated("password")) {
+        errorElement = (
+            <p className="alert alert-warning">
+                {language.get("ui.video_player.login_user_password.current_password_incorrect")}
+            </p>
+        );
+    }
+
+    return (
+        <>
+            <h4 className="text-center">
+                <span className="bi bi-lock" aria-hidden="true" />{" "}
+                {language.get("ui.video_player.login_user_password.description")}
+            </h4>
+            {errorElement}
+            <form onSubmit={handleUserPass}>
+                <div className="mb-3">
+                    <label htmlFor="exampleInputEmail1" className="form-label">
+                        {language.get("object.permission.username")}
+                    </label>
+                    <input
+                        type="text"
+                        className="form-control"
+                        id="username"
+                        name="username"
+                        placeholder=""
+                    />
+                </div>
+                <div className="mb-3">
+                    <label htmlFor="exampleInputPassword1" className="form-label">
+                        {language.get("object.permission.password")}
+                    </label>
+                    <input
+                        type="password"
+                        className="form-control"
+                        id="password"
+                        name="password"
+                        placeholder=""
+                    />
+                </div>
+                <button type="submit" className="btn btn-secondary" disabled={isLoggingIn}>
+                    {language.get("ui.video_player.login")}
+                </button>
+                {isLoggingIn && <div className="ms-2 spinner-border text-primary" role="status" />}
+            </form>
+        </>
+    );
+}
+
+class DummyObject {}
+
+function OAuthComponent({
+    type,
+    course,
+    lecture,
+}: {
+    type: "moodle" | "rwth";
+    course: course;
+    lecture: lecture;
+}) {
+    const api = useApi();
+    const reloadFunc = useReloadBoundary();
+    const authStatus = useAuthStatus();
+    const { language } = useLanguage();
+
+    const [isOAuthRunning, setIsOAuthRunning] = useState(false);
+    const [waitingForFinish, setWaitingForFinish] = useState(false);
+    const remainingFinishAttempts = useRef<number>(0);
+    const popupClosed = useRef<boolean>(false);
+
+    const [oauthInfo, setOauthInfo] = useState<any>(undefined);
+
+    // this will prevent the automatic fetching from running after the component is unmounted
+    useEffect(() => {
+        return () => {
+            remainingFinishAttempts.current = 0;
+        };
+    }, []);
+
+    // useDebounceUpdateData is not quite intended for this, but works perfectly in ensuring we only do one fetch at a time
+    const [fetchStatusDeferred, fetchStatusNow] = useDebounceUpdateData<DummyObject, void>(
+        (dummy) =>
+            api
+                .getAuthenticationStatus({ lecture_id: lecture.id })
+                .then((res) => {
+                    if (res.is_lecture_authenticated) {
+                        setIsOAuthRunning(false);
+                        setWaitingForFinish(false);
+                        authStatus.addAuthenticatedMethod(type);
+                        authStatus.setCurrentlyAuthenticatedLectureId(lecture.id);
+                    } else if (res.in_progress_authentication !== type) {
+                        setIsOAuthRunning(false);
+                        setWaitingForFinish(false);
+                        setOauthInfo(
+                            <span className="text-warning">
+                                {language.get("ui.video_player.login_oauth.finished_no_access")}
+                            </span>,
+                        );
+                    } else {
+                        if (remainingFinishAttempts.current > 0) {
+                            remainingFinishAttempts.current--;
+                            fetchStatusDeferred(new DummyObject());
+                        } else if (popupClosed.current) {
+                            setIsOAuthRunning(false);
+                            setWaitingForFinish(false);
+                            setOauthInfo(
+                                <span className="text-warning">
+                                    {language.get(
+                                        "ui.video_player.login_oauth.unfinished_popup_closed",
+                                    )}
+                                </span>,
+                            );
+                        } else if (remainingFinishAttempts.current <= 0) {
+                            console.log("Stopping automatic authentication status fetching");
+                        }
+                    }
+                })
+                .catch((e) => {
+                    showError(e, language.get("ui.video_player.login_oauth.error_finish"));
+                    remainingFinishAttempts.current = 0;
+                    if (popupClosed.current) {
+                        setIsOAuthRunning(false);
+                        setWaitingForFinish(false);
+                        setOauthInfo(undefined);
+                    }
+                }),
+        1000,
+    );
+
+    const startFinishListener = (popup: any) => {
+        setWaitingForFinish(true);
+        setOauthInfo(undefined);
+        popupClosed.current = false;
+        remainingFinishAttempts.current = 120;
+        fetchStatusDeferred(new DummyObject());
+
+        let popInterval = setInterval(() => {
+            if (popup.closed) {
+                remainingFinishAttempts.current = 1;
+                popupClosed.current = true;
+                fetchStatusNow(new DummyObject());
+                clearInterval(popInterval);
+            }
+        }, 100);
+    };
+
+    const startOauth = () => {
+        if (isOAuthRunning) return;
+        setIsOAuthRunning(true);
+        const popup = window.open(undefined, "_blank"); // Open popup here already, so the browser doesn't block it (safari gets confused by the async code below)
+        if (!popup) {
+            showErrorToast(language.get("ui.video_player.login_oauth.error_no_popup"));
+            return;
+        }
+        api.startAuthentication({ type: type })
+            .then((res) => {
+                popup.location = res.verification_url;
+                popup.focus();
+                startFinishListener(popup);
+            })
+            .catch((e) => {
+                setIsOAuthRunning(false);
+                showError(e, language.get("ui.video_player.login_oauth.error_start"));
+            });
+    };
+
+    const isRefreshMoodle = type == "moodle" && authStatus.isMethodAuthenticated("moodle");
+
+    return (
+        <>
+            <h4 className="text-center">
+                <span className="bi bi-person-fill" aria-hidden="true" />
+                {type === "rwth" ? "RWTH" : "Moodle"}
+            </h4>
+            <p>{language.get(`ui.video_player.login_${type}.description`)}</p>
+            {isRefreshMoodle && (
+                <p className="alert alert-info">
+                    {language.get("ui.video_player.login_moodle.not_in_course")}
+                </p>
+            )}
+            <div className="d-flex gap-2 justify-content-start align-items-center">
+                <button
+                    type="button"
+                    onClick={() => startOauth()}
+                    className="btn btn-secondary"
+                    disabled={isOAuthRunning}
+                >
+                    {language.get(
+                        isRefreshMoodle
+                            ? "ui.video_player.login_moodle.refresh_course"
+                            : "ui.video_player.login",
+                    )}
+                </button>
+                {isOAuthRunning && (
+                    <div className="flex-shrink-0 spinner-border text-primary" role="status" />
+                )}
+                {waitingForFinish && (
+                    <span>{language.get("ui.video_player.login_oauth.waiting_for_finish")}</span>
+                )}
+                {oauthInfo && <span>{oauthInfo}</span>}
+            </div>
+        </>
+    );
+}
+
+export function ViewPermissionAuthorization({
+    course,
+    lecture,
+}: {
+    course: course;
+    lecture: lecture;
+}) {
+    const { language } = useLanguage();
+    return (
+        <div
+            className="col-12 rounded-2 h-100"
+            style={{ padding: "10px", backgroundColor: "rgba(0, 0, 0, 0.3)" }}
+        >
+            <h3 className="text-center my-2">{language.get("ui.video_player.login_required")}</h3>
+            {course.authentication_information !== undefined && (
+                <span className="text-center">
+                    <StylizedText markdown>{course.authentication_information}</StylizedText>
+                </span>
+            )}
+            <div style={{ paddingBottom: "20px" }} className="container-fluid">
+                <div className="row">
+                    {lecture.authentication_methods.includes("password") && (
+                        <div className="col-sm-4">
+                            <PasswordAuthComponent course={course} lecture={lecture} />
+                        </div>
+                    )}
+                    {lecture.authentication_methods.includes("rwth") && (
+                        <div className="col-sm-4">
+                            <OAuthComponent type="rwth" course={course} lecture={lecture} />
+                        </div>
+                    )}
+                    {lecture.authentication_methods.includes("moodle") && (
+                        <div className="col-sm-4">
+                            <OAuthComponent type="moodle" course={course} lecture={lecture} />
+                        </div>
+                    )}
+                </div>
+                {!lecture.authentication_methods.includes("rwth") &&
+                    !lecture.authentication_methods.includes("moodle") &&
+                    !lecture.authentication_methods.includes("password") && (
+                        <p className="alert alert-info" style={{ marginTop: "2em" }}>
+                            {language.get("ui.video_player.student_council_only")}
+                        </p>
+                    )}
+            </div>
+        </div>
+    );
+}
diff --git a/src/videoag/course/Chapter.tsx b/src/videoag/course/Chapter.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e397aa2426ac7ed87cb4c1bce26c1664afb27bd9
--- /dev/null
+++ b/src/videoag/course/Chapter.tsx
@@ -0,0 +1,268 @@
+import type React from "react";
+import { useEffect, useRef, useState } from "react";
+import { Popover, OverlayTrigger } from "react-bootstrap";
+
+import { int, chapter } from "@/videoag/api/types";
+import { useApi } from "@/videoag/api/ApiProvider";
+import { useAuthStatus } from "@/videoag/authentication/AuthStatus";
+import { showError } from "@/videoag/error/ErrorDisplay";
+import { StringEditor } from "@/videoag/form/TypeEditor";
+import { useLanguage } from "@/videoag/localization/LanguageProvider";
+import { timestampToString } from "@/videoag/miscellaneous/Formatting";
+import { useReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary";
+import { showInfoToast } from "@/videoag/miscellaneous/Util";
+import { useEditMode } from "@/videoag/object_management/EditModeProvider";
+import {
+    OMDelete,
+    OMEdit,
+    OMHistory,
+    EmbeddedOMFieldComponent,
+} from "@/videoag/object_management/OMConfigComponent";
+
+export function Chapters({
+    chapters,
+    seekTo,
+}: {
+    chapters?: chapter[];
+    seekTo?: (time: number) => void;
+}) {
+    const { editMode } = useEditMode();
+
+    if (!chapters || chapters.length == 0) return <></>;
+
+    return (
+        <div className="col-12 table-responsive" style={{ paddingTop: "10px" }}>
+            <h5>Kapitel:</h5>
+            <table className="table">
+                <tbody>
+                    {chapters
+                        .sort((a, b) => a.start_time - b.start_time)
+                        .map((c) => (
+                            <tr
+                                key={`${c.start_time}+${c.name}`}
+                                className={
+                                    "w-100 " +
+                                    (c.visible === false ? "bg-danger-subtle" : "bg-body")
+                                }
+                            >
+                                <td style={{ width: "130px" }}>
+                                    {seekTo === undefined ? (
+                                        timestampToString(c.start_time)
+                                    ) : (
+                                        <a
+                                            className="chapterlink"
+                                            onClick={() => {
+                                                seekTo(c.start_time);
+                                            }}
+                                            href="#"
+                                        >
+                                            {timestampToString(c.start_time)}
+                                        </a>
+                                    )}
+                                </td>
+                                <td>
+                                    <EmbeddedOMFieldComponent
+                                        object_type="chapter"
+                                        object_id={c.id!}
+                                        field_id="name"
+                                        field_type="string"
+                                        initialValue={c.name}
+                                    />
+                                </td>
+                                {editMode && (
+                                    <td className="d-flex">
+                                        <div className="flex-fill" />
+                                        <EmbeddedOMFieldComponent
+                                            object_type="chapter"
+                                            object_id={c.id!}
+                                            field_id="visible"
+                                            field_type="boolean"
+                                            initialValue={c.visible}
+                                            className="me-2 align-self-center"
+                                            changeIndicator="background"
+                                        />
+                                        <OMHistory object_type="chapter" object_id={c.id!} />
+                                        <OMEdit object_type="chapter" object_id={c.id!} />
+                                        <OMDelete object_type="chapter" object_id={c.id!} />
+                                    </td>
+                                )}
+                            </tr>
+                        ))}
+                </tbody>
+            </table>
+        </div>
+    );
+}
+
+function AddChapterPopover({
+    courseHandle,
+    lectureId,
+    reloadTimestampState,
+    closePopup,
+}: {
+    courseHandle: string;
+    lectureId: int;
+    reloadTimestampState: int;
+    closePopup: () => void;
+}) {
+    const api = useApi();
+    const authStatus = useAuthStatus();
+    const { language } = useLanguage();
+    const reloadFunc = useReloadBoundary();
+    const knownTimestampState = useRef(-1);
+
+    const [timestampString, setTimestampString] = useState<string | undefined>(undefined);
+    const [name, setName] = useState<string>("");
+    const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
+
+    useEffect(() => {
+        if (reloadTimestampState == knownTimestampState.current) return;
+        knownTimestampState.current = reloadTimestampState;
+        setTimestampString(undefined);
+        setName("");
+
+        import("video.js").then((videojs) => {
+            const timestamp = Math.floor(videojs.default("videoplayer").currentTime()!);
+            setTimestampString(timestampToString(timestamp));
+        });
+    }, [reloadTimestampState, knownTimestampState]);
+
+    const handleSubmit = (e: React.MouseEvent<HTMLButtonElement>) => {
+        e.preventDefault();
+        if (isSubmitting || timestampString === undefined) return;
+        setIsSubmitting(true);
+
+        let timestamp = 0;
+        const timeSplit = timestampString.split(":");
+        for (let i = 0; i < timeSplit.length; i++) {
+            timestamp +=
+                Math.max(0, Math.min(60, parseInt(timeSplit[i]))) *
+                Math.pow(60, timeSplit.length - i - 1);
+        }
+        let promise;
+        if (authStatus.canEditStuff()) {
+            promise = api.createOMObject("chapter", {
+                parent_type: "lecture",
+                parent_id: lectureId,
+                values: {
+                    start_time: timestamp,
+                    name: name,
+                    visible: true,
+                },
+            });
+        } else {
+            promise = api.suggestChapter(courseHandle, lectureId, {
+                start_time: timestamp,
+                name: name,
+            });
+        }
+        promise
+            .then(() => {
+                if (!authStatus.canEditStuff())
+                    showInfoToast(language.get("ui.chapter_suggestion.popup.received_suggestion"));
+                closePopup();
+                reloadFunc();
+            })
+            .catch((e) => {
+                showError(e, "Unable to create chapter");
+            })
+            .finally(() => setIsSubmitting(false));
+
+        return false;
+    };
+
+    return (
+        <>
+            {timestampString === undefined || isSubmitting ? (
+                <div className="spinner-border ms-2" role="status" />
+            ) : (
+                <>
+                    <ul className="list-unstyled m-0">
+                        <li className="p-1">
+                            <StringEditor
+                                value={timestampString}
+                                updateValue={setTimestampString}
+                                maxLength={100 /* For small input */}
+                                regex="^(|[0-9]{1,2}:)[0-9]{1,2}:[0-9]{1,2}$"
+                                regexInvalidMessage={language.get(
+                                    "ui.chapter_suggestion.popup.invalid_time_message",
+                                )}
+                                placeholder={language.get("object.chapter.start_time")}
+                            />
+                        </li>
+                        <li className="p-1">
+                            <StringEditor
+                                value={name}
+                                updateValue={setName}
+                                minLength={1}
+                                maxLength={250}
+                                placeholder={language.get("object.chapter.name")}
+                            />
+                        </li>
+                        <li className="p-1">
+                            <button className="btn btn-primary" onClick={handleSubmit}>
+                                {authStatus.canEditStuff()
+                                    ? language.get("ui.chapter_suggestion.popup.send_create")
+                                    : language.get("ui.chapter_suggestion.popup.send_suggestion")}
+                            </button>
+                        </li>
+                    </ul>
+                </>
+            )}
+        </>
+    );
+}
+
+export function AddChapterButton({
+    courseHandle,
+    lectureId,
+}: {
+    courseHandle: string;
+    lectureId: int;
+}) {
+    const authStatus = useAuthStatus();
+    const { language } = useLanguage();
+
+    const [showPop, setShowPop] = useState(false);
+    const [reloadTimestampState, setReloadTimestampState] = useState(0);
+
+    const buttonPress = () => {
+        if (showPop) {
+            setShowPop(false);
+            return;
+        }
+        setShowPop(true);
+        setReloadTimestampState((old) => old + 1);
+    };
+
+    return (
+        <OverlayTrigger
+            trigger="click"
+            placement="top"
+            overlay={
+                <Popover>
+                    <Popover.Header as="h3">
+                        {authStatus.canEditStuff()
+                            ? language.get("ui.chapter_suggestion.create")
+                            : language.get("ui.chapter_suggestion.suggest")}
+                    </Popover.Header>
+                    <Popover.Body>
+                        <AddChapterPopover
+                            courseHandle={courseHandle}
+                            lectureId={lectureId}
+                            reloadTimestampState={reloadTimestampState}
+                            closePopup={() => setShowPop(false)}
+                        />
+                    </Popover.Body>
+                </Popover>
+            }
+            show={showPop}
+        >
+            <button className="btn btn-primary" onClick={buttonPress}>
+                {authStatus.canEditStuff()
+                    ? language.get("ui.chapter_suggestion.create")
+                    : language.get("ui.chapter_suggestion.suggest")}
+            </button>
+        </OverlayTrigger>
+    );
+}
diff --git a/src/videoag/course/CourseListing.tsx b/src/videoag/course/CourseListing.tsx
index 95ab384c08e7f98220b36f1cd20d4bd0c4d0cdbc..710226c757c1cc57b5925fc0c46fc6b1fcc70bda 100644
--- a/src/videoag/course/CourseListing.tsx
+++ b/src/videoag/course/CourseListing.tsx
@@ -1,7 +1,7 @@
 import Link from "next/link";
 
 import type { GetCourseResponse } from "@/videoag/api/types";
-import { useUserContext } from "@/videoag/authentication/UserDataProvider";
+import { useAuthStatus } from "@/videoag/authentication/AuthStatus";
 import { AuthenticationMethodIcons } from "@/videoag/authentication/ViewPermissions";
 import { useLanguage } from "@/videoag/localization/LanguageProvider";
 import { ResourceType } from "@/videoag/miscellaneous/PromiseHelpers";
@@ -20,8 +20,8 @@ import { LectureListItem } from "./Lecture";
 import DownloadAllModal from "./DownloadManager";
 
 function ListingHeader({ course }: { course: GetCourseResponse }) {
-    const userContext = useUserContext();
-    const hasUserInfo = userContext.hasUserInfo();
+    const authStatus = useAuthStatus();
+    const hasUserInfo = authStatus.hasUserInfo();
     const { editMode } = useEditMode();
     const { language } = useLanguage();
 
diff --git a/src/videoag/course/Lecture.tsx b/src/videoag/course/Lecture.tsx
index 2f9975e9678523077ba53776ee9d8826d9d512e1..6f137290af41983ac294482c2e925e353ccdb453 100644
--- a/src/videoag/course/Lecture.tsx
+++ b/src/videoag/course/Lecture.tsx
@@ -21,7 +21,7 @@ export function urlForLecture(course_id: string, lecture_id: string | number) {
     return `${course_id}/${lecture_id}`;
 }
 
-export function getLectureThumbnailUrl(lecture: lecture): string | undefined {
+export function getLectureThumbnailUrlNoPlaceholder(lecture: lecture): string | undefined {
     if (lecture.is_authenticated) {
         for (let medium of lecture.publish_media ?? []) {
             if (medium.medium_metadata.type === "thumbnail") {
@@ -29,7 +29,11 @@ export function getLectureThumbnailUrl(lecture: lecture): string | undefined {
             }
         }
     }
-    return "/static/empty_thumbnail.png";
+    return undefined;
+}
+
+export function getLectureThumbnailUrl(lecture: lecture): string | undefined {
+    return getLectureThumbnailUrlNoPlaceholder(lecture) ?? "/static/empty_thumbnail.png";
 }
 
 export function LectureListItem({
@@ -65,7 +69,10 @@ export function LectureListItem({
                     >
                         <Link href={`/${course_handle}/${lecture.id}`}>
                             <span className="centered-overlay-container" aria-hidden="true">
-                                <span className={"bi bi-play-circle fs-1"} style={{ color: "lightgrey" }} />
+                                <span
+                                    className={"bi bi-play-circle fs-1"}
+                                    style={{ color: "lightgrey" }}
+                                />
                             </span>
                         </Link>
                     </div>
@@ -112,9 +119,7 @@ export function LectureListItem({
                         {editMode && (
                             <OverlayTrigger
                                 overlay={
-                                    <Tooltip>
-                                        {language.get("object.lecture.description")}
-                                    </Tooltip>
+                                    <Tooltip>{language.get("object.lecture.description")}</Tooltip>
                                 }
                             >
                                 <span className="bi bi-chat-left-quote ms-1 me-1" />
@@ -132,7 +137,10 @@ export function LectureListItem({
                         </span>
                     </li>
                     {(editMode || lecture.internal_comment) && (
-                        <li className="text-muted bg-danger-subtle d-flex mb-1" style={{ borderRadius: "0.3em" }}>
+                        <li
+                            className="text-muted bg-danger-subtle d-flex mb-1"
+                            style={{ borderRadius: "0.3em" }}
+                        >
                             <OverlayTrigger
                                 overlay={
                                     <Tooltip>
@@ -161,12 +169,17 @@ export function LectureListItem({
                                     key={chapter.name}
                                     className={
                                         "d-flex " +
-                                        (chapter.visible === false ? "bg-danger-subtle rounded" : "")
+                                        (chapter.visible === false
+                                            ? "bg-danger-subtle rounded"
+                                            : "")
                                     }
                                 >
                                     <span className="bi bi-play" />
                                     <Link
-                                        href={`/${course_handle}/${lecture.id}?t=` + chapter.start_time}
+                                        href={
+                                            `/${course_handle}/${lecture.id}?t=` +
+                                            chapter.start_time
+                                        }
                                     >
                                         {chapter.name}
                                     </Link>
@@ -197,12 +210,11 @@ export function LectureListItem({
                 <ul className="list-unstyled col-md-4 col-12 pe-0 row align-content-start">
                     <li className="col-xl-7 col-12 p-0">
                         {editMode ? (
-                            <PublishMediumList publish_media={lecture.publish_media} />
+                            <PublishMediumList publish_media={lecture.publish_media!} />
                         ) : (
                             <PublishMediumDownloadButton
                                 publish_media={lecture.publish_media ?? []}
                                 direction="down"
-                                tabIndex="-1"
                                 className="pl-1"
                             />
                         )}
@@ -263,10 +275,13 @@ export function LectureCard({
             {/* display width >= sz */}
             <div className={`d-none d-${sz}-block`}>
                 <div className="row">
-                    <div className="col-4 position-relative" style={{
-                        maxHeight: "6em",
-                        maxWidth: "11em",
-                    }}>
+                    <div
+                        className="col-4 position-relative"
+                        style={{
+                            maxHeight: "6em",
+                            maxWidth: "11em",
+                        }}
+                    >
                         <img
                             style={{
                                 height: "100%",
@@ -277,7 +292,10 @@ export function LectureCard({
                             alt="Vorschaubild"
                         />
                         <span className="centered-overlay-container" aria-hidden="true">
-                            <span className={"bi bi-play-circle fs-3"} style={{ color: "lightgrey" }} />
+                            <span
+                                className={"bi bi-play-circle fs-3"}
+                                style={{ color: "lightgrey" }}
+                            />
                         </span>
                     </div>
                     <div className="col-5 p-0">
@@ -316,7 +334,10 @@ export function LectureCard({
                             alt="Vorschaubild"
                         />
                         <span className="centered-overlay-container" aria-hidden="true">
-                            <span className={"bi bi-play-circle fs-3"} style={{ color: "lightgrey" }} />
+                            <span
+                                className={"bi bi-play-circle fs-3"}
+                                style={{ color: "lightgrey" }}
+                            />
                         </span>
                     </li>
                     <li>
diff --git a/src/videoag/course/Medium.tsx b/src/videoag/course/Medium.tsx
index 3a7a195ae5d9332c6030885724a398e2cf320652..8694cd1ecc9fcbe2aa47ece7932988580121f18b 100644
--- a/src/videoag/course/Medium.tsx
+++ b/src/videoag/course/Medium.tsx
@@ -68,6 +68,10 @@ export function getSortedDownloadablePublishMedia(media: publish_medium[]): publ
     return sortPublishMediaByQuality(media.filter((m) => m.allow_download));
 }
 
+export function getSortedPlayerPublishMedia(media: publish_medium[]): publish_medium[] {
+    return sortPublishMediaByQuality(media.filter((m) => m.include_in_player));
+}
+
 export function PublishMediumList({ publish_media }: { publish_media: publish_medium[] }) {
     const { editMode } = useEditMode();
 
diff --git a/src/videoag/course/Player.tsx b/src/videoag/course/Player.tsx
index a48e8a34b4784aef09257da151b2bafea816d2f0..397c28fa2f40334d33cfcaf7dafc6b59e7f692e8 100644
--- a/src/videoag/course/Player.tsx
+++ b/src/videoag/course/Player.tsx
@@ -1,37 +1,22 @@
 import type React from "react";
-import { MouseEvent, useEffect, useRef, useState } from "react";
+import { MouseEvent, useEffect, useRef } from "react";
 import Link from "next/link";
 import Head from "next/head";
 import OverlayTrigger from "react-bootstrap/OverlayTrigger";
 import Popover from "react-bootstrap/Popover";
-import Dropdown from "react-bootstrap/Dropdown";
 
 import "video.js/dist/video-js.min.css";
 import VideoJsPlayerType from "video.js/dist/types/player";
 import "@silvermine/videojs-quality-selector/dist/css/quality-selector.css";
 import VideoJsMarkers from "@/videoag/miscellaneous/videojs-markers";
 
-import type {
-    course,
-    GetCourseResponse,
-    authentication_method,
-    authentication_methods,
-    chapter,
-    publish_medium,
-    lecture,
-    int,
-} from "@/videoag/api/types";
-import { ApiError } from "@/videoag/api/ApiError";
-import { useApi } from "@/videoag/api/ApiProvider";
-import { useUserContext } from "@/videoag/authentication/UserDataProvider";
-import { showError, showErrorToast } from "@/videoag/error/ErrorDisplay";
+import type { course, chapter, publish_medium, lecture } from "@/videoag/api/types";
+import { ViewPermissionAuthorization } from "@/videoag/authentication/ViewPermissions";
 import { useLanguage } from "@/videoag/localization/LanguageProvider";
-import { filesizeToHuman, timestampToString } from "@/videoag/miscellaneous/Formatting";
-import { useReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary";
 import Title from "@/videoag/miscellaneous/TitleComponent";
 import { ResourceType } from "@/videoag/miscellaneous/PromiseHelpers";
 import { UpdateOverlay } from "@/videoag/miscellaneous/UpdateOverlay";
-import { StylizedText } from "@/videoag/miscellaneous/StylizedText";
+import { showInfoToast } from "@/videoag/miscellaneous/Util";
 import { useEditMode } from "@/videoag/object_management/EditModeProvider";
 import {
     OMDelete,
@@ -41,7 +26,13 @@ import {
 } from "@/videoag/object_management/OMConfigComponent";
 import { basePath } from "@/../basepath";
 
-import { LectureCard, urlForLecture, getLectureThumbnailUrl } from "./Lecture";
+import { LectureCard, urlForLecture, getLectureThumbnailUrlNoPlaceholder } from "./Lecture";
+import {
+    PublishMediumDownloadButton,
+    getSortedPlayerPublishMedia,
+    getNamedPublishMedia,
+} from "./Medium";
+import { Chapters, AddChapterButton } from "./Chapter";
 
 import { PlayerData } from "@/pages/dynamic_player";
 
@@ -77,20 +68,16 @@ function initPlayer(
             .getChild("PlaybackRateMenuButton")!
             .on("contextmenu", changeRate(-1));
     }
-    const player_media = [...publish_media];
-    player_media.filter((medium) =>
-        ["plain_video", "plain_audio"].includes(medium.medium_metadata.type),
+    const sources = getNamedPublishMedia(getSortedPlayerPublishMedia(publish_media)).map(
+        ({ name, medium }) => {
+            return {
+                src: medium.url,
+                type: medium.medium_metadata.type === "plain_video" ? "video/mp4" : "audio/mpeg",
+                label: name,
+                selected: false,
+            };
+        },
     );
-    sortPublishMediaByQuality(player_media);
-
-    const sources = getNamedPublishMedia(player_media).map(({ name, medium }) => {
-        return {
-            src: medium.url,
-            type: medium.medium_metadata.type === "plain_video" ? "video/mp4" : "audio/mpeg",
-            label: name,
-            selected: false,
-        };
-    });
 
     if (sources.length > 0) {
         sources[0].selected = true;
@@ -203,425 +190,17 @@ function VideoPlayer({ lecture, className }: { lecture: lecture; className?: str
     );
 }
 
-function KapitelPopover({
-    timeStr,
-    closePop,
-    courseIdOrHandle,
-    lectureId,
-}: {
-    timeStr: string;
-    closePop: () => void;
-    courseIdOrHandle: string | int;
-    lectureId: number;
-}) {
-    const userContext = useUserContext();
-    const api = useApi();
-    const reloadFunc = useReloadBoundary();
-    const [isSubmitting, setIsSubmitting] = useState(false);
-
-    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
-        e.preventDefault();
-        if (isSubmitting) return;
-        let form = e.currentTarget;
-        let timeHuman = form.time.value; // Format HH:MM:SS
-        let time = 0;
-        let timeSplit = timeHuman.split(":");
-        for (let i = 0; i < timeSplit.length; i++) {
-            time +=
-                Math.max(0, Math.min(60, parseInt(timeSplit[i]))) *
-                Math.pow(60, timeSplit.length - i - 1);
-        }
-
-        let text = form.text.value;
-        setIsSubmitting(true);
-        if (userContext.canEditStuff()) {
-            api.createOMObject("chapter", {
-                parent_type: "lecture",
-                parent_id: lectureId,
-                values: {
-                    start_time: time,
-                    name: text,
-                    visible: true,
-                },
-            })
-                .then(() => {
-                    closePop();
-                    reloadFunc();
-                })
-                .catch((e) => {
-                    showError(e, "Unable to create chapter");
-                })
-                .finally(() => setIsSubmitting(false));
-        } else {
-            api.suggestChapter(courseIdOrHandle, lectureId, {
-                start_time: time,
-                name: text,
-            })
-                .then(() => {
-                    closePop();
-                    reloadFunc();
-                })
-                .catch((e) => {
-                    showError(e, "Unable to suggest chapter");
-                })
-                .finally(() => setIsSubmitting(false));
-        }
-
-        return false;
-    };
-
-    return (
-        <form className="needs-validation" onSubmit={handleSubmit}>
-            <input
-                className="form-control"
-                style={{ marginTop: "6px", marginBottom: "6px" }}
-                placeholder="00:00"
-                name="time"
-                type="text"
-                defaultValue={timeStr}
-                pattern="(|[0-9]{1,2}:(|[0-9]{1,2}:))[0-9]{1,2}"
-            />
-            <input
-                className="form-control"
-                placeholder="Titel"
-                name="text"
-                type="text"
-                style={{ marginBottom: "15px" }}
-                required
-            />
-            <input
-                type="submit"
-                className="btn btn-primary"
-                style={{ marginBottom: "6px" }}
-                value={userContext.canEditStuff() ? "Hinzufügen" : "Vorschlagen"}
-                disabled={isSubmitting}
-            />
-            {isSubmitting && <div className="spinner-border ms-2" role="status" />}
-        </form>
-    );
-}
-
-function AuthorizeHelper({
-    course,
-    lecture,
-    authed_methods,
-}: {
-    course: GetCourseResponse;
-    lecture: lecture;
-    authed_methods: authentication_methods;
-}) {
-    const api = useApi();
-    const reloadFunc = useReloadBoundary();
-    const [userPwErrorState, setUserPwErrorState] = useState<any>();
-    const userContext = useUserContext();
-    const hasUserInfo = userContext.hasUserInfo();
-    const [isLoggingIn, setIsLoggingIn] = useState(false);
-    const { language } = useLanguage();
-
-    useEffect(() => {
-        if (hasUserInfo === true) {
-            // reload page when the user logs in
-            reloadFunc();
-        }
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [hasUserInfo]);
-
-    useEffect(() => {
-        return () => {
-            setIsLoggingIn(false); // this will prevent the interval from running after the component is unmounted
-        };
-    }, []);
-
-    const startOauth = (method: authentication_method) => {
-        if (method !== "moodle" && method !== "rwth") return;
-        if (isLoggingIn) return;
-        setIsLoggingIn(true);
-        const pop = window.open(undefined, "_blank"); // Open popup here already, so the browser doesn't block it (safari gets confused by the async code below)
-        if (!pop) {
-            showErrorToast(
-                "Cannot open popup window, cannot start OAuth. Please send a bug report to video@fsmpi.rwth-aachen.de",
-            );
-            return;
-        }
-        api.startAuthentication({ type: method })
-            .then((res) => {
-                pop.location = res.verification_url;
-                pop.focus();
-                let triggerReload = false;
-                let tries = 0;
-                const testAuth = () => {
-                    tries++;
-                    if (pop.closed || triggerReload || tries > 120 || isLoggingIn === false) {
-                        return; // stop trying
-                    }
-                    api.getAuthenticationStatus({ lecture_id: lecture.id })
-                        .then((res) => {
-                            if (res.is_lecture_authenticated) {
-                                triggerReload = true;
-                            } else if ((res.in_progress_authentication ?? "").length === 0) {
-                                console.log(
-                                    "Authentication appears to have been cancelled by the user",
-                                );
-                                triggerReload = true;
-                            } else {
-                                setTimeout(testAuth, 1000);
-                            }
-                        })
-                        .catch((e) => {
-                            console.error(e);
-                        });
-                };
-                setTimeout(testAuth, 5000);
-                let popInterval = setInterval(() => {
-                    if (pop.closed || triggerReload) {
-                        triggerReload = true;
-                        clearInterval(popInterval);
-                        setIsLoggingIn(false);
-                        reloadFunc();
-                    } else if (isLoggingIn === false) {
-                        clearInterval(popInterval);
-                    }
-                }, 100);
-            })
-            .catch((e) => {
-                showError(e, "Cannot start OAuth");
-                setIsLoggingIn(false);
-            });
-    };
-
-    const handleUserPass = (e: React.FormEvent<HTMLFormElement>) => {
-        e.preventDefault();
-        let form = e.currentTarget;
-        let user = form.username.value;
-        let pass = form.password.value;
-        setIsLoggingIn(true);
-        api.authenticatePassword({ username: user, password: pass, lecture_id: lecture.id })
-            .then(reloadFunc)
-            .catch(setUserPwErrorState)
-            .finally(() => setIsLoggingIn(false));
-        return false;
-    };
-
-    let errorElement = <></>;
-    if (userPwErrorState) {
-        if (
-            userPwErrorState instanceof ApiError &&
-            userPwErrorState.error_code === "authentication_failed"
-        ) {
-            errorElement = (
-                <p className="alert alert-warning">
-                    {language.get("ui.video_player.login_user_password.user_or_pass_incorrect")}
-                </p>
-            );
-        } else errorElement = <p className="alert alert-warning">{userPwErrorState + ""}</p>;
-    } else if (authed_methods.includes("password")) {
-        errorElement = (
-            <p className="alert alert-warning">
-                {language.get("ui.video_player.login_user_password.current_password_incorrect")}
-            </p>
-        );
-    }
-
-    return (
-        <div
-            className="col-12 rounded-2 h-100"
-            style={{ padding: "10px", backgroundColor: "rgba(0, 0, 0, 0.3)" }}
-        >
-            <h3 className="text-center my-2">{language.get("ui.video_player.login_required")}</h3>
-            {course.authentication_information !== undefined && (
-                <p className="text-center">
-                    <StylizedText markdown>{course.authentication_information}</StylizedText>
-                </p>
-            )}
-            <div style={{ paddingBottom: "20px" }} className="container-fluid">
-                <div className="row">
-                    {lecture.authentication_methods.includes("password") && (
-                        <div className="col-sm-4">
-                            <h4 className="text-center">
-                                <span className="bi bi-lock" aria-hidden="true" />{" "}
-                                {language.get("ui.video_player.login_user_password.description")}
-                            </h4>
-                            {errorElement}
-                            <form onSubmit={handleUserPass}>
-                                <div className="mb-3">
-                                    <label htmlFor="exampleInputEmail1" className="form-label">
-                                        {language.get("object.permission.username")}
-                                    </label>
-                                    <input
-                                        type="text"
-                                        className="form-control"
-                                        id="username"
-                                        name="username"
-                                        placeholder=""
-                                    />
-                                </div>
-                                <div className="mb-3">
-                                    <label htmlFor="exampleInputPassword1" className="form-label">
-                                        {language.get("object.permission.password")}
-                                    </label>
-                                    <input
-                                        type="password"
-                                        className="form-control"
-                                        id="password"
-                                        name="password"
-                                        placeholder=""
-                                    />
-                                </div>
-                                <button
-                                    type="submit"
-                                    className="btn btn-secondary"
-                                    disabled={isLoggingIn}
-                                >
-                                    {language.get("ui.video_player.login")}
-                                </button>
-                                {isLoggingIn && (
-                                    <div
-                                        className="ms-2 spinner-border text-primary"
-                                        role="status"
-                                    />
-                                )}
-                            </form>
-                        </div>
-                    )}
-                    {lecture.authentication_methods.includes("rwth") && (
-                        <div className="col-sm-4">
-                            <h4 className="text-center">
-                                <span className="bi bi-person-fill" aria-hidden="true" /> RWTH
-                            </h4>
-                            <p>{language.get("ui.video_player.login_rwth.description")}</p>
-                            <button
-                                type="button"
-                                onClick={() => startOauth("rwth")}
-                                className="btn btn-secondary"
-                                disabled={isLoggingIn}
-                            >
-                                {language.get("ui.video_player.login")}
-                            </button>
-                            {isLoggingIn && (
-                                <div className="ms-2 spinner-border text-primary" role="status" />
-                            )}
-                        </div>
-                    )}
-                    {lecture.authentication_methods.includes("moodle") && (
-                        <div className="col-sm-4">
-                            <h4 className="text-center">
-                                <span className="bi bi-person-fill" aria-hidden="true" /> Moodle
-                            </h4>
-                            <p>{language.get("ui.video_player.login_moodle.description")}</p>
-
-                            {authed_methods.includes("moodle") ? (
-                                <>
-                                    <p className="alert alert-info">
-                                        {language.get("ui.video_player.login_moodle.not_in_course")}
-                                    </p>
-                                    <button
-                                        type="button"
-                                        onClick={() => startOauth("moodle")}
-                                        className="btn btn-secondary"
-                                        disabled={isLoggingIn}
-                                    >
-                                        {language.get(
-                                            "ui.video_player.login_moodle.refresh_course",
-                                        )}
-                                    </button>
-                                </>
-                            ) : (
-                                <button
-                                    type="button"
-                                    onClick={() => startOauth("moodle")}
-                                    className="btn btn-secondary"
-                                    disabled={isLoggingIn}
-                                >
-                                    {language.get("ui.video_player.login")}
-                                </button>
-                            )}
-                            {isLoggingIn && (
-                                <div className="ms-2 spinner-border text-primary" role="status" />
-                            )}
-                        </div>
-                    )}
-                </div>
-                {!lecture.authentication_methods.includes("rwth") &&
-                    !lecture.authentication_methods.includes("moodle") &&
-                    !lecture.authentication_methods.includes("password") && (
-                        <p className="alert alert-info" style={{ marginTop: "2em" }}>
-                            {language.get("ui.video_player.student_council_only")}
-                        </p>
-                    )}
-            </div>
-        </div>
-    );
-}
-
-function Chapters({ chapters, seekTo }: { chapters?: chapter[]; seekTo: (time: number) => void }) {
-    const { editMode } = useEditMode();
-
-    if (!chapters || chapters.length == 0) return <></>;
-
-    return (
-        <div className="col-12 table-responsive" style={{ paddingTop: "10px" }}>
-            <h4>
-                <strong>Kapitel:</strong>
-            </h4>
-            <table className="table">
-                <tbody>
-                    {chapters
-                        .sort((a, b) => a.start_time - b.start_time)
-                        .map((c) => {
-                            const bgColor = c.visible === false ? "bg-danger-subtle" : "bg-body";
-                            return (
-                                <tr
-                                    key={`${c.start_time}+${c.name}`}
-                                    className={"w-100 " + bgColor}
-                                >
-                                    <td style={{ width: "130px" }} className={bgColor}>
-                                        <a
-                                            className="chapterlink"
-                                            onClick={() => {
-                                                seekTo(c.start_time);
-                                            }}
-                                            href="#"
-                                        >
-                                            {timestampToString(c.start_time)}
-                                        </a>
-                                    </td>
-                                    <td className={bgColor}>{c.name}</td>
-                                    {editMode && (
-                                        <td className={"d-flex " + bgColor}>
-                                            <div className="flex-fill" />
-                                            <EmbeddedOMFieldComponent
-                                                object_type="chapter"
-                                                object_id={c.id!}
-                                                field_id="visible"
-                                                field_type="boolean"
-                                                initialValue={c.visible}
-                                                className="me-2 align-self-center"
-                                                changeIndicator="background"
-                                            />
-                                            <OMDelete object_type="chapter" object_id={c.id!} />
-                                        </td>
-                                    )}
-                                </tr>
-                            );
-                        })}
-                </tbody>
-            </table>
-        </div>
-    );
-}
-
-function VideoSuggestions({ course, lecture }: { course: course; lecture: lecture }) {
+function LectureSuggestions({ course, lecture }: { course: course; lecture: lecture }) {
     const { language } = useLanguage();
-    // find previous and next lecture
-    const sortedLectures = course.lectures?.sort((a, b) => {
-        // time looks like this: 2015-10-20T12:15:00
-        return new Date(a.time).valueOf() - new Date(b.time).valueOf();
-    });
-    const prevLecture = sortedLectures?.findLast(
-        (l) => new Date(l.time) < new Date(lecture.time) && (l.publish_media ?? []).length > 0,
+    const prevLecture = course.lectures?.findLast(
+        (l) =>
+            new Date(l.time) < new Date(lecture.time) &&
+            (l.publish_media ?? []).find((m) => m.include_in_player),
     );
-    const nextLecture = sortedLectures?.find(
-        (l) => new Date(l.time) > new Date(lecture.time) && (l.publish_media ?? []).length > 0,
+    const nextLecture = course.lectures?.find(
+        (l) =>
+            new Date(l.time) > new Date(lecture.time) &&
+            (l.publish_media ?? []).find((m) => m.include_in_player),
     );
 
     return (
@@ -670,8 +249,67 @@ function VideoSuggestions({ course, lecture }: { course: course; lecture: lectur
     );
 }
 
-// TODO move and rework
+function EmbedButton({
+    course,
+    lecture,
+    className,
+}: {
+    course: course;
+    lecture: lecture;
+    className?: string | undefined;
+}) {
+    const { language } = useLanguage();
+
+    if (!course.allow_embed) {
+        return <></>;
+    }
+    const embedCode = `<div style="position:relative;padding-top:56.25%;"><iframe style="position:absolute;top:0;left:0;width:100%;height:100%;overflow: hidden;border: none;" src="${window.location.origin}${basePath}/${course.handle}/${lecture.id}/embed" allowfullscreen="true" scrolling="no"></iframe></div>`;
+    const copyEmbedCode = () => {
+        var embedCodeInput = document.getElementById("embedInput") as HTMLInputElement;
+
+        embedCodeInput.select();
+        embedCodeInput.setSelectionRange(0, embedCodeInput.value.length); // For mobile devices
 
+        navigator.clipboard.writeText(embedCodeInput.value);
+        showInfoToast("Copied embed code!");
+    };
+    return (
+        <span className={className}>
+            <OverlayTrigger
+                trigger="click"
+                placement="top"
+                overlay={
+                    <Popover>
+                        <Popover.Header as="h3">
+                            {language.get("ui.video_player.embed")}
+                        </Popover.Header>
+                        <Popover.Body>
+                            <input
+                                id="embedInput"
+                                type="text"
+                                className="w-100"
+                                onClick={(e: MouseEvent<HTMLInputElement>) =>
+                                    (e.target as HTMLInputElement).select()
+                                }
+                                value={embedCode}
+                                readOnly
+                            />
+                            <button
+                                type="button"
+                                className="btn btn-primary mt-2"
+                                onClick={copyEmbedCode}
+                            >
+                                Kopieren
+                            </button>
+                        </Popover.Body>
+                    </Popover>
+                }
+            >
+                <button className="btn btn-primary">{language.get("ui.video_player.embed")}</button>
+            </OverlayTrigger>
+        </span>
+    );
+}
 
 export function EmbeddedPlayer({
     playerData,
@@ -680,9 +318,7 @@ export function EmbeddedPlayer({
     playerData: ResourceType<PlayerData>;
     disabled: boolean;
 }) {
-    const { course, perms, lectureId } = playerData.read()!;
-
-    const lecture = course.lectures!.find((l) => l.id === lectureId)!;
+    const { course, lecture } = playerData.read()!;
 
     useEffect(() => {
         import("video.js");
@@ -696,7 +332,7 @@ export function EmbeddedPlayer({
         );
     }
 
-    if (!perms.is_lecture_authenticated) {
+    if (!lecture.is_authenticated) {
         // Show link to page
         return (
             <div className="h-100 w-100 d-flex flex-column justify-content-center">
@@ -729,176 +365,53 @@ export default function Player({
     playerData: ResourceType<PlayerData>;
     disabled: boolean;
 }) {
-    const { course, lectureId, perms, loaderHadUserInfo } = playerData.read()!;
-    const api = useApi();
-    const userContext = useUserContext();
-    const hasUserInfo = userContext.hasUserInfo();
     const { language } = useLanguage();
 
-    const lecture = course.lectures!.find((l) => l.id === lectureId)!;
+    const { course, lecture } = playerData.read()!;
 
     useEffect(() => {
         import("video.js");
     }, []);
 
-    const [popContent, setPopContent] = useState<React.ReactNode | null>(null);
-    const [showPop, setShowPop] = useState(false);
     const { editMode } = useEditMode();
-    const reloadFunc = useReloadBoundary();
-
-    useEffect(() => {
-        if (hasUserInfo === loaderHadUserInfo) return;
-        reloadFunc();
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [loaderHadUserInfo, hasUserInfo]);
-
-    const vorschlagenClick = () => {
-        if (showPop) {
-            setShowPop(false);
-            return;
-        }
-        import("video.js").then((videojs) => {
-            let timestamp = videojs.default("videoplayer").currentTime()!;
-            let tstr = timestampToString(timestamp);
-
-            setPopContent(
-                <KapitelPopover
-                    timeStr={tstr}
-                    key={tstr}
-                    closePop={() => setShowPop(false)}
-                    courseIdOrHandle={course.handle}
-                    lectureId={lectureId}
-                />,
-            );
-            setShowPop(true);
-        });
-    };
 
-    let seekTo = (time: number) => {
-        import("video.js").then((videojs) => {
-            let player = videojs.default("videoplayer");
-            player.currentTime(time);
-        });
-    };
-
-    const kapitelPopover = (
-        <Popover>
-            <Popover.Header as="h3">
-                {userContext.canEditStuff() ? "Neues Kapitel" : "Kapitelmarker vorschlagen"}
-            </Popover.Header>
-            <Popover.Body>{popContent}</Popover.Body>
-        </Popover>
-    );
-
-    let einbettenText = `<div style="position:relative;padding-top:56.25%;"><iframe style="position:absolute;top:0;left:0;width:100%;height:100%;overflow: hidden;border: none;" src="${window.location.origin}${basePath}/${course.handle}/${lecture.id}/embed" allowfullscreen="true" scrolling="no"></iframe></div>`;
-    const einbettenCopy = () => {
-        var copyText = document.getElementById("einbettenInput") as HTMLInputElement;
-
-        copyText.select();
-        copyText.setSelectionRange(0, copyText.value.length); // For mobile devices
-
-        navigator.clipboard.writeText(copyText.value);
-    };
-    const einbettenPopover = (
-        <Popover>
-            <Popover.Header as="h3">Einbetten</Popover.Header>
-            <Popover.Body>
-                <input
-                    id="einbettenInput"
-                    type="text"
-                    className="w-100"
-                    onClick={(e: MouseEvent<HTMLInputElement>) =>
-                        (e.target as HTMLInputElement).select()
-                    }
-                    value={einbettenText}
-                    readOnly
-                />
-                <button type="button" className="btn btn-primary mt-2" onClick={einbettenCopy}>
-                    Kopieren
-                </button>
-            </Popover.Body>
-        </Popover>
-    );
-
-    let pageContent = (
-        <AuthorizeHelper
-            lecture={lecture}
-            course={course}
-            authed_methods={perms.authenticated_methods}
-        />
-    );
-    if (perms.is_lecture_authenticated) {
+    let seekTo = undefined;
+    let pageContent;
+    if (lecture.is_authenticated) {
+        seekTo = (time: number) => {
+            import("video.js").then((videojs) => {
+                let player = videojs.default("videoplayer");
+                player.currentTime(time);
+            });
+        };
         pageContent = (
             <>
                 <div className="col-12 p-0">
                     <VideoPlayer lecture={lecture} />
                 </div>
-                <div className="col-12 pt-4">
-                    <OverlayTrigger
-                        trigger="click"
-                        placement="top"
-                        overlay={kapitelPopover}
-                        show={showPop}
-                    >
-                        <button
-                            className="btn btn-primary"
-                            id="hintnewchapter"
-                            onClick={vorschlagenClick}
-                        >
-                            {userContext.canEditStuff()
-                                ? language.get("ui.video_player.add_chapter")
-                                : language.get("ui.video_player.suggest_chapter")}
-                        </button>
-                    </OverlayTrigger>
+                <div className="col-12 pt-3">
+                    <AddChapterButton courseHandle={course.handle} lectureId={lecture.id} />
 
                     <ul className="list-inline float-none float-sm-end mb-0 mt-2 mt-sm-0">
-                        {course.allow_embed && (
-                            <li className="list-inline-item">
-                                <OverlayTrigger
-                                    trigger="click"
-                                    placement="top"
-                                    overlay={einbettenPopover}
-                                >
-                                    <a className="btn btn-primary">
-                                        <span>{language.get("ui.video_player.embed")}</span>
-                                    </a>
-                                </OverlayTrigger>
-                            </li>
-                        )}
-                        {lecture.publish_media &&
-                            lecture.publish_media.some((ele) => ele.url !== undefined) && (
-                                <li className="list-inline-item">
-                                    <DownloadMedia
-                                        className="list-inline-item"
-                                        direction="up"
-                                        publish_media={lecture.publish_media ?? []}
-                                    />
-                                </li>
-                            )}
+                        <EmbedButton
+                            course={course}
+                            lecture={lecture}
+                            className="list-inline-item"
+                        />
+                        <PublishMediumDownloadButton
+                            publish_media={lecture.publish_media ?? []}
+                            direction="up"
+                            className="list-inline-item"
+                        />
                     </ul>
                 </div>
-                {lecture.description.length > 0 && (
-                    <div className="mt-2">
-                        <h5>Beschreibung</h5>
-
-                        {
-                            <EmbeddedOMFieldComponent
-                                object_type="lecture"
-                                object_id={lecture.id}
-                                field_id="description"
-                                field_type="string"
-                                initialValue={lecture.description}
-                                allowMarkdown={true}
-                            />
-                        }
-                    </div>
-                )}
-                <Chapters chapters={lecture.chapters} seekTo={seekTo} />
             </>
         );
+    } else {
+        pageContent = <ViewPermissionAuthorization lecture={lecture} course={course} />;
     }
 
-    const thumbnailUrl = getLectureThumbnailUrl(lecture);
+    const thumbnailUrlNoPlaceholder = getLectureThumbnailUrlNoPlaceholder(lecture);
 
     return (
         <>
@@ -906,7 +419,9 @@ export default function Player({
                 <meta property="og:title" content={`${course.short_name} - ${lecture.title}`} />
                 {/* TODO Why not lecture description? */}
                 <meta property="og:description" content={course.description} />
-                {thumbnailUrl && <meta property="og:image" content={thumbnailUrl} />}
+                {thumbnailUrlNoPlaceholder && (
+                    <meta property="og:image" content={thumbnailUrlNoPlaceholder} />
+                )}
             </Head>
             <Title title={`${course.short_name} - ${lecture.title}`} />
             <UpdateOverlay show={disabled} />
@@ -969,7 +484,25 @@ export default function Player({
                     </div>
                     <div className="row mb-2">{pageContent}</div>
 
-                    <VideoSuggestions course={course} lecture={lecture} />
+                    {lecture.description.length > 0 && (
+                        <div className="mt-2">
+                            <h5>Beschreibung:</h5>
+
+                            {
+                                <EmbeddedOMFieldComponent
+                                    object_type="lecture"
+                                    object_id={lecture.id}
+                                    field_id="description"
+                                    field_type="string"
+                                    initialValue={lecture.description}
+                                    allowMarkdown={true}
+                                />
+                            }
+                        </div>
+                    )}
+                    <Chapters chapters={lecture.chapters} seekTo={seekTo} />
+
+                    <LectureSuggestions course={course} lecture={lecture} />
 
                     {lecture.visible === false && (
                         <>
diff --git a/src/videoag/miscellaneous/PromiseHelpers.tsx b/src/videoag/miscellaneous/PromiseHelpers.tsx
index 76f38d029237093090215ee680b7c9bf6faba994..60508a2327db123a84f990013430ae4eb3928c5d 100644
--- a/src/videoag/miscellaneous/PromiseHelpers.tsx
+++ b/src/videoag/miscellaneous/PromiseHelpers.tsx
@@ -124,7 +124,9 @@ export function useDebounceUpdateData<D, R>(
             if (unprocessedDataRef.current !== undefined) {
                 if (unprocessedDataRef.current === data) {
                     unprocessedDataRef.current = undefined;
-                } else {
+                } else if (timeoutIdRef.current === undefined) {
+                    // Only run immediately when no timeout is set. A call to deferred should still be deferred even if
+                    // an update was running during the call.
                     updateData();
                 }
             }
@@ -139,10 +141,17 @@ export function useDebounceUpdateData<D, R>(
         (newData: D) => {
             unprocessedDataRef.current = newData;
             clearTimeout(timeoutIdRef.current);
-            timeoutIdRef.current = setTimeout(updateData, delay);
+            timeoutIdRef.current = undefined;
+
+            const timeoutRef = setTimeout(() => {
+                if (timeoutIdRef.current === timeoutRef) timeoutIdRef.current = undefined;
+                updateData();
+            }, delay);
+            timeoutIdRef.current = timeoutRef;
         },
         (newData: D) => {
             clearTimeout(timeoutIdRef.current);
+            timeoutIdRef.current = undefined;
             unprocessedDataRef.current = newData;
             return updateData()!;
         },
diff --git a/src/videoag/site/DefaultLayout.tsx b/src/videoag/site/DefaultLayout.tsx
index d0fe15bd578c7b1fe2a098a8d827650995619fa8..6546a935becab144ff2091a61a4743d0b1b0a85e 100644
--- a/src/videoag/site/DefaultLayout.tsx
+++ b/src/videoag/site/DefaultLayout.tsx
@@ -1,22 +1,19 @@
 import Link from "next/link";
 import type React from "react";
-import { MouseEvent, startTransition, useEffect, useState } from "react";
+import { MouseEvent, useEffect, useState } from "react";
 import { useRouter } from "next/router";
 import { DropdownButton } from "react-bootstrap";
 import Collapse from "react-bootstrap/Collapse";
 import Dropdown from "react-bootstrap/Dropdown";
-import OverlayTrigger from "react-bootstrap/OverlayTrigger";
-import Popover from "react-bootstrap/Popover";
 
 import type { GetStatusResponse } from "@/videoag/api/types";
 import { useApi } from "@/videoag/api/ApiProvider";
-import { ApiError } from "@/videoag/api/ApiError";
-import { useUserContext } from "@/videoag/authentication/UserDataProvider";
-import { showError, showErrorToast, ErrorComponent } from "@/videoag/error/ErrorDisplay";
+import { useAuthStatus } from "@/videoag/authentication/AuthStatus";
+import UserField from "@/videoag/authentication/UserField";
+import { showErrorToast, ErrorComponent } from "@/videoag/error/ErrorDisplay";
 import { FallbackErrorBoundary } from "@/videoag/error/FallbackErrorBoundary";
 import { useLanguage } from "@/videoag/localization/LanguageProvider";
 import { storageGetOrDefault, storageSet } from "@/videoag/miscellaneous/Storage";
-import { TooltipButton } from "@/videoag/miscellaneous/Util";
 import { ReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary";
 import { StylizedText } from "@/videoag/miscellaneous/StylizedText";
 import { useEditMode } from "@/videoag/object_management/EditModeProvider";
@@ -67,200 +64,6 @@ function NavBarIcon({
     );
 }
 
-function UserField({ isUnavailable }: { isUnavailable: boolean }) {
-    const api = useApi();
-    const [loginError, setLoginError] = useState("");
-    const userContext = useUserContext();
-    const [showPop, setShowPop] = useState(false);
-    const [forceReload, setForceReload] = useState(0);
-    const [triedRelogin, setTriedRelogin] = useState(false);
-    const { editMode, setEditMode } = useEditMode();
-    const [isLoggingIn, setIsLoggingIn] = useState(false);
-    const { language } = useLanguage();
-
-    useEffect(() => {
-        if (!isUnavailable && triedRelogin === false && userContext.hasUserInfo() === false) {
-            api.getAuthenticationStatus()
-                .then((resp) => {
-                    startTransition(() => {
-                        setTriedRelogin(true);
-                        if (resp.user) {
-                            userContext.replace(resp.user);
-                            setForceReload((f) => f + 1);
-                            setShowPop(false);
-                        } else {
-                            userContext.replace(null);
-                        }
-                    });
-                })
-                .catch((e) => {
-                    startTransition(() => {
-                        setTriedRelogin(true);
-                        userContext.replace(null);
-                        console.error("Failed to get authentication status", e);
-                    });
-                });
-        }
-    }, [api, triedRelogin, userContext, isUnavailable]);
-
-    const onFsmpiLogin = (e: React.FormEvent<HTMLFormElement>) => {
-        e.preventDefault();
-        const form = e.currentTarget;
-        const name = form.username.value;
-        const password = form.password.value;
-        setIsLoggingIn(true);
-
-        api.authenticateFsmpi({ username: name, password })
-            .then(
-                (resp) => {
-                    userContext.replace(resp.user!);
-                    setForceReload(forceReload + 1);
-                    setShowPop(false);
-                    setLoginError("");
-                },
-                (err) => {
-                    if (err instanceof ApiError) {
-                        setLoginError("Error: " + err.api_message);
-                    } else {
-                        setLoginError("Login failed " + err.message);
-                    }
-                    userContext.replace(null);
-                },
-            )
-            .then(() => {
-                setIsLoggingIn(false);
-            });
-    };
-
-    const onLogout = () => {
-        api.logout()
-            .then(() => {
-                userContext.replace(null);
-                setEditMode(false);
-                setForceReload(forceReload + 1);
-            })
-            .catch((e) => {
-                console.error(e);
-                showError(e, "Unable to log out");
-            });
-    };
-
-    if (isUnavailable) {
-        return (
-            <TooltipButton iconClassName="bi-box-arrow-in-right">
-                {language.get("ui.login.unavailable_message")}
-            </TooltipButton>
-        );
-    }
-
-    if (triedRelogin === false) {
-        return <div className="spinner-border text-primary me-2" role="status" />;
-    }
-
-    if (userContext.hasUserInfo() === false) {
-        const loginPop = (
-            <Popover>
-                <Popover.Header as="h3">Login für FSMPI</Popover.Header>
-                <Popover.Body>
-                    <form onSubmit={onFsmpiLogin}>
-                        <input
-                            placeholder="User"
-                            name="username"
-                            type="text"
-                            className="form-control mb-2"
-                        />
-                        <input
-                            placeholder="Password"
-                            name="password"
-                            type="password"
-                            className="form-control mb-3"
-                        />
-                        {loginError !== "" ? (
-                            <div className="alert alert-danger" role="alert">
-                                {loginError}
-                            </div>
-                        ) : (
-                            <></>
-                        )}
-                        <div className="d-flex align-items-center">
-                            <input
-                                type="submit"
-                                value="Login"
-                                className="btn btn-primary"
-                                disabled={isLoggingIn}
-                            />
-                            {isLoggingIn && <div className="spinner-border ms-3" />}
-                        </div>
-                    </form>
-                </Popover.Body>
-            </Popover>
-        );
-
-        return (
-            <OverlayTrigger trigger="click" placement="bottom" overlay={loginPop}>
-                <button className="btn" type="button" onClick={() => setShowPop(!showPop)}>
-                    <span className="bi bi-box-arrow-in-right" />
-                </button>
-            </OverlayTrigger>
-        );
-    }
-
-    return (
-        <>
-            {userContext.canEditStuff() && (
-                <>
-                    <div
-                        className="form-check form-switch me-2"
-                        title="Press 'e' to toggle edit mode"
-                    >
-                        <input
-                            className="form-check-input"
-                            type="checkbox"
-                            role="switch"
-                            id="flexSwitchCheckDefault"
-                            onChange={() => {
-                                setEditMode(!editMode);
-                            }}
-                            checked={editMode}
-                        />
-                        <label className="form-check-label" htmlFor="flexSwitchCheckDefault">
-                            Edit Mode
-                        </label>
-                    </div>
-                    <div className="vr d-none d-lg-inline-block" />
-                </>
-            )}
-            <Dropdown className="ms-1">
-                <Dropdown.Toggle variant="" style={{ padding: "10px 6px" }}>
-                    {userContext.getUserInfo()!.display_name} <span className="caret" />
-                </Dropdown.Toggle>
-                <Dropdown.Menu className="me-2">
-                    <li>
-                        <Dropdown.Item as={Link} href="/internal/changelog">
-                            Changelog
-                        </Dropdown.Item>
-                    </li>
-                    <li>
-                        <Dropdown.Item as={Link} href="/internal/timetable">
-                            Drehplan
-                        </Dropdown.Item>
-                    </li>
-                    <li className="dropdown-divider" />
-                    <li>
-                        <Dropdown.Item as={Link} href="/internal/user">
-                            Settings
-                        </Dropdown.Item>
-                    </li>
-                    <li className="dropdown-divider" />
-                    <li>
-                        <Dropdown.Item onClick={onLogout}>Logout</Dropdown.Item>
-                    </li>
-                </Dropdown.Menu>
-            </Dropdown>
-        </>
-    );
-}
-
 export function Search({
     className,
     queryCallback,
@@ -526,8 +329,8 @@ function NavBar({ status }: { status?: GetStatusResponse }) {
 
 function Footer({ status }: { status?: GetStatusResponse }) {
     const { language } = useLanguage();
-    const userContext = useUserContext();
-    const hasUserInfo = userContext.hasUserInfo();
+    const authStatus = useAuthStatus();
+    const hasUserInfo = authStatus.hasUserInfo();
 
     return (
         <footer className={"footer bg-body-tertiary"}>
@@ -646,8 +449,8 @@ function Footer({ status }: { status?: GetStatusResponse }) {
 export default function DefaultLayout({ children }: { children: React.ReactNode }) {
     const route = useRouter().pathname;
     const api = useApi();
-    const userContext = useUserContext();
-    const hasUserInfo = userContext.hasUserInfo();
+    const authStatus = useAuthStatus();
+    const hasUserInfo = authStatus.hasUserInfo();
     const { setEditMode } = useEditMode();
     const { language } = useLanguage();
     const [status, setStatus] = useState<GetStatusResponse>();
@@ -660,7 +463,7 @@ export default function DefaultLayout({ children }: { children: React.ReactNode
         const onKeydown = (e: KeyboardEvent) => {
             if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement)
                 return;
-            if (e.key === "e" && userContext.canEditStuff()) {
+            if (e.key === "e" && authStatus.canEditStuff()) {
                 setEditMode((e) => !e);
             }
         };
@@ -669,7 +472,7 @@ export default function DefaultLayout({ children }: { children: React.ReactNode
         return () => {
             document.removeEventListener("keydown", onKeydown);
         };
-    }, [isEmbed, setEditMode, userContext]);
+    }, [isEmbed, setEditMode, authStatus]);
 
     useEffect(() => {
         api.getStatus()