diff --git a/api/routers/lectures.py b/api/routers/lectures.py
index 94983ce..5e3c42c 100644
--- a/api/routers/lectures.py
+++ b/api/routers/lectures.py
@@ -13,6 +13,7 @@
get_lecture_by_public_id_and_language,
delete_lecture_course_relation,
get_all_denied_lectures,
+ get_all_failed_lectures,
get_unfinished_lectures,
find_course_code,
get_all_lectures,
@@ -86,6 +87,7 @@ def get_all(
summary: Union[bool, None] = None,
only_unfinished: Union[bool, None] = None,
only_denied: Union[bool, None] = None,
+ only_failed: Union[bool, None] = None,
include_denied: Union[bool, None] = False,
include_failed: Union[bool, None] = False,
random: Union[bool, None] = None,
@@ -95,6 +97,9 @@ def get_all(
elif only_denied:
include_denied = True
lectures = get_all_denied_lectures()
+ elif only_failed:
+ include_failed = True
+ lectures = get_all_failed_lectures()
else:
lectures = get_all_lectures()
diff --git a/db/crud.py b/db/crud.py
index f92403b..e8b3e52 100644
--- a/db/crud.py
+++ b/db/crud.py
@@ -48,6 +48,18 @@ def get_all_denied_lectures():
return out
+def get_all_failed_lectures():
+ from db.models.lecture import Analysis
+ lectures = get_all_lectures()
+
+ out = []
+ for lecture in lectures:
+ if lecture.get_last_analysis().state == Analysis.State.FAILURE:
+ out.append(lecture)
+
+ return out
+
+
def get_unfinished_lectures():
from db.models.lecture import Analysis
lectures = get_all_lectures()
diff --git a/web-ui/.umirc.ts b/web-ui/.umirc.ts
index fcd3110..837be0b 100644
--- a/web-ui/.umirc.ts
+++ b/web-ui/.umirc.ts
@@ -7,6 +7,7 @@ export default defineConfig({
{ path: '/questions/lectures/:id/:language', component: 'questions' },
{ path: '/queue', component: 'queue' },
{ path: '/denied', component: 'denied' },
+ { path: '/failures', component: 'failures' },
{ path: '/about', component: 'about' },
{ path: '*', component: '404' },
diff --git a/web-ui/src/components/selector/lecture-adder.tsx b/web-ui/src/components/selector/lecture-adder.tsx
index 53b4afe..180ba43 100644
--- a/web-ui/src/components/selector/lecture-adder.tsx
+++ b/web-ui/src/components/selector/lecture-adder.tsx
@@ -360,7 +360,7 @@ export default function LectureAdder() {
- This process usually takes between 10 and 30 minutes, depending on the
+ This process usually takes between 5 and 15 minutes, depending on the
length of the lecture and how many other lectures are being watched.
diff --git a/web-ui/src/components/tables/failures-table.less b/web-ui/src/components/tables/failures-table.less
new file mode 100644
index 0000000..c635376
--- /dev/null
+++ b/web-ui/src/components/tables/failures-table.less
@@ -0,0 +1,10 @@
+
+.flag {
+ width: 100% !important;
+ height: 50px;
+ border-radius: 3px;
+}
+
+.logo {
+ border-radius: 3px;
+}
diff --git a/web-ui/src/components/tables/failures-table.tsx b/web-ui/src/components/tables/failures-table.tsx
new file mode 100644
index 0000000..80d8111
--- /dev/null
+++ b/web-ui/src/components/tables/failures-table.tsx
@@ -0,0 +1,157 @@
+import { Table, Image, Typography } from 'antd';
+import { notification } from 'antd';
+import { useEffect, useState } from 'react';
+import { useMutation } from '@tanstack/react-query';
+import apiClient, { ServerErrorResponse, ServerResponse } from '@/http';
+import { Lecture } from '@/components/lecture';
+import styles from './denied-table.less';
+import { ColumnsType } from 'antd/es/table';
+import svFlag from '@/assets/flag-sv.svg';
+import enFlag from '@/assets/flag-en.svg';
+import kthLogo from '@/assets/kth.svg';
+import youtubeLogo from '@/assets/youtube.svg';
+
+const { Link } = Typography;
+
+const UPDATE_INTERVAL = 5000;
+
+const columns: ColumnsType = [
+ {
+ title: 'Source',
+ dataIndex: 'source',
+ render: (source: string) => {
+ let icon = '';
+ if (source === 'youtube') {
+ icon = youtubeLogo;
+ } else if (source === 'kth') {
+ icon = kthLogo;
+ }
+ return (
+
+ );
+ },
+ },
+ {
+ title: 'Title',
+ dataIndex: 'title',
+ },
+ {
+ title: 'Analysis',
+ dataIndex: 'combined_public_id_and_lang',
+ render: (combined_public_id_and_lang: string) => (
+ <>
+
+ View Progress
+
+ >
+ ),
+ },
+ {
+ title: 'Content Link',
+ dataIndex: 'content_link',
+ render: (content_link: string) => (
+ <>
+
+ {content_link}
+
+ >
+ ),
+ },
+ {
+ title: 'Language',
+ dataIndex: 'language',
+ render: (val: string) => {
+ let flagIcon = '';
+ if (val === 'sv') {
+ flagIcon = svFlag;
+ } else if (val === 'en') {
+ flagIcon = enFlag;
+ }
+ return (
+
+ );
+ },
+ },
+ {
+ title: 'Added At',
+ dataIndex: 'created_at',
+ render: (val: string) => {
+ const d = new Date(val);
+ const date = d.toDateString();
+ const time = d.toLocaleTimeString('sv');
+ return (
+ <>
+ {date}, {time}
+ >
+ );
+ },
+ },
+ {
+ title: 'Progress',
+ dataIndex: 'overall_progress',
+ render: (val: string) => <>{val}%>,
+ },
+];
+
+interface LectureResponse extends ServerResponse {
+ data: Lecture[];
+}
+
+export default function FailuresTable() {
+ const [lectures, setLectures] = useState([]);
+ const [notificationApi, contextHolder] = notification.useNotification();
+
+ const { mutate: fetchLectures } = useMutation(
+ async () => {
+ return await apiClient.get(`/lectures?summary=true&only_failed=true`);
+ },
+ {
+ onSuccess: (res: LectureResponse) => {
+ const result = {
+ status: res.status + '-' + res.statusText,
+ headers: res.headers,
+ data: res.data,
+ };
+ for (let i = 0; i < res.data.length; i++) {
+ const lecture = res.data[i];
+ lecture.combined_public_id_and_lang = `${lecture.public_id}/${lecture.language}`;
+ lecture['key'] = lecture.combined_public_id_and_lang;
+ }
+ setLectures(result.data as Lecture[]);
+ },
+ onError: (err: ServerErrorResponse) => {
+ notificationApi['error']({
+ message: 'Failed to get lectures',
+ description: err.response.data.detail,
+ });
+ },
+ }
+ );
+
+ useEffect(() => {
+ fetchLectures();
+ }, [fetchLectures]);
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ fetchLectures();
+ }, UPDATE_INTERVAL);
+
+ return () => clearInterval(interval);
+ }, [fetchLectures]);
+
+ return (
+ <>
+ {contextHolder}
+
+ >
+ );
+}
diff --git a/web-ui/src/pages/failures.tsx b/web-ui/src/pages/failures.tsx
new file mode 100644
index 0000000..c5525fc
--- /dev/null
+++ b/web-ui/src/pages/failures.tsx
@@ -0,0 +1,24 @@
+import Frame from '@/components/main/frame';
+import DeniedTable from '@/components/tables/failures-table';
+import { registerPageLoad } from '@/matomo';
+import { useEffect } from 'react';
+import { Typography } from 'antd';
+
+const { Title } = Typography;
+
+export default function FailuresPage() {
+ useEffect(() => {
+ registerPageLoad();
+ }, []);
+
+ return (
+ <>
+
+ <>
+ Lectures that has failed
+
+ >
+
+ >
+ );
+}