diff --git a/src/pages/search.tsx b/src/pages/search.tsx index 9c7af24519bd4a3413ec4c8e58b79b9199ea8815..8345e466918cb2e1f0442095d275ad449a38bf1c 100644 --- a/src/pages/search.tsx +++ b/src/pages/search.tsx @@ -43,7 +43,7 @@ function LectureResults({ <LectureListItem key={lecture.id} lecture={lecture} - course_handle={course_context[lecture.course_id.toString()].handle} + courseHandle={course_context[lecture.course_id.toString()].handle} show_chapters={ course_context[lecture.course_id.toString()].show_chapters_on_course } diff --git a/src/videoag/api/types.ts b/src/videoag/api/types.ts index 8e0011256cd19968638b817ebeaa71c6679fc371..8862e113464d54c9a11196a542722e22566d63df 100644 --- a/src/videoag/api/types.ts +++ b/src/videoag/api/types.ts @@ -135,6 +135,7 @@ export interface medium_metadata { type: medium_metadata_type; publish_medium_id?: int; file_size: int; + file_format: string; // type: image, plain_video, thumbnail horizontal_resolution?: int; diff --git a/src/videoag/course/CourseListing.tsx b/src/videoag/course/CourseListing.tsx index 710226c757c1cc57b5925fc0c46fc6b1fcc70bda..79f6be781b40ea739f1154ea4e6c16ab53553fcb 100644 --- a/src/videoag/course/CourseListing.tsx +++ b/src/videoag/course/CourseListing.tsx @@ -186,7 +186,7 @@ function ListingBody({ course }: { course: GetCourseResponse }) { <LectureListItem key={lecture.id} lecture={lecture} - course_handle={course.handle} + courseHandle={course.handle} show_chapters={course.show_chapters_on_course} /> ))} diff --git a/src/videoag/course/DownloadManager.tsx b/src/videoag/course/DownloadManager.tsx index 2c6e3f6d86d81a3e39e851fde5ece716c1e8d206..1da161676b292f0967b24122905808d4520dc06c 100644 --- a/src/videoag/course/DownloadManager.tsx +++ b/src/videoag/course/DownloadManager.tsx @@ -13,6 +13,7 @@ import { getNamedPublishMedia, getSortedDownloadablePublishMedia, getMediumQualityName, + getMediumFileNameForDownload, } from "./Medium"; interface DownloadStatus { @@ -229,13 +230,10 @@ async function downloadMedia( filesToDownload.push({ lectureId: lecture.id, url: medium.download_url!, - // TODO file ending mp4 - // TODO lecture date and not id - filename: `videoag_${course.handle}_${lecture.id}_${getMediumQualityName(medium)}.mp4`, + filename: getMediumFileNameForDownload(course.handle, lecture, medium), fileSize: medium.medium_metadata.file_size, }); } - setDownloadStatus((oldStatus) => new Map<int, DownloadStatus>()); const replaceStatus = (lectureId: int, fn: (old?: DownloadStatus) => DownloadStatus) => { setDownloadStatus((oldStatus) => { @@ -288,12 +286,14 @@ async function downloadMedia( } function DownloadAllLectureListItem({ + courseHandle, lecture, selectedMediumId, setSelectedMediumId, isDownloading, downloadStatus, }: { + courseHandle: string; lecture: lecture; selectedMediumId: int | undefined; setSelectedMediumId: (mediumId: int | undefined) => void; @@ -349,7 +349,15 @@ function DownloadAllLectureListItem({ } else { const selectedMedium = media.find((m) => m.id == selectedMediumId); displayedProgress = ( - <a href={selectedMedium?.download_url} download target="_blank"> + <a + href={selectedMedium?.download_url} + download={ + selectedMedium !== undefined + ? getMediumFileNameForDownload(courseHandle, lecture, selectedMedium) + : "" + } + target="_blank" + > <button type="button" className="btn btn-primary" @@ -595,6 +603,7 @@ export default function DownloadAllModal({ course }: { course: course }) { {course.lectures!.map((lecture) => ( <DownloadAllLectureListItem key={lecture.id} + courseHandle={course.handle} lecture={lecture} selectedMediumId={chosenMediumIdByLectureId.get(lecture.id)} setSelectedMediumId={(medium_id) => diff --git a/src/videoag/course/Lecture.tsx b/src/videoag/course/Lecture.tsx index c6a56a3dc3d91d4c2d6836e6975b3c88b01e1dac..778368bc13f9da79529f32d4fc2f9d7a852e205b 100644 --- a/src/videoag/course/Lecture.tsx +++ b/src/videoag/course/Lecture.tsx @@ -43,12 +43,12 @@ export function getLectureThumbnailUrl(lecture: lecture): string | undefined { export function LectureListItem({ lecture, - course_handle, + courseHandle, show_chapters, ...props }: { lecture: lecture; - course_handle: string; + courseHandle: string; show_chapters: boolean; [key: string]: any; }) { @@ -72,7 +72,7 @@ export function LectureListItem({ }} className="col-md-2 col-12 center-background-img position-relative h-5" > - <Link href={`/${course_handle}/${lecture.id}`}> + <Link href={`/${courseHandle}/${lecture.id}`}> <span className="centered-overlay-container" aria-hidden="true"> <span className={"bi bi-play-circle fs-1"} @@ -182,8 +182,7 @@ export function LectureListItem({ <span className="bi bi-play" /> <Link href={ - `/${course_handle}/${lecture.id}?t=` + - chapter.start_time + `/${courseHandle}/${lecture.id}?t=` + chapter.start_time } > {chapter.name} @@ -216,7 +215,7 @@ export function LectureListItem({ <li className="col-xl-7 col-12 p-0"> {editMode ? ( <> - <PublishMediumList publish_media={lecture.publish_media!} /> + <PublishMediumList courseHandle={courseHandle} lecture={lecture} /> <ModalMediaProcessOverview lectureId={lecture.id} className="mt-2" @@ -224,7 +223,8 @@ export function LectureListItem({ </> ) : ( <PublishMediumDownloadButton - publish_media={lecture.publish_media ?? []} + courseHandle={courseHandle} + lecture={lecture} direction="down" className="pl-1" /> diff --git a/src/videoag/course/Medium.tsx b/src/videoag/course/Medium.tsx index 645c424f0a6cbbe09203f73f12776a9009569d37..c2dadb97fc22db351b9c38f7a5727107f8328e21 100644 --- a/src/videoag/course/Medium.tsx +++ b/src/videoag/course/Medium.tsx @@ -1,7 +1,12 @@ import { useEffect, useState } from "react"; import { Dropdown, Modal } from "react-bootstrap"; -import { int, publish_medium, GetLectureMediaProcessOverviewResponse } from "@/videoag/api/types"; +import { + int, + lecture, + publish_medium, + GetLectureMediaProcessOverviewResponse, +} from "@/videoag/api/types"; import { useApi } from "@/videoag/api/ApiProvider"; import { showError, ErrorComponent } from "@/videoag/error/ErrorDisplay"; import { JobStatusCard } from "@/videoag/job/Job"; @@ -13,7 +18,13 @@ import { } from "@/videoag/miscellaneous/Formatting"; import { useMutexCall } from "@/videoag/miscellaneous/PromiseHelpers"; import { ReloadBoundary } from "@/videoag/miscellaneous/ReloadBoundary"; -import { Spinner, showInfoToast, TooltipButton } from "@/videoag/miscellaneous/Util"; +import { + Spinner, + showInfoToast, + TooltipButton, + parseApiDatetime, + zeropad, +} from "@/videoag/miscellaneous/Util"; import { useEditMode } from "@/videoag/object_management/EditModeProvider"; import { OMDelete, EmbeddedOMFieldComponent } from "@/videoag/object_management/OMConfigComponent"; @@ -83,16 +94,32 @@ export function getSortedDownloadablePublishMedia(media: publish_medium[]): publ return sortPublishMediaByQuality(media.filter((m) => m.download_url !== undefined)); } +export function getMediumFileNameForDownload( + courseHandle: string, + lecture: lecture, + publishMedium: publish_medium, +) { + const lectureDateTime = parseApiDatetime(lecture.time); + const timeString = `${zeropad(lectureDateTime.year, 4)}.${zeropad(lectureDateTime.month, 2)}.${zeropad(lectureDateTime.day, 2)}`; + return `videoag_${courseHandle}_${timeString}_${getMediumQualityName(publishMedium)}.${publishMedium.medium_metadata.file_format}`; +} + 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[] }) { +export function PublishMediumList({ + courseHandle, + lecture, +}: { + courseHandle: string; + lecture: lecture; +}) { const { editMode } = useEditMode(); return ( <ul className="p-0"> - {getNamedPublishMedia(sortPublishMediaByQuality(publish_media)).map( + {getNamedPublishMedia(sortPublishMediaByQuality(lecture.publish_media ?? [])).map( ({ medium, name }) => ( <li key={medium.id} @@ -102,7 +129,12 @@ export function PublishMediumList({ publish_media }: { publish_media: publish_me } > <span className="bi bi-download" /> - <a href={medium.download_url} download target="_blank" className="mx-1"> + <a + href={medium.download_url} + download={getMediumFileNameForDownload(courseHandle, lecture, medium)} + target="_blank" + className="mx-1" + > {name} </a> {editMode && ( @@ -131,27 +163,28 @@ export function PublishMediumList({ publish_media }: { publish_media: publish_me } export function PublishMediumDownloadButton({ - publish_media, + courseHandle, + lecture, direction, ...props }: { - publish_media: publish_medium[]; + courseHandle: string; + lecture: lecture; direction: "up" | "down"; [key: string]: any; }) { const { editMode } = useEditMode(); const { language } = useLanguage(); - const namedMedia = getNamedPublishMedia(getSortedDownloadablePublishMedia(publish_media)); + const namedMedia = getNamedPublishMedia( + getSortedDownloadablePublishMedia(lecture.publish_media ?? []), + ); if (namedMedia.length === 0) return <></>; return ( <span {...props}> <Dropdown drop={direction}> - <Dropdown.Toggle - variant="primary" - disabled={publish_media === undefined || publish_media.length === 0} - > + <Dropdown.Toggle variant="primary"> {language.get("ui.generic.download")} </Dropdown.Toggle> @@ -162,7 +195,15 @@ export function PublishMediumDownloadButton({ return ( <div key={medium.id} className={"d-flex align-items-center " + bgColor}> - <Dropdown.Item href={medium.download_url} download target="_blank"> + <Dropdown.Item + href={medium.download_url} + download={getMediumFileNameForDownload( + courseHandle, + lecture, + medium, + )} + target="_blank" + > {name} </Dropdown.Item> {editMode && ( @@ -277,6 +318,10 @@ function MediumFileListItem({ {filesizeToHuman(metadata.file_size)} ({metadata.file_size} B) </td> </tr> + <tr> + <td>File Format</td> + <td>{metadata.file_format}</td> + </tr> <tr> <td>Type</td> <td>{metadata.type}</td> diff --git a/src/videoag/course/Player.tsx b/src/videoag/course/Player.tsx index 6d20efaf341046d532ac951e64aa942de98cede1..8d27440c34b2d88f6b811cdea181bbdd9e4098c5 100644 --- a/src/videoag/course/Player.tsx +++ b/src/videoag/course/Player.tsx @@ -451,7 +451,8 @@ export default function Player({ className="list-inline-item" /> <PublishMediumDownloadButton - publish_media={lecture.publish_media ?? []} + courseHandle={course.handle} + lecture={lecture} direction="up" className="list-inline-item" />