diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx index b38030b3..e1e46aa1 100644 --- a/src/components/Dropdown.tsx +++ b/src/components/Dropdown.tsx @@ -4,7 +4,7 @@ import React, { Fragment } from "react"; import { Listbox, Transition } from "@headlessui/react"; export interface OptionItem { - id: number; + id: string; name: string; } diff --git a/src/components/layout/Seasons.tsx b/src/components/layout/Seasons.tsx index abbf9f67..ddfef7b9 100644 --- a/src/components/layout/Seasons.tsx +++ b/src/components/layout/Seasons.tsx @@ -1,4 +1,4 @@ -import { Dropdown } from "components/Dropdown"; +import { Dropdown, OptionItem } from "components/Dropdown"; import { WatchedEpisode } from "components/media/WatchedEpisodeButton"; import { useLoading } from "hooks/useLoading"; import { serializePortableMedia } from "hooks/usePortableMedia"; @@ -6,6 +6,7 @@ import { convertMediaToPortable, MWMedia, MWMediaSeasons, + MWMediaSeason, MWPortableMedia, } from "providers"; import { getSeasonDataFromMedia } from "providers/methods/seasons"; @@ -22,8 +23,8 @@ export function Seasons(props: SeasonsProps) { ); const history = useHistory(); const [seasons, setSeasons] = useState({ seasons: [] }); - const seasonSelected = props.media.season as number; - const episodeSelected = props.media.episode as number; + const seasonSelected = props.media.seasonId as string; + const episodeSelected = props.media.episodeId as string; useEffect(() => { (async () => { @@ -32,10 +33,10 @@ export function Seasons(props: SeasonsProps) { })(); }, [searchSeasons, props.media]); - function navigateToSeasonAndEpisode(season: number, episode: number) { + function navigateToSeasonAndEpisode(seasonId: string, episodeId: string) { const newMedia: MWMedia = { ...props.media }; - newMedia.episode = episode; - newMedia.season = season; + newMedia.episodeId = episodeId; + newMedia.seasonId = seasonId; history.replace( `/media/${newMedia.mediaType}/${serializePortableMedia( convertMediaToPortable(newMedia) @@ -43,15 +44,17 @@ export function Seasons(props: SeasonsProps) { ); } - const options = seasons.seasons.map((season) => ({ - id: season.seasonNumber, - name: `Season ${season.seasonNumber}`, - })); + const mapSeason = (season: MWMediaSeason) => ({ + id: season.id, + name: season.title || `Season ${season.sort}`, + }); - const selectedItem = { - id: seasonSelected, - name: `Season ${seasonSelected}`, - }; + const options = seasons.seasons.map(mapSeason); + + const foundSeason = seasons.seasons.find( + (season) => season.id === seasonSelected + ); + const selectedItem = foundSeason ? mapSeason(foundSeason) : null; return ( <> @@ -60,29 +63,31 @@ export function Seasons(props: SeasonsProps) { {success && seasons.seasons.length ? ( <> navigateToSeasonAndEpisode( 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) => ( - - navigateToSeasonAndEpisode(seasonSelected, v.episodeNumber) - } - /> - ))} + {seasons.seasons + .find((s) => s.id === seasonSelected) + ?.episodes.map((v) => ( + navigateToSeasonAndEpisode(seasonSelected, v.id)} + /> + ))} ) : null} diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 112003ea..b34fac33 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -1,5 +1,6 @@ import { convertMediaToPortable, + getEpisodeFromMedia, getProviderFromId, MWMediaMeta, MWMediaType, @@ -53,9 +54,9 @@ function MediaCardContent({

{media.title} - {series ? ( + {series && media.seasonId && media.episodeId ? ( - S{media.season} E{media.episode} + S{media.seasonId} E{media.episodeId} ) : null}

diff --git a/src/components/media/WatchedEpisodeButton.tsx b/src/components/media/WatchedEpisodeButton.tsx index d95551f3..8c31db02 100644 --- a/src/components/media/WatchedEpisodeButton.tsx +++ b/src/components/media/WatchedEpisodeButton.tsx @@ -1,9 +1,9 @@ -import { MWMediaMeta } from "providers"; +import { getEpisodeFromMedia, MWMedia } from "providers"; import { useWatchedContext, getWatchedFromPortable } from "state/watched"; import { Episode } from "./EpisodeButton"; export interface WatchedEpisodeProps { - media: MWMediaMeta; + media: MWMedia; onClick?: () => void; active?: boolean; } @@ -11,12 +11,13 @@ export interface WatchedEpisodeProps { export function WatchedEpisode(props: WatchedEpisodeProps) { const { watched } = useWatchedContext(); const foundWatched = getWatchedFromPortable(watched.items, props.media); + const episode = getEpisodeFromMedia(props.media); const watchedPercentage = (foundWatched && foundWatched.percentage) || 0; return ( diff --git a/src/components/media/WatchedMediaCard.tsx b/src/components/media/WatchedMediaCard.tsx index c7d68e8e..294e74fb 100644 --- a/src/components/media/WatchedMediaCard.tsx +++ b/src/components/media/WatchedMediaCard.tsx @@ -16,7 +16,7 @@ export function WatchedMediaCard(props: WatchedMediaCardProps) { ); diff --git a/src/providers/index.ts b/src/providers/index.ts index 1638f01a..5ea5cfbb 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -14,8 +14,8 @@ export function convertMediaToPortable(media: MWMedia): MWPortableMedia { mediaId: media.mediaId, providerId: media.providerId, mediaType: media.mediaType, - episode: media.episode, - season: media.season, + episodeId: media.episodeId, + seasonId: media.seasonId, }; } diff --git a/src/providers/list/theflix/index.ts b/src/providers/list/theflix/index.ts index 06e1e1c4..38441c1e 100644 --- a/src/providers/list/theflix/index.ts +++ b/src/providers/list/theflix/index.ts @@ -53,7 +53,7 @@ export const theFlixScraper: MWMediaProvider = { if (media.mediaType === MWMediaType.MOVIE) { url = `${CORS_PROXY_URL}https://theflix.to/movie/${media.mediaId}?movieInfo=${media.mediaId}`; } 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()); @@ -75,7 +75,7 @@ export const theFlixScraper: MWMediaProvider = { async getSeasonDataFromMedia( media: MWPortableMedia ): Promise { - 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 node: Element = Array.from( @@ -87,10 +87,14 @@ export const theFlixScraper: MWMediaProvider = { const data = JSON.parse(node.innerHTML).props.pageProps.selectedTv.seasons; return { 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", + title: d.seasonNumber === 0 ? "Specials" : undefined, episodes: d.episodes.map((e: any) => ({ title: e.name, + sort: e.episodeNumber, + id: e.episodeNumber.toString(), episodeNumber: e.episodeNumber, })), })), diff --git a/src/providers/list/theflix/portableToMedia.ts b/src/providers/list/theflix/portableToMedia.ts index 17e76e38..6a2b75d1 100644 --- a/src/providers/list/theflix/portableToMedia.ts +++ b/src/providers/list/theflix/portableToMedia.ts @@ -6,7 +6,7 @@ const getTheFlixUrl = (media: MWPortableMedia, params?: URLSearchParams) => { return `https://theflix.to/movie/${media.mediaId}?${params}`; } 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 ""; diff --git a/src/providers/list/theflix/search.ts b/src/providers/list/theflix/search.ts index 8ff49386..db4df27e 100644 --- a/src/providers/list/theflix/search.ts +++ b/src/providers/list/theflix/search.ts @@ -38,11 +38,11 @@ export function turnDataIntoMedia(data: any): MWProviderMediaResult { title: data.name, year: new Date(data.releaseDate).getFullYear().toString(), seasonCount: data.numberOfSeasons, - episode: data.lastReleasedEpisode - ? data.lastReleasedEpisode.episodeNumber + episodeId: data.lastReleasedEpisode + ? data.lastReleasedEpisode.episodeNumber.toString() : null, - season: data.lastReleasedEpisode - ? data.lastReleasedEpisode.seasonNumber + seasonId: data.lastReleasedEpisode + ? data.lastReleasedEpisode.seasonNumber.toString() : null, }; } diff --git a/src/providers/methods/helpers.ts b/src/providers/methods/helpers.ts index e8d41def..8ed1aa67 100644 --- a/src/providers/methods/helpers.ts +++ b/src/providers/methods/helpers.ts @@ -1,4 +1,5 @@ import { MWMediaType, MWMediaProviderMetadata } from "providers"; +import { MWMedia, MWMediaEpisode, MWMediaSeason } from "providers/types"; import { mediaProviders, mediaProvidersUnchecked } from "./providers"; /* @@ -38,3 +39,27 @@ export function getProviderMetadata(id: string): MWMediaProviderMetadata { 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, + }; +} diff --git a/src/providers/methods/seasons.ts b/src/providers/methods/seasons.ts index a8530f1c..a2b730d4 100644 --- a/src/providers/methods/seasons.ts +++ b/src/providers/methods/seasons.ts @@ -28,10 +28,8 @@ export async function getSeasonDataFromMedia( } const seasonData = await provider.getSeasonDataFromMedia(media); - seasonData.seasons.sort((a, b) => a.seasonNumber - b.seasonNumber); - seasonData.seasons.forEach((s) => - s.episodes.sort((a, b) => a.episodeNumber - b.episodeNumber) - ); + seasonData.seasons.sort((a, b) => a.sort - b.sort); + seasonData.seasons.forEach((s) => s.episodes.sort((a, b) => a.sort - b.sort)); // cache it seasonCache.set(media, seasonData, 60 * 60); // cache it for an hour diff --git a/src/providers/types.ts b/src/providers/types.ts index 6dde564a..1bd47a48 100644 --- a/src/providers/types.ts +++ b/src/providers/types.ts @@ -8,8 +8,8 @@ export interface MWPortableMedia { mediaId: string; mediaType: MWMediaType; providerId: string; - season?: number; - episode?: number; + seasonId?: string; + episodeId?: string; } export type MWMediaStreamType = "m3u8" | "mp4"; @@ -24,15 +24,20 @@ export interface MWMediaMeta extends MWPortableMedia { 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 { - seasons: { - seasonNumber: number; - type: "season" | "special"; - episodes: { - title: string; - episodeNumber: number; - }[]; - }[]; + seasons: MWMediaSeason[]; } export interface MWMedia extends MWMediaMeta { diff --git a/src/providers/wrapper.ts b/src/providers/wrapper.ts index b4bb7927..35727d97 100644 --- a/src/providers/wrapper.ts +++ b/src/providers/wrapper.ts @@ -23,8 +23,8 @@ export function WrapProvider( // consult cache first const output = contentCache.get(media); if (output) { - output.season = media.season; - output.episode = media.episode; + output.seasonId = media.seasonId; + output.episodeId = media.episodeId; return output; } diff --git a/src/state/bookmark/context.tsx b/src/state/bookmark/context.tsx index 9a63ac77..04b3b9de 100644 --- a/src/state/bookmark/context.tsx +++ b/src/state/bookmark/context.tsx @@ -35,8 +35,8 @@ function getBookmarkIndexFromMedia( (v) => v.mediaId === media.mediaId && v.providerId === media.providerId && - v.episode === media.episode && - v.season === media.season + v.episodeId === media.episodeId && + v.seasonId === media.seasonId ); return a; } @@ -75,8 +75,8 @@ export function BookmarkContextProvider(props: { children: ReactNode }) { providerId: media.providerId, title: media.title, year: media.year, - episode: media.episode, - season: media.season, + episodeId: media.episodeId, + seasonId: media.seasonId, }; data.bookmarks.push(item); } diff --git a/src/state/watched/context.tsx b/src/state/watched/context.tsx index e2689375..32ea5920 100644 --- a/src/state/watched/context.tsx +++ b/src/state/watched/context.tsx @@ -1,4 +1,9 @@ -import { MWMediaMeta, getProviderMetadata, MWMediaType } from "providers"; +import { + MWMediaMeta, + getProviderMetadata, + MWMediaType, + getEpisodeFromMedia, +} from "providers"; import React, { createContext, ReactNode, @@ -32,8 +37,8 @@ export function getWatchedFromPortable( (v) => v.mediaId === media.mediaId && v.providerId === media.providerId && - v.episode === media.episode && - v.season === media.season + v.episodeId === media.episodeId && + v.seasonId === media.seasonId ); } @@ -84,8 +89,8 @@ export function WatchedContextProvider(props: { children: ReactNode }) { year: media.year, percentage: 0, progress: 0, - episode: media.episode, - season: media.season, + episodeId: media.episodeId, + seasonId: media.seasonId, }; data.items.push(item); } @@ -112,8 +117,8 @@ export function WatchedContextProvider(props: { children: ReactNode }) { ) { const key = `${item.mediaType}-${item.mediaId}`; const current: [number, number] = [ - item.season ?? -1, - item.episode ?? -1, + item.episodeId ? parseInt(item.episodeId, 10) : -1, + item.seasonId ? parseInt(item.seasonId, 10) : -1, ]; let existing = highestEpisode[key]; if (!existing) {