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