From 6b27a070c473d08862f18d07067bef4398c3f697 Mon Sep 17 00:00:00 2001 From: Christian Benincasa Date: Tue, 9 Dec 2025 15:51:06 -0500 Subject: [PATCH] fix: allow using non-synced sources in filler / custom-shows --- eslint.config.js | 2 +- server/package.json | 4 +- server/scripts/download-meilisearch.ts | 5 ++ server/turbo.json | 6 ++- web/src/Tunarr.tsx | 2 +- .../channel_config/ImportedLibrarySeletor.tsx | 6 +-- .../channel_config/ProgrammingSelector.tsx | 6 +-- .../emby/EmbyLibrarySelector.tsx | 10 ++-- .../jellyfin/JellyfinLibrarySelector.tsx | 12 ++--- .../plex/PlexLibrarySelector.tsx | 30 +++++------ .../context/ProgrammingSelectionContext.ts | 4 +- web/src/hooks/useNavItems.tsx | 6 +-- web/src/router.d.ts | 8 +++ web/src/{router.tsx => router.ts} | 7 --- .../channels_/$channelId/programming/add.tsx | 51 ++++--------------- .../custom-shows_/$showId/programming.tsx | 3 +- .../library/custom-shows_/new/programming.tsx | 3 +- .../fillers_/$fillerId/programming.tsx | 13 ++--- .../library/fillers_/new/programming.tsx | 3 +- web/tsconfig.build.json | 1 + web/tsconfig.json | 6 +-- 21 files changed, 79 insertions(+), 109 deletions(-) create mode 100644 web/src/router.d.ts rename web/src/{router.tsx => router.ts} (64%) diff --git a/eslint.config.js b/eslint.config.js index ab746045..4ad6bc46 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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: { diff --git a/server/package.json b/server/package.json index 0aaebed9..bbbb47d2 100644 --- a/server/package.json +++ b/server/package.json @@ -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", diff --git a/server/scripts/download-meilisearch.ts b/server/scripts/download-meilisearch.ts index 4c4c38ec..29c8877c 100755 --- a/server/scripts/download-meilisearch.ts +++ b/server/scripts/download-meilisearch.ts @@ -99,6 +99,11 @@ async function needsToDownloadNewBinary() { } } } + try { + await fs.mkdir('./bin'); + } catch { + console.debug('./bin already exists...'); + } return shouldDownload; } diff --git a/server/turbo.json b/server/turbo.json index b3e6c96b..55e34bbf 100644 --- a/server/turbo.json +++ b/server/turbo.json @@ -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 diff --git a/web/src/Tunarr.tsx b/web/src/Tunarr.tsx index 1f06388e..34de7877 100644 --- a/web/src/Tunarr.tsx +++ b/web/src/Tunarr.tsx @@ -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 = () => { diff --git a/web/src/components/channel_config/ImportedLibrarySeletor.tsx b/web/src/components/channel_config/ImportedLibrarySeletor.tsx index 0f1c3081..a4fd766f 100644 --- a/web/src/components/channel_config/ImportedLibrarySeletor.tsx +++ b/web/src/components/channel_config/ImportedLibrarySeletor.tsx @@ -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 ( diff --git a/web/src/components/channel_config/ProgrammingSelector.tsx b/web/src/components/channel_config/ProgrammingSelector.tsx index 7438412b..7cc0da8d 100644 --- a/web/src/components/channel_config/ProgrammingSelector.tsx +++ b/web/src/components/channel_config/ProgrammingSelector.tsx @@ -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 = () => { diff --git a/web/src/components/channel_config/emby/EmbyLibrarySelector.tsx b/web/src/components/channel_config/emby/EmbyLibrarySelector.tsx index e54e75e1..e5efa03a 100644 --- a/web/src/components/channel_config/emby/EmbyLibrarySelector.tsx +++ b/web/src/components/channel_config/emby/EmbyLibrarySelector.tsx @@ -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 ( diff --git a/web/src/components/channel_config/jellyfin/JellyfinLibrarySelector.tsx b/web/src/components/channel_config/jellyfin/JellyfinLibrarySelector.tsx index d77508cf..33a346f7 100644 --- a/web/src/components/channel_config/jellyfin/JellyfinLibrarySelector.tsx +++ b/web/src/components/channel_config/jellyfin/JellyfinLibrarySelector.tsx @@ -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 = () => { diff --git a/web/src/components/channel_config/plex/PlexLibrarySelector.tsx b/web/src/components/channel_config/plex/PlexLibrarySelector.tsx index 4bd07d31..ee9715c6 100644 --- a/web/src/components/channel_config/plex/PlexLibrarySelector.tsx +++ b/web/src/components/channel_config/plex/PlexLibrarySelector.tsx @@ -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 = diff --git a/web/src/context/ProgrammingSelectionContext.ts b/web/src/context/ProgrammingSelectionContext.ts index 077ca2a7..390ee827 100644 --- a/web/src/context/ProgrammingSelectionContext.ts +++ b/web/src/context/ProgrammingSelectionContext.ts @@ -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; }; diff --git a/web/src/hooks/useNavItems.tsx b/web/src/hooks/useNavItems.tsx index 50cb0032..9de497d0 100644 --- a/web/src/hooks/useNavItems.tsx +++ b/web/src/hooks/useNavItems.tsx @@ -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: , }, { @@ -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; diff --git a/web/src/router.d.ts b/web/src/router.d.ts new file mode 100644 index 00000000..e442efa5 --- /dev/null +++ b/web/src/router.d.ts @@ -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; + } +} diff --git a/web/src/router.tsx b/web/src/router.ts similarity index 64% rename from web/src/router.tsx rename to web/src/router.ts index 1bae6077..3bbdbdba 100644 --- a/web/src/router.tsx +++ b/web/src/router.ts @@ -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; - } -} diff --git a/web/src/routes/channels_/$channelId/programming/add.tsx b/web/src/routes/channels_/$channelId/programming/add.tsx index c5841260..9976b24d 100644 --- a/web/src/routes/channels_/$channelId/programming/add.tsx +++ b/web/src/routes/channels_/$channelId/programming/add.tsx @@ -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 ( { - 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, }} > ); diff --git a/web/src/routes/library/custom-shows_/$showId/programming.tsx b/web/src/routes/library/custom-shows_/$showId/programming.tsx index 50f3f51b..9ad89f68 100644 --- a/web/src/routes/library/custom-shows_/$showId/programming.tsx +++ b/web/src/routes/library/custom-shows_/$showId/programming.tsx @@ -26,8 +26,7 @@ function CustomShowProgrammingSelectorPage() { }).catch(console.error); }, entityType: 'custom-show', - onMediaSourceChange: noop, - onLibraryChange: noop, + onSourceChange: noop, onSearchChange: noop, }} > diff --git a/web/src/routes/library/custom-shows_/new/programming.tsx b/web/src/routes/library/custom-shows_/new/programming.tsx index 7f6cda17..ee9707de 100644 --- a/web/src/routes/library/custom-shows_/new/programming.tsx +++ b/web/src/routes/library/custom-shows_/new/programming.tsx @@ -18,8 +18,7 @@ function CustomShowProgrammingSelectorPage() { navigate({ to: '..' }).catch(console.error); }, entityType: 'custom-show', - onMediaSourceChange: noop, - onLibraryChange: noop, + onSourceChange: noop, onSearchChange: noop, }} > diff --git a/web/src/routes/library/fillers_/$fillerId/programming.tsx b/web/src/routes/library/fillers_/$fillerId/programming.tsx index 1015c7ab..68b2bfb5 100644 --- a/web/src/routes/library/fillers_/$fillerId/programming.tsx +++ b/web/src/routes/library/fillers_/$fillerId/programming.tsx @@ -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', }} diff --git a/web/src/routes/library/fillers_/new/programming.tsx b/web/src/routes/library/fillers_/new/programming.tsx index 285ed9ff..723b0314 100644 --- a/web/src/routes/library/fillers_/new/programming.tsx +++ b/web/src/routes/library/fillers_/new/programming.tsx @@ -17,8 +17,7 @@ function FillerProgrammingSelectorPage() { onAddMediaSuccess: () => { navigate({ to: '..' }).catch(console.error); }, - onMediaSourceChange: noop, - onLibraryChange: noop, + onSourceChange: noop, onSearchChange: noop, entityType: 'filler-list', }} diff --git a/web/tsconfig.build.json b/web/tsconfig.build.json index fb283112..2f3c8fc3 100644 --- a/web/tsconfig.build.json +++ b/web/tsconfig.build.json @@ -3,6 +3,7 @@ "files": [], "include": [ "./src/**/*.ts", + "./src/**/*.d.ts", "./src/**/*.tsx", ], "exclude": [ diff --git a/web/tsconfig.json b/web/tsconfig.json index d6f35972..4ecc3f69 100644 --- a/web/tsconfig.json +++ b/web/tsconfig.json @@ -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",