fix: allow using non-synced sources in filler / custom-shows

This commit is contained in:
Christian Benincasa
2025-12-09 15:51:06 -05:00
parent d1e66c5fe4
commit 6b27a070c4
21 changed files with 79 additions and 109 deletions

View File

@@ -61,7 +61,7 @@ export default tseslint.config(
},
},
{
files: ['web/src/**/*.tsx', 'web/src/**/*.ts'],
files: ['web/src/**/*.tsx', 'web/src/**/*.ts', 'web/src/**/*.d.ts'],
...reactRecommended,
extends: [jsxRuntime],
plugins: {

View File

@@ -14,8 +14,8 @@
"bundle": "dotenv -- tsx scripts/bundle.ts",
"make-bin": "dotenv -- tsx scripts/make-bin.ts",
"clean": "rimraf --glob ./build/ ./dist/ ./bin/tunar*",
"debug": "dotenv -e .env.development -- tsx watch --trace-warnings --tsconfig ./tsconfig.build.json --ignore 'src/streams' --inspect-wait ./src",
"dev": "dotenv -e .env.development -- tsx watch --trace-warnings --tsconfig ./tsconfig.build.json --ignore 'build' --ignore 'src/streams' --ignore 'src/**/*.test.ts' ./src/index.ts",
"debug": "dotenv -e .env.development -v NODE_ENV=development -- tsx watch --trace-warnings --tsconfig ./tsconfig.build.json --ignore 'src/streams' --inspect-wait ./src",
"dev": "dotenv -e .env.development -v NODE_ENV=development -- tsx watch --trace-warnings --tsconfig ./tsconfig.build.json --ignore 'build' --ignore 'src/streams' --ignore 'src/**/*.test.ts' ./src/index.ts",
"generate-openapi": "tsx src/index.ts generate-openapi",
"install-meilisearch": "tsx scripts/download-meilisearch.ts",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",

View File

@@ -99,6 +99,11 @@ async function needsToDownloadNewBinary() {
}
}
}
try {
await fs.mkdir('./bin');
} catch {
console.debug('./bin already exists...');
}
return shouldDownload;
}

View File

@@ -23,8 +23,12 @@
"lint": {
"dependsOn": ["lint-staged"]
},
"install-meilisearch": {
"inputs": ["./scripts/download-meilisearch.ts"],
"cache": false
},
"dev": {
"dependsOn": ["@tunarr/shared#build"],
"dependsOn": ["install-meilisearch", "@tunarr/shared#build"],
"persistent": true,
"cache": false,
"interruptible": true

View File

@@ -11,7 +11,7 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import { ServerEventsProvider } from './components/server_events/ServerEventsProvider.tsx';
import { TunarrApiProvider } from './context/TunarrApiContext.tsx';
import { queryClient } from './queryClient.ts';
import { router } from './router.tsx';
import { router } from './router.ts';
import { Theme } from './theme.ts';
export const Tunarr = () => {

View File

@@ -41,7 +41,7 @@ type Props = {
};
export const ImportedLibrarySelector = ({ initialLibraryId }: Props) => {
const { onLibraryChange } = useProgrammingSelectionContext();
const { onSourceChange } = useProgrammingSelectionContext();
const selectedServer = useStore((s) => s.currentMediaSource);
const selectedLibrary = useStore((s) => s.currentMediaSourceView);
@@ -86,9 +86,9 @@ export const ImportedLibrarySelector = ({ initialLibraryId }: Props) => {
}
setProgrammingListLibrary({ type: Imported, view: library });
onLibraryChange(library.id);
onSourceChange({ libraryId: library.id });
},
[libraries, selectedServer, onLibraryChange],
[selectedServer, libraries, onSourceChange],
);
return (

View File

@@ -56,7 +56,7 @@ export const ProgrammingSelector = ({
initialLibraryId,
toggleOrSetSelectedProgramsDrawer,
}: Props) => {
const { entityType, onMediaSourceChange } = useProgrammingSelectionContext();
const { entityType, onSourceChange } = useProgrammingSelectionContext();
const { data: mediaSources, isLoading: mediaSourcesLoading } =
useMediaSources();
const selectedServer = useStore((s) => s.currentMediaSource);
@@ -101,11 +101,11 @@ export const ProgrammingSelector = ({
if (server) {
setProgrammingListingServer(server);
setMediaSource(server.name);
onMediaSourceChange(server.id);
onSourceChange({ mediaSourceId: server.id });
}
}
},
[mediaSources, onMediaSourceChange],
[mediaSources, onSourceChange],
);
const renderMediaSourcePrograms = () => {

View File

@@ -13,7 +13,7 @@ type Props = {
};
export const EmbyLibrarySelector = ({ initialLibraryId }: Props) => {
const { onLibraryChange } = useProgrammingSelectionContext();
const { onSourceChange } = useProgrammingSelectionContext();
const selectedServer = useStore((s) => s.currentMediaSource);
const selectedLibrary = useStore((s) => s.currentMediaSourceView);
@@ -40,7 +40,7 @@ export const EmbyLibrarySelector = ({ initialLibraryId }: Props) => {
type: Emby,
view,
});
onLibraryChange(view.externalId);
onSourceChange({ libraryId: view.externalId });
}
// addKnownMediaForJellyfinServer(selectedServer.id, [...jellyfinLibraries]);
}
@@ -49,7 +49,7 @@ export const EmbyLibrarySelector = ({ initialLibraryId }: Props) => {
embyLibraries,
selectedLibrary,
selectedServer,
onLibraryChange,
onSourceChange,
]);
const handleLibraryChange = useCallback(
@@ -63,10 +63,10 @@ export const EmbyLibrarySelector = ({ initialLibraryId }: Props) => {
type: Emby,
view,
});
onLibraryChange(view.externalId);
onSourceChange({ libraryId: view.externalId });
}
},
[embyLibraries, onLibraryChange, selectedServer],
[embyLibraries, onSourceChange, selectedServer],
);
return (

View File

@@ -14,17 +14,15 @@ import {
setProgrammingGenre,
setProgrammingListLibrary,
} from '../../../store/programmingSelector/actions.ts';
import { useKnownMedia } from '../../../store/programmingSelector/selectors.ts';
type Props = {
initialLibraryId?: string;
};
export const JellyfinLibrarySelector = ({ initialLibraryId }: Props) => {
const { onLibraryChange } = useProgrammingSelectionContext();
const { onSourceChange } = useProgrammingSelectionContext();
const selectedServer = useStore((s) => s.currentMediaSource);
const selectedLibrary = useStore((s) => s.currentMediaSourceView);
const knownMedia = useKnownMedia();
const selectedGenre = useStore((s) => s.currentMediaGenre);
const { data: jellyfinLibraries } = useJellyfinUserLibraries(
@@ -56,7 +54,7 @@ export const JellyfinLibrarySelector = ({ initialLibraryId }: Props) => {
type: Jellyfin,
view,
});
onLibraryChange(view.externalId);
onSourceChange({ libraryId: view.externalId });
}
// addKnownMediaForJellyfinServer(selectedServer.id, [...jellyfinLibraries]);
}
@@ -65,7 +63,7 @@ export const JellyfinLibrarySelector = ({ initialLibraryId }: Props) => {
jellyfinLibraries,
selectedLibrary,
selectedServer,
onLibraryChange,
onSourceChange,
]);
const handleLibraryChange = useCallback(
@@ -81,10 +79,10 @@ export const JellyfinLibrarySelector = ({ initialLibraryId }: Props) => {
type: Jellyfin,
view,
});
onLibraryChange(view.externalId);
onSourceChange({ libraryId: view.externalId });
}
},
[knownMedia, selectedServer],
[jellyfinLibraries, onSourceChange, selectedServer],
);
const renderGenreChoices = () => {

View File

@@ -1,12 +1,12 @@
import { FormControl, InputLabel, MenuItem, Select } from '@mui/material';
import { find, isNil, map } from 'lodash-es';
import { useCallback, useEffect } from 'react';
import { useCallback, useContext, useEffect } from 'react';
import { ProgrammingSelectionContext } from '../../../context/ProgrammingSelectionContext.ts';
import { Plex } from '../../../helpers/constants.ts';
import {
usePlexLibraries,
usePlexPlaylists,
} from '../../../hooks/plex/usePlex.ts';
import { Route } from '../../../routes/channels_/$channelId/programming/add.tsx';
import useStore from '../../../store/index.ts';
import {
addKnownMediaForServer,
@@ -21,7 +21,7 @@ type Props = {
export const PlexLibrarySelector = ({ initialLibraryId }: Props) => {
const selectedServer = useStore((s) => s.currentMediaSource);
const selectedLibrary = useStore((s) => s.currentMediaSourceView);
const navigate = Route.useNavigate();
const selectionCtx = useContext(ProgrammingSelectionContext);
const { data: plexLibraryChildren } = usePlexLibraries(
selectedServer?.id ?? '',
@@ -54,17 +54,15 @@ export const PlexLibrarySelector = ({ initialLibraryId }: Props) => {
type: Plex,
view: { type: 'library', library },
});
navigate({
search: {
mediaSourceId: selectedServer.id,
libraryId: library.uuid,
},
}).catch(console.error);
selectionCtx?.onSourceChange({
mediaSourceId: selectedServer.id,
libraryId: library.uuid,
});
} else {
console.warn('Not found in local store', libraryUuid);
}
},
[knownMedia, navigate, plexPlaylists, selectedServer],
[knownMedia, plexPlaylists, selectionCtx, selectedServer],
);
useEffect(() => {
@@ -86,20 +84,18 @@ export const PlexLibrarySelector = ({ initialLibraryId }: Props) => {
library: initialLibrary ?? plexLibraryChildren[0],
},
});
navigate({
search: {
libraryId: plexLibraryChildren[0].externalId,
mediaSourceId: selectedServer.id,
},
}).catch(console.error);
selectionCtx?.onSourceChange({
libraryId: plexLibraryChildren[0].externalId,
mediaSourceId: selectedServer.id,
});
}
}
}, [
initialLibraryId,
navigate,
plexLibraryChildren,
selectedLibrary,
selectedServer,
selectionCtx,
]);
const selectedPlexLibrary =

View File

@@ -4,6 +4,7 @@ import { type AddedMedia } from '../types/index.ts';
import { type Nullable } from '../types/util.ts';
type EntityType = 'channel' | 'filler-list' | 'custom-show';
type MediaSourceChange = { mediaSourceId?: string; libraryId?: string };
export type ProgrammingSelectionContextType = {
onAddSelectedMedia: (programs: AddedMedia[]) => void;
@@ -11,8 +12,7 @@ export type ProgrammingSelectionContextType = {
entityType: EntityType;
initialMediaSourceId?: string;
initialLibraryId?: string;
onMediaSourceChange: (mediaSourceId: string) => void;
onLibraryChange: (libraryId: string) => void;
onSourceChange: (change: MediaSourceChange) => void;
onSearchChange: (searchRequest: SearchRequest) => void;
};

View File

@@ -1,3 +1,4 @@
import type { router } from '@/router.ts';
import {
Computer,
Delete,
@@ -11,7 +12,6 @@ import {
VideoLibrary,
} from '@mui/icons-material';
import type { BadgeProps } from '@mui/material';
import type { Register } from '@tanstack/react-router';
import { useRouterState } from '@tanstack/react-router';
import { countBy, last, trimEnd } from 'lodash-es';
import { useCallback, useMemo, type ReactNode } from 'react';
@@ -83,7 +83,7 @@ export const useNavItems = () => {
},
{
name: 'Custom Shows',
path: '/library/custom-shows',
path: '/library/custom-shows' as const,
icon: <Theaters />,
},
{
@@ -163,7 +163,7 @@ export const useNavItems = () => {
export interface NavItem {
name: string;
path: keyof Register['router']['routesByPath'];
path: keyof (typeof router)['routesByPath'];
hidden?: boolean;
children?: NavItem[];
icon?: ReactNode;

8
web/src/router.d.ts vendored Normal file
View File

@@ -0,0 +1,8 @@
import type { router } from './router.ts';
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}

View File

@@ -8,10 +8,3 @@ export const router = createRouter({
routeTree,
context: { queryClient },
});
// Register the router instance for type safety
declare module '@tanstack/react-router' {
interface Register {
router: typeof router;
}
}

View File

@@ -6,9 +6,8 @@ import ProgrammingSelectorPage from '@/pages/channels/ProgrammingSelectorPage';
import { addMediaToCurrentChannel } from '@/store/channelEditor/actions';
import { setPlexFilter } from '@/store/programmingSelector/actions';
import { createFileRoute } from '@tanstack/react-router';
import type { SearchRequest } from '@tunarr/types/api';
import { SearchRequestSchema } from '@tunarr/types/api';
import { useCallback, useMemo } from 'react';
import { useCallback } from 'react';
import { noop } from 'ts-essentials';
import { z } from 'zod/v4';
import { ProgrammingSelectionContext } from '../../../../context/ProgrammingSelectionContext.ts';
import useStore from '../../../../store/index.ts';
@@ -16,18 +15,16 @@ import useStore from '../../../../store/index.ts';
const channelProgrammingSchema = z.object({
mediaSourceId: z.string().optional().catch(undefined),
libraryId: z.string().optional().catch(undefined),
searchRequest: z.base64().optional().catch(undefined),
});
export const Route = createFileRoute('/channels_/$channelId/programming/add')({
validateSearch: (search) => channelProgrammingSchema.parse(search),
loader: (args: ChannelArgs) => {
loader: async (args: ChannelArgs) => {
useStore.setState((s) => {
s.currentSearchRequest = null;
});
return preloadChannelAndProgramming(args).then(() => {
setPlexFilter(undefined);
});
await preloadChannelAndProgramming(args);
setPlexFilter(undefined);
},
component: ChannelProgrammingSelectorPage,
});
@@ -35,17 +32,7 @@ export const Route = createFileRoute('/channels_/$channelId/programming/add')({
function ChannelProgrammingSelectorPage() {
const search = Route.useSearch();
const navigate = Route.useNavigate();
const { mediaSourceId, libraryId, searchRequest } = Route.useSearch();
const parsedSearchRequest = useMemo(() => {
if (searchRequest) {
try {
return SearchRequestSchema.parse(JSON.parse(atob(searchRequest)));
} catch (e) {
console.warn(e);
}
}
return;
}, [searchRequest]);
const { mediaSourceId, libraryId } = Route.useSearch();
return (
<ProgrammingSelectionContext.Provider
value={{
@@ -54,37 +41,21 @@ function ChannelProgrammingSelectorPage() {
navigate({ to: '..' }).catch(console.error);
}, [navigate]),
entityType: 'channel',
onMediaSourceChange: useCallback(
(mediaSourceId: string) => {
navigate({ search: { ...search, mediaSourceId } }).catch(
onSourceChange: useCallback(
({ mediaSourceId, libraryId }) => {
navigate({ search: { ...search, mediaSourceId, libraryId } }).catch(
console.error,
);
},
[navigate, search],
),
onLibraryChange: useCallback(
(libraryId: string) => {
navigate({ search: { ...search, libraryId } }).catch(console.error);
},
[navigate, search],
),
onSearchChange: useCallback(
(searchReq: SearchRequest) => {
navigate({
search: {
...search,
searchRequest: btoa(JSON.stringify(searchReq)),
},
}).catch(console.error);
},
[search, navigate],
),
onSearchChange: noop,
}}
>
<ProgrammingSelectorPage
initialMediaSourceId={mediaSourceId}
initialLibraryId={libraryId}
initialSearchRequest={parsedSearchRequest}
// initialSearchRequest={parsedSearchRequest}
/>
</ProgrammingSelectionContext.Provider>
);

View File

@@ -26,8 +26,7 @@ function CustomShowProgrammingSelectorPage() {
}).catch(console.error);
},
entityType: 'custom-show',
onMediaSourceChange: noop,
onLibraryChange: noop,
onSourceChange: noop,
onSearchChange: noop,
}}
>

View File

@@ -18,8 +18,7 @@ function CustomShowProgrammingSelectorPage() {
navigate({ to: '..' }).catch(console.error);
},
entityType: 'custom-show',
onMediaSourceChange: noop,
onLibraryChange: noop,
onSourceChange: noop,
onSearchChange: noop,
}}
>

View File

@@ -5,10 +5,12 @@ import { createFileRoute } from '@tanstack/react-router';
import { noop } from 'ts-essentials';
import { ProgrammingSelectionContext } from '../../../../context/ProgrammingSelectionContext.ts';
export const Route = createFileRoute('/library/fillers_/$fillerId/programming')({
loader: preloadFillerAndProgramming,
component: FillerProgrammingSelectorPage,
});
export const Route = createFileRoute('/library/fillers_/$fillerId/programming')(
{
loader: preloadFillerAndProgramming,
component: FillerProgrammingSelectorPage,
},
);
function FillerProgrammingSelectorPage() {
const navigate = Route.useNavigate();
@@ -23,8 +25,7 @@ function FillerProgrammingSelectorPage() {
params: { fillerId },
}).catch(console.error);
},
onMediaSourceChange: noop,
onLibraryChange: noop,
onSourceChange: noop,
onSearchChange: noop,
entityType: 'filler-list',
}}

View File

@@ -17,8 +17,7 @@ function FillerProgrammingSelectorPage() {
onAddMediaSuccess: () => {
navigate({ to: '..' }).catch(console.error);
},
onMediaSourceChange: noop,
onLibraryChange: noop,
onSourceChange: noop,
onSearchChange: noop,
entityType: 'filler-list',
}}

View File

@@ -3,6 +3,7 @@
"files": [],
"include": [
"./src/**/*.ts",
"./src/**/*.d.ts",
"./src/**/*.tsx",
],
"exclude": [

View File

@@ -33,11 +33,7 @@
"openapi-ts.config.ts",
"vite.config.ts",
"vitest.config.ts",
"src/components/slot_scheduler/RandomSlotFormContext.tsx",
"./src/components/slot_scheduler/TimeSlotFormContext.tsx",
"./src/providers/DayjsContext.tsx",
"./src/components/library/extractSubtitle.tsx",
"./src/router.tsx",
"./src/router.d.ts",
],
"include": [
"./src/**/*.ts",