diff --git a/src/components/MovieRow.css b/src/components/MovieRow.css index a32463b5..4ef12f41 100644 --- a/src/components/MovieRow.css +++ b/src/components/MovieRow.css @@ -24,6 +24,7 @@ margin-right: 0.5rem; } +.movieRow .left .seasonEpisodeSubtitle, .movieRow .left .year { color: var(--text-secondary); } diff --git a/src/components/MovieRow.js b/src/components/MovieRow.js index d8ae3162..d15d35ef 100644 --- a/src/components/MovieRow.js +++ b/src/components/MovieRow.js @@ -20,14 +20,14 @@ export function MovieRow(props) { return (
props.onClick && props.onClick()}>
- {props.title}  + {props.title} {props.place ? ` — Season ${props.place.season}: episode ${props.place.episode}` : ''}  ({props.year})

Watch {props.type}

- +
) } diff --git a/src/views/Movie.js b/src/views/Movie.js index cec810e9..71c72f0c 100644 --- a/src/views/Movie.js +++ b/src/views/Movie.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useCallback } from 'react' import { useRouteMatch, useHistory } from 'react-router-dom' import { Helmet } from 'react-helmet'; import { Title } from '../components/Title' @@ -14,15 +14,47 @@ export function MovieView(props) { const baseRouteMatch = useRouteMatch('/:type/:source/:title/:slug'); const showRouteMatch = useRouteMatch('/:type/:source/:title/:slug/season/:season/episode/:episode'); const history = useHistory(); + const { streamUrl, streamData, setStreamUrl } = useMovie(); - const [seasonList, setSeasonList] = React.useState([]); - const [episodeLists, setEpisodeList] = React.useState([]); - const [loading, setLoading] = React.useState(false); + const [ seasonList, setSeasonList ] = React.useState([]); + const [ episodeLists, setEpisodeList ] = React.useState([]); + const [ loading, setLoading ] = React.useState(false); const [ selectedSeason, setSelectedSeason ] = React.useState("1"); const season = showRouteMatch?.params.season || "1"; const episode = showRouteMatch?.params.episode || "1"; + // eslint-disable-next-line react-hooks/exhaustive-deps + function setEpisode({ season, episode }) { + // getStream(title, slug, type, source, year); + // console.log(season, episode) + let tmpSeason = showRouteMatch.params.season; + let tmpEpisode = showRouteMatch.params.episode; + if (tmpSeason != season && tmpEpisode != episode) + history.replace(`${baseRouteMatch.url}/season/${season}/episode/${episode}`); + } + + React.useEffect(() => { + // Cache streamData continue watching on home page + let movieCache = JSON.parse(localStorage.getItem("movie-cache") || "{}"); + + if (!movieCache[streamData.source]) movieCache[streamData.source] = {} + movieCache[streamData.source][streamData.slug] = { + cachedAt: Date.now() + } + + localStorage.setItem("movie-cache", JSON.stringify(movieCache)); + + // Set season and episode list for GUI + if (streamData.type === "show") { + setSeasonList(streamData.seasons); + setSelectedSeason(streamData.seasons[0]) + // TODO load from localstorage last watched + setEpisode({ episode: streamData.episodes[streamData.seasons[0]][0], season: streamData.seasons[0] }) + setEpisodeList(streamData.episodes[streamData.seasons[0]]); + } + }, [streamData, setEpisode]) + React.useEffect(() => { setEpisodeList(streamData.episodes[selectedSeason]); }, [selectedSeason, streamData.episodes]) @@ -39,14 +71,9 @@ export function MovieView(props) { if (streamData.type === "show" && !showRouteMatch) history.replace(`${baseRouteMatch.url}/season/1/episode/1`); }, [streamData, showRouteMatch, history, baseRouteMatch.url]); - React.useEffect(() => { - if (streamData.type === "show" && !showRouteMatch) history.replace(`${baseRouteMatch.url}/season/1/episode/1`); - }, [streamData, showRouteMatch, history, baseRouteMatch.url]); - React.useEffect(() => { if (streamData.type === "show" && showRouteMatch) setSelectedSeason(showRouteMatch.params.season.toString()); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [showRouteMatch, streamData]); React.useEffect(() => { let cancel = false; @@ -71,19 +98,38 @@ export function MovieView(props) { if (cancel) return; console.error(e) }) + return () => { cancel = true; } }, [episode, streamData, setStreamUrl, season]); - function setEpisode({ season, episode }) { - history.push(`${baseRouteMatch.url}/season/${season}/episode/${episode}`); - } + React.useEffect(() => { + // Cache streamData continue watching on home page + let movieCache = JSON.parse(localStorage.getItem("movie-cache") || "{}"); + + if(!movieCache[streamData.source]) movieCache[streamData.source] = {} + movieCache[streamData.source][streamData.slug] = { + cachedAt: Date.now() + } + + localStorage.setItem("movie-cache", JSON.stringify(movieCache)); + + // Set season and episode list for GUI + if (streamData.type === "show") { + setSeasonList(streamData.seasons); + setSelectedSeason(streamData.seasons[0]) + // TODO load from localstorage last watched + setEpisode({ episode: streamData.episodes[streamData.seasons[0]][0], season: streamData.seasons[0] }) + setEpisodeList(streamData.episodes[streamData.seasons[0]]); + } + }, [streamData, setEpisode]) const setProgress = (evt) => { let ls = JSON.parse(localStorage.getItem("video-progress") || "{}") - // We're just checking lookmovie for now since there is only one scraper + console.log(streamData); + if(!ls[streamData.source]) ls[streamData.source] = {} if(!ls[streamData.source][streamData.type]) ls[streamData.source][streamData.type] = {} if(!ls[streamData.source][streamData.type][streamData.slug]) { @@ -91,17 +137,18 @@ export function MovieView(props) { } // Store real data - let key = streamData.type === "show" ? `${season}-${episode}` : "full" + let key = streamData.type === "show" ? `${season}-${episode.episode}` : "full" ls[streamData.source][streamData.type][streamData.slug][key] = { currentlyAt: Math.floor(evt.currentTarget.currentTime), totalDuration: Math.floor(evt.currentTarget.duration), - updatedAt: Date.now() + updatedAt: Date.now(), + meta: streamData } if(streamData.type === "show") { ls[streamData.source][streamData.type][streamData.slug][key].show = { season, - episode: episode + episode: episode.episode } } diff --git a/src/views/Search.css b/src/views/Search.css index 6795e5a1..903b854c 100644 --- a/src/views/Search.css +++ b/src/views/Search.css @@ -8,11 +8,30 @@ box-sizing: border-box; } -.cardView > div { +.cardView nav { + width: 100%; + max-width: 624px; +} +.cardView nav a { + padding: 8px 16px; + margin-right: 10px; + border-radius: 4px; + color: var(--text); +} +.cardView nav a:not(.selected-link) { + cursor: pointer; +} +.cardView nav a.selected-link { + background: var(--button); + color: var(--button-text); + font-weight: bold; +} + +.cardView > * { margin-top: 2rem; } -.cardView > div:first-child { +.cardView > *:first-child { margin-top: 38px; } diff --git a/src/views/Search.js b/src/views/Search.js index 836c9088..715534d0 100644 --- a/src/views/Search.js +++ b/src/views/Search.js @@ -30,6 +30,8 @@ export function SearchView() { const [failed, setFailed] = React.useState(false); const [showingOptions, setShowingOptions] = React.useState(false); const [errorStatus, setErrorStatus] = React.useState(false); + const [page, setPage] = React.useState('search'); + const [continueWatching, setContinueWatching] = React.useState([]) const fail = (str) => { setProgress(maxSteps); @@ -37,7 +39,7 @@ export function SearchView() { setFailed(true) } - async function getStream(title, slug, type, source) { + async function getStream(title, slug, type, source, year) { setStreamUrl(""); try { @@ -71,7 +73,8 @@ export function SearchView() { seasons, episodes, slug, - source + source, + year }) setText(`Streaming...`) navigate("movie") @@ -100,9 +103,9 @@ export function SearchView() { return; } - const { title, slug, type, source } = options[0]; + const { title, slug, type, source, year } = options[0]; history.push(`${routeMatch.url}/${source}/${title}/${slug}`); - getStream(title, slug, type, source); + getStream(title, slug, type, source, year); } catch (err) { console.error(err); fail(`Failed to watch ${contentType}`) @@ -128,7 +131,38 @@ export function SearchView() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - if (!type || (type !== 'movie' && type !== 'show')) return + React.useEffect(() => { + const progressData = JSON.parse(localStorage.getItem('video-progress') || "{}") + let newContinueWatching = [] + Object.keys(progressData).forEach(source => { + const all = [ + ...Object.entries(progressData[source]?.show ?? {}), + ...Object.entries(progressData[source]?.movie ?? {}) + ] + for (const [slug, data] of all) { + for (let subselection of Object.values(data)) { + let entry = { + slug, + data: subselection, + type: subselection.show ? 'show' : 'movie', + percentageDone: Math.floor((subselection.currentlyAt / subselection.totalDuration) * 100), + source + } + if (entry.percentageDone < 90) { + newContinueWatching.push(entry) + } + } + } + newContinueWatching = newContinueWatching.sort((a, b) => { + return b.data.updatedAt - a.data.updatedAt + }) + setContinueWatching(newContinueWatching) + }) + }, []); + + if (!type || (type !== 'movie' && type !== 'show')) { + return + } return (
@@ -136,47 +170,73 @@ export function SearchView() { {type === 'movie' ? 'Movies' : 'TV Shows'} | movie-web - - - {errorStatus ? {errorStatus} : ''} - - What do you wanna watch? - - history.push(`/${type}`)} - choices={[ - { label: "Movie", value: "movie" }, - { label: "TV Show", value: "show" } - ]} - noWrap={true} - selected={type} - /> - searchMovie(str, type)} /> - 0} failed={failed} progress={progress} steps={maxSteps} text={text} /> - + {/* Nav */} + + + {/* Search */} + {page === 'search' ? + + + + {errorStatus ? {errorStatus} : ''} + + What do you wanna watch? + + history.push(`/${type}`)} + choices={[ + { label: "Movie", value: "movie" }, + { label: "TV Show", value: "show" } + ]} + noWrap={true} + selected={type} + /> + searchMovie(str, type)} /> + 0} failed={failed} progress={progress} steps={maxSteps} text={text} /> + + + + + Whoops, there are a few {type}s like that + + {Object.entries(options.reduce((a, v) => { + if (!a[v.source]) a[v.source] = [] + a[v.source].push(v) + return a; + }, {})).map(v => ( +
+

{v[0]}

+ {v[1].map((v, i) => ( + { + history.push(`${routeMatch.url}/${v.source}/${v.title}/${v.slug}`); + setShowingOptions(false) + getStream(v.title, v.slug, v.type, v.source, v.year) + }} /> + ))} +
+ )) + } +
+
: } + + {/* Continue watching */} + {continueWatching.length > 0 && page === 'watching' ? + Continue watching + {console.log(continueWatching)} + {continueWatching?.map((v, i) => ( + { + history.push(`${routeMatch.url}/${v.source}/${v.data.meta.title}/${v.slug}`) + setShowingOptions(false) + getStream(v.data.meta.title, v.data.meta.slug, v.type, v.source, v.year) + }} /> + ))} + : } - - - Whoops, there are a few {type}s like that - - { Object.entries(options.reduce((a, v) => { - if (!a[v.source]) a[v.source] = [] - a[v.source].push(v) - return a; - }, {})).map(v => ( -
-

{v[0]}

- {v[1].map((v, i) => ( - { - history.push(`${routeMatch.url}/${v.source}/${v.title}/${v.slug}`); - setShowingOptions(false) - getStream(v.title, v.slug, v.type, v.source) - }} /> - ))} -
- )) - } -
Check it out on GitHub