mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-22 10:26:33 +02:00
more lint fixes
This commit is contained in:
@@ -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> = {};
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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>;
|
||||
|
||||
Reference in New Issue
Block a user