include lastfm/mbz links

This commit is contained in:
Kendall Garner 2024-01-15 22:10:50 -08:00
parent 5516daab6e
commit ea67a18962
No known key found for this signature in database
GPG key ID: 18D2767419676C87
10 changed files with 146 additions and 3 deletions

View file

@ -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",

View file

@ -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 || '',

View file

@ -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

View file

@ -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',

View file

@ -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,

View file

@ -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;

View file

@ -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 (
<ContentContainer>
<LibraryBackgroundOverlay $backgroundColor={background} />
@ -397,6 +403,46 @@ export const AlbumDetailContent = ({ tableRef, background }: AlbumDetailContentP
</Group>
</Box>
)}
{externalLinks ? (
<Box component="section">
<Group spacing="sm">
<Button
compact
component="a"
href={`https://www.last.fm/music/${encodeURIComponent(
detailQuery?.data?.albumArtist || '',
)}/${encodeURIComponent(detailQuery.data?.name || '')}`}
radius="md"
rel="noopener noreferrer"
size="md"
target="_blank"
tooltip={{
label: t('action.openIn.lastfm'),
}}
variant="subtle"
>
<FaLastfmSquare size={25} />
</Button>
{mbzId ? (
<Button
compact
component="a"
href={`https://musicbrainz.org/release/${mbzId}`}
radius="md"
rel="noopener noreferrer"
size="md"
target="_blank"
tooltip={{
label: t('action.openIn.musicbrainz'),
}}
variant="subtle"
>
<SiMusicbrainz size={25} />
</Button>
) : null}
</Group>
</Box>
) : null}
{comment && (
<Box component="section">
<Spoiler

View file

@ -1,7 +1,10 @@
import { useMemo } from 'react';
import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
import { Box, Group, Stack } from '@mantine/core';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { FaLastfmSquare } from 'react-icons/fa';
import { RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
import { SiMusicbrainz } from 'react-icons/si';
import { generatePath, useParams } from 'react-router';
import { createSearchParams, Link } from 'react-router-dom';
import styled from 'styled-components';
@ -34,7 +37,7 @@ import { LibraryBackgroundOverlay } from '/@/renderer/features/shared/components
import { useContainerQuery } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { useGeneralSettings, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { CardRow, Play, TableColumn } from '/@/renderer/types';
const ContentContainer = styled.div`
@ -59,6 +62,8 @@ interface AlbumArtistDetailContentProps {
}
export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailContentProps) => {
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
</Group>
</Box>
) : null}
{externalLinks ? (
<Box
component="section"
mb={-20}
mt={-20}
>
<Group spacing="sm">
<Button
compact
component="a"
href={`https://www.last.fm/music/${encodeURIComponent(
detailQuery?.data?.name || '',
)}`}
radius="md"
rel="noopener noreferrer"
size="md"
target="_blank"
tooltip={{
label: t('action.openIn.lastfm'),
}}
variant="subtle"
>
<FaLastfmSquare size={25} />
</Button>
{mbzId ? (
<Button
compact
component="a"
href={`https://musicbrainz.org/artist/${mbzId}`}
radius="md"
rel="noopener noreferrer"
size="md"
target="_blank"
tooltip={{
label: t('action.openIn.musicbrainz'),
}}
variant="subtle"
>
<SiMusicbrainz size={25} />
</Button>
) : null}
</Group>
</Box>
) : null}
{showBiography ? (
<Box
component="section"

View file

@ -268,6 +268,26 @@ export const ControlSettings = () => {
isHidden: false,
title: t('setting.skipPlaylistPage', { postProcess: 'sentenceCase' }),
},
{
control: (
<Switch
defaultChecked={settings.externalLinks}
onChange={(e) => {
setSettings({
general: {
...settings,
externalLinks: e.currentTarget.checked,
},
});
}}
/>
),
description: t('setting.externalLinks', {
context: 'description',
postProcess: 'sentenceCase',
}),
title: t('setting.externalLinks', { postProcess: 'sentenceCase' }),
},
];
return <SettingsSection options={controlOptions} />;

View file

@ -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,