Skip to content

Commit

Permalink
Display support for Collections in programming selector; Simplify com…
Browse files Browse the repository at this point in the history
…ponents into one
  • Loading branch information
chrisbenincasa committed Jan 29, 2024
1 parent 19c292d commit e67caba
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 117 deletions.
156 changes: 119 additions & 37 deletions types/src/plex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import z from 'zod';

type Alias<t> = t & { _?: never };

// Marker field used to allow directories and non-directories both have
// this field. This is never defined for non-directories, but can be
// used in type-guards to differentiate
const neverDirectory = z.object({ directory: z.unknown().optional() });

export const PlexLibrarySectionSchema = z.object({
allowSync: z.boolean(),
art: z.string(),
Expand Down Expand Up @@ -39,32 +44,34 @@ export type PlexLibrarySection = Alias<

export type PlexLibrarySections = z.infer<typeof PlexLibrarySectionsSchema>;

export const PlexLibraryCollectionSchema = z.object({
ratingKey: z.string(),
key: z.string(),
guid: z.string(),
type: z.string(),
title: z.string(),
titleSort: z.string(),
subtype: z.string(),
contentRating: z.string().optional(),
summary: z.string(),
index: z.number(),
content: z.string().optional(),
ratingCount: z.number(),
thumb: z.string(),
addedAt: z.number(),
updatedAt: z.number(),
childCount: z.string(),
collectionSort: z.string().optional(),
smart: z.string(),
maxYear: z.string().optional(),
minYear: z.string().optional(),
});
export const PlexLibraryCollectionSchema = z
.object({
ratingKey: z.string(),
key: z.string(),
guid: z.string(),
type: z.literal('collection'),
title: z.string(),
titleSort: z.string(),
subtype: z.string(),
contentRating: z.string().optional(),
summary: z.string(),
index: z.number(),
content: z.string().optional(),
ratingCount: z.number(),
thumb: z.string(),
addedAt: z.number(),
updatedAt: z.number(),
childCount: z.string(),
collectionSort: z.string().optional(),
smart: z.string(),
maxYear: z.string().optional(),
minYear: z.string().optional(),
})
.merge(neverDirectory);

export type PlexLibraryCollection = z.infer<typeof PlexLibraryCollectionSchema>;

const basePlexLibraryCollectionsSchema = z.object({
const basePlexLibrarySchema = z.object({
allowSync: z.boolean(),
art: z.string(),
identifier: z.string(),
Expand All @@ -83,8 +90,10 @@ const basePlexLibraryCollectionsSchema = z.object({
viewMode: z.number(),
});

const makePlexLibraryCollectionsSchema = (metadata: z.AnyZodObject) => {
return basePlexLibraryCollectionsSchema.extend({
const makePlexLibraryCollectionsSchema = <T extends z.AnyZodObject>(
metadata: T,
) => {
return basePlexLibrarySchema.extend({
Metadata: z.array(metadata),
});
};
Expand Down Expand Up @@ -136,8 +145,6 @@ export const PlexJoinItemSchema = z.object({

export type PlexJoinItem = z.infer<typeof PlexJoinItemSchema>;

const neverDirectory = z.object({ directory: z.unknown().optional() });

export const PlexMovieSchema = z
.object({
ratingKey: z.string(),
Expand Down Expand Up @@ -241,7 +248,7 @@ export type PlexTvSeason = Alias<z.infer<typeof PlexTvSeasonSchema>>;

// /library/section/{id}/all for a Movie Library

export const PlexLibraryMoviesSchema = basePlexLibraryCollectionsSchema.extend({
export const PlexLibraryMoviesSchema = basePlexLibrarySchema.extend({
Metadata: z.array(PlexMovieSchema),
});

Expand Down Expand Up @@ -381,15 +388,6 @@ export function isPlexShowLibrary(
return lib.viewGroup === 'show';
}

export type PlexMedia = PlexMovie | PlexTvShow | PlexTvSeason | PlexEpisode;
export type PlexTerminalMedia = PlexMovie | PlexEpisode; // Media that has no children
export type PlexParentMediaType = PlexTvShow | PlexTvSeason;
export type PlexChildType<T> = PlexTvShow extends T
? PlexTvSeason
: PlexTvSeason extends T
? PlexEpisode
: never;

export function isPlexMediaType<T extends PlexMedia>(discrim: string) {
return (media: PlexLibrarySection | PlexMedia | undefined): media is T => {
return !isPlexDirectory(media) && media?.type === discrim;
Expand All @@ -400,9 +398,93 @@ export const isPlexMovie = isPlexMediaType<PlexMovie>('movie');
export const isPlexShow = isPlexMediaType<PlexTvShow>('show');
export const isPlexSeason = isPlexMediaType<PlexTvSeason>('season');
export const isPlexEpisode = isPlexMediaType<PlexEpisode>('episode');
export const isPlexCollection =
isPlexMediaType<PlexLibraryCollection>('collection');
const funcs = [
isPlexMovie,
isPlexShow,
isPlexSeason,
isPlexEpisode,
isPlexCollection,
];
export const isPlexMedia = (
media: PlexLibrarySection | PlexMedia | undefined,
): media is PlexMedia => {
for (const func of funcs) {
if (func(media)) {
return true;
}
}
return false;
};

export function isTerminalItem(
item: PlexMedia | PlexLibrarySection,
): item is PlexMovie | PlexEpisode {
return !isPlexDirectory(item) && (isPlexMovie(item) || isPlexEpisode(item));
}

// /library/collections/{id}/children
const basePlexCollectionContentsSchema = z.object({
size: z.number(),
});

export const PlexMovieCollectionContentsSchema =
basePlexCollectionContentsSchema.extend({
Metadata: z.array(PlexMovieSchema),
});

export const PlexTvShowCollectionContentsSchema =
basePlexCollectionContentsSchema.extend({
Metadata: z.array(PlexTvShowSchema),
});

export type PlexMovieCollectionContents = Alias<
z.infer<typeof PlexMovieCollectionContentsSchema>
>;
export type PlexTvShowCollectionContents = Alias<
z.infer<typeof PlexTvShowCollectionContentsSchema>
>;

export type PlexCollectionContents =
| PlexMovieCollectionContents
| PlexTvShowCollectionContents;

export type PlexMedia = Alias<
PlexMovie | PlexTvShow | PlexTvSeason | PlexEpisode | PlexLibraryCollection
>;
export type PlexTerminalMedia = PlexMovie | PlexEpisode; // Media that has no children
export type PlexParentMediaType = PlexTvShow | PlexTvSeason;

type PlexMediaApiChildType = [
[PlexTvShow, PlexSeasonView],
[PlexTvSeason, PlexEpisodeView],
[
PlexLibraryCollection,
PlexMovieCollectionContents | PlexTvShowCollectionContents,
],
];

type PlexMediaToChildType = [
[PlexTvShow, PlexTvSeason],
[PlexTvSeason, PlexEpisode],
];

type FindChild0<Target, Arr extends unknown[] = []> = Arr extends [
[infer Head, infer Child],
...infer Tail,
]
? Head extends Target
? Child
: FindChild0<Target, Tail>
: never;

export type PlexChildMediaType<Target extends PlexMedia> =
Target extends PlexTerminalMedia
? Target
: FindChild0<Target, PlexMediaToChildType>;

export type PlexChildMediaApiType<Target extends PlexMedia> = FindChild0<
Target,
PlexMediaApiChildType
>;
68 changes: 37 additions & 31 deletions web2/src/components/channel_config/PlexDirectoryListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,48 @@ import {
} from '@mui/material';
import { PlexServerSettings } from '@tunarr/types';
import {
PlexLibraryCollections,
PlexLibraryMovies,
PlexLibrarySection,
PlexLibraryShows,
isPlexMovie,
isPlexShow,
isPlexMedia,
} from '@tunarr/types/plex';
import { take } from 'lodash-es';
import { useCallback, useEffect, useRef, useState, MouseEvent } from 'react';
import { usePlexTyped } from '../../hooks/plexHooks.ts';
import { MouseEvent, useCallback, useEffect, useRef, useState } from 'react';
import { usePlexTyped2 } from '../../hooks/plexHooks.ts';
import useStore from '../../store/index.ts';
import {
addKnownMediaForServer,
addSelectedMedia,
} from '../../store/programmingSelector/actions.ts';
import { PlexMovieListItem } from './PlexMovieListItem.tsx';
import { PlexTvListItem } from './PlexShowListItem.tsx';
import { PlexListItem } from './PlexListItem.tsx';

export function PlexDirectoryListItem(props: {
server: PlexServerSettings;
item: PlexLibrarySection;
}) {
const { server, item } = props;
const [open, setOpen] = useState(false);
const { isPending, data } = usePlexTyped<
PlexLibraryMovies | PlexLibraryShows
>(props.server.name, `/library/sections/${item.key}/all`, open);
const {
isPending,
first: children,
second: collections,
} = usePlexTyped2<
PlexLibraryMovies | PlexLibraryShows,
PlexLibraryCollections
>([
{
serverName: props.server.name,
path: `/library/sections/${item.key}/all`,
enabled: open,
},
{
serverName: props.server.name,
path: `/library/sections/${item.key}/collections`,
enabled: open,
},
]);

const listings = useStore((s) => s.knownMediaByServer[server.name]);
const hierarchy = useStore(
(s) => s.contentHierarchyByServer[server.name][item.uuid],
Expand Down Expand Up @@ -69,10 +85,14 @@ export function PlexDirectoryListItem(props: {
}, [observerTarget, limit, hierarchy, setLimit]);

useEffect(() => {
if (data) {
addKnownMediaForServer(server.name, data.Metadata, item.uuid);
if (children) {
addKnownMediaForServer(server.name, children.Metadata, item.uuid);
}
}, [item.uuid, item.key, server.name, data]);

if (collections) {
addKnownMediaForServer(server.name, collections.Metadata, item.uuid);
}
}, [item.uuid, item.key, server.name, children, collections]);

const handleClick = () => {
setLimit(Math.min(hierarchy.length, 20));
Expand All @@ -87,27 +107,13 @@ export function PlexDirectoryListItem(props: {
[item, server.name],
);

const renderCollectionRow2 = (id: string) => {
const renderCollectionRow = (id: string) => {
const media = listings[id];

if (isPlexShow(media)) {
return (
<PlexTvListItem
key={media.guid}
item={media}
length={hierarchy.length}
/>
);
} else if (isPlexMovie(media)) {
if (isPlexMedia(media)) {
return (
<PlexMovieListItem
key={media.guid}
item={media}
length={hierarchy.length}
/>
<PlexListItem key={media.guid} item={media} length={hierarchy.length} />
);
} else {
return null;
}
};

Expand All @@ -120,14 +126,14 @@ export function PlexDirectoryListItem(props: {
<Button onClick={(e) => addItems(e)}>Add All</Button>
</ListItemButton>
</ListItem>
<Collapse in={open} timeout="auto" unmountOnExit>
<Collapse in={open && !isPending} timeout="auto" unmountOnExit>
{isPending ? (
<Skeleton>
<Box sx={{ width: '100%', height: 400, pl: 4 }} />
</Skeleton>
) : (
<Box sx={{ width: '100%', height: 400, pl: 4, overflowY: 'scroll' }}>
{take(hierarchy, limit).map(renderCollectionRow2)}
{take(hierarchy, limit).map(renderCollectionRow)}
<div style={{ height: 40 }} ref={observerTarget}></div>
</Box>
)}
Expand Down
Loading

0 comments on commit e67caba

Please sign in to comment.