From ea67a1896272b9384a4d9f44dfdccf0c533c11d1 Mon Sep 17 00:00:00 2001 From: Kendall Garner <17521368+kgarner7@users.noreply.github.com> Date: Mon, 15 Jan 2024 22:10:50 -0800 Subject: [PATCH] include lastfm/mbz links --- src/i18n/locales/en.json | 8 ++- .../api/jellyfin/jellyfin-normalize.ts | 3 ++ src/renderer/api/jellyfin/jellyfin-types.ts | 7 +++ .../api/navidrome/navidrome-normalize.ts | 3 ++ .../api/subsonic/subsonic-normalize.ts | 3 ++ src/renderer/api/types.ts | 3 ++ .../components/album-detail-content.tsx | 46 ++++++++++++++++ .../album-artist-detail-content.tsx | 54 ++++++++++++++++++- .../components/general/control-settings.tsx | 20 +++++++ src/renderer/store/settings.store.ts | 2 + 10 files changed, 146 insertions(+), 3 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 3997bd64..aaa91ae7 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -16,7 +16,11 @@ "removeFromQueue": "remove from queue", "setRating": "set rating", "toggleSmartPlaylistEditor": "toggle $t(entity.smartPlaylist) editor", - "viewPlaylists": "view $t(entity.playlist_other)" + "viewPlaylists": "view $t(entity.playlist_other)", + "openIn": { + "lastfm": "Open in Last.fm", + "musicbrainz": "Open in MusicBrainz" + } }, "common": { "action_one": "action", @@ -414,6 +418,8 @@ "discordUpdateInterval_description": "the time in seconds between each update (minimum 15 seconds)", "enableRemote": "enable remote control server", "enableRemote_description": "enables the remote control server to allow other devices to control the application", + "externalLinks": "show external links", + "externalLinks_description": "enables showing external links (Last.fm, MusicBrainz) on artist/album pages", "exitToTray": "exit to tray", "exitToTray_description": "exit the application to the system tray", "floatingQueueArea": "show floating queue hover area", diff --git a/src/renderer/api/jellyfin/jellyfin-normalize.ts b/src/renderer/api/jellyfin/jellyfin-normalize.ts index ffef489a..a351d6c9 100644 --- a/src/renderer/api/jellyfin/jellyfin-normalize.ts +++ b/src/renderer/api/jellyfin/jellyfin-normalize.ts @@ -202,6 +202,7 @@ const normalizeAlbum = ( imageSize?: number, ): Album => { return { + albumArtist: item.AlbumArtist, albumArtists: item.AlbumArtists.map((entry) => ({ id: entry.Id, @@ -233,6 +234,7 @@ const normalizeAlbum = ( isCompilation: null, itemType: LibraryItem.ALBUM, lastPlayedAt: null, + mbzId: item.ProviderIds?.MusicBrainzAlbum || null, name: item.Name, playCount: item.UserData?.PlayCount || 0, releaseDate: item.PremiereDate?.split('T')[0] || null, @@ -288,6 +290,7 @@ const normalizeAlbumArtist = ( }), itemType: LibraryItem.ALBUM_ARTIST, lastPlayedAt: null, + mbz: item.ProviderIds?.MusicBrainzArtist || null, name: item.Name, playCount: item.UserData?.PlayCount || 0, serverId: server?.id || '', diff --git a/src/renderer/api/jellyfin/jellyfin-types.ts b/src/renderer/api/jellyfin/jellyfin-types.ts index 9721ae8a..41ec5da3 100644 --- a/src/renderer/api/jellyfin/jellyfin-types.ts +++ b/src/renderer/api/jellyfin/jellyfin-types.ts @@ -422,6 +422,11 @@ const song = z.object({ UserData: userData.optional(), }); +const providerIds = z.object({ + MusicBrainzAlbum: z.string().optional(), + MusicBrainzArtist: z.string().optional(), +}); + const albumArtist = z.object({ BackdropImageTags: z.array(z.string()), ChannelId: z.null(), @@ -435,6 +440,7 @@ const albumArtist = z.object({ LocationType: z.string(), Name: z.string(), Overview: z.string(), + ProviderIds: providerIds.optional(), RunTimeTicks: z.number(), ServerId: z.string(), Type: z.string(), @@ -466,6 +472,7 @@ const album = z.object({ ParentLogoItemId: z.string(), PremiereDate: z.string().optional(), ProductionYear: z.number(), + ProviderIds: providerIds.optional(), RunTimeTicks: z.number(), ServerId: z.string(), Songs: z.array(song).optional(), // This is not a native Jellyfin property -- this is used for combined album detail diff --git a/src/renderer/api/navidrome/navidrome-normalize.ts b/src/renderer/api/navidrome/navidrome-normalize.ts index 717ba2ca..8ccc4210 100644 --- a/src/renderer/api/navidrome/navidrome-normalize.ts +++ b/src/renderer/api/navidrome/navidrome-normalize.ts @@ -151,6 +151,7 @@ const normalizeAlbum = ( const imageBackdropUrl = imageUrl?.replace(/size=\d+/, 'size=1000') || null; return { + albumArtist: item.albumArtist, albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }], artists: [{ id: item.artistId, imageUrl: null, name: item.artist }], backdropImageUrl: imageBackdropUrl, @@ -169,6 +170,7 @@ const normalizeAlbum = ( isCompilation: item.compilation, itemType: LibraryItem.ALBUM, lastPlayedAt: normalizePlayDate(item), + mbzId: item.mbzAlbumId || null, name: item.name, playCount: item.playCount, releaseDate: new Date(item.minYear, 0, 1).toISOString(), @@ -217,6 +219,7 @@ const normalizeAlbumArtist = ( imageUrl: imageUrl || null, itemType: LibraryItem.ALBUM_ARTIST, lastPlayedAt: normalizePlayDate(item), + mbz: item.mbzArtistId || null, name: item.name, playCount: item.playCount, serverId: server?.id || 'unknown', diff --git a/src/renderer/api/subsonic/subsonic-normalize.ts b/src/renderer/api/subsonic/subsonic-normalize.ts index 31c2f6c8..58c30026 100644 --- a/src/renderer/api/subsonic/subsonic-normalize.ts +++ b/src/renderer/api/subsonic/subsonic-normalize.ts @@ -126,6 +126,7 @@ const normalizeAlbumArtist = ( imageUrl, itemType: LibraryItem.ALBUM_ARTIST, lastPlayedAt: null, + mbz: null, name: item.name, playCount: null, serverId: server?.id || 'unknown', @@ -150,6 +151,7 @@ const normalizeAlbum = ( }) || null; return { + albumArtist: item.artist, albumArtists: item.artistId ? [{ id: item.artistId, imageUrl: null, name: item.artist }] : [], @@ -174,6 +176,7 @@ const normalizeAlbum = ( isCompilation: null, itemType: LibraryItem.ALBUM, lastPlayedAt: null, + mbzId: null, name: item.name, playCount: null, releaseDate: item.year ? new Date(item.year, 0, 1).toISOString() : null, diff --git a/src/renderer/api/types.ts b/src/renderer/api/types.ts index 3abf9f64..8193dcd1 100644 --- a/src/renderer/api/types.ts +++ b/src/renderer/api/types.ts @@ -144,6 +144,7 @@ export type Genre = { }; export type Album = { + albumArtist: string; albumArtists: RelatedArtist[]; artists: RelatedArtist[]; backdropImageUrl: string | null; @@ -157,6 +158,7 @@ export type Album = { isCompilation: boolean | null; itemType: LibraryItem.ALBUM; lastPlayedAt: string | null; + mbzId: string | null; name: string; playCount: number | null; releaseDate: string | null; @@ -229,6 +231,7 @@ export type AlbumArtist = { imageUrl: string | null; itemType: LibraryItem.ALBUM_ARTIST; lastPlayedAt: string | null; + mbz: string | null; name: string; playCount: number | null; serverId: string; diff --git a/src/renderer/features/albums/components/album-detail-content.tsx b/src/renderer/features/albums/components/album-detail-content.tsx index 97addc46..48b154bb 100644 --- a/src/renderer/features/albums/components/album-detail-content.tsx +++ b/src/renderer/features/albums/components/album-detail-content.tsx @@ -4,7 +4,9 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li import { Box, Group, Spoiler, Stack } from '@mantine/core'; import { useSetState } from '@mantine/hooks'; import { useTranslation } from 'react-i18next'; +import { FaLastfmSquare } from 'react-icons/fa'; import { RiHeartFill, RiHeartLine, RiMoreFill, RiSettings2Fill } from 'react-icons/ri'; +import { SiMusicbrainz } from 'react-icons/si'; import { generatePath, useParams } from 'react-router'; import { Link } from 'react-router-dom'; import styled from 'styled-components'; @@ -36,6 +38,7 @@ import { useAppFocus, useContainerQuery } from '/@/renderer/hooks'; import { AppRoute } from '/@/renderer/router/routes'; import { useCurrentServer, useCurrentSong, useCurrentStatus } from '/@/renderer/store'; import { + useGeneralSettings, usePlayButtonBehavior, useSettingsStoreActions, useTableSettings, @@ -76,6 +79,7 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP const status = useCurrentStatus(); const isFocused = useAppFocus(); const currentSong = useCurrentSong(); + const { externalLinks } = useGeneralSettings(); const columnDefs = useMemo( () => getColumnDefs(tableConfig.columns, false, 'albumDetail'), @@ -315,6 +319,8 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP const { rowClassRules } = useCurrentSongRowStyles({ tableRef }); + const mbzId = detailQuery?.data?.mbzId; + return ( @@ -397,6 +403,46 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP )} + {externalLinks ? ( + + + + {mbzId ? ( + + ) : null} + + + ) : null} {comment && ( { + const { t } = useTranslation(); + const { externalLinks } = useGeneralSettings(); const { albumArtistId } = useParams() as { albumArtistId: string }; const cq = useContainerQuery(); const handlePlayQueueAdd = usePlayQueueAdd(); @@ -324,6 +329,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten detailQuery?.data?.biography !== undefined && detailQuery?.data?.biography !== null; const showTopSongs = topSongsQuery?.data?.items?.length; const showGenres = detailQuery?.data?.genres ? detailQuery?.data?.genres.length !== 0 : false; + const mbzId = detailQuery?.data?.mbz; const isLoading = detailQuery?.isLoading || @@ -411,6 +417,50 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten ) : null} + {externalLinks ? ( + + + + {mbzId ? ( + + ) : null} + + + ) : null} {showBiography ? ( { isHidden: false, title: t('setting.skipPlaylistPage', { postProcess: 'sentenceCase' }), }, + { + control: ( + { + setSettings({ + general: { + ...settings, + externalLinks: e.currentTarget.checked, + }, + }); + }} + /> + ), + description: t('setting.externalLinks', { + context: 'description', + postProcess: 'sentenceCase', + }), + title: t('setting.externalLinks', { postProcess: 'sentenceCase' }), + }, ]; return ; diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 75144475..a795178f 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -170,6 +170,7 @@ export interface SettingsState { general: { accent: string; defaultFullPlaylist: boolean; + externalLinks: boolean; followSystemTheme: boolean; language: string; playButtonBehavior: Play; @@ -281,6 +282,7 @@ const initialState: SettingsState = { general: { accent: 'rgb(53, 116, 252)', defaultFullPlaylist: true, + externalLinks: false, followSystemTheme: false, language: 'en', playButtonBehavior: Play.NOW,