add id's to portables for better seasons

This commit is contained in:
mrjvs 2022-03-13 16:55:59 +01:00
parent 9b47f81afb
commit 570ca14905
15 changed files with 118 additions and 74 deletions

View File

@ -4,7 +4,7 @@ import React, { Fragment } from "react";
import { Listbox, Transition } from "@headlessui/react"; import { Listbox, Transition } from "@headlessui/react";
export interface OptionItem { export interface OptionItem {
id: number; id: string;
name: string; name: string;
} }

View File

@ -1,4 +1,4 @@
import { Dropdown } from "components/Dropdown"; import { Dropdown, OptionItem } from "components/Dropdown";
import { WatchedEpisode } from "components/media/WatchedEpisodeButton"; import { WatchedEpisode } from "components/media/WatchedEpisodeButton";
import { useLoading } from "hooks/useLoading"; import { useLoading } from "hooks/useLoading";
import { serializePortableMedia } from "hooks/usePortableMedia"; import { serializePortableMedia } from "hooks/usePortableMedia";
@ -6,6 +6,7 @@ import {
convertMediaToPortable, convertMediaToPortable,
MWMedia, MWMedia,
MWMediaSeasons, MWMediaSeasons,
MWMediaSeason,
MWPortableMedia, MWPortableMedia,
} from "providers"; } from "providers";
import { getSeasonDataFromMedia } from "providers/methods/seasons"; import { getSeasonDataFromMedia } from "providers/methods/seasons";
@ -22,8 +23,8 @@ export function Seasons(props: SeasonsProps) {
); );
const history = useHistory(); const history = useHistory();
const [seasons, setSeasons] = useState<MWMediaSeasons>({ seasons: [] }); const [seasons, setSeasons] = useState<MWMediaSeasons>({ seasons: [] });
const seasonSelected = props.media.season as number; const seasonSelected = props.media.seasonId as string;
const episodeSelected = props.media.episode as number; const episodeSelected = props.media.episodeId as string;
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@ -32,10 +33,10 @@ export function Seasons(props: SeasonsProps) {
})(); })();
}, [searchSeasons, props.media]); }, [searchSeasons, props.media]);
function navigateToSeasonAndEpisode(season: number, episode: number) { function navigateToSeasonAndEpisode(seasonId: string, episodeId: string) {
const newMedia: MWMedia = { ...props.media }; const newMedia: MWMedia = { ...props.media };
newMedia.episode = episode; newMedia.episodeId = episodeId;
newMedia.season = season; newMedia.seasonId = seasonId;
history.replace( history.replace(
`/media/${newMedia.mediaType}/${serializePortableMedia( `/media/${newMedia.mediaType}/${serializePortableMedia(
convertMediaToPortable(newMedia) convertMediaToPortable(newMedia)
@ -43,15 +44,17 @@ export function Seasons(props: SeasonsProps) {
); );
} }
const options = seasons.seasons.map((season) => ({ const mapSeason = (season: MWMediaSeason) => ({
id: season.seasonNumber, id: season.id,
name: `Season ${season.seasonNumber}`, name: season.title || `Season ${season.sort}`,
})); });
const selectedItem = { const options = seasons.seasons.map(mapSeason);
id: seasonSelected,
name: `Season ${seasonSelected}`, const foundSeason = seasons.seasons.find(
}; (season) => season.id === seasonSelected
);
const selectedItem = foundSeason ? mapSeason(foundSeason) : null;
return ( return (
<> <>
@ -60,29 +63,31 @@ export function Seasons(props: SeasonsProps) {
{success && seasons.seasons.length ? ( {success && seasons.seasons.length ? (
<> <>
<Dropdown <Dropdown
selectedItem={selectedItem} selectedItem={selectedItem as OptionItem}
options={options} options={options}
setSelectedItem={(seasonItem) => setSelectedItem={(seasonItem) =>
navigateToSeasonAndEpisode( navigateToSeasonAndEpisode(
seasonItem.id, seasonItem.id,
seasons.seasons[seasonItem.id]?.episodes[0].episodeNumber seasons.seasons.find((s) => s.id === seasonItem.id)?.episodes[0]
.id as string
) )
} }
/> />
{seasons.seasons[seasonSelected]?.episodes.map((v) => ( {seasons.seasons
<WatchedEpisode .find((s) => s.id === seasonSelected)
key={v.episodeNumber} ?.episodes.map((v) => (
media={{ <WatchedEpisode
...props.media, key={v.id}
episode: v.episodeNumber, media={{
season: seasonSelected, ...props.media,
}} seriesData: seasons,
active={v.episodeNumber === episodeSelected} episodeId: v.id,
onClick={() => seasonId: seasonSelected,
navigateToSeasonAndEpisode(seasonSelected, v.episodeNumber) }}
} active={v.id === episodeSelected}
/> onClick={() => navigateToSeasonAndEpisode(seasonSelected, v.id)}
))} />
))}
</> </>
) : null} ) : null}
</> </>

View File

@ -1,5 +1,6 @@
import { import {
convertMediaToPortable, convertMediaToPortable,
getEpisodeFromMedia,
getProviderFromId, getProviderFromId,
MWMediaMeta, MWMediaMeta,
MWMediaType, MWMediaType,
@ -53,9 +54,9 @@ function MediaCardContent({
<div className="flex-1"> <div className="flex-1">
<h1 className="mb-1 font-bold text-white"> <h1 className="mb-1 font-bold text-white">
{media.title} {media.title}
{series ? ( {series && media.seasonId && media.episodeId ? (
<span className="text-denim-700 ml-2 text-xs"> <span className="text-denim-700 ml-2 text-xs">
S{media.season} E{media.episode} S{media.seasonId} E{media.episodeId}
</span> </span>
) : null} ) : null}
</h1> </h1>

View File

@ -1,9 +1,9 @@
import { MWMediaMeta } from "providers"; import { getEpisodeFromMedia, MWMedia } from "providers";
import { useWatchedContext, getWatchedFromPortable } from "state/watched"; import { useWatchedContext, getWatchedFromPortable } from "state/watched";
import { Episode } from "./EpisodeButton"; import { Episode } from "./EpisodeButton";
export interface WatchedEpisodeProps { export interface WatchedEpisodeProps {
media: MWMediaMeta; media: MWMedia;
onClick?: () => void; onClick?: () => void;
active?: boolean; active?: boolean;
} }
@ -11,12 +11,13 @@ export interface WatchedEpisodeProps {
export function WatchedEpisode(props: WatchedEpisodeProps) { export function WatchedEpisode(props: WatchedEpisodeProps) {
const { watched } = useWatchedContext(); const { watched } = useWatchedContext();
const foundWatched = getWatchedFromPortable(watched.items, props.media); const foundWatched = getWatchedFromPortable(watched.items, props.media);
const episode = getEpisodeFromMedia(props.media);
const watchedPercentage = (foundWatched && foundWatched.percentage) || 0; const watchedPercentage = (foundWatched && foundWatched.percentage) || 0;
return ( return (
<Episode <Episode
progress={watchedPercentage} progress={watchedPercentage}
episodeNumber={props.media.episode ?? 1} episodeNumber={episode?.episode?.sort ?? 1}
active={props.active} active={props.active}
onClick={props.onClick} onClick={props.onClick}
/> />

View File

@ -16,7 +16,7 @@ export function WatchedMediaCard(props: WatchedMediaCardProps) {
<MediaCard <MediaCard
watchedPercentage={watchedPercentage} watchedPercentage={watchedPercentage}
media={props.media} media={props.media}
series={props.series && props.media.episode !== undefined} series={props.series && props.media.episodeId !== undefined}
linkable linkable
/> />
); );

View File

@ -14,8 +14,8 @@ export function convertMediaToPortable(media: MWMedia): MWPortableMedia {
mediaId: media.mediaId, mediaId: media.mediaId,
providerId: media.providerId, providerId: media.providerId,
mediaType: media.mediaType, mediaType: media.mediaType,
episode: media.episode, episodeId: media.episodeId,
season: media.season, seasonId: media.seasonId,
}; };
} }

View File

@ -53,7 +53,7 @@ export const theFlixScraper: MWMediaProvider = {
if (media.mediaType === MWMediaType.MOVIE) { if (media.mediaType === MWMediaType.MOVIE) {
url = `${CORS_PROXY_URL}https://theflix.to/movie/${media.mediaId}?movieInfo=${media.mediaId}`; url = `${CORS_PROXY_URL}https://theflix.to/movie/${media.mediaId}?movieInfo=${media.mediaId}`;
} else if (media.mediaType === MWMediaType.SERIES) { } else if (media.mediaType === MWMediaType.SERIES) {
url = `${CORS_PROXY_URL}https://theflix.to/tv-show/${media.mediaId}/season-${media.season}/episode-${media.episode}`; url = `${CORS_PROXY_URL}https://theflix.to/tv-show/${media.mediaId}/season-${media.seasonId}/episode-${media.episodeId}`;
} }
const res = await fetch(url).then((d) => d.text()); const res = await fetch(url).then((d) => d.text());
@ -75,7 +75,7 @@ export const theFlixScraper: MWMediaProvider = {
async getSeasonDataFromMedia( async getSeasonDataFromMedia(
media: MWPortableMedia media: MWPortableMedia
): Promise<MWMediaSeasons> { ): Promise<MWMediaSeasons> {
const url = `${CORS_PROXY_URL}https://theflix.to/tv-show/${media.mediaId}/season-${media.season}/episode-${media.episode}`; const url = `${CORS_PROXY_URL}https://theflix.to/tv-show/${media.mediaId}/season-${media.seasonId}/episode-${media.episodeId}`;
const res = await fetch(url).then((d) => d.text()); const res = await fetch(url).then((d) => d.text());
const node: Element = Array.from( const node: Element = Array.from(
@ -87,10 +87,14 @@ export const theFlixScraper: MWMediaProvider = {
const data = JSON.parse(node.innerHTML).props.pageProps.selectedTv.seasons; const data = JSON.parse(node.innerHTML).props.pageProps.selectedTv.seasons;
return { return {
seasons: data.map((d: any) => ({ seasons: data.map((d: any) => ({
seasonNumber: d.seasonNumber === 0 ? 999 : d.seasonNumber, sort: d.seasonNumber === 0 ? 999 : d.seasonNumber,
id: d.seasonNumber.toString(),
type: d.seasonNumber === 0 ? "special" : "season", type: d.seasonNumber === 0 ? "special" : "season",
title: d.seasonNumber === 0 ? "Specials" : undefined,
episodes: d.episodes.map((e: any) => ({ episodes: d.episodes.map((e: any) => ({
title: e.name, title: e.name,
sort: e.episodeNumber,
id: e.episodeNumber.toString(),
episodeNumber: e.episodeNumber, episodeNumber: e.episodeNumber,
})), })),
})), })),

View File

@ -6,7 +6,7 @@ const getTheFlixUrl = (media: MWPortableMedia, params?: URLSearchParams) => {
return `https://theflix.to/movie/${media.mediaId}?${params}`; return `https://theflix.to/movie/${media.mediaId}?${params}`;
} }
if (media.mediaType === MWMediaType.SERIES) { if (media.mediaType === MWMediaType.SERIES) {
return `https://theflix.to/tv-show/${media.mediaId}/season-${media.season}/episode-${media.episode}`; return `https://theflix.to/tv-show/${media.mediaId}/season-${media.seasonId}/episode-${media.episodeId}`;
} }
return ""; return "";

View File

@ -38,11 +38,11 @@ export function turnDataIntoMedia(data: any): MWProviderMediaResult {
title: data.name, title: data.name,
year: new Date(data.releaseDate).getFullYear().toString(), year: new Date(data.releaseDate).getFullYear().toString(),
seasonCount: data.numberOfSeasons, seasonCount: data.numberOfSeasons,
episode: data.lastReleasedEpisode episodeId: data.lastReleasedEpisode
? data.lastReleasedEpisode.episodeNumber ? data.lastReleasedEpisode.episodeNumber.toString()
: null, : null,
season: data.lastReleasedEpisode seasonId: data.lastReleasedEpisode
? data.lastReleasedEpisode.seasonNumber ? data.lastReleasedEpisode.seasonNumber.toString()
: null, : null,
}; };
} }

View File

@ -1,4 +1,5 @@
import { MWMediaType, MWMediaProviderMetadata } from "providers"; import { MWMediaType, MWMediaProviderMetadata } from "providers";
import { MWMedia, MWMediaEpisode, MWMediaSeason } from "providers/types";
import { mediaProviders, mediaProvidersUnchecked } from "./providers"; import { mediaProviders, mediaProvidersUnchecked } from "./providers";
/* /*
@ -38,3 +39,27 @@ export function getProviderMetadata(id: string): MWMediaProviderMetadata {
provider, provider,
}; };
} }
/*
** get episode and season from media
*/
export function getEpisodeFromMedia(
media: MWMedia
): { season: MWMediaSeason; episode: MWMediaEpisode } | null {
if (
media.seasonId === undefined ||
media.episodeId === undefined ||
media.seriesData === undefined
) {
return null;
}
const season = media.seriesData.seasons.find((v) => v.id === media.seasonId);
if (!season) return null;
const episode = season?.episodes.find((v) => v.id === media.episodeId);
if (!episode) return null;
return {
season,
episode,
};
}

View File

@ -28,10 +28,8 @@ export async function getSeasonDataFromMedia(
} }
const seasonData = await provider.getSeasonDataFromMedia(media); const seasonData = await provider.getSeasonDataFromMedia(media);
seasonData.seasons.sort((a, b) => a.seasonNumber - b.seasonNumber); seasonData.seasons.sort((a, b) => a.sort - b.sort);
seasonData.seasons.forEach((s) => seasonData.seasons.forEach((s) => s.episodes.sort((a, b) => a.sort - b.sort));
s.episodes.sort((a, b) => a.episodeNumber - b.episodeNumber)
);
// cache it // cache it
seasonCache.set(media, seasonData, 60 * 60); // cache it for an hour seasonCache.set(media, seasonData, 60 * 60); // cache it for an hour

View File

@ -8,8 +8,8 @@ export interface MWPortableMedia {
mediaId: string; mediaId: string;
mediaType: MWMediaType; mediaType: MWMediaType;
providerId: string; providerId: string;
season?: number; seasonId?: string;
episode?: number; episodeId?: string;
} }
export type MWMediaStreamType = "m3u8" | "mp4"; export type MWMediaStreamType = "m3u8" | "mp4";
@ -24,15 +24,20 @@ export interface MWMediaMeta extends MWPortableMedia {
seasonCount?: number; seasonCount?: number;
} }
export interface MWMediaEpisode {
sort: number;
id: string;
title: string;
}
export interface MWMediaSeason {
sort: number;
id: string;
title?: string;
type: "season" | "special";
episodes: MWMediaEpisode[];
}
export interface MWMediaSeasons { export interface MWMediaSeasons {
seasons: { seasons: MWMediaSeason[];
seasonNumber: number;
type: "season" | "special";
episodes: {
title: string;
episodeNumber: number;
}[];
}[];
} }
export interface MWMedia extends MWMediaMeta { export interface MWMedia extends MWMediaMeta {

View File

@ -23,8 +23,8 @@ export function WrapProvider(
// consult cache first // consult cache first
const output = contentCache.get(media); const output = contentCache.get(media);
if (output) { if (output) {
output.season = media.season; output.seasonId = media.seasonId;
output.episode = media.episode; output.episodeId = media.episodeId;
return output; return output;
} }

View File

@ -35,8 +35,8 @@ function getBookmarkIndexFromMedia(
(v) => (v) =>
v.mediaId === media.mediaId && v.mediaId === media.mediaId &&
v.providerId === media.providerId && v.providerId === media.providerId &&
v.episode === media.episode && v.episodeId === media.episodeId &&
v.season === media.season v.seasonId === media.seasonId
); );
return a; return a;
} }
@ -75,8 +75,8 @@ export function BookmarkContextProvider(props: { children: ReactNode }) {
providerId: media.providerId, providerId: media.providerId,
title: media.title, title: media.title,
year: media.year, year: media.year,
episode: media.episode, episodeId: media.episodeId,
season: media.season, seasonId: media.seasonId,
}; };
data.bookmarks.push(item); data.bookmarks.push(item);
} }

View File

@ -1,4 +1,9 @@
import { MWMediaMeta, getProviderMetadata, MWMediaType } from "providers"; import {
MWMediaMeta,
getProviderMetadata,
MWMediaType,
getEpisodeFromMedia,
} from "providers";
import React, { import React, {
createContext, createContext,
ReactNode, ReactNode,
@ -32,8 +37,8 @@ export function getWatchedFromPortable(
(v) => (v) =>
v.mediaId === media.mediaId && v.mediaId === media.mediaId &&
v.providerId === media.providerId && v.providerId === media.providerId &&
v.episode === media.episode && v.episodeId === media.episodeId &&
v.season === media.season v.seasonId === media.seasonId
); );
} }
@ -84,8 +89,8 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
year: media.year, year: media.year,
percentage: 0, percentage: 0,
progress: 0, progress: 0,
episode: media.episode, episodeId: media.episodeId,
season: media.season, seasonId: media.seasonId,
}; };
data.items.push(item); data.items.push(item);
} }
@ -112,8 +117,8 @@ export function WatchedContextProvider(props: { children: ReactNode }) {
) { ) {
const key = `${item.mediaType}-${item.mediaId}`; const key = `${item.mediaType}-${item.mediaId}`;
const current: [number, number] = [ const current: [number, number] = [
item.season ?? -1, item.episodeId ? parseInt(item.episodeId, 10) : -1,
item.episode ?? -1, item.seasonId ? parseInt(item.seasonId, 10) : -1,
]; ];
let existing = highestEpisode[key]; let existing = highestEpisode[key];
if (!existing) { if (!existing) {