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
+
+
+
+ }
+ >
+
+