more lint fixes

This commit is contained in:
ChrisScott9456
2026-07-12 14:04:53 -04:00
parent dd0b645ee4
commit 030ecca898
10 changed files with 88 additions and 82 deletions
@@ -14,15 +14,6 @@ const getTagLib = async (): Promise<TagLib> => {
const BATCH_CONCURRENCY = 8; const BATCH_CONCURRENCY = 8;
/** Reads an image file and returns it as base64 + MIME type for IPC transport to the renderer. */
export async function readLocalImageFile(filePath: string) {
const buf = await fsPromises.readFile(filePath);
return {
data: buf.toString('base64'),
mimeType: getImageMimeTypeFromPath(filePath),
};
}
/** Returns an error entry for each path that is missing or not writable by the current process. */ /** Returns an error entry for each path that is missing or not writable by the current process. */
export async function checkPathsWritable(paths: string[]): Promise<BatchFileError[]> { export async function checkPathsWritable(paths: string[]): Promise<BatchFileError[]> {
const failed: BatchFileError[] = []; const failed: BatchFileError[] = [];
@@ -41,6 +32,15 @@ export async function checkPathsWritable(paths: string[]): Promise<BatchFileErro
return failed; return failed;
} }
/** Reads an image file and returns it as base64 + MIME type for IPC transport to the renderer. */
export async function readLocalImageFile(filePath: string) {
const buf = await fsPromises.readFile(filePath);
return {
data: buf.toString('base64'),
mimeType: getImageMimeTypeFromPath(filePath),
};
}
/** Flattens a taglib-wasm PropertyMap to a string record, dropping ALL_CAPS alias keys already covered by a camelCase equivalent. */ /** Flattens a taglib-wasm PropertyMap to a string record, dropping ALL_CAPS alias keys already covered by a camelCase equivalent. */
function flattenProperties(props: Record<string, string[] | undefined>): Record<string, string> { function flattenProperties(props: Record<string, string[] | undefined>): Record<string, string> {
const flat: Record<string, string> = {}; const flat: Record<string, string> = {};
+12 -12
View File
@@ -786,6 +786,18 @@ export const controller: GeneralController = {
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
}, },
refreshItems(args) {
const server = getServerById(args.apiClientProps.serverId);
if (!server) {
throw new Error(`${i18n.t('error.apiRouteError')}: refreshItems`);
}
return apiController(
'refreshItems',
server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
},
removeFromPlaylist(args) { removeFromPlaylist(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
@@ -888,18 +900,6 @@ export const controller: GeneralController = {
server.type, server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
}, },
refreshItems(args) {
const server = getServerById(args.apiClientProps.serverId);
if (!server) {
throw new Error(`${i18n.t('error.apiRouteError')}: refreshItems`);
}
return apiController(
'refreshItems',
server.type,
)?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } }));
},
updateInternetRadioStation(args) { updateInternetRadioStation(args) {
const server = getServerById(args.apiClientProps.serverId); const server = getServerById(args.apiClientProps.serverId);
+12 -12
View File
@@ -301,6 +301,18 @@ export const contract = c.router({
400: jfType._response.error, 400: jfType._response.error,
}, },
}, },
refreshItem: {
body: z.null(),
method: 'POST',
path: 'Items/:id/Refresh',
query: z.object({
MetadataRefreshMode: z.string().optional(),
}),
responses: {
204: z.null(),
400: jfType._response.error,
},
},
removeFavorite: { removeFavorite: {
body: jfType._parameters.favorite, body: jfType._parameters.favorite,
method: 'DELETE', method: 'DELETE',
@@ -365,18 +377,6 @@ export const contract = c.router({
400: jfType._response.error, 400: jfType._response.error,
}, },
}, },
refreshItem: {
body: z.null(),
method: 'POST',
path: 'Items/:id/Refresh',
query: z.object({
MetadataRefreshMode: z.string().optional(),
}),
responses: {
204: z.null(),
400: jfType._response.error,
},
},
updatePlaylist: { updatePlaylist: {
body: jfType._parameters.updatePlaylist, body: jfType._parameters.updatePlaylist,
method: 'POST', method: 'POST',
@@ -1556,6 +1556,21 @@ export const JellyfinController: InternalControllerEndpoint = {
throw new Error('Failed to move item in playlist'); throw new Error('Failed to move item in playlist');
} }
}, },
refreshItems: async (args) => {
const { apiClientProps, query } = args;
await Promise.all(
query.ids.map((id) =>
jfApiClient(apiClientProps).refreshItem({
body: null,
params: { id },
query: { MetadataRefreshMode: 'FullRefresh' },
}),
),
);
return null;
},
removeFromPlaylist: async (args) => { removeFromPlaylist: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -1874,21 +1889,6 @@ export const JellyfinController: InternalControllerEndpoint = {
return null; return null;
}, },
refreshItems: async (args) => {
const { apiClientProps, query } = args;
await Promise.all(
query.ids.map((id) =>
jfApiClient(apiClientProps).refreshItem({
body: null,
params: { id },
query: { MetadataRefreshMode: 'FullRefresh' },
}),
),
);
return null;
},
updateInternetRadioStation: async (args) => { updateInternetRadioStation: async (args) => {
const { apiClientProps, body, query } = args; const { apiClientProps, body, query } = args;
@@ -1027,6 +1027,7 @@ export const NavidromeController: InternalControllerEndpoint = {
throw new Error('Failed to move item in playlist'); throw new Error('Failed to move item in playlist');
} }
}, },
refreshItems: SubsonicController.refreshItems,
removeFromPlaylist: async (args) => { removeFromPlaylist: async (args) => {
const { apiClientProps, query } = args; const { apiClientProps, query } = args;
@@ -1185,7 +1186,6 @@ export const NavidromeController: InternalControllerEndpoint = {
id: res.body.data.id, id: res.body.data.id,
}; };
}, },
refreshItems: SubsonicController.refreshItems,
updateInternetRadioStation: async (args) => { updateInternetRadioStation: async (args) => {
const { apiClientProps, body, query } = args; const { apiClientProps, body, query } = args;
@@ -2082,6 +2082,17 @@ export const SubsonicController: InternalControllerEndpoint = {
return res.body; return res.body;
}, },
refreshItems: async (args) => {
const { apiClientProps } = args;
const res = await ssApiClient(apiClientProps).startScan({ query: {} });
if (res.status !== 200) {
throw new Error('Failed to start scan');
}
return null;
},
removeFromPlaylist: async ({ apiClientProps, query }) => { removeFromPlaylist: async ({ apiClientProps, query }) => {
const res = await ssApiClient(apiClientProps).updatePlaylist({ const res = await ssApiClient(apiClientProps).updatePlaylist({
query: { query: {
@@ -2356,17 +2367,6 @@ export const SubsonicController: InternalControllerEndpoint = {
return null; return null;
}, },
refreshItems: async (args) => {
const { apiClientProps } = args;
const res = await ssApiClient(apiClientProps).startScan({ query: {} });
if (res.status !== 200) {
throw new Error('Failed to start scan');
}
return null;
},
updateInternetRadioStation: async (args) => { updateInternetRadioStation: async (args) => {
const { apiClientProps, body, query } = args; const { apiClientProps, body, query } = args;
@@ -15,7 +15,7 @@ type SongEditInnerProps = {
export const SongEditContextModal = ({ innerProps }: ContextModalProps<SongEditInnerProps>) => { export const SongEditContextModal = ({ innerProps }: ContextModalProps<SongEditInnerProps>) => {
const server = useCurrentServer(); const server = useCurrentServer();
const [resolvedSongs, setResolvedSongs] = useState<Song[] | null>(null); const [resolvedSongs, setResolvedSongs] = useState<null | Song[]>(null);
useEffect(() => { useEffect(() => {
if (innerProps.albumIds) { if (innerProps.albumIds) {
@@ -1,8 +1,8 @@
import { Autocomplete } from '@mantine/core'; import { Autocomplete } from '@mantine/core';
import { closeAllModals } from '@mantine/modals'; import { closeAllModals } from '@mantine/modals';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { RiAddLine } from 'react-icons/ri';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { RiAddLine } from 'react-icons/ri';
import { useMetadataEditor } from '../hooks/use-metadata-editor'; import { useMetadataEditor } from '../hooks/use-metadata-editor';
import { KNOWN_TAG_MAP, resolveTagKey } from '../utils/known-tags'; import { KNOWN_TAG_MAP, resolveTagKey } from '../utils/known-tags';
@@ -40,14 +40,17 @@ export const SongEditModal = ({ songs }: { songs: Song[] }) => {
trimmedInput && !KNOWN_TAG_MAP.has(trimmedInput) trimmedInput && !KNOWN_TAG_MAP.has(trimmedInput)
? trimmedInput.includes('=') ? trimmedInput.includes('=')
? "Tag key cannot contain '='" ? "Tag key cannot contain '='"
: /[^\x00-\x7F]/.test(trimmedInput) : // eslint-disable-next-line no-control-regex
/[^\x00-\x7F]/.test(trimmedInput)
? 'Tag key must use ASCII characters only' ? 'Tag key must use ASCII characters only'
: null : null
: null; : null;
const resolvedInputKey = trimmedInput ? resolveTagKey(trimmedInput) : ''; const resolvedInputKey = trimmedInput ? resolveTagKey(trimmedInput) : '';
const duplicateError = const duplicateError =
trimmedInput && !customKeyError && editor.sortedFieldEntries.some(([k]) => k === resolvedInputKey) trimmedInput &&
!customKeyError &&
editor.sortedFieldEntries.some(([k]) => k === resolvedInputKey)
? 'Field already exists' ? 'Field already exists'
: null; : null;
const fieldError = customKeyError ?? (duplicateAttempted ? duplicateError : null); const fieldError = customKeyError ?? (duplicateAttempted ? duplicateError : null);
@@ -55,7 +58,12 @@ export const SongEditModal = ({ songs }: { songs: Song[] }) => {
const handleAddField = (key: string): boolean => { const handleAddField = (key: string): boolean => {
const trimmed = key.trim(); const trimmed = key.trim();
if (!trimmed) return false; if (!trimmed) return false;
if (!KNOWN_TAG_MAP.has(trimmed) && (trimmed.includes('=') || /[^\x00-\x7F]/.test(trimmed))) return false; if (
!KNOWN_TAG_MAP.has(trimmed) &&
(trimmed.includes('=') || // eslint-disable-next-line no-control-regex
/[^\x00-\x7F]/.test(trimmed))
)
return false;
const normalizedKey = resolveTagKey(trimmed); const normalizedKey = resolveTagKey(trimmed);
if (editor.sortedFieldEntries.some(([k]) => k === normalizedKey)) { if (editor.sortedFieldEntries.some(([k]) => k === normalizedKey)) {
setDuplicateAttempted(true); setDuplicateAttempted(true);
@@ -25,14 +25,20 @@ export type TagFieldType = 'boolean' | 'number' | 'string' | 'textarea';
* label/type overridden; keys absent from PROPERTIES are appended as extra entries. * label/type overridden; keys absent from PROPERTIES are appended as extra entries.
*/ */
const TAG_CONFIG: Record<string, { label?: string; type?: TagFieldType }> = { const TAG_CONFIG: Record<string, { label?: string; type?: TagFieldType }> = {
acoustidFingerprint: { type: 'textarea' },
acoustidId: { label: 'AcoustID' }, acoustidId: { label: 'AcoustID' },
albumArtist: { label: 'Album Artist' }, albumArtist: { label: 'Album Artist' },
albumArtistSort: { label: 'Album Artist Sort' }, albumArtistSort: { label: 'Album Artist Sort' },
albumSort: { label: 'Album Sort' }, albumSort: { label: 'Album Sort' },
// extras not in PROPERTIES (common MusicBrainz Picard tags)
ARTISTS: { label: 'Artists', type: 'string' },
artistSort: { label: 'Artist Sort' }, artistSort: { label: 'Artist Sort' },
bpm: { type: 'number' },
catalogNumber: { label: 'Catalog Number' }, catalogNumber: { label: 'Catalog Number' },
comment: { type: 'textarea' },
composerSort: { label: 'Composer Sort' }, composerSort: { label: 'Composer Sort' },
discNumber: { label: 'Disc Number' }, discNumber: { label: 'Disc Number' },
lyrics: { type: 'textarea' },
musicbrainzArtistId: { label: 'MusicBrainz Artist ID' }, musicbrainzArtistId: { label: 'MusicBrainz Artist ID' },
musicbrainzReleaseArtistId: { label: 'MusicBrainz Album Artist ID' }, musicbrainzReleaseArtistId: { label: 'MusicBrainz Album Artist ID' },
musicbrainzReleaseGroupId: { label: 'MusicBrainz Release Group ID' }, musicbrainzReleaseGroupId: { label: 'MusicBrainz Release Group ID' },
@@ -43,21 +49,15 @@ const TAG_CONFIG: Record<string, { label?: string; type?: TagFieldType }> = {
originalAlbum: { label: 'Original Album' }, originalAlbum: { label: 'Original Album' },
originalArtist: { label: 'Original Artist' }, originalArtist: { label: 'Original Artist' },
originalDate: { label: 'Original Date' }, originalDate: { label: 'Original Date' },
ORIGINALYEAR: { label: 'Original Year', type: 'number' },
RELEASECOUNTRY: { label: 'Release Country', type: 'string' },
RELEASESTATUS: { label: 'Release Status', type: 'string' },
RELEASETYPE: { label: 'Release Type', type: 'string' },
remixedBy: { label: 'Remixer' }, remixedBy: { label: 'Remixer' },
titleSort: { label: 'Title Sort' }, titleSort: { label: 'Title Sort' },
totalDiscs: { label: 'Total Discs' }, totalDiscs: { label: 'Total Discs' },
totalTracks: { label: 'Total Tracks' }, totalTracks: { label: 'Total Tracks' },
trackNumber: { label: 'Track Number' }, trackNumber: { label: 'Track Number' },
acoustidFingerprint: { type: 'textarea' },
bpm: { type: 'number' },
comment: { type: 'textarea' },
lyrics: { type: 'textarea' },
// extras not in PROPERTIES (common MusicBrainz Picard tags)
ARTISTS: { label: 'Artists', type: 'string' },
ORIGINALYEAR: { label: 'Original Year', type: 'number' },
RELEASECOUNTRY: { label: 'Release Country', type: 'string' },
RELEASESTATUS: { label: 'Release Status', type: 'string' },
RELEASETYPE: { label: 'Release Type', type: 'string' },
}; };
const humanizeKey = (key: string): string => const humanizeKey = (key: string): string =>
+5 -7
View File
@@ -1384,6 +1384,10 @@ export type RandomSongListQuery = {
export type RandomSongListResponse = SongListResponse; export type RandomSongListResponse = SongListResponse;
export type RefreshItemsArgs = BaseEndpointArgs & { query: { ids: string[] } };
export type RefreshItemsResponse = null;
export type ScrobbleArgs = BaseEndpointArgs & { export type ScrobbleArgs = BaseEndpointArgs & {
query: ScrobbleQuery; query: ScrobbleQuery;
}; };
@@ -1443,10 +1447,6 @@ export type SearchSongsQuery = {
songStartIndex?: number; songStartIndex?: number;
}; };
export type RefreshItemsArgs = BaseEndpointArgs & { query: { ids: string[] } };
export type RefreshItemsResponse = null;
export type SyncedCueLine = { export type SyncedCueLine = {
agentId?: string; agentId?: string;
endMs: number; endMs: number;
@@ -1750,9 +1750,7 @@ export type InternalControllerEndpoint = {
args: ReplaceApiClientProps<JukeboxControlArgs>, args: ReplaceApiClientProps<JukeboxControlArgs>,
) => Promise<JukeboxControlResponse>; ) => Promise<JukeboxControlResponse>;
movePlaylistItem?: (args: ReplaceApiClientProps<MoveItemArgs>) => Promise<void>; movePlaylistItem?: (args: ReplaceApiClientProps<MoveItemArgs>) => Promise<void>;
refreshItems: ( refreshItems: (args: ReplaceApiClientProps<RefreshItemsArgs>) => Promise<RefreshItemsResponse>;
args: ReplaceApiClientProps<RefreshItemsArgs>,
) => Promise<RefreshItemsResponse>;
removeFromPlaylist: ( removeFromPlaylist: (
args: ReplaceApiClientProps<RemoveFromPlaylistArgs>, args: ReplaceApiClientProps<RemoveFromPlaylistArgs>,
) => Promise<RemoveFromPlaylistResponse>; ) => Promise<RemoveFromPlaylistResponse>;