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;
/** 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. */
export async function checkPathsWritable(paths: string[]): Promise<BatchFileError[]> {
const failed: BatchFileError[] = [];
@@ -41,6 +32,15 @@ export async function checkPathsWritable(paths: string[]): Promise<BatchFileErro
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. */
function flattenProperties(props: Record<string, string[] | undefined>): Record<string, string> {
const flat: Record<string, string> = {};
+12 -12
View File
@@ -786,6 +786,18 @@ export const controller: GeneralController = {
server.type,
)?.(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) {
const server = getServerById(args.apiClientProps.serverId);
@@ -888,18 +900,6 @@ export const controller: GeneralController = {
server.type,
)?.(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) {
const server = getServerById(args.apiClientProps.serverId);
+12 -12
View File
@@ -301,6 +301,18 @@ export const contract = c.router({
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: {
body: jfType._parameters.favorite,
method: 'DELETE',
@@ -365,18 +377,6 @@ export const contract = c.router({
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: {
body: jfType._parameters.updatePlaylist,
method: 'POST',
@@ -1556,6 +1556,21 @@ export const JellyfinController: InternalControllerEndpoint = {
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) => {
const { apiClientProps, query } = args;
@@ -1874,21 +1889,6 @@ export const JellyfinController: InternalControllerEndpoint = {
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) => {
const { apiClientProps, body, query } = args;
@@ -1027,6 +1027,7 @@ export const NavidromeController: InternalControllerEndpoint = {
throw new Error('Failed to move item in playlist');
}
},
refreshItems: SubsonicController.refreshItems,
removeFromPlaylist: async (args) => {
const { apiClientProps, query } = args;
@@ -1185,7 +1186,6 @@ export const NavidromeController: InternalControllerEndpoint = {
id: res.body.data.id,
};
},
refreshItems: SubsonicController.refreshItems,
updateInternetRadioStation: async (args) => {
const { apiClientProps, body, query } = args;
@@ -2082,6 +2082,17 @@ export const SubsonicController: InternalControllerEndpoint = {
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 }) => {
const res = await ssApiClient(apiClientProps).updatePlaylist({
query: {
@@ -2356,17 +2367,6 @@ export const SubsonicController: InternalControllerEndpoint = {
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) => {
const { apiClientProps, body, query } = args;
@@ -15,7 +15,7 @@ type SongEditInnerProps = {
export const SongEditContextModal = ({ innerProps }: ContextModalProps<SongEditInnerProps>) => {
const server = useCurrentServer();
const [resolvedSongs, setResolvedSongs] = useState<Song[] | null>(null);
const [resolvedSongs, setResolvedSongs] = useState<null | Song[]>(null);
useEffect(() => {
if (innerProps.albumIds) {
@@ -1,8 +1,8 @@
import { Autocomplete } from '@mantine/core';
import { closeAllModals } from '@mantine/modals';
import { useRef, useState } from 'react';
import { RiAddLine } from 'react-icons/ri';
import { useTranslation } from 'react-i18next';
import { RiAddLine } from 'react-icons/ri';
import { useMetadataEditor } from '../hooks/use-metadata-editor';
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.includes('=')
? "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'
: null
: null;
const resolvedInputKey = trimmedInput ? resolveTagKey(trimmedInput) : '';
const duplicateError =
trimmedInput && !customKeyError && editor.sortedFieldEntries.some(([k]) => k === resolvedInputKey)
trimmedInput &&
!customKeyError &&
editor.sortedFieldEntries.some(([k]) => k === resolvedInputKey)
? 'Field already exists'
: null;
const fieldError = customKeyError ?? (duplicateAttempted ? duplicateError : null);
@@ -55,7 +58,12 @@ export const SongEditModal = ({ songs }: { songs: Song[] }) => {
const handleAddField = (key: string): boolean => {
const trimmed = key.trim();
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);
if (editor.sortedFieldEntries.some(([k]) => k === normalizedKey)) {
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.
*/
const TAG_CONFIG: Record<string, { label?: string; type?: TagFieldType }> = {
acoustidFingerprint: { type: 'textarea' },
acoustidId: { label: 'AcoustID' },
albumArtist: { label: 'Album Artist' },
albumArtistSort: { label: 'Album Artist Sort' },
albumSort: { label: 'Album Sort' },
// extras not in PROPERTIES (common MusicBrainz Picard tags)
ARTISTS: { label: 'Artists', type: 'string' },
artistSort: { label: 'Artist Sort' },
bpm: { type: 'number' },
catalogNumber: { label: 'Catalog Number' },
comment: { type: 'textarea' },
composerSort: { label: 'Composer Sort' },
discNumber: { label: 'Disc Number' },
lyrics: { type: 'textarea' },
musicbrainzArtistId: { label: 'MusicBrainz Artist ID' },
musicbrainzReleaseArtistId: { label: 'MusicBrainz Album Artist ID' },
musicbrainzReleaseGroupId: { label: 'MusicBrainz Release Group ID' },
@@ -43,21 +49,15 @@ const TAG_CONFIG: Record<string, { label?: string; type?: TagFieldType }> = {
originalAlbum: { label: 'Original Album' },
originalArtist: { label: 'Original Artist' },
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' },
titleSort: { label: 'Title Sort' },
totalDiscs: { label: 'Total Discs' },
totalTracks: { label: 'Total Tracks' },
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 =>
+5 -7
View File
@@ -1384,6 +1384,10 @@ export type RandomSongListQuery = {
export type RandomSongListResponse = SongListResponse;
export type RefreshItemsArgs = BaseEndpointArgs & { query: { ids: string[] } };
export type RefreshItemsResponse = null;
export type ScrobbleArgs = BaseEndpointArgs & {
query: ScrobbleQuery;
};
@@ -1443,10 +1447,6 @@ export type SearchSongsQuery = {
songStartIndex?: number;
};
export type RefreshItemsArgs = BaseEndpointArgs & { query: { ids: string[] } };
export type RefreshItemsResponse = null;
export type SyncedCueLine = {
agentId?: string;
endMs: number;
@@ -1750,9 +1750,7 @@ export type InternalControllerEndpoint = {
args: ReplaceApiClientProps<JukeboxControlArgs>,
) => Promise<JukeboxControlResponse>;
movePlaylistItem?: (args: ReplaceApiClientProps<MoveItemArgs>) => Promise<void>;
refreshItems: (
args: ReplaceApiClientProps<RefreshItemsArgs>,
) => Promise<RefreshItemsResponse>;
refreshItems: (args: ReplaceApiClientProps<RefreshItemsArgs>) => Promise<RefreshItemsResponse>;
removeFromPlaylist: (
args: ReplaceApiClientProps<RemoveFromPlaylistArgs>,
) => Promise<RemoveFromPlaylistResponse>;