mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
add settings configuration for integrations
This commit is contained in:
@@ -583,6 +583,7 @@
|
|||||||
"analytics": "analytics",
|
"analytics": "analytics",
|
||||||
"generalTab": "general",
|
"generalTab": "general",
|
||||||
"hotkeysTab": "hotkeys",
|
"hotkeysTab": "hotkeys",
|
||||||
|
"integrationsTab": "integrations",
|
||||||
"playbackTab": "playback",
|
"playbackTab": "playback",
|
||||||
"windowTab": "window",
|
"windowTab": "window",
|
||||||
"updates": "update",
|
"updates": "update",
|
||||||
@@ -893,6 +894,14 @@
|
|||||||
"mpvExtraParameters_help": "one per line",
|
"mpvExtraParameters_help": "one per line",
|
||||||
"musicbrainz_description": "show links to MusicBrainz on artist/album pages, where MusicBrainz ID exists",
|
"musicbrainz_description": "show links to MusicBrainz on artist/album pages, where MusicBrainz ID exists",
|
||||||
"musicbrainz": "show MusicBrainz links",
|
"musicbrainz": "show MusicBrainz links",
|
||||||
|
"musicBrainzQueries": "enable MusicBrainz integration",
|
||||||
|
"musicBrainzQueries_description": "the integration will query MusicBrainz for missing artist releases and other miscellaneous data",
|
||||||
|
"musicbrainzExcludeReleaseTypes": "exclude MusicBrainz release types",
|
||||||
|
"musicbrainzExcludeReleaseTypes_description": "release types to exclude when loading MusicBrainz artist releases (e.g. album, single, ep)",
|
||||||
|
"musicbrainzPrioritizeCountries": "prioritize MusicBrainz countries",
|
||||||
|
"musicbrainzPrioritizeCountries_description": "country codes to prioritize when ordering MusicBrainz releases, comma separated and non case-sensitive (e.g. us, gb, de)",
|
||||||
|
"youtube": "enable youtube integration",
|
||||||
|
"youtube_description": "external songs will attempt to use YouTube to resolve stream URLs for playback",
|
||||||
"neteaseTranslation_description": "When enabled, fetches and displays translated lyrics from NetEase if available",
|
"neteaseTranslation_description": "When enabled, fetches and displays translated lyrics from NetEase if available",
|
||||||
"neteaseTranslation": "Enable NetEase translations",
|
"neteaseTranslation": "Enable NetEase translations",
|
||||||
"notify": "enable song notifications",
|
"notify": "enable song notifications",
|
||||||
|
|||||||
@@ -1379,10 +1379,10 @@ const ArtistAlbums = ({ albumsQuery }: ArtistAlbumsProps) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const artistReleaseTypeItems = useArtistReleaseTypeItems();
|
const artistReleaseTypeItems = useArtistReleaseTypeItems();
|
||||||
const musicBrainzExcludeReleaseTypes = useSettingsStore(
|
const musicBrainzExcludeReleaseTypes = useSettingsStore(
|
||||||
(state) => state.general.musicBrainzExcludeReleaseTypes,
|
(state) => state.integrations.musicBrainzExcludeReleaseTypes,
|
||||||
);
|
);
|
||||||
const musicBrainzPrioritizeCountries = useSettingsStore(
|
const musicBrainzPrioritizeCountries = useSettingsStore(
|
||||||
(state) => state.general.musicBrainzPrioritizeCountries,
|
(state) => state.integrations.musicBrainzPrioritizeCountries,
|
||||||
);
|
);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
|
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
|
||||||
@@ -1407,12 +1407,14 @@ const ArtistAlbums = ({ albumsQuery }: ArtistAlbumsProps) => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const musicBrainzEnabled = useSettingsStore((state) => state.integrations.musicBrainz);
|
||||||
const musicbrainzArtistQuery = useQuery({
|
const musicbrainzArtistQuery = useQuery({
|
||||||
...musicbrainzQueries.artist({
|
...musicbrainzQueries.artist({
|
||||||
excludeReleaseTypes: musicBrainzExcludeReleaseTypes,
|
excludeReleaseTypes: musicBrainzExcludeReleaseTypes,
|
||||||
mbzArtistId: detailQuery.data?.mbz as string,
|
mbzArtistId: detailQuery.data?.mbz as string,
|
||||||
prioritizeCountries: musicBrainzPrioritizeCountries,
|
prioritizeCountries: musicBrainzPrioritizeCountries,
|
||||||
}),
|
}),
|
||||||
|
enabled: Boolean(musicBrainzEnabled && detailQuery.data?.mbz),
|
||||||
meta: {
|
meta: {
|
||||||
albumArtist: detailQuery.data,
|
albumArtist: detailQuery.data,
|
||||||
albums: albumsQuery.data?.items || [],
|
albums: albumsQuery.data?.items || [],
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
getImageUrl,
|
getImageUrl,
|
||||||
normalizeReleaseToAlbum,
|
normalizeReleaseToAlbum,
|
||||||
} from '/@/renderer/features/musicbrainz/utils';
|
} from '/@/renderer/features/musicbrainz/utils';
|
||||||
|
import { logFn } from '/@/renderer/utils/logger';
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
AlbumArtist,
|
AlbumArtist,
|
||||||
@@ -283,6 +284,12 @@ const RELEASE_INCLUDES: Array<
|
|||||||
| 'release-groups'
|
| 'release-groups'
|
||||||
> = ['artist-credits', 'artists', 'media', 'recording-level-rels', 'recordings', 'release-groups'];
|
> = ['artist-credits', 'artists', 'media', 'recording-level-rels', 'recordings', 'release-groups'];
|
||||||
|
|
||||||
|
const EMPTY_BROWSE_RELEASES: IBrowseReleasesResult = {
|
||||||
|
'release-count': 0,
|
||||||
|
'release-offset': 0,
|
||||||
|
releases: [],
|
||||||
|
};
|
||||||
|
|
||||||
export const musicbrainzQueries = {
|
export const musicbrainzQueries = {
|
||||||
artist: (args: {
|
artist: (args: {
|
||||||
excludeReleaseTypes?: string[];
|
excludeReleaseTypes?: string[];
|
||||||
@@ -297,13 +304,30 @@ export const musicbrainzQueries = {
|
|||||||
return queryOptions({
|
return queryOptions({
|
||||||
gcTime: CACHE_TIME,
|
gcTime: CACHE_TIME,
|
||||||
queryFn: async ({ meta }) => {
|
queryFn: async ({ meta }) => {
|
||||||
|
try {
|
||||||
const artist = await musicbrainzApi.lookup('artist', args.mbzArtistId);
|
const artist = await musicbrainzApi.lookup('artist', args.mbzArtistId);
|
||||||
const releases = await fetchMbzReleasesByArtistId(args.mbzArtistId);
|
const releases = await fetchMbzReleasesByArtistId(args.mbzArtistId);
|
||||||
|
|
||||||
|
logFn.debug('MusicBrainz artist lookup API queried', {
|
||||||
|
meta: { artistId: args.mbzArtistId, releases },
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: { artist, releases },
|
data: { artist, releases },
|
||||||
meta: meta as MusicBrainzArtistSelectMeta,
|
meta: meta as MusicBrainzArtistSelectMeta,
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
logFn.warn('MusicBrainz artist lookup failed', {
|
||||||
|
meta: { artistId: args.mbzArtistId, error },
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
artist: {} as IArtist,
|
||||||
|
releases: EMPTY_BROWSE_RELEASES,
|
||||||
|
},
|
||||||
|
meta: meta as MusicBrainzArtistSelectMeta,
|
||||||
|
};
|
||||||
|
}
|
||||||
},
|
},
|
||||||
queryKey: queryKeys.musicbrainz.artist(undefined, args.mbzArtistId, config),
|
queryKey: queryKeys.musicbrainz.artist(undefined, args.mbzArtistId, config),
|
||||||
select: artistSelect,
|
select: artistSelect,
|
||||||
@@ -314,6 +338,7 @@ export const musicbrainzQueries = {
|
|||||||
queryOptions({
|
queryOptions({
|
||||||
gcTime: CACHE_TIME,
|
gcTime: CACHE_TIME,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
|
try {
|
||||||
const mbzRelease = await musicbrainzApi.lookup(
|
const mbzRelease = await musicbrainzApi.lookup(
|
||||||
'release',
|
'release',
|
||||||
args.releaseId,
|
args.releaseId,
|
||||||
@@ -321,7 +346,18 @@ export const musicbrainzQueries = {
|
|||||||
);
|
);
|
||||||
const release = normalizeReleaseToAlbum(mbzRelease);
|
const release = normalizeReleaseToAlbum(mbzRelease);
|
||||||
const works = collectWorksFromRelease(mbzRelease);
|
const works = collectWorksFromRelease(mbzRelease);
|
||||||
|
|
||||||
|
logFn.debug('MusicBrainz release lookup API queried', {
|
||||||
|
meta: { release, releaseId: args.releaseId },
|
||||||
|
});
|
||||||
|
|
||||||
return { release, works };
|
return { release, works };
|
||||||
|
} catch (error) {
|
||||||
|
logFn.warn('MusicBrainz release lookup failed', {
|
||||||
|
meta: { error, releaseId: args.releaseId },
|
||||||
|
});
|
||||||
|
return { release: null, works: [] };
|
||||||
|
}
|
||||||
},
|
},
|
||||||
queryKey: queryKeys.musicbrainz.release(args.releaseId),
|
queryKey: queryKeys.musicbrainz.release(args.releaseId),
|
||||||
staleTime: CACHE_TIME,
|
staleTime: CACHE_TIME,
|
||||||
@@ -331,6 +367,52 @@ export const musicbrainzQueries = {
|
|||||||
export const MUSICBRAINZ_ID_PREFIX = 'musicbrainz-';
|
export const MUSICBRAINZ_ID_PREFIX = 'musicbrainz-';
|
||||||
|
|
||||||
export async function fetchMbzReleaseAsAlbum(releaseId: string): Promise<Album> {
|
export async function fetchMbzReleaseAsAlbum(releaseId: string): Promise<Album> {
|
||||||
|
try {
|
||||||
const mbzRelease = await musicbrainzApi.lookup('release', releaseId, RELEASE_INCLUDES);
|
const mbzRelease = await musicbrainzApi.lookup('release', releaseId, RELEASE_INCLUDES);
|
||||||
return normalizeReleaseToAlbum(mbzRelease);
|
return normalizeReleaseToAlbum(mbzRelease);
|
||||||
|
} catch (error) {
|
||||||
|
logFn.warn('MusicBrainz release fetch failed', { meta: { error, releaseId } });
|
||||||
|
return createEmptyMbzAlbum(releaseId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createEmptyMbzAlbum(releaseId: string): Album {
|
||||||
|
return {
|
||||||
|
_itemType: LibraryItem.ALBUM,
|
||||||
|
_serverId: 'musicbrainz',
|
||||||
|
_serverType: ServerType.EXTERNAL,
|
||||||
|
albumArtistName: '',
|
||||||
|
albumArtists: [],
|
||||||
|
artists: [],
|
||||||
|
comment: null,
|
||||||
|
createdAt: '',
|
||||||
|
duration: null,
|
||||||
|
explicitStatus: null,
|
||||||
|
genres: [],
|
||||||
|
id: `musicbrainz-${releaseId}`,
|
||||||
|
imageId: null,
|
||||||
|
imageUrl: null,
|
||||||
|
isCompilation: null,
|
||||||
|
lastPlayedAt: null,
|
||||||
|
mbzId: releaseId,
|
||||||
|
mbzReleaseGroupId: null,
|
||||||
|
name: '',
|
||||||
|
originalDate: null,
|
||||||
|
originalYear: null,
|
||||||
|
participants: {},
|
||||||
|
playCount: null,
|
||||||
|
recordLabels: [],
|
||||||
|
releaseDate: null,
|
||||||
|
releaseType: null,
|
||||||
|
releaseTypes: [],
|
||||||
|
releaseYear: null,
|
||||||
|
size: null,
|
||||||
|
songCount: null,
|
||||||
|
sortName: '',
|
||||||
|
tags: {},
|
||||||
|
updatedAt: '',
|
||||||
|
userFavorite: false,
|
||||||
|
userRating: null,
|
||||||
|
version: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { queryOptions } from '@tanstack/react-query';
|
import { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { logFn } from '/@/renderer/utils/logger';
|
||||||
|
|
||||||
async function searchYoutube(query: string): Promise<Array<{ type: string; videoId?: string }>> {
|
async function searchYoutube(query: string): Promise<Array<{ type: string; videoId?: string }>> {
|
||||||
if (typeof window !== 'undefined' && window.api?.youtube) {
|
if (typeof window !== 'undefined' && window.api?.youtube) {
|
||||||
return window.api.youtube.search(query);
|
return window.api.youtube.search(query);
|
||||||
@@ -11,7 +13,11 @@ export const youtubeQueries = {
|
|||||||
search: (args: { query: string }) => {
|
search: (args: { query: string }) => {
|
||||||
return queryOptions({
|
return queryOptions({
|
||||||
gcTime: 1000 * 60 * 1,
|
gcTime: 1000 * 60 * 1,
|
||||||
queryFn: () => searchYoutube(args.query),
|
queryFn: async () => {
|
||||||
|
const results = await searchYoutube(args.query);
|
||||||
|
logFn.debug('Youtube API queried', { meta: { query: args.query, results } });
|
||||||
|
return results;
|
||||||
|
},
|
||||||
queryKey: ['youtube', 'search', args.query],
|
queryKey: ['youtube', 'search', args.query],
|
||||||
staleTime: 1000 * 60 * 1,
|
staleTime: 1000 * 60 * 1,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useMemo, useRef } from 'react';
|
|||||||
|
|
||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { youtubeQueries } from '/@/renderer/features/musicbrainz/api/youtube-api';
|
import { youtubeQueries } from '/@/renderer/features/musicbrainz/api/youtube-api';
|
||||||
import { TranscodingConfig } from '/@/renderer/store';
|
import { TranscodingConfig, useSettingsStore } from '/@/renderer/store';
|
||||||
import { QueueSong, ServerType } from '/@/shared/types/domain-types';
|
import { QueueSong, ServerType } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
const YOUTUBE_WATCH_BASE = 'https://www.youtube.com/watch?v=';
|
const YOUTUBE_WATCH_BASE = 'https://www.youtube.com/watch?v=';
|
||||||
@@ -16,12 +16,13 @@ export function useSongUrl(
|
|||||||
const prior = useRef(['', '']);
|
const prior = useRef(['', '']);
|
||||||
|
|
||||||
const isExternal = song?._serverType === ServerType.EXTERNAL;
|
const isExternal = song?._serverType === ServerType.EXTERNAL;
|
||||||
|
const youtubeEnabled = useSettingsStore((state) => state.integrations.youtube);
|
||||||
const searchQuery =
|
const searchQuery =
|
||||||
song && isExternal ? `${song.artistName ?? ''} ${song.name ?? ''}`.trim() : '';
|
song && isExternal ? buildYoutubeSearchQuery(song.name, song.artistName) : '';
|
||||||
|
|
||||||
const youtubeSearch = useQuery({
|
const youtubeSearch = useQuery({
|
||||||
...youtubeQueries.search({ query: searchQuery }),
|
...youtubeQueries.search({ query: searchQuery }),
|
||||||
enabled: Boolean(song && isExternal && searchQuery),
|
enabled: Boolean(song && isExternal && searchQuery && youtubeEnabled),
|
||||||
});
|
});
|
||||||
|
|
||||||
const externalUrl = useMemo(() => {
|
const externalUrl = useMemo(() => {
|
||||||
@@ -77,6 +78,16 @@ export function useSongUrl(
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildYoutubeSearchQuery(
|
||||||
|
title: string | undefined,
|
||||||
|
artistName: string | undefined,
|
||||||
|
): string {
|
||||||
|
const t = (title ?? '').trim();
|
||||||
|
const a = (artistName ?? '').trim();
|
||||||
|
if (t && a) return `${t} by ${a}`;
|
||||||
|
return t || a || '';
|
||||||
|
}
|
||||||
|
|
||||||
function getYoutubeUrlFromSearchResults(
|
function getYoutubeUrlFromSearchResults(
|
||||||
results: Array<{ type: string; videoId?: string }> | undefined,
|
results: Array<{ type: string; videoId?: string }> | undefined,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
@@ -112,10 +123,11 @@ export async function getSongUrlAsync(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (song._serverType === ServerType.EXTERNAL) {
|
if (song._serverType === ServerType.EXTERNAL) {
|
||||||
if (typeof window === 'undefined' || !window.api?.youtube) {
|
const youtubeEnabled = useSettingsStore.getState().integrations?.youtube ?? true;
|
||||||
|
if (!youtubeEnabled || typeof window === 'undefined' || !window.api?.youtube) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const searchQuery = `${song.artistName ?? ''} ${song.name ?? ''}`.trim();
|
const searchQuery = buildYoutubeSearchQuery(song.name, song.artistName);
|
||||||
if (!searchQuery) {
|
if (!searchQuery) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SettingOption,
|
||||||
|
SettingsSection,
|
||||||
|
} from '/@/renderer/features/settings/components/settings-section';
|
||||||
|
import {
|
||||||
|
useGeneralSettings,
|
||||||
|
useIntegrationsSettings,
|
||||||
|
useSettingsStoreActions,
|
||||||
|
} from '/@/renderer/store';
|
||||||
|
import { MultiSelect } from '/@/shared/components/multi-select/multi-select';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
import { Switch } from '/@/shared/components/switch/switch';
|
||||||
|
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||||
|
|
||||||
|
const MUSICBRAINZ_RELEASE_TYPES = [
|
||||||
|
'album',
|
||||||
|
'single',
|
||||||
|
'ep',
|
||||||
|
'broadcast',
|
||||||
|
'compilation',
|
||||||
|
'live',
|
||||||
|
'remix',
|
||||||
|
'appears-on',
|
||||||
|
'audiobook',
|
||||||
|
'audio drama',
|
||||||
|
'demo',
|
||||||
|
'dj-mix',
|
||||||
|
'field recording',
|
||||||
|
'interview',
|
||||||
|
'mixtape/street',
|
||||||
|
'other',
|
||||||
|
'soundtrack',
|
||||||
|
'spokenword',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const IntegrationsTab = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { musicBrainz } = useGeneralSettings();
|
||||||
|
const settings = useIntegrationsSettings();
|
||||||
|
const { setSettings } = useSettingsStoreActions();
|
||||||
|
|
||||||
|
const updateIntegrations = (updates: Partial<typeof settings>) => {
|
||||||
|
setSettings({
|
||||||
|
integrations: {
|
||||||
|
...settings,
|
||||||
|
...updates,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const options: SettingOption[] = [
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
aria-label={t('setting.musicBrainzQueries', { postProcess: 'sentenceCase' })}
|
||||||
|
defaultChecked={settings.musicBrainz}
|
||||||
|
onChange={(e) => updateIntegrations({ musicBrainz: e.currentTarget.checked })}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.musicBrainzQueries', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
title: t('setting.musicBrainzQueries', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<MultiSelect
|
||||||
|
aria-label={t('setting.musicbrainzExcludeReleaseTypes', {
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
})}
|
||||||
|
clearable
|
||||||
|
data={MUSICBRAINZ_RELEASE_TYPES}
|
||||||
|
defaultValue={settings.musicBrainzExcludeReleaseTypes}
|
||||||
|
onChange={(value) =>
|
||||||
|
updateIntegrations({ musicBrainzExcludeReleaseTypes: value })
|
||||||
|
}
|
||||||
|
width={300}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.musicbrainzExcludeReleaseTypes', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: !musicBrainz || !settings.musicBrainz,
|
||||||
|
title: t('setting.musicbrainzExcludeReleaseTypes', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<TextInput
|
||||||
|
defaultValue={settings.musicBrainzPrioritizeCountries.join(', ')}
|
||||||
|
key={settings.musicBrainzPrioritizeCountries.join(',')}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const value = e.currentTarget.value
|
||||||
|
.split(/[,;\s]+/)
|
||||||
|
.map((s) => s.trim().toUpperCase())
|
||||||
|
.filter(Boolean);
|
||||||
|
updateIntegrations({ musicBrainzPrioritizeCountries: value });
|
||||||
|
}}
|
||||||
|
placeholder="e.g. US, GB, DE"
|
||||||
|
width={300}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.musicbrainzPrioritizeCountries', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
isHidden: !musicBrainz || !settings.musicBrainz,
|
||||||
|
title: t('setting.musicbrainzPrioritizeCountries', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
control: (
|
||||||
|
<Switch
|
||||||
|
aria-label={t('setting.youtube', { postProcess: 'sentenceCase' })}
|
||||||
|
defaultChecked={settings.youtube}
|
||||||
|
onChange={(e) => updateIntegrations({ youtube: e.currentTarget.checked })}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
description: t('setting.youtube', {
|
||||||
|
context: 'description',
|
||||||
|
postProcess: 'sentenceCase',
|
||||||
|
}),
|
||||||
|
title: t('setting.youtube', { postProcess: 'sentenceCase' }),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack gap="md">
|
||||||
|
<SettingsSection
|
||||||
|
options={options}
|
||||||
|
title={t('page.setting.integrationsTab', { postProcess: 'sentenceCase' })}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -24,6 +24,14 @@ const HotkeysTab = lazy(() =>
|
|||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const IntegrationsTab = lazy(() =>
|
||||||
|
import('/@/renderer/features/settings/components/integrations/integrations-tab').then(
|
||||||
|
(module) => ({
|
||||||
|
default: module.IntegrationsTab,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
const WindowTab = lazy(() =>
|
const WindowTab = lazy(() =>
|
||||||
import('/@/renderer/features/settings/components/window/window-tab').then((module) => ({
|
import('/@/renderer/features/settings/components/window/window-tab').then((module) => ({
|
||||||
default: module.WindowTab,
|
default: module.WindowTab,
|
||||||
@@ -61,6 +69,9 @@ export const SettingsContent = () => {
|
|||||||
<Tabs.Tab value="hotkeys">
|
<Tabs.Tab value="hotkeys">
|
||||||
{t('page.setting.hotkeysTab', { postProcess: 'sentenceCase' })}
|
{t('page.setting.hotkeysTab', { postProcess: 'sentenceCase' })}
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="integrations">
|
||||||
|
{t('page.setting.integrationsTab', { postProcess: 'sentenceCase' })}
|
||||||
|
</Tabs.Tab>
|
||||||
{isElectron() && (
|
{isElectron() && (
|
||||||
<Tabs.Tab value="window">
|
<Tabs.Tab value="window">
|
||||||
{t('page.setting.windowTab', { postProcess: 'sentenceCase' })}
|
{t('page.setting.windowTab', { postProcess: 'sentenceCase' })}
|
||||||
@@ -85,6 +96,11 @@ export const SettingsContent = () => {
|
|||||||
<HotkeysTab />
|
<HotkeysTab />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
<Tabs.Panel value="integrations">
|
||||||
|
<Suspense fallback={null}>
|
||||||
|
<IntegrationsTab />
|
||||||
|
</Suspense>
|
||||||
|
</Tabs.Panel>
|
||||||
{isElectron() && (
|
{isElectron() && (
|
||||||
<Tabs.Panel value="window">
|
<Tabs.Panel value="window">
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
|
|||||||
@@ -440,8 +440,6 @@ export const GeneralSettingsSchema = z.object({
|
|||||||
lastFM: z.boolean(),
|
lastFM: z.boolean(),
|
||||||
lastfmApiKey: z.string(),
|
lastfmApiKey: z.string(),
|
||||||
musicBrainz: z.boolean(),
|
musicBrainz: z.boolean(),
|
||||||
musicBrainzExcludeReleaseTypes: z.array(z.string()),
|
|
||||||
musicBrainzPrioritizeCountries: z.array(z.string()),
|
|
||||||
nativeAspectRatio: z.boolean(),
|
nativeAspectRatio: z.boolean(),
|
||||||
passwordStore: z.string().optional(),
|
passwordStore: z.string().optional(),
|
||||||
pathReplace: z.string(),
|
pathReplace: z.string(),
|
||||||
@@ -615,6 +613,13 @@ const QueryBuilderSettingsSchema = z.object({
|
|||||||
tag: z.array(QueryBuilderCustomFieldSchema),
|
tag: z.array(QueryBuilderCustomFieldSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const IntegrationsSettingsSchema = z.object({
|
||||||
|
musicBrainz: z.boolean(),
|
||||||
|
musicBrainzExcludeReleaseTypes: z.array(z.string()),
|
||||||
|
musicBrainzPrioritizeCountries: z.array(z.string()),
|
||||||
|
youtube: z.boolean(),
|
||||||
|
});
|
||||||
|
|
||||||
const AutoDJSettingsSchema = z.object({
|
const AutoDJSettingsSchema = z.object({
|
||||||
enabled: z.boolean(),
|
enabled: z.boolean(),
|
||||||
itemCount: z.number(),
|
itemCount: z.number(),
|
||||||
@@ -631,6 +636,7 @@ export const ValidationSettingsStateSchema = z.object({
|
|||||||
font: FontSettingsSchema,
|
font: FontSettingsSchema,
|
||||||
general: GeneralSettingsSchema,
|
general: GeneralSettingsSchema,
|
||||||
hotkeys: HotkeysSettingsSchema,
|
hotkeys: HotkeysSettingsSchema,
|
||||||
|
integrations: IntegrationsSettingsSchema,
|
||||||
lists: z.record(z.nativeEnum(ItemListKey), ItemListConfigSchema),
|
lists: z.record(z.nativeEnum(ItemListKey), ItemListConfigSchema),
|
||||||
lyrics: LyricsSettingsSchema,
|
lyrics: LyricsSettingsSchema,
|
||||||
lyricsDisplay: z.record(z.string(), LyricsDisplaySettingsSchema),
|
lyricsDisplay: z.record(z.string(), LyricsDisplaySettingsSchema),
|
||||||
@@ -640,6 +646,7 @@ export const ValidationSettingsStateSchema = z.object({
|
|||||||
tab: z.union([
|
tab: z.union([
|
||||||
z.literal('general'),
|
z.literal('general'),
|
||||||
z.literal('hotkeys'),
|
z.literal('hotkeys'),
|
||||||
|
z.literal('integrations'),
|
||||||
z.literal('playback'),
|
z.literal('playback'),
|
||||||
z.literal('window'),
|
z.literal('window'),
|
||||||
z.string(),
|
z.string(),
|
||||||
@@ -1015,8 +1022,6 @@ const initialState: SettingsState = {
|
|||||||
lastFM: true,
|
lastFM: true,
|
||||||
lastfmApiKey: '',
|
lastfmApiKey: '',
|
||||||
musicBrainz: true,
|
musicBrainz: true,
|
||||||
musicBrainzExcludeReleaseTypes: [],
|
|
||||||
musicBrainzPrioritizeCountries: [],
|
|
||||||
nativeAspectRatio: false,
|
nativeAspectRatio: false,
|
||||||
passwordStore: undefined,
|
passwordStore: undefined,
|
||||||
pathReplace: '',
|
pathReplace: '',
|
||||||
@@ -1099,6 +1104,12 @@ const initialState: SettingsState = {
|
|||||||
},
|
},
|
||||||
globalMediaHotkeys: true,
|
globalMediaHotkeys: true,
|
||||||
},
|
},
|
||||||
|
integrations: {
|
||||||
|
musicBrainz: true,
|
||||||
|
musicBrainzExcludeReleaseTypes: [],
|
||||||
|
musicBrainzPrioritizeCountries: [],
|
||||||
|
youtube: true,
|
||||||
|
},
|
||||||
lists: {
|
lists: {
|
||||||
['albumDetail']: {
|
['albumDetail']: {
|
||||||
display: ListDisplayType.TABLE,
|
display: ListDisplayType.TABLE,
|
||||||
@@ -2093,10 +2104,36 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (version <= 24) {
|
||||||
|
// Move MusicBrainz release/country options to Integrations; keep musicBrainz in general
|
||||||
|
const general = state.general as Record<string, unknown>;
|
||||||
|
state.integrations = {
|
||||||
|
musicBrainz: initialState.integrations.musicBrainz,
|
||||||
|
musicBrainzExcludeReleaseTypes:
|
||||||
|
(general?.musicBrainzExcludeReleaseTypes as string[]) ??
|
||||||
|
initialState.integrations.musicBrainzExcludeReleaseTypes,
|
||||||
|
musicBrainzPrioritizeCountries:
|
||||||
|
(general?.musicBrainzPrioritizeCountries as string[]) ??
|
||||||
|
initialState.integrations.musicBrainzPrioritizeCountries,
|
||||||
|
youtube: initialState.integrations.youtube,
|
||||||
|
};
|
||||||
|
delete general.musicBrainzExcludeReleaseTypes;
|
||||||
|
delete general.musicBrainzPrioritizeCountries;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (version <= 25) {
|
||||||
|
// Add integrations.musicBrainz to enable/disable MusicBrainz API queries
|
||||||
|
state.integrations = {
|
||||||
|
...state.integrations,
|
||||||
|
musicBrainz:
|
||||||
|
(state.integrations as { musicBrainz?: boolean })?.musicBrainz ?? true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return persistedState;
|
return persistedState;
|
||||||
},
|
},
|
||||||
name: 'store_settings',
|
name: 'store_settings',
|
||||||
version: 24,
|
version: 26,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -2252,6 +2289,9 @@ export const useAlbumBackground = () =>
|
|||||||
shallow,
|
shallow,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const useIntegrationsSettings = () =>
|
||||||
|
useSettingsStore((state) => state.integrations, shallow);
|
||||||
|
|
||||||
export const useExternalLinks = () =>
|
export const useExternalLinks = () =>
|
||||||
useSettingsStore(
|
useSettingsStore(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
|
|||||||
@@ -34,17 +34,18 @@ export const idbStateStorage: StateStorage = {
|
|||||||
const settingsKeys = [
|
const settingsKeys = [
|
||||||
'store_settings_autoDJ',
|
'store_settings_autoDJ',
|
||||||
'store_settings_general',
|
'store_settings_general',
|
||||||
'store_settings_lists',
|
|
||||||
'store_settings_hotkeys',
|
'store_settings_hotkeys',
|
||||||
'store_settings_playback',
|
'store_settings_integrations',
|
||||||
|
'store_settings_lists',
|
||||||
'store_settings_lyrics',
|
'store_settings_lyrics',
|
||||||
|
'store_settings_playback',
|
||||||
|
'store_settings_queryBuilder',
|
||||||
|
'store_settings_remote',
|
||||||
|
'store_settings_tab',
|
||||||
'store_settings_window',
|
'store_settings_window',
|
||||||
'store_settings_discord',
|
'store_settings_discord',
|
||||||
'store_settings_font',
|
'store_settings_font',
|
||||||
'store_settings_css',
|
'store_settings_css',
|
||||||
'store_settings_remote',
|
|
||||||
'store_settings_queryBuilder',
|
|
||||||
'store_settings_tab',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const splitSettingsStorage: StateStorage = {
|
export const splitSettingsStorage: StateStorage = {
|
||||||
|
|||||||
Reference in New Issue
Block a user