From d240b6d5d30549c1d0eda4d77401fc2fa188ccd4 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 12 Apr 2022 21:10:01 -0700 Subject: [PATCH] Add playlist download (#266) --- src/components/playlist/PlaylistView.tsx | 30 +++++++- src/hooks/useBrowserDownload.ts | 91 ++++++++++++++++++++++++ 2 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useBrowserDownload.ts diff --git a/src/components/playlist/PlaylistView.tsx b/src/components/playlist/PlaylistView.tsx index 634633b..670b40f 100644 --- a/src/components/playlist/PlaylistView.tsx +++ b/src/components/playlist/PlaylistView.tsx @@ -10,6 +10,7 @@ import { useParams, useHistory } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { DeleteButton, + DownloadButton, EditButton, PlayAppendButton, PlayAppendNextButton, @@ -26,6 +27,7 @@ import { formatDate, formatDateTime, formatDuration, + getAlbumSize, getCurrentEntryList, getRecoveryPath, getUniqueRandomNumberArr, @@ -52,6 +54,7 @@ import Popup from '../shared/Popup'; import usePlayQueueHandler from '../../hooks/usePlayQueueHandler'; import useFavorite from '../../hooks/useFavorite'; import { useRating } from '../../hooks/useRating'; +import { useBrowserDownload } from '../../hooks/useBrowserDownload'; interface PlaylistParams { id: string; @@ -148,6 +151,7 @@ const PlaylistView = ({ ...rest }) => { }); const { handlePlayQueueAdd } = usePlayQueueHandler(); + const { handleDownload } = useBrowserDownload(); const handleSave = async (recovery: boolean) => { dispatch(clearSelected()); @@ -554,7 +558,31 @@ const PlaylistView = ({ ...rest }) => { disabled={misc.isProcessingPlaylist.includes(data?.id)} /> - + + + handleDownload(data, 'download', true)}> + {t('Download')} + + handleDownload(data, 'copy', true)}> + {t('Copy to clipboard')} + + + + } + > + + { + const { t } = useTranslation(); + const config = useAppSelector((state) => state.config); + + const handleDownload = useCallback( + async (data, type: 'copy' | 'download', playlist?: boolean) => { + const downloadUrls = []; + + if (config.serverType === Server.Jellyfin) { + for (let i = 0; i < data.song.length; i += 1) { + downloadUrls.push( + await apiController({ + serverType: Server.Jellyfin, + endpoint: 'getDownloadUrl', + args: { id: data.song[i].id }, + }) + ); + } + } + + if (config.serverType === Server.Subsonic) { + if (playlist) { + // This matches Navidrome's playlist GUID Id format + if (data.id.includes('-')) { + downloadUrls.push( + await apiController({ + serverType: Server.Subsonic, + endpoint: 'getDownloadUrl', + args: { id: data.id }, + }) + ); + } else { + for (let i = 0; i < data.song.length; i += 1) { + downloadUrls.push( + await apiController({ + serverType: Server.Subsonic, + endpoint: 'getDownloadUrl', + args: { id: data.song[i].id }, + }) + ); + } + } + } + // If not Navidrome (this assumes Airsonic), then we need to use a song's parent + // to download. This is because Airsonic does not support downloading via album ids + // that are provided by /getAlbum or /getAlbumList2 + else if (data.song[0]?.parent) { + downloadUrls.push( + await apiController({ + serverType: Server.Subsonic, + endpoint: 'getDownloadUrl', + args: { id: data.song[0].parent }, + }) + ); + } else { + downloadUrls.push( + await apiController({ + serverType: Server.Subsonic, + endpoint: 'getDownloadUrl', + args: { id: data.song[0].parent }, + }) + ); + notifyToast('info', t('Download links copied!')); + } + } + + if (downloadUrls.length === 0) { + return notifyToast('warning', t('No parent album found')); + } + + if (type === 'download') { + return downloadUrls.forEach((url) => shell.openExternal(url)); + } + + clipboard.writeText(downloadUrls.join('\n')); + return notifyToast('info', t('Download links copied!')); + }, + [config.serverType, t] + ); + + return { handleDownload }; +};