Skip to content

Commit

Permalink
Series indexer (#7)
Browse files Browse the repository at this point in the history
* Add series metadata interface

* Add series indexing
  • Loading branch information
undyingwraith authored May 10, 2024
1 parent 38e925d commit 9692106
Show file tree
Hide file tree
Showing 17 changed files with 310 additions and 57 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Card, CardContent, CardMedia } from "@mui/material";
import React from "react";
import { useFileUrl } from "../../../hooks";
import { ISeriesMetaData } from "../../../service";
import fallback from './no-poster.png';

export function SeriesPosterGridItem(props: { serie: ISeriesMetaData; }) {
const url = useFileUrl(props.serie.posters[0]?.cid, fallback);

return (
<Card sx={{ width: 240 }}>
<CardMedia image={url} sx={{ height: 360, width: 240 }} />
<CardContent>{props.serie.title}</CardContent>
</Card>
);
}
2 changes: 1 addition & 1 deletion packages/core/src/components/organisms/MovieLibrary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function MovieLibrary(props: ILibraryProps<IMovieLibrary>) {
return i?.cid == undefined ? (
<LoadScreen text={_t('Loading')} />
) : (
<Grid container spacing={1}>
<Grid container spacing={1} sx={{ height: '100%', justifyContent: 'center' }}>
{i.values.map(v => <Grid item key={v.file.cid}>{display == Display.Poster ? <MoviePosterGridItem movie={v} /> : <MovieThumbnailGridItem movie={v} />}</Grid>)}
</Grid>
);
Expand Down
29 changes: 29 additions & 0 deletions packages/core/src/components/organisms/SeriesLibrary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Grid } from "@mui/material";
import { useComputed } from "@preact/signals-react";
import React from "react";
import { useTranslation } from "react-i18next";
import { ISeriesLibrary } from '../../service/Library/ILibrary';
import { SeriesPosterGridItem } from '../molecules/GridItems/SeriesGridItem';
import { LoadScreen } from "../molecules/LoadScreen";
import { useApp } from "../pages/AppContext";
import { ILibraryProps } from "../pages/LibraryManager";

export function SeriesLibrary(props: ILibraryProps<ISeriesLibrary>) {
const [_t] = useTranslation();
const { profile } = useApp();

const library = profile.libraries.get(props.library.name);
const index = useComputed(() => library?.value.index);

return useComputed(() => {
const i = index.value;

return i?.cid == undefined ? (
<LoadScreen text={_t('Loading')} />
) : (
<Grid container spacing={1} sx={{ height: '100%', justifyContent: 'center' }}>
{i.values.map(v => <Grid item key={v.title}><SeriesPosterGridItem serie={v} /></Grid>)}
</Grid>
);
});
}
2 changes: 1 addition & 1 deletion packages/core/src/components/pages/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export function AppContextProvider(props: PropsWithChildren<IAppInit>) {
return useComputed(() => (
<ThemeProvider theme={theme.value}>
<CssBaseline />
<Stack sx={{ height: '100vh', overflow: 'hidden' }}>
<Stack sx={{ height: '100vh', width: '100vw', overflow: 'hidden' }}>
<AppBar shutdownProfile={stop} ipfs={node} profile={profile} darkMode={darkMode} />
{content}
</Stack>
Expand Down
60 changes: 37 additions & 23 deletions packages/core/src/components/pages/LibraryManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import { Signal, useComputed, useSignal } from "@preact/signals-react";
import React from "react";
import { useTranslation } from "react-i18next";
import { ILibrary } from "../../service";
import { isMovieLibrary } from '../../service/Library/ILibrary';
import { isMovieLibrary, isSeriesLibrary } from '../../service/Library/ILibrary';
import { LibraryAppBar } from "../organisms/LibraryAppBar";
import { MovieLibrary } from "../organisms/MovieLibrary";
import { useApp } from "./AppContext";
import { SeriesLibrary } from '../organisms/SeriesLibrary';

const icons = {
movie: <MovieIcon />,
Expand All @@ -29,26 +30,37 @@ export function LibraryManager() {

const content = useComputed(() => {
const lib = library.value;
let component = (<Box>Unknown Library</Box>);

if (lib == undefined) {
component = (<Box>Home</Box>);
}

if (isMovieLibrary(lib)) {
return (
<Stack sx={{ height: '100%' }}>
<LibraryAppBar display={display} query={query} />
<Box sx={{ overflow: 'auto', flexShrink: 1, flexGrow: 1 }}>
<MovieLibrary
display={display}
library={lib}
/>
</Box>
</Stack>
component = (
<MovieLibrary
display={display}
library={lib}
/>
);
}

if (lib == undefined) {
return (<Box>Home</Box>);
if (isSeriesLibrary(lib)) {
component = (
<SeriesLibrary
display={display}
library={lib}
/>
);
}

return (<Box>Unknown Library</Box>);
return (
<Stack sx={{ maxHeight: '100%', height: '100%', overflow: 'hidden' }}>
<LibraryAppBar display={display} query={query} />
<Box sx={{ overflow: 'auto', flexGrow: 1 }}>
{component}
</Box>
</Stack>
);
});

const sidebar = useComputed(() => (
Expand Down Expand Up @@ -82,14 +94,16 @@ export function LibraryManager() {
</List>
));

return <Stack direction={"row"} sx={{ height: '100%' }}>
<Paper sx={{ width: '25vw', flexShrink: 0 }}>
{sidebar}
</Paper>
<Box sx={{ flexGrow: 1 }}>
{content}
</Box>
</Stack>;
return (
<Stack direction={"row"} sx={{ flexGrow: 1, width: '100vw', height: '100%', overflow: 'hidden' }}>
<Paper sx={{ width: '25vw', flexShrink: 0 }}>
{sidebar}
</Paper>
<Box sx={{ flexGrow: 1 }}>
{content}
</Box>
</Stack>
);
}

export enum Display {
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/service/IIpfsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@ export interface IIpfsService {
* Stops the node.
*/
stop(): Promise<void>;

/**
* Resolves a ipns address.
* @param ipns ipns address.
*/
resolve(ipns: string): Promise<string>;
}
7 changes: 4 additions & 3 deletions packages/core/src/service/Library/ILibrary.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { IMovieMetaData } from './IMovieMetaData';
import { IGenericLibrary, isGenericLibrary } from './IGenericLibrary';
import { ISeriesMetaData } from './ISeriesMetaData';

export type IMovieLibrary = IGenericLibrary<IMovieMetaData, 'movie'>;

export function isMovieLibrary(item: any): item is IMovieLibrary {
return isGenericLibrary<IMovieMetaData, 'movie'>(item) && item.type === 'movie';
}

export type ISeriesLibrary = IGenericLibrary<any, 'series'>;
export type ISeriesLibrary = IGenericLibrary<ISeriesMetaData, 'series'>;

export function isSeriesLibrary(item: any): item is IMovieLibrary {
export function isSeriesLibrary(item: any): item is ISeriesLibrary {
return isGenericLibrary<any, 'series'>(item) && item.type === 'series';
}

export type IMusicLibrary = IGenericLibrary<any, 'music'>;

export function isMusicLibrary(item: any): item is IMovieLibrary {
export function isMusicLibrary(item: any): item is IMusicLibrary {
return isGenericLibrary<any, 'music'>(item) && item.type === 'music';
}

Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/service/Library/ISeriesMetaData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { IFileInfo } from '../indexer';

export interface ISeriesMetaData {
title: string;
posters: IFileInfo[];
yearStart?: number;
yearEnd?: number;
seasons: ISeasonMetaData[];
}

export interface ISeasonMetaData {
posters: IFileInfo[];
episodes: IEpisodeMetaData[];
}

export interface IEpisodeMetaData {
title: string;
file: IFileInfo;
thumbnails: IFileInfo[];
date?: string;
}
3 changes: 2 additions & 1 deletion packages/core/src/service/Library/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export type { IGenericMetaData } from './IGenericMetaData'
export type { IGenericMetaData } from './IGenericMetaData';
export type { IGenericLibrary } from './IGenericLibrary';
export type { ILibrary, IMovieLibrary } from './ILibrary';
export type { IMovieMetaData } from './IMovieMetaData';
export type { IEpisodeMetaData, ISeriesMetaData, ISeasonMetaData } from './ISeriesMetaData';
59 changes: 36 additions & 23 deletions packages/core/src/service/ProfileManager/BaseProfileManager.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Signal } from "@preact/signals-react";
import { IIpfsService } from '../IIpfsService';
import { ITask } from '../ITask';
import { ILibrary } from "../Library";
import { isMovieLibrary } from "../Library/ILibrary";
import { isMovieLibrary, isSeriesLibrary } from "../Library/ILibrary";
import { IProfile } from '../Profile/IProfile';
import { MovieIndexFetcher } from '../indexer/MovieIndexFetcher';
import { MovieIndexFetcher, SeriesIndexFetcher } from '../indexer';
import { IProfileManager, ProfileManagerState } from "./IProfileManager";
import { ITask } from '../ITask';

export abstract class BaseProfileManager implements IProfileManager {
protected constructor(public readonly profile: IProfile) {
Expand Down Expand Up @@ -63,29 +63,42 @@ export abstract class BaseProfileManager implements IProfileManager {
}
}

private updateLibrary(library: ILibrary): Promise<void> {
//TODO: update root from upstream
//TODO: verify fetch is needed
return this.fetchIndex(library)
.then(index => {
const lib = this.libraries.get(library.name);
if (lib != undefined) {
lib.value = {
...lib.value, index: {
values: index,
cid: lib.value.root,
}
};
private async updateLibrary(library: ILibrary): Promise<void> {
if (library.upstream != undefined) {
try {
const cid = await this.ipfs!.resolve(library.upstream);
if (library.root != cid) {
const lib = this.libraries.get(library.name);
if (lib != undefined) {
lib.value = {
...lib.value, root: cid
};
}
}
});
}
} catch (ex) {
console.error(ex);
}
}

private fetchIndex(library: ILibrary) {
if (isMovieLibrary(library)) {
const indexer = new MovieIndexFetcher(this.ipfs!, library);
return indexer.fetchIndex();
if (library.index?.cid !== library.root || library.index?.cid !== undefined) {
const lib = this.libraries.get(library.name);
if (lib != undefined) {
const indexer = isMovieLibrary(lib.value) ? new MovieIndexFetcher(this.ipfs!, lib.value) : isSeriesLibrary(lib.value) ? new SeriesIndexFetcher(this.ipfs!, lib.value) : undefined;
if (indexer == undefined) {
throw new Error(`Unknown library type [${library.type}]`);
}

const index = await indexer.fetchIndex();

//@ts-ignore
lib.value = {
...lib.value, index: {
values: index,
cid: lib.value.root,
}
};
}
}
return Promise.reject(new Error(`Unknown library type [${library.type}]`));
}

private updates = new Map<string, Promise<void>>();
Expand Down
52 changes: 52 additions & 0 deletions packages/core/src/service/indexer/SeriesIndexFetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { IIpfsService } from "../IIpfsService";
import { IEpisodeMetaData, IGenericLibrary, ISeasonMetaData, ISeriesMetaData } from "../Library";
import { IFileInfo } from "./IFileInfo";
import { IIndexFetcher } from './IIndexFetcher';
import { Regexes } from "./Regexes";

export class SeriesIndexFetcher implements IIndexFetcher<ISeriesMetaData[]> {
constructor(private readonly node: IIpfsService, private readonly lib: IGenericLibrary<ISeriesMetaData, 'series'>) {
}

public async fetchIndex(): Promise<ISeriesMetaData[]> {
const files = (await this.node.ls(this.lib.root.toString())).filter(f => f.type == 'dir');
const index = [];
for (const file of files) {
index.push(await this.extractSeriesMetaData(this.node, file));
}

return index;
}

public async extractSeriesMetaData(node: IIpfsService, entry: IFileInfo): Promise<ISeriesMetaData> {
const entries = await this.node.ls(entry.cid);
const files = entries.filter(f => f.type == 'file');
const folders = entries.filter(f => f.type !== 'file');

return {
title: entry.name,
posters: files.filter(f => Regexes.Poster.exec(f.name) != null),
seasons: await Promise.all(folders.map(season => this.extractSeasonMetaData(node, season))),
};
}

public async extractSeasonMetaData(node: IIpfsService, entry: IFileInfo, skeleton?: any): Promise<ISeasonMetaData> {
const entries = await this.node.ls(entry.cid);
const files = entries.filter(f => f.type == 'file');
const folders = entries.filter(f => f.type !== 'file');

return {
posters: files.filter(f => Regexes.Poster.exec(f.name) != null),
episodes: await Promise.all(folders.map(episode => this.extractEpisodeMetaData(node, episode))),
};
}
public async extractEpisodeMetaData(node: IIpfsService, entry: IFileInfo, skeleton?: any): Promise<IEpisodeMetaData> {
const files = (await this.node.ls(entry.cid)).filter(f => f.type == 'file');

return {
title: entry.name,
file: files.filter(f => f.name.endsWith('.mp4'))[0],
thumbnails: files.filter(f => Regexes.Thumbnail.exec(f.name) != null),
};
}
}
1 change: 1 addition & 0 deletions packages/core/src/service/indexer/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export type { IFileInfo } from './IFileInfo';
export { MovieIndexFetcher } from './MovieIndexFetcher';
export { SeriesIndexFetcher } from './SeriesIndexFetcher';
export { Regexes } from './Regexes';
1 change: 1 addition & 0 deletions packages/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@helia/ipns": "^7.2.2",
"@preact/signals-react-transform": "^0.3.1",
"@rollup/plugin-commonjs": "^25.0.7",
"@types/express": "^4",
Expand Down
Loading

0 comments on commit 9692106

Please sign in to comment.