diff --git a/src/api/api.ts b/src/api/api.ts index 89d4603..f8c8855 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -174,6 +174,29 @@ const getCoverArtUrl = (item: any, useLegacyAuth: boolean, size?: number) => { ); }; +export const getDownloadUrl = (id: string, useLegacyAuth = legacyAuth) => { + if (useLegacyAuth) { + return ( + `${API_BASE_URL}/download` + + `?id=${id}` + + `&u=${auth.username}` + + `&p=${auth.password}` + + `&v=1.15.0` + + `&c=sonixd` + ); + } + + return ( + `${API_BASE_URL}/download` + + `?id=${id}` + + `&u=${auth.username}` + + `&s=${auth.salt}` + + `&t=${auth.hash}` + + `&v=1.15.0` + + `&c=sonixd` + ); +}; + const getStreamUrl = (id: string, useLegacyAuth: boolean) => { if (useLegacyAuth) { return ( @@ -254,8 +277,14 @@ export const getStream = async (id: string) => { return data; }; -export const getDownload = async (id: string) => { - const { data } = await api.get(`/download?id=${id}`); +export const getDownload = async (options: { id: string }) => { + const { data } = await api.get(`/download?id=${options.id}`, { + responseType: 'blob', + onDownloadProgress: (progressEvent) => { + const percentCompleted = Math.floor((progressEvent.loaded / progressEvent.total) * 100); + console.log(`percentCompleted`, percentCompleted); + }, + }); return data; }; @@ -825,6 +854,51 @@ export const getGenres = async () => { })); }; +export const search2 = async (options: { + query: string; + artistCount?: number; + artistOffset?: 0; + albumCount?: number; + albumOffset?: 0; + songCount?: number; + songOffset?: 0; + musicFolderId?: string | number; +}) => { + const { data } = await api.get(`/search2`, { params: options }); + + const results = data.searchResult3; + + return { + artist: (results.artist || []).map((entry: any, index: any) => ({ + ...entry, + image: getCoverArtUrl(entry, legacyAuth, 350), + starred: entry.starred || undefined, + type: 'artist', + index, + uniqueId: nanoid(), + })), + album: (results.album || []).map((entry: any, index: any) => ({ + ...entry, + albumId: entry.id, + image: getCoverArtUrl(entry, legacyAuth, 350), + starred: entry.starred || undefined, + type: 'album', + isDir: false, + index, + uniqueId: nanoid(), + })), + song: (results.song || []).map((entry: any, index: any) => ({ + ...entry, + streamUrl: getStreamUrl(entry.id, legacyAuth), + image: getCoverArtUrl(entry, legacyAuth, 150), + type: 'music', + starred: entry.starred || undefined, + index, + uniqueId: nanoid(), + })), + }; +}; + export const search3 = async (options: { query: string; artistCount?: number; diff --git a/src/components/library/AlbumView.tsx b/src/components/library/AlbumView.tsx index cfea77b..c869c04 100644 --- a/src/components/library/AlbumView.tsx +++ b/src/components/library/AlbumView.tsx @@ -1,16 +1,18 @@ import React from 'react'; import _ from 'lodash'; +import { clipboard, shell } from 'electron'; import settings from 'electron-settings'; -import { ButtonToolbar } from 'rsuite'; +import { ButtonToolbar, Whisper } from 'rsuite'; import { useQuery, useQueryClient } from 'react-query'; import { useParams, useHistory } from 'react-router-dom'; import { + DownloadButton, FavoriteButton, PlayAppendButton, PlayAppendNextButton, PlayButton, } from '../shared/ToolbarButtons'; -import { getAlbum, star, unstar } from '../../api/api'; +import { getAlbum, getDownloadUrl, star, unstar } from '../../api/api'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { appendPlayQueue, @@ -38,10 +40,11 @@ import { filterPlayQueue, formatDate, formatDuration, + getAlbumSize, getPlayedSongsNotification, isCached, } from '../../shared/utils'; -import { StyledTagLink } from '../shared/styled'; +import { StyledButton, StyledPopover, StyledTagLink } from '../shared/styled'; import { setActive } from '../../redux/albumSlice'; import { BlurredBackground, @@ -172,6 +175,23 @@ const AlbumView = ({ ...rest }: any) => { } }; + const handleDownload = (type: 'copy' | 'download') => { + // 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 + + if (data.song[0]?.parent) { + if (type === 'download') { + shell.openExternal(getDownloadUrl(data.song[0].parent)); + } else { + clipboard.writeText(getDownloadUrl(data.song[0].parent)); + notifyToast('info', 'Download links copied!'); + } + } else { + notifyToast('warning', 'No parent album found'); + } + }; + if (isLoading) { return ; } @@ -329,6 +349,27 @@ const AlbumView = ({ ...rest }: any) => { size="lg" onClick={() => handlePlayAppend('later')} /> + + + handleDownload('download')}> + Download + + handleDownload('copy')}> + Copy to clipboard + + + + } + > + + diff --git a/src/components/library/ArtistView.tsx b/src/components/library/ArtistView.tsx index 6013154..767ad29 100644 --- a/src/components/library/ArtistView.tsx +++ b/src/components/library/ArtistView.tsx @@ -2,18 +2,27 @@ import React, { useEffect, useState } from 'react'; import _ from 'lodash'; import FastAverageColor from 'fast-average-color'; -import { shell } from 'electron'; +import { clipboard, shell } from 'electron'; import settings from 'electron-settings'; import { ButtonToolbar, Whisper, TagGroup } from 'rsuite'; import { useQuery, useQueryClient } from 'react-query'; import { useParams, useHistory } from 'react-router-dom'; import { + DownloadButton, FavoriteButton, PlayAppendButton, PlayAppendNextButton, PlayButton, } from '../shared/ToolbarButtons'; -import { getAllArtistSongs, getArtist, getArtistInfo, star, unstar } from '../../api/api'; +import { + getAlbum, + getAllArtistSongs, + getArtist, + getArtistInfo, + getDownloadUrl, + star, + unstar, +} from '../../api/api'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { toggleSelected, @@ -167,6 +176,38 @@ const ArtistView = ({ ...rest }: any) => { } }; + const handleDownload = async (type: 'copy' | 'download') => { + if (data.album[0]?.parent) { + if (type === 'download') { + shell.openExternal(getDownloadUrl(data.album[0].parent)); + } else { + clipboard.writeText(getDownloadUrl(data.album[0].parent)); + notifyToast('info', 'Download links copied!'); + } + } else { + const downloadUrls: string[] = []; + + for (let i = 0; i < data.album.length; i += 1) { + // eslint-disable-next-line no-await-in-loop + const albumRes = await getAlbum(data.album[i].id); + if (albumRes.song[0]?.parent) { + downloadUrls.push(getDownloadUrl(albumRes.song[0].parent)); + } else { + notifyToast('warning', `[${albumRes.name}] No parent album found`); + } + } + + if (type === 'download') { + downloadUrls.forEach((link) => shell.openExternal(link)); + } + + if (type === 'copy') { + clipboard.writeText(downloadUrls.join('\n')); + notifyToast('info', 'Download links copied!'); + } + } + }; + useEffect(() => { if (!isLoading) { const img = isCached(`${misc.imageCachePath}artist_${data?.id}.jpg`) @@ -303,11 +344,34 @@ const ArtistView = ({ ...rest }: any) => { size="lg" onClick={() => handlePlayAppend('later')} /> + + + handleDownload('download')}> + Download + + handleDownload('copy')}> + Copy to clipboard + + + + } + > + +