mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-20 14:37:43 +01:00
Merge pull request #24 from JamesHawkinss/feature/continue-watching
Continue watching
This commit is contained in:
commit
903cd633cc
13 changed files with 255 additions and 151 deletions
|
@ -1,7 +1,7 @@
|
||||||
import './index.css';
|
|
||||||
import { SearchView } from './views/Search';
|
import { SearchView } from './views/Search';
|
||||||
import { MovieView } from './views/Movie';
|
import { MovieView } from './views/Movie';
|
||||||
import { useMovie, MovieProvider} from './hooks/useMovie';
|
import { useMovie, MovieProvider } from './hooks/useMovie';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
const { streamData } = useMovie();
|
const { streamData } = useMovie();
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
.discordBanner {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
border-inline-start: none;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: normal;
|
|
||||||
letter-spacing: -.01em;
|
|
||||||
padding: .5rem 1rem .5rem .75rem;
|
|
||||||
border-radius: .25rem;
|
|
||||||
background-color: var(--button);
|
|
||||||
color: var(--button-text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.discordBanner a {
|
|
||||||
color: var(--button-text);
|
|
||||||
/* text-decoration: none; */
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import './DiscordBanner.css';
|
|
||||||
|
|
||||||
export function DiscordBanner() {
|
|
||||||
return (
|
|
||||||
<div className="discordBanner">
|
|
||||||
<a href="https://discord.gg/vXsRvye8BS">
|
|
||||||
Join our Discord server
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -3,15 +3,16 @@ import { TypeSelector } from './TypeSelector';
|
||||||
import { NumberSelector } from './NumberSelector';
|
import { NumberSelector } from './NumberSelector';
|
||||||
import './EpisodeSelector.css'
|
import './EpisodeSelector.css'
|
||||||
|
|
||||||
export function EpisodeSelector({ setSelectedSeason, setEpisode, seasons, selectedSeason, season, episodes, currentSeason, currentEpisode, source }) {
|
export function EpisodeSelector({ setSelectedSeason, selectedSeason, setEpisode, seasons, episodes, currentSeason, currentEpisode, streamData }) {
|
||||||
const choices = episodes ? episodes.map(v => {
|
const choices = episodes ? episodes.map(v => {
|
||||||
let progressData = JSON.parse(localStorage.getItem('video-progress') || "{}")
|
let progressData = JSON.parse(localStorage.getItem('video-progress') || "{}")
|
||||||
|
|
||||||
let currentlyAt = 0;
|
let currentlyAt = 0;
|
||||||
let totalDuration = 0;
|
let totalDuration = 0;
|
||||||
|
|
||||||
const progress = progressData?.[source]?.show?.slug?.[`${season}-${v}`]
|
const progress = progressData?.[streamData.source]?.[streamData.type]?.[streamData.slug]?.[`${selectedSeason}-${v}`]
|
||||||
if(progress) {
|
|
||||||
|
if (progress) {
|
||||||
currentlyAt = progress.currentlyAt
|
currentlyAt = progress.currentlyAt
|
||||||
totalDuration = progress.totalDuration
|
totalDuration = progress.totalDuration
|
||||||
}
|
}
|
||||||
|
@ -27,7 +28,7 @@ export function EpisodeSelector({ setSelectedSeason, setEpisode, seasons, select
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="episodeSelector">
|
<div className="episodeSelector">
|
||||||
<TypeSelector setType={setSelectedSeason} choices={seasons.map(v=>({ value: v.toString(), label: `Season ${v}`}))} selected={selectedSeason}/><br></br>
|
<TypeSelector setType={setSelectedSeason} selected={selectedSeason} choices={seasons.map(v=>({ value: v.toString(), label: `Season ${v}`}))} /><br></br>
|
||||||
<NumberSelector setType={(e) => setEpisode({episode: e, season: selectedSeason})} choices={choices} selected={(selectedSeason.toString() === currentSeason) ? currentEpisode : null} />
|
<NumberSelector setType={(e) => setEpisode({episode: e, season: selectedSeason})} choices={choices} selected={(selectedSeason.toString() === currentSeason) ? currentEpisode : null} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.movieRow .left .seasonEpisodeSubtitle,
|
||||||
.movieRow .left .year {
|
.movieRow .left .year {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,20 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Arrow } from './Arrow'
|
import { Arrow } from './Arrow'
|
||||||
import './MovieRow.css'
|
// import { Cross } from './Crosss'
|
||||||
import { PercentageOverlay } from './PercentageOverlay'
|
import { PercentageOverlay } from './PercentageOverlay'
|
||||||
|
import './MovieRow.css'
|
||||||
|
|
||||||
// title: string
|
// title: string
|
||||||
// onClick: () => void
|
// onClick: () => void
|
||||||
export function MovieRow(props) {
|
export function MovieRow(props) {
|
||||||
|
|
||||||
const progressData = JSON.parse(localStorage.getItem("video-progress") || "{}")
|
const progressData = JSON.parse(localStorage.getItem("video-progress") || "{}")
|
||||||
let progress;
|
let progress;
|
||||||
let percentage = null;
|
let percentage = null;
|
||||||
if(props.type === "movie") {
|
|
||||||
|
if (props.type === "movie") {
|
||||||
progress = progressData?.[props.source]?.movie?.[props.slug]?.full
|
progress = progressData?.[props.source]?.movie?.[props.slug]?.full
|
||||||
if(progress) {
|
|
||||||
|
if (progress) {
|
||||||
percentage = Math.floor((progress.currentlyAt / progress.totalDuration) * 100)
|
percentage = Math.floor((progress.currentlyAt / progress.totalDuration) * 100)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,14 +22,17 @@ export function MovieRow(props) {
|
||||||
return (
|
return (
|
||||||
<div className="movieRow" onClick={() => props.onClick && props.onClick()}>
|
<div className="movieRow" onClick={() => props.onClick && props.onClick()}>
|
||||||
<div className="left">
|
<div className="left">
|
||||||
{props.title}
|
{/* <Cross /> */}
|
||||||
|
{props.title}<span className="seasonEpisodeSubtitle">{props.place ? ` - S${props.place.season}:E${props.place.episode}` : ''}</span>
|
||||||
<span className="year">({props.year})</span>
|
<span className="year">({props.year})</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="watch">
|
<div className="watch">
|
||||||
<p>Watch {props.type}</p>
|
<p>Watch {props.type}</p>
|
||||||
<Arrow/>
|
<Arrow/>
|
||||||
</div>
|
</div>
|
||||||
<PercentageOverlay percentage={percentage} />
|
|
||||||
|
<PercentageOverlay percentage={props.percentage || percentage} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ import './Progress.css'
|
||||||
// failed: boolean
|
// failed: boolean
|
||||||
export function Progress(props) {
|
export function Progress(props) {
|
||||||
return (
|
return (
|
||||||
<div className={`progress ${props.show?'':'hide'} ${props.failed?'failed':''}`}>
|
<div className={`progress ${props.show ? '' : 'hide'} ${props.failed ? 'failed' : ''}`}>
|
||||||
{ props.text && props.text.length > 0 ? (
|
{ props.text && props.text.length > 0 ? (
|
||||||
<p>{props.text}</p>) : null}
|
<p>{props.text}</p>) : null}
|
||||||
<div className="bar">
|
<div className="bar">
|
||||||
|
|
|
@ -14,6 +14,7 @@ export function Title(props) {
|
||||||
|
|
||||||
const accentLink = props.accentLink || "";
|
const accentLink = props.accentLink || "";
|
||||||
const accent = props.accent || "";
|
const accent = props.accent || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{accent.length > 0 ? (
|
{accent.length > 0 ? (
|
||||||
|
@ -26,7 +27,7 @@ export function Title(props) {
|
||||||
{accentLink.length > 0 ? (<Arrow left/>) : null}{accent}
|
{accentLink.length > 0 ? (<Arrow left/>) : null}{accent}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<h1 className={"title " + ( size ? 'title-size-' + size : '' )}>{props.children}</h1>
|
<h1 className={"title " + ( size ? `title-size-${size}` : '' )}>{props.children}</h1>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,17 @@ import './VideoElement.css'
|
||||||
|
|
||||||
// streamUrl: string
|
// streamUrl: string
|
||||||
// loading: boolean
|
// loading: boolean
|
||||||
export function VideoElement({ streamUrl, loading, setProgress }) {
|
// setProgress: (event: NativeEvent) => void
|
||||||
const videoRef = React.useRef(null);
|
// videoRef: useRef
|
||||||
|
// startTime: number
|
||||||
|
export function VideoElement({ streamUrl, loading, setProgress, videoRef, startTime }) {
|
||||||
const [error, setError] = React.useState(false);
|
const [error, setError] = React.useState(false);
|
||||||
|
|
||||||
|
function onLoad() {
|
||||||
|
if (startTime)
|
||||||
|
videoRef.current.currentTime = startTime;
|
||||||
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!streamUrl.endsWith('.mp4')) {
|
if (!streamUrl.endsWith('.mp4')) {
|
||||||
setError(false)
|
setError(false)
|
||||||
|
@ -28,7 +35,7 @@ export function VideoElement({ streamUrl, loading, setProgress }) {
|
||||||
hls.attachMedia(videoRef.current);
|
hls.attachMedia(videoRef.current);
|
||||||
hls.loadSource(streamUrl);
|
hls.loadSource(streamUrl);
|
||||||
}
|
}
|
||||||
}, [videoRef, streamUrl, loading])
|
}, [videoRef, streamUrl, loading]);
|
||||||
|
|
||||||
if (error)
|
if (error)
|
||||||
return (<VideoPlaceholder>Your browser is not supported</VideoPlaceholder>)
|
return (<VideoPlaceholder>Your browser is not supported</VideoPlaceholder>)
|
||||||
|
@ -41,11 +48,11 @@ export function VideoElement({ streamUrl, loading, setProgress }) {
|
||||||
|
|
||||||
if (!streamUrl.endsWith('.mp4')) {
|
if (!streamUrl.endsWith('.mp4')) {
|
||||||
return (
|
return (
|
||||||
<video className="videoElement" ref={videoRef} controls autoPlay onProgress={setProgress} />
|
<video className="videoElement" ref={videoRef} controls autoPlay onProgress={setProgress} onLoadedData={onLoad} />
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<video className="videoElement" ref={videoRef} controls autoPlay onProgress={setProgress}>
|
<video className="videoElement" ref={videoRef} controls autoPlay onProgress={setProgress} onLoadedData={onLoad}>
|
||||||
<source src={streamUrl} type="video/mp4" />
|
<source src={streamUrl} type="video/mp4" />
|
||||||
</video>
|
</video>
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,34 +14,28 @@ export function MovieView(props) {
|
||||||
const baseRouteMatch = useRouteMatch('/:type/:source/:title/:slug');
|
const baseRouteMatch = useRouteMatch('/:type/:source/:title/:slug');
|
||||||
const showRouteMatch = useRouteMatch('/:type/:source/:title/:slug/season/:season/episode/:episode');
|
const showRouteMatch = useRouteMatch('/:type/:source/:title/:slug/season/:season/episode/:episode');
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const { streamUrl, streamData, setStreamUrl } = useMovie();
|
const { streamUrl, streamData, setStreamUrl } = useMovie();
|
||||||
const [seasonList, setSeasonList] = React.useState([]);
|
const [ seasonList, setSeasonList ] = React.useState([]);
|
||||||
const [episodeLists, setEpisodeList] = React.useState([]);
|
const [ episodeLists, setEpisodeList ] = React.useState([]);
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [ loading, setLoading ] = React.useState(false);
|
||||||
const [ selectedSeason, setSelectedSeason ] = React.useState("1");
|
const [ selectedSeason, setSelectedSeason ] = React.useState("1");
|
||||||
|
const [ startTime, setStartTime ] = React.useState(0);
|
||||||
|
const videoRef = React.useRef(null);
|
||||||
|
let isVideoTimeSet = React.useRef(false);
|
||||||
|
|
||||||
const season = showRouteMatch?.params.season || "1";
|
const season = showRouteMatch?.params.season || "1";
|
||||||
const episode = showRouteMatch?.params.episode || "1";
|
const episode = showRouteMatch?.params.episode || "1";
|
||||||
|
|
||||||
React.useEffect(() => {
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
setEpisodeList(streamData.episodes[selectedSeason]);
|
function setEpisode({ season, episode }) {
|
||||||
}, [selectedSeason, streamData.episodes])
|
history.push(`${baseRouteMatch.url}/season/${season}/episode/${episode}`);
|
||||||
|
isVideoTimeSet.current = false;
|
||||||
React.useEffect(() => {
|
}
|
||||||
if (streamData.type === "show") {
|
|
||||||
setSeasonList(streamData.seasons);
|
|
||||||
// TODO load from localstorage last watched
|
|
||||||
setEpisodeList(streamData.episodes[streamData.seasons[0]]);
|
|
||||||
}
|
|
||||||
}, [streamData])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (streamData.type === "show" && !showRouteMatch) history.replace(`${baseRouteMatch.url}/season/1/episode/1`);
|
if (streamData.type === "show" && !showRouteMatch) history.replace(`${baseRouteMatch.url}/season/1/episode/1`);
|
||||||
}, [streamData, showRouteMatch, history, baseRouteMatch.url]);
|
}, [streamData.type, 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(() => {
|
React.useEffect(() => {
|
||||||
if (streamData.type === "show" && showRouteMatch) setSelectedSeason(showRouteMatch.params.season.toString());
|
if (streamData.type === "show" && showRouteMatch) setSelectedSeason(showRouteMatch.params.season.toString());
|
||||||
|
@ -50,15 +44,17 @@ export function MovieView(props) {
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
let cancel = false;
|
let cancel = false;
|
||||||
// ignore if not a show
|
|
||||||
if (streamData.type !== "show") return () => {
|
if (streamData.type !== "show") return () => {
|
||||||
cancel = true;
|
cancel = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!episode) {
|
if (!episode) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setStreamUrl('');
|
setStreamUrl('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
getStreamUrl(streamData.slug, streamData.type, streamData.source, season, episode)
|
getStreamUrl(streamData.slug, streamData.type, streamData.source, season, episode)
|
||||||
|
@ -67,41 +63,54 @@ export function MovieView(props) {
|
||||||
setStreamUrl(url)
|
setStreamUrl(url)
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
if (cancel) return;
|
if (cancel) return;
|
||||||
console.error(e)
|
console.error(e)
|
||||||
})
|
})
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancel = true;
|
cancel = true;
|
||||||
}
|
}
|
||||||
}, [episode, streamData, setStreamUrl, season]);
|
}, [episode, streamData, setStreamUrl, season]);
|
||||||
|
|
||||||
function setEpisode({ season, episode }) {
|
React.useEffect(() => {
|
||||||
history.push(`${baseRouteMatch.url}/season/${season}/episode/${episode}`);
|
if (streamData.type === "show") {
|
||||||
}
|
setSeasonList(streamData.seasons);
|
||||||
|
setEpisodeList(streamData.episodes[selectedSeason]);
|
||||||
|
}
|
||||||
|
}, [streamData.seasons, streamData.episodes, streamData.type, selectedSeason])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
let ls = JSON.parse(localStorage.getItem("video-progress") || "{}")
|
||||||
|
let key = streamData.type === "show" ? `${season}-${episode}` : "full"
|
||||||
|
let time = ls?.[streamData.source]?.[streamData.type]?.[streamData.slug]?.[key]?.currentlyAt;
|
||||||
|
setStartTime(time);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [baseRouteMatch, showRouteMatch]);
|
||||||
|
|
||||||
const setProgress = (evt) => {
|
const setProgress = (evt) => {
|
||||||
let ls = JSON.parse(localStorage.getItem("video-progress") || "{}")
|
let ls = JSON.parse(localStorage.getItem("video-progress") || "{}")
|
||||||
|
|
||||||
// We're just checking lookmovie for now since there is only one scraper
|
if (!ls[streamData.source])
|
||||||
if(!ls[streamData.source]) ls[streamData.source] = {}
|
ls[streamData.source] = {}
|
||||||
if(!ls[streamData.source][streamData.type]) ls[streamData.source][streamData.type] = {}
|
if (!ls[streamData.source][streamData.type])
|
||||||
if(!ls[streamData.source][streamData.type][streamData.slug]) {
|
ls[streamData.source][streamData.type] = {}
|
||||||
|
if (!ls[streamData.source][streamData.type][streamData.slug])
|
||||||
ls[streamData.source][streamData.type][streamData.slug] = {}
|
ls[streamData.source][streamData.type][streamData.slug] = {}
|
||||||
}
|
|
||||||
|
|
||||||
// Store real data
|
// Store real data
|
||||||
let key = streamData.type === "show" ? `${season}-${episode}` : "full"
|
let key = streamData.type === "show" ? `${season}-${episode}` : "full"
|
||||||
ls[streamData.source][streamData.type][streamData.slug][key] = {
|
ls[streamData.source][streamData.type][streamData.slug][key] = {
|
||||||
currentlyAt: Math.floor(evt.currentTarget.currentTime),
|
currentlyAt: Math.floor(evt.currentTarget.currentTime),
|
||||||
totalDuration: Math.floor(evt.currentTarget.duration),
|
totalDuration: Math.floor(evt.currentTarget.duration),
|
||||||
updatedAt: Date.now()
|
updatedAt: Date.now(),
|
||||||
|
meta: streamData
|
||||||
}
|
}
|
||||||
|
|
||||||
if(streamData.type === "show") {
|
if(streamData.type === "show") {
|
||||||
ls[streamData.source][streamData.type][streamData.slug][key].show = {
|
ls[streamData.source][streamData.type][streamData.slug][key].show = {
|
||||||
season,
|
season,
|
||||||
episode: episode
|
episode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,7 +130,9 @@ export function MovieView(props) {
|
||||||
{streamData.type === "show" ? <Title size="small">
|
{streamData.type === "show" ? <Title size="small">
|
||||||
Season {season}: Episode {episode}
|
Season {season}: Episode {episode}
|
||||||
</Title> : undefined}
|
</Title> : undefined}
|
||||||
<VideoElement streamUrl={streamUrl} loading={loading} setProgress={setProgress} />
|
|
||||||
|
<VideoElement streamUrl={streamUrl} loading={loading} setProgress={setProgress} videoRef={videoRef} startTime={startTime} />
|
||||||
|
|
||||||
{streamData.type === "show" ?
|
{streamData.type === "show" ?
|
||||||
<EpisodeSelector
|
<EpisodeSelector
|
||||||
setSelectedSeason={setSelectedSeason}
|
setSelectedSeason={setSelectedSeason}
|
||||||
|
@ -135,7 +146,7 @@ export function MovieView(props) {
|
||||||
currentSeason={season}
|
currentSeason={season}
|
||||||
currentEpisode={episode}
|
currentEpisode={episode}
|
||||||
|
|
||||||
source={streamData.source}
|
streamData={streamData}
|
||||||
/>
|
/>
|
||||||
: ''}
|
: ''}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import { Title } from '../components/Title'
|
|
||||||
import { Card } from '../components/Card'
|
|
||||||
|
|
||||||
export function NotFound(props) {
|
|
||||||
return (
|
|
||||||
<div className="cardView">
|
|
||||||
<Card>
|
|
||||||
<Title accent="How did you end up here?">
|
|
||||||
Oopsie doopsie
|
|
||||||
</Title>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -8,11 +8,30 @@
|
||||||
box-sizing: border-box;
|
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(--card);
|
||||||
|
color: var(--button-text);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cardView > * {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardView > div:first-child {
|
.cardView > *:first-child {
|
||||||
margin-top: 38px;
|
margin-top: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Redirect, useRouteMatch, useHistory } from 'react-router-dom';
|
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { InputBox } from '../components/InputBox';
|
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom';
|
||||||
import { Title } from '../components/Title';
|
import { Arrow } from '../components/Arrow';
|
||||||
import { Card } from '../components/Card';
|
import { Card } from '../components/Card';
|
||||||
import { ErrorBanner } from '../components/ErrorBanner';
|
import { ErrorBanner } from '../components/ErrorBanner';
|
||||||
|
import { InputBox } from '../components/InputBox';
|
||||||
import { MovieRow } from '../components/MovieRow';
|
import { MovieRow } from '../components/MovieRow';
|
||||||
import { Arrow } from '../components/Arrow';
|
|
||||||
import { Progress } from '../components/Progress';
|
import { Progress } from '../components/Progress';
|
||||||
import { findContent, getStreamUrl, getEpisodes } from '../lib/index';
|
import { Title } from '../components/Title';
|
||||||
import { useMovie } from '../hooks/useMovie';
|
|
||||||
import { TypeSelector } from '../components/TypeSelector';
|
import { TypeSelector } from '../components/TypeSelector';
|
||||||
|
import { useMovie } from '../hooks/useMovie';
|
||||||
|
import { findContent, getEpisodes, getStreamUrl } from '../lib/index';
|
||||||
|
|
||||||
import './Search.css';
|
import './Search.css';
|
||||||
import { DiscordBanner } from '../components/DiscordBanner';
|
|
||||||
|
|
||||||
export function SearchView() {
|
export function SearchView() {
|
||||||
const { navigate, setStreamUrl, setStreamData } = useMovie();
|
const { navigate, setStreamUrl, setStreamData } = useMovie();
|
||||||
|
@ -30,6 +29,8 @@ export function SearchView() {
|
||||||
const [failed, setFailed] = React.useState(false);
|
const [failed, setFailed] = React.useState(false);
|
||||||
const [showingOptions, setShowingOptions] = React.useState(false);
|
const [showingOptions, setShowingOptions] = React.useState(false);
|
||||||
const [errorStatus, setErrorStatus] = React.useState(false);
|
const [errorStatus, setErrorStatus] = React.useState(false);
|
||||||
|
const [page, setPage] = React.useState('search');
|
||||||
|
const [continueWatching, setContinueWatching] = React.useState([])
|
||||||
|
|
||||||
const fail = (str) => {
|
const fail = (str) => {
|
||||||
setProgress(maxSteps);
|
setProgress(maxSteps);
|
||||||
|
@ -37,7 +38,7 @@ export function SearchView() {
|
||||||
setFailed(true)
|
setFailed(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getStream(title, slug, type, source) {
|
async function getStream(title, slug, type, source, year) {
|
||||||
setStreamUrl("");
|
setStreamUrl("");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -54,7 +55,6 @@ export function SearchView() {
|
||||||
|
|
||||||
let realUrl = '';
|
let realUrl = '';
|
||||||
if (type === "movie") {
|
if (type === "movie") {
|
||||||
// getStreamUrl(slug, type, source, season, episode)
|
|
||||||
const { url } = await getStreamUrl(slug, type, source);
|
const { url } = await getStreamUrl(slug, type, source);
|
||||||
|
|
||||||
if (url === '') {
|
if (url === '') {
|
||||||
|
@ -71,7 +71,8 @@ export function SearchView() {
|
||||||
seasons,
|
seasons,
|
||||||
episodes,
|
episodes,
|
||||||
slug,
|
slug,
|
||||||
source
|
source,
|
||||||
|
year
|
||||||
})
|
})
|
||||||
setText(`Streaming...`)
|
setText(`Streaming...`)
|
||||||
navigate("movie")
|
navigate("movie")
|
||||||
|
@ -100,9 +101,9 @@ export function SearchView() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, slug, type, source } = options[0];
|
const { title, slug, type, source, year } = options[0];
|
||||||
history.push(`${routeMatch.url}/${source}/${title}/${slug}`);
|
history.push(`${routeMatch.url}/${source}/${title}/${slug}`);
|
||||||
getStream(title, slug, type, source);
|
getStream(title, slug, type, source, year);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
fail(`Failed to watch ${contentType}`)
|
fail(`Failed to watch ${contentType}`)
|
||||||
|
@ -128,55 +129,156 @@ export function SearchView() {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!type || (type !== 'movie' && type !== 'show')) return <Redirect to="/movie" />
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// due to a constraint with incompatible localStorage data,
|
||||||
|
// we must quit here if episode and season data is not included
|
||||||
|
// in the show's data. watching the show will resolve.
|
||||||
|
if (!subselection.meta) continue;
|
||||||
|
|
||||||
|
if (entry.percentageDone < 90) {
|
||||||
|
newContinueWatching.push(entry)
|
||||||
|
// begin next episode logic
|
||||||
|
} else {
|
||||||
|
// we can't do next episode for movies!
|
||||||
|
if (!subselection.show) continue;
|
||||||
|
|
||||||
|
let newShow = {};
|
||||||
|
|
||||||
|
// if the current season has a next episode, load it
|
||||||
|
if (subselection.meta.episodes[subselection.show.season].includes(`${parseInt(subselection.show.episode) + 1}`)) {
|
||||||
|
newShow.season = subselection.show.season;
|
||||||
|
newShow.episode = `${parseInt(subselection.show.episode) + 1}`;
|
||||||
|
entry.percentageDone = 0;
|
||||||
|
// if the current season does not have a next epsiode, and the next season has a first episode, load that
|
||||||
|
} else if (subselection.meta.episodes[`${parseInt(subselection.show.season) + 1}`][0]) {
|
||||||
|
newShow.season = `${parseInt(subselection.show.season) + 1}`;
|
||||||
|
newShow.episode = subselection.meta.episodes[`${parseInt(subselection.show.season) + 1}`][0];
|
||||||
|
entry.percentageDone = 0;
|
||||||
|
// the next episode does not exist
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// assign the new episode and season data
|
||||||
|
entry.data.show = { ...newShow };
|
||||||
|
|
||||||
|
// if the next episode exists, continue. we don't want to end up with duplicate data.
|
||||||
|
let nextEpisode = progressData?.[source]?.show?.[slug]?.[`${entry.data.show.season}-${entry.data.show.episode}`];
|
||||||
|
if (nextEpisode) continue;
|
||||||
|
|
||||||
|
newContinueWatching.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newContinueWatching = newContinueWatching.sort((a, b) => {
|
||||||
|
return b.data.updatedAt - a.data.updatedAt
|
||||||
|
});
|
||||||
|
|
||||||
|
setContinueWatching(newContinueWatching)
|
||||||
|
})
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!type || (type !== 'movie' && type !== 'show')) {
|
||||||
|
return <Redirect to="/movie" />
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="cardView">
|
<div className="cardView">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{type === 'movie' ? 'Movies' : 'TV Shows'} | movie-web</title>
|
<title>{type === 'movie' ? 'movies' : 'shows'} | movie-web</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<Card>
|
{/* Nav */}
|
||||||
<DiscordBanner />
|
<nav>
|
||||||
{errorStatus ? <ErrorBanner>{errorStatus}</ErrorBanner> : ''}
|
<a className={page === 'search' ? 'selected-link' : ''} onClick={() => setPage('search')} href>Search</a>
|
||||||
<Title accent="Because watching content legally is boring">
|
{continueWatching.length > 0 ?
|
||||||
What do you wanna watch?
|
<a className={page === 'watching' ? 'selected-link' : ''} onClick={() => setPage('watching')} href>Continue watching</a>
|
||||||
</Title>
|
: ''}
|
||||||
<TypeSelector
|
</nav>
|
||||||
setType={(type) => history.push(`/${type}`)}
|
|
||||||
choices={[
|
{/* Search */}
|
||||||
{ label: "Movie", value: "movie" },
|
{page === 'search' ?
|
||||||
{ label: "TV Show", value: "show" }
|
<React.Fragment>
|
||||||
]}
|
<Card>
|
||||||
noWrap={true}
|
{errorStatus ? <ErrorBanner>{errorStatus}</ErrorBanner> : ''}
|
||||||
selected={type}
|
<Title accent="Because watching content legally is boring">
|
||||||
/>
|
What do you wanna watch?
|
||||||
<InputBox placeholder={type === "movie" ? "Hamilton" : "Atypical"} onSubmit={(str) => searchMovie(str, type)} />
|
</Title>
|
||||||
<Progress show={progress > 0} failed={failed} progress={progress} steps={maxSteps} text={text} />
|
<TypeSelector
|
||||||
</Card>
|
setType={(type) => history.push(`/${type}`)}
|
||||||
|
choices={[
|
||||||
|
{ label: "Movie", value: "movie" },
|
||||||
|
{ label: "TV Show", value: "show" }
|
||||||
|
]}
|
||||||
|
noWrap={true}
|
||||||
|
selected={type}
|
||||||
|
/>
|
||||||
|
<InputBox placeholder={type === "movie" ? "Hamilton" : "Atypical"} onSubmit={(str) => searchMovie(str, type)} />
|
||||||
|
<Progress show={progress > 0} failed={failed} progress={progress} steps={maxSteps} text={text} />
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card show={showingOptions} doTransition>
|
||||||
|
<Title size="medium">
|
||||||
|
Whoops, there are a few {type}s like that
|
||||||
|
</Title>
|
||||||
|
{Object.entries(options.reduce((a, v) => {
|
||||||
|
if (!a[v.source]) a[v.source] = []
|
||||||
|
a[v.source].push(v)
|
||||||
|
return a;
|
||||||
|
}, {})).map(v => (
|
||||||
|
<div key={v[0]}>
|
||||||
|
<p className="source">{v[0]}</p>
|
||||||
|
{v[1].map((v, i) => (
|
||||||
|
<MovieRow key={i} title={v.title} slug={v.slug} type={v.type} year={v.year} source={v.source} onClick={() => {
|
||||||
|
history.push(`${routeMatch.url}/${v.source}/${v.title}/${v.slug}`);
|
||||||
|
setShowingOptions(false)
|
||||||
|
getStream(v.title, v.slug, v.type, v.source, v.year)
|
||||||
|
}} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Card>
|
||||||
|
</React.Fragment> : <React.Fragment />}
|
||||||
|
|
||||||
|
{/* Continue watching */}
|
||||||
|
{continueWatching.length > 0 && page === 'watching' ? <Card>
|
||||||
|
<Title>Continue watching</Title>
|
||||||
|
{continueWatching?.map((v, i) => (
|
||||||
|
// <div>
|
||||||
|
<MovieRow key={i} title={v.data.meta.title} slug={v.data.meta.slug} type={v.type} year={v.data.meta.year} source={v.source} place={v.data.show} percentage={v.percentageDone} deletable onClick={() => {
|
||||||
|
if (v.type === 'show') {
|
||||||
|
history.push(`${routeMatch.url}/${v.source}/${v.data.meta.title}/${v.slug}/season/${v.data.show.season}/episode/${v.data.show.episode}`)
|
||||||
|
} else {
|
||||||
|
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.data.meta.year)
|
||||||
|
}} />
|
||||||
|
// </div>
|
||||||
|
))}
|
||||||
|
</Card> : <React.Fragment></React.Fragment>}
|
||||||
|
|
||||||
<Card show={showingOptions} doTransition>
|
|
||||||
<Title size="medium">
|
|
||||||
Whoops, there are a few {type}s like that
|
|
||||||
</Title>
|
|
||||||
{ Object.entries(options.reduce((a, v) => {
|
|
||||||
if (!a[v.source]) a[v.source] = []
|
|
||||||
a[v.source].push(v)
|
|
||||||
return a;
|
|
||||||
}, {})).map(v => (
|
|
||||||
<div key={v[0]}>
|
|
||||||
<p className="source">{v[0]}</p>
|
|
||||||
{v[1].map((v, i) => (
|
|
||||||
<MovieRow key={i} title={v.title} slug={v.slug} type={v.type} year={v.year} source={v.source} onClick={() => {
|
|
||||||
history.push(`${routeMatch.url}/${v.source}/${v.title}/${v.slug}`);
|
|
||||||
setShowingOptions(false)
|
|
||||||
getStream(v.title, v.slug, v.type, v.source)
|
|
||||||
}} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Card>
|
|
||||||
<div className="topRightCredits">
|
<div className="topRightCredits">
|
||||||
<a href="https://github.com/JamesHawkinss/movie-web" target="_blank" rel="noreferrer">Check it out on GitHub <Arrow /></a>
|
<a href="https://github.com/JamesHawkinss/movie-web" target="_blank" rel="noreferrer">Check it out on GitHub <Arrow /></a>
|
||||||
<br />
|
<br />
|
||||||
|
|
Loading…
Reference in a new issue