mirror of
https://github.com/chrisbenincasa/tunarr.git
synced 2026-04-18 09:03:35 -04:00
fix: allow using non-synced sources in filler / custom-shows
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -99,6 +99,11 @@ async function needsToDownloadNewBinary() {
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
await fs.mkdir('./bin');
|
||||
} catch {
|
||||
console.debug('./bin already exists...');
|
||||
}
|
||||
return shouldDownload;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
8
web/src/router.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -26,8 +26,7 @@ function CustomShowProgrammingSelectorPage() {
|
||||
}).catch(console.error);
|
||||
},
|
||||
entityType: 'custom-show',
|
||||
onMediaSourceChange: noop,
|
||||
onLibraryChange: noop,
|
||||
onSourceChange: noop,
|
||||
onSearchChange: noop,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -18,8 +18,7 @@ function CustomShowProgrammingSelectorPage() {
|
||||
navigate({ to: '..' }).catch(console.error);
|
||||
},
|
||||
entityType: 'custom-show',
|
||||
onMediaSourceChange: noop,
|
||||
onLibraryChange: noop,
|
||||
onSourceChange: noop,
|
||||
onSearchChange: noop,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
|
||||
@@ -17,8 +17,7 @@ function FillerProgrammingSelectorPage() {
|
||||
onAddMediaSuccess: () => {
|
||||
navigate({ to: '..' }).catch(console.error);
|
||||
},
|
||||
onMediaSourceChange: noop,
|
||||
onLibraryChange: noop,
|
||||
onSourceChange: noop,
|
||||
onSearchChange: noop,
|
||||
entityType: 'filler-list',
|
||||
}}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"files": [],
|
||||
"include": [
|
||||
"./src/**/*.ts",
|
||||
"./src/**/*.d.ts",
|
||||
"./src/**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user