diff --git a/.vscode/settings.json b/.vscode/settings.json index 7a94be02..71081949 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,7 @@ "cSpell.words": [ "akamaihd", "alllangs", + "axios", "ayfm", "AYFM", "cctv", diff --git a/src-electron/electron-preload.ts b/src-electron/electron-preload.ts index 417088fc..71f38b89 100644 --- a/src-electron/electron-preload.ts +++ b/src-electron/electron-preload.ts @@ -87,13 +87,17 @@ const moveMediaWindow = ({ const mediaWindow = getMediaWindow(); if (!mediaWindow || !mainWindow) return; if (targetScreen === undefined || windowedMode === undefined) { - const screenPreferences = JSON.parse( - localStorage.getItem('screenPreferences') ?? '{}', - ) as ScreenPreferences; - // todo: fix this, not reading right values - console.log('screenPreferences', screenPreferences); - targetScreen = screenPreferences.preferredScreenNumber; - windowedMode = screenPreferences.preferWindowed; + try { + const screenPreferences = JSON.parse( + localStorage.getItem('screenPreferences') ?? '{}', + ) as ScreenPreferences; + // todo: fix this, not reading right values + console.log('screenPreferences', screenPreferences); + targetScreen = screenPreferences.preferredScreenNumber; + windowedMode = screenPreferences.preferWindowed; + } catch (err) { + console.error(err); + } } const getWindowScreen = (window: Electron.BrowserWindow) => { const windowDisplay = screen.getDisplayMatching(window.getBounds()); diff --git a/src/components/media/MediaDisplayButton.vue b/src/components/media/MediaDisplayButton.vue index 49411f65..7ca1ef80 100644 --- a/src/components/media/MediaDisplayButton.vue +++ b/src/components/media/MediaDisplayButton.vue @@ -55,8 +55,8 @@ screen.mainWindow ? 'mdi-monitor-dashboard' : screenPreferences.preferredScreenNumber === index - ? 'mdi-monitor-shimmer' - : 'mdi-monitor' + ? 'mdi-monitor-shimmer' + : 'mdi-monitor' " :text-color=" screenList.length < 2 || @@ -130,8 +130,7 @@ import { useAppSettingsStore } from 'src/stores/app-settings'; import { useCurrentStateStore } from 'src/stores/current-state'; import { onMounted, onUnmounted, ref, watch } from 'vue'; -const { getAllScreens, moveMediaWindow, toggleMediaWindow } = - electronApi; +const { getAllScreens, moveMediaWindow, toggleMediaWindow } = electronApi; defineProps<{ disabled?: boolean; @@ -178,13 +177,21 @@ watch( ); const targetScreenListener = (event: CustomEventInit) => { - console.log('targetScreen-update', event.detail); - screenPreferences.value.preferredScreenNumber = event.detail; + try { + console.log('targetScreen-update', event.detail); + screenPreferences.value.preferredScreenNumber = event.detail; + } catch (error) { + console.error(error); + } }; const windowedModeListener = (event: CustomEventInit) => { - console.log('windowedMode-update', event.detail); - screenPreferences.value.preferWindowed = event.detail; + try { + console.log('windowedMode-update', event.detail); + screenPreferences.value.preferWindowed = event.detail; + } catch (error) { + console.error(error); + } }; onMounted(() => { diff --git a/src/helpers/jw-media.ts b/src/helpers/jw-media.ts index e0450c50..3919e7f6 100644 --- a/src/helpers/jw-media.ts +++ b/src/helpers/jw-media.ts @@ -1017,7 +1017,7 @@ const getPubMediaLinks = async (publication: PublicationFetcher) => { }; export function findBestResolution( - mediaLinks: (MediaItemsMediatorFile | MediaLink)[], + mediaLinks: MediaItemsMediatorFile[] | MediaLink[], ) { const currentState = useCurrentStateStore(); const { getSettingValue } = currentState; @@ -1033,7 +1033,9 @@ export function findBestResolution( mediaLink.frameHeight <= maxRes && mediaLink.frameHeight >= bestHeight ) { - bestItem = mediaLink; + bestItem = Object.hasOwn(mediaLink, 'progressiveDownloadURL') + ? (mediaLink as MediaItemsMediatorFile) + : (mediaLink as MediaLink); bestHeight = mediaLink.frameHeight; } } @@ -1114,6 +1116,32 @@ const downloadMissingMedia = async (publication: PublicationFetcher) => { return downloadedFile?.path; }; +const downloadAdditionalRemoteVideo = async ( + mediaItemLinks: MediaItemsMediatorFile[], +) => { + window.dispatchEvent( + new CustomEvent('remoteVideo-loading', { + detail: true, + }), + ); + const currentState = useCurrentStateStore(); + const { getDatedAdditionalMediaDirectory } = storeToRefs(currentState); + const bestItem = findBestResolution(mediaItemLinks) as MediaItemsMediatorFile; + let downloadedFile: DownloadedFile = { path: '' }; + if (bestItem) { + downloadedFile = (await downloadFileIfNeeded({ + dir: getDatedAdditionalMediaDirectory.value, + size: bestItem.filesize, + url: bestItem.progressiveDownloadURL, + })) as DownloadedFile; + } + window.dispatchEvent( + new CustomEvent('remoteVideo-loaded', { + detail: downloadedFile, + }), + ); +}; + function getBestImageUrl(images: ImageTypeSizes, size?: keyof ImageSizes) { const preferredOrder: (keyof ImageTypeSizes)[] = ['wss', 'lsr', 'sqr', 'pnr']; for (const key of preferredOrder) { @@ -1287,6 +1315,7 @@ const downloadJwpub = async ( export { addFullFilePathToMultimediaItem, + downloadAdditionalRemoteVideo, downloadBackgroundMusic, downloadFileIfNeeded, downloadPubMediaFiles, diff --git a/src/layouts/MainLayout.vue b/src/layouts/MainLayout.vue index 865e69be..bd19c197 100644 --- a/src/layouts/MainLayout.vue +++ b/src/layouts/MainLayout.vue @@ -169,17 +169,20 @@ text-color="white" /> - {{ $t('add-media-files') }} - - - - +
- +
- - - @@ -375,7 +368,11 @@ import SongPicker from 'src/components/media/SongPicker.vue'; import SubtitlesButton from 'src/components/media/SubtitlesButton.vue'; import { getLookupPeriod } from 'src/helpers/date'; import { electronApi } from 'src/helpers/electron-api'; -import { downloadBackgroundMusic, getBestImageUrl } from 'src/helpers/jw-media'; +import { + downloadAdditionalRemoteVideo, + downloadBackgroundMusic, + getBestImageUrl, +} from 'src/helpers/jw-media'; import { createTemporaryNotification } from 'src/helpers/notifications'; import { useAppSettingsStore } from 'src/stores/app-settings'; import { useCongregationSettingsStore } from 'src/stores/congregation-settings'; @@ -548,6 +545,7 @@ const localUploadPopup = ref(false); const importMediaMenuActive = ref(false); const datePickerActive = ref(false); const remoteVideoPopup = ref(false); +const remoteVideosLoadingProgress = ref(0); const remoteVideos: Ref = ref([]); // const remoteVideosByCategory: Ref<{ // [key: string]: MediaItemsMediatorItem[]; @@ -586,81 +584,85 @@ const getLocalFiles = async () => { }; const getJwVideos = async () => { - const currentState = useCurrentStateStore(); - const { getSettingValue } = currentState; - const getSubcategories = async (category: string) => { - return (await get( - `https://b.jw-cdn.org/apis/mediator/v1/categories/${ - getSettingValue('lang') as string - }/${category}?detailed=1&mediaLimit=0&clientType=www`, - )) as JwVideoCategory; - }; - const subcategories: { - key: string; - parentCategory: string; - }[] = [{ key: 'LatestVideos', parentCategory: '' }]; - const subcategoriesRequest = await getSubcategories('VideoOnDemand'); - console.log('subcategoriesRequest', subcategoriesRequest); - const subcategoriesFirstLevel = - subcategoriesRequest.category.subcategories.map((s) => s.key); - for (const subcategoryFirstLevel of subcategoriesFirstLevel) { - subcategories.push( - ...( - await getSubcategories(subcategoryFirstLevel) - ).category.subcategories.map((s) => { - return { key: s.key, parentCategory: subcategoryFirstLevel }; - }), - ); - } - console.log('subcategories', subcategories); - for (const category of subcategories) { - const request = (await get( - `https://b.jw-cdn.org/apis/mediator/v1/categories/${ - getSettingValue('lang') as string - }/${category.key}?detailed=0&clientType=www`, - )) as JwVideoCategory; - // remoteVideosByCategory.value[category.parentCategory ?? category.key] = - // request.category.media; - remoteVideos.value = remoteVideos.value - .concat(request.category.media) - .reduce((accumulator: MediaItemsMediatorItem[], current) => { - const guids = new Set(accumulator.map((item) => item.guid)); - if (!guids.has(current.guid)) { - accumulator.push(current); - } - return accumulator; - }, []) - .sort((a, b) => { - return ( - new Date(b.firstPublished).getTime() - - new Date(a.firstPublished).getTime() + try { + if (remoteVideosLoadingProgress.value < 1) { + const currentState = useCurrentStateStore(); + const { getSettingValue } = currentState; + const getSubcategories = async (category: string) => { + return (await get( + `https://b.jw-cdn.org/apis/mediator/v1/categories/${ + getSettingValue('lang') as string + }/${category}?detailed=1&mediaLimit=0&clientType=www`, + )) as JwVideoCategory; + }; + const subcategories: { + key: string; + parentCategory: string; + }[] = [{ key: 'LatestVideos', parentCategory: '' }]; + const subcategoriesRequest = await getSubcategories('VideoOnDemand'); + console.log('subcategoriesRequest', subcategoriesRequest); + const subcategoriesFirstLevel = + subcategoriesRequest.category.subcategories.map((s) => s.key); + for (const subcategoryFirstLevel of subcategoriesFirstLevel) { + subcategories.push( + ...( + await getSubcategories(subcategoryFirstLevel) + ).category.subcategories.map((s) => { + return { key: s.key, parentCategory: subcategoryFirstLevel }; + }), ); - }); + } + console.log('subcategories', subcategories); + let index = 0; + for (const category of subcategories) { + const request = (await get( + `https://b.jw-cdn.org/apis/mediator/v1/categories/${ + getSettingValue('lang') as string + }/${category.key}?detailed=0&clientType=www`, + )) as JwVideoCategory; + // remoteVideosByCategory.value[category.parentCategory ?? category.key] = + // request.category.media; + remoteVideos.value = remoteVideos.value + .concat(request.category.media) + .reduce((accumulator: MediaItemsMediatorItem[], current) => { + const guids = new Set(accumulator.map((item) => item.guid)); + if (!guids.has(current.guid)) { + accumulator.push(current); + } + return accumulator; + }, []) + .sort((a, b) => { + return ( + new Date(b.firstPublished).getTime() - + new Date(a.firstPublished).getTime() + ); + }); + index++; + remoteVideosLoadingProgress.value = index / subcategories.length; + } + } + } catch (error) { + console.error(error); } - // make sure values are unique, based on guid }; const remoteVideosFiltered = computed(() => { - return ( - remoteVideoFilter.value?.length > 2 - ? remoteVideos.value.filter((video) => - video.title - .toLowerCase() - .includes(remoteVideoFilter.value.toLowerCase()), - ) - : remoteVideos.value - ) - .filter( + const useableVideos = ref( + remoteVideos.value.filter( (v) => remoteVideosIncludeAudioDescription.value || !v.primaryCategory.endsWith('AD'), - ) - .slice(0, 100); + ), + ); + if (remoteVideoFilter.value?.length > 2) + useableVideos.value = useableVideos.value.filter((video) => + video.title.toLowerCase().includes(remoteVideoFilter.value.toLowerCase()), + ); + return useableVideos.value.slice(0, 50); }); onMounted(() => { document.title = 'Meeting Media Manager'; - console.log(currentSettings.value); if (!currentSettings.value) navigateToCongregationSelector(); }); diff --git a/src/pages/MediaCalendarPage.vue b/src/pages/MediaCalendarPage.vue index 8f387da8..f4be5dfb 100644 --- a/src/pages/MediaCalendarPage.vue +++ b/src/pages/MediaCalendarPage.vue @@ -637,7 +637,6 @@ import { dragAndDrop } from '@formkit/drag-and-drop/vue'; import Panzoom, { PanzoomObject } from '@panzoom/panzoom'; import { Buffer } from 'buffer'; import DOMPurify from 'dompurify'; -import { PathLike } from 'fs'; import mime from 'mime'; import { storeToRefs } from 'pinia'; import { date, uid } from 'quasar'; @@ -682,7 +681,7 @@ import { setObsScene } from 'src/helpers/obs'; import { useCurrentStateStore } from 'src/stores/current-state'; import { useJwStore } from 'src/stores/jw'; import { useObsStateStore } from 'src/stores/obs-state'; -import { DynamicMediaObject } from 'src/types/media'; +import { DownloadedFile, DynamicMediaObject } from 'src/types/media'; import { DocumentItem, TableItem } from 'src/types/sqlite'; import { computed, onMounted, onUnmounted, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; @@ -724,7 +723,7 @@ const mediaStopPending = computed(() => !!mediaToStop.value); const mediaToDelete = ref(''); const mediaDeletePending = computed(() => !!mediaToDelete.value); -const { decompress, executeQuery, fileUrlToPath, fs, openFileDialog, path } = +const { decompress, executeQuery, fs, openFileDialog, path } = electronApi; const zoomReset = (elemId: string, forced = false) => { @@ -801,19 +800,19 @@ const mapOrder = return sortOrder.indexOf(a[key]) > sortOrder.indexOf(b[key]) ? 1 : -1; }; -const fileUrlIsValid = (fileUrl: PathLike) => { - console.log(fileUrlToPath(fileUrl)); - if (!fileUrl) { - return false; - } else if (!fs.existsSync(fileUrlToPath(fileUrl))) return false; - else { - return true; - } -}; +// const fileUrlIsValid = (fileUrl: PathLike) => { +// if (!fileUrl) { +// return false; +// } else if (!fs.existsSync(fileUrlToPath(fileUrl))) return false; +// else { +// return true; +// } +// }; +// todo: watch length instead? less heavy? const mediaItems = computed(() => { return datedAdditionalMediaMap.value - .filter((mediaItem) => fileUrlIsValid(mediaItem?.fileUrl)) + // .filter((mediaItem) => fileUrlIsValid(mediaItem?.fileUrl)) .concat(selectedDateObject.value?.dynamicMedia) .filter((mediaItem) => mediaItem?.fileUrl) as DynamicMediaObject[]; }); @@ -966,7 +965,7 @@ const addJwpubDocumentMediaToFiles = async (document: DocumentItem) => { const dynamicMediaItems = await dynamicMediaMapper( multimediaItems, selectedDateObject.value?.date, - true + true, ); addToAdditionMediaMap(dynamicMediaItems); jwpubImportDb.value = ''; @@ -1011,39 +1010,55 @@ const copyToDatedAdditionalMedia = async (files: string[]) => { getFileUrl(datedAdditionalMediaPath), ); if (fs.existsSync(datedAdditionalMediaPath)) { - fs.removeSync(datedAdditionalMediaPath); + if (filepathToCopy !== datedAdditionalMediaPath) + fs.removeSync(datedAdditionalMediaPath); removeFromAdditionMediaMap(uniqueId); } - fs.copySync(filepathToCopy, datedAdditionalMediaPath); - - const isVideoFile = isVideo(datedAdditionalMediaPath); - const isAudioFile = isAudio(datedAdditionalMediaPath); - let duration = 0; - - if (isVideoFile || isAudioFile) { - duration = await getDurationFromMediaPath(datedAdditionalMediaPath); - } + if (filepathToCopy !== datedAdditionalMediaPath) + fs.copySync(filepathToCopy, datedAdditionalMediaPath); - addToAdditionMediaMap([ - { - duration, - fileUrl: getFileUrl(datedAdditionalMediaPath), - isAdditional: true, - isAudio: isAudioFile, - isImage: isImage(datedAdditionalMediaPath), - isVideo: isVideoFile, - section: 'additional', - thumbnailUrl: await getThumbnailUrl(datedAdditionalMediaPath, true), - title: path.basename(datedAdditionalMediaPath), - uniqueId, - }, - ]); + await addToAdditionMediaMapFromPath(datedAdditionalMediaPath, uniqueId); } catch (error) { console.error(error, filepathToCopy, datedAdditionalMediaPath); } } }; +const addToAdditionMediaMapFromPath = async ( + additionalFilePath: string, + uniqueId?: string, +) => { + const isVideoFile = isVideo(additionalFilePath); + const isAudioFile = isAudio(additionalFilePath); + let duration = 0; + + if (isVideoFile || isAudioFile) { + duration = await getDurationFromMediaPath(additionalFilePath); + } + + if (!uniqueId) { + uniqueId = sanitizeId( + date.formatDate(selectedDate.value, 'YYYYMMDD') + + '-' + + getFileUrl(additionalFilePath), + ); + } + addToAdditionMediaMap([ + { + duration, + fileUrl: getFileUrl(additionalFilePath), + isAdditional: true, + isAudio: isAudioFile, + isImage: isImage(additionalFilePath), + isVideo: isVideoFile, + section: 'additional', + thumbnailUrl: await getThumbnailUrl(additionalFilePath, true), + title: path.basename(additionalFilePath), + uniqueId, + }, + ]); +}; + const addToFiles = async ( files: { filetype?: string; path: string }[] | FileList, ) => { @@ -1268,12 +1283,25 @@ const getLocalFiles = async () => { }); }; +const remoteVideoLoading = () => { + additionalLoading.value = true; +}; + +const remoteVideoLoaded = (event: CustomEventInit) => { + addToAdditionMediaMapFromPath((event.detail as DownloadedFile).path); + additionalLoading.value = false; +}; + onMounted(() => { window.addEventListener('localFiles-browsed', localFilesBrowsedListener); + window.addEventListener('remoteVideo-loading', remoteVideoLoading); + window.addEventListener('remoteVideo-loaded', remoteVideoLoaded); }); onUnmounted(() => { window.removeEventListener('localFiles-browsed', localFilesBrowsedListener); + window.removeEventListener('remoteVideo-loading', remoteVideoLoading); + window.removeEventListener('remoteVideo-loaded', remoteVideoLoaded); });