50 changed files with 0 additions and 2923 deletions
@ -1 +0,0 @@ |
|||||||
REACT_APP_CORS_PROXY_URL=https://proxy-1.movie-web.workers.dev/?destination= |
|
@ -1,19 +0,0 @@ |
|||||||
import { SearchView } from './views/Search'; |
|
||||||
import { MovieView } from './views/Movie'; |
|
||||||
import { useMovie, MovieProvider } from './hooks/useMovie'; |
|
||||||
import './index.css'; |
|
||||||
|
|
||||||
function Router() { |
|
||||||
const { streamData } = useMovie(); |
|
||||||
return streamData ? <MovieView /> : <SearchView />; |
|
||||||
} |
|
||||||
|
|
||||||
function App() { |
|
||||||
return ( |
|
||||||
<MovieProvider> |
|
||||||
<Router /> |
|
||||||
</MovieProvider> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
export default App; |
|
@ -1,7 +0,0 @@ |
|||||||
.feather.left { |
|
||||||
transform: rotate(180deg); |
|
||||||
} |
|
||||||
|
|
||||||
.arrow { |
|
||||||
display: inline-block; |
|
||||||
} |
|
@ -1,15 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
import './Arrow.css' |
|
||||||
|
|
||||||
// left?: boolean
|
|
||||||
export function Arrow(props) { |
|
||||||
return ( |
|
||||||
<span className="arrow" dangerouslySetInnerHTML={{ __html: ` |
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather ${props.left?'left':''}"}> |
|
||||||
<line x1="5" y1="12" x2="19" y2="12"></line> |
|
||||||
<polyline points="12 5 19 12 12 19"></polyline> |
|
||||||
</svg> |
|
||||||
`}}>
|
|
||||||
</span> |
|
||||||
) |
|
||||||
} |
|
@ -1,31 +0,0 @@ |
|||||||
.card { |
|
||||||
background-color: var(--card); |
|
||||||
padding: 3rem 4rem; |
|
||||||
margin: 0 3rem; |
|
||||||
margin-bottom: 6rem; |
|
||||||
border-radius: 10px; |
|
||||||
box-sizing: border-box; |
|
||||||
transition: height 500ms ease-in-out; |
|
||||||
} |
|
||||||
|
|
||||||
.card-wrapper.full { |
|
||||||
width: 81rem; |
|
||||||
} |
|
||||||
|
|
||||||
.card-wrapper { |
|
||||||
transition: height 500ms ease-in-out; |
|
||||||
width: 45rem; |
|
||||||
max-width: 100%; |
|
||||||
} |
|
||||||
|
|
||||||
.card-wrapper.overflow-hidden { |
|
||||||
overflow: hidden; |
|
||||||
} |
|
||||||
|
|
||||||
@media screen and (max-width: 700px) { |
|
||||||
.card { |
|
||||||
margin: 0; |
|
||||||
margin-bottom: 6rem; |
|
||||||
padding: 3rem 2rem; |
|
||||||
} |
|
||||||
} |
|
@ -1,28 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
import './Card.css' |
|
||||||
|
|
||||||
// fullWidth: boolean
|
|
||||||
// show: boolean
|
|
||||||
// doTransition: boolean
|
|
||||||
export function Card(props) { |
|
||||||
|
|
||||||
const [showing, setShowing] = React.useState(false); |
|
||||||
const measureRef = React.useRef(null) |
|
||||||
const [height, setHeight] = React.useState(0); |
|
||||||
|
|
||||||
React.useEffect(() => { |
|
||||||
if (!measureRef?.current) return; |
|
||||||
setShowing(props.show); |
|
||||||
setHeight(measureRef.current.clientHeight) |
|
||||||
}, [props.show, measureRef]) |
|
||||||
|
|
||||||
return ( |
|
||||||
<div className={`card-wrapper ${ props.fullWidth ? 'full' : '' } ${ props.doTransition ? 'overflow-hidden' : '' }`} style={{ |
|
||||||
height: props.doTransition ? (showing ? height : 0) : "initial", |
|
||||||
}}> |
|
||||||
<div className={`card ${ showing ? 'show' : '' } ${ props.doTransition ? 'doTransition' : '' }`} ref={measureRef}> |
|
||||||
{props.children} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,3 +0,0 @@ |
|||||||
.episodeSelector { |
|
||||||
margin-top: 20px; |
|
||||||
} |
|
@ -1,50 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { TypeSelector } from './TypeSelector'; |
|
||||||
import { NumberSelector } from './NumberSelector'; |
|
||||||
import { VideoProgressStore } from '../lib/storage/VideoProgress' |
|
||||||
import { SelectBox } from '../components/SelectBox'; |
|
||||||
import './EpisodeSelector.css' |
|
||||||
import { useWindowSize } from '../hooks/useWindowSize'; |
|
||||||
|
|
||||||
export function EpisodeSelector({ setSelectedSeason, selectedSeason, setEpisode, seasons, episodes, currentSeason, currentEpisode, streamData }) { |
|
||||||
const choices = episodes ? episodes.map(v => { |
|
||||||
const progressData = VideoProgressStore.get(); |
|
||||||
|
|
||||||
let currentlyAt = 0; |
|
||||||
let totalDuration = 0; |
|
||||||
|
|
||||||
const progress = progressData?.[streamData.source]?.[streamData.type]?.[streamData.slug]?.[`${selectedSeason}-${v}`] |
|
||||||
|
|
||||||
if (progress) { |
|
||||||
currentlyAt = progress.currentlyAt |
|
||||||
totalDuration = progress.totalDuration |
|
||||||
} |
|
||||||
|
|
||||||
const percentage = Math.round((currentlyAt / totalDuration) * 100) |
|
||||||
|
|
||||||
return { |
|
||||||
value: v.toString(), |
|
||||||
label: v, |
|
||||||
percentage |
|
||||||
} |
|
||||||
}) : []; |
|
||||||
|
|
||||||
const windowSize = useWindowSize() |
|
||||||
|
|
||||||
return ( |
|
||||||
<div className="episodeSelector"> |
|
||||||
{ |
|
||||||
(seasons.length > 0 && (windowSize.width <= 768 || seasons.length > 4)) ? |
|
||||||
( |
|
||||||
<SelectBox setSelectedItem={(index) => setSelectedSeason(seasons[index])} selectedItem={seasons.findIndex(s => s === selectedSeason)} options={seasons.map(season => { return {id: season, name: `Season ${season}` }})}/> |
|
||||||
) |
|
||||||
: |
|
||||||
( |
|
||||||
<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} /> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,11 +0,0 @@ |
|||||||
.errorBanner { |
|
||||||
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); |
|
||||||
} |
|
@ -1,10 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import './ErrorBanner.css'; |
|
||||||
|
|
||||||
export function ErrorBanner({children}) { |
|
||||||
return ( |
|
||||||
<div className="errorBanner"> |
|
||||||
{children} |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,95 +0,0 @@ |
|||||||
.inputBar { |
|
||||||
width: 100%; |
|
||||||
display: flex; |
|
||||||
height: 3rem; |
|
||||||
} |
|
||||||
|
|
||||||
.inputBar > *:first-child{ |
|
||||||
border-radius: 0 !important; |
|
||||||
border-top-left-radius: 10px !important; |
|
||||||
border-bottom-left-radius: 10px !important; |
|
||||||
} |
|
||||||
|
|
||||||
.inputBar > *:last-child { |
|
||||||
border-radius: 0 !important; |
|
||||||
border-top-right-radius: 10px !important; |
|
||||||
border-bottom-right-radius: 10px !important; |
|
||||||
} |
|
||||||
|
|
||||||
.inputTextBox { |
|
||||||
border-width: 0; |
|
||||||
outline: none; |
|
||||||
background-color: var(--content); |
|
||||||
color: var(--text); |
|
||||||
padding: .7rem 1.5rem; |
|
||||||
height: auto; |
|
||||||
flex: 1; |
|
||||||
box-sizing: border-box; |
|
||||||
} |
|
||||||
|
|
||||||
.inputSearchButton { |
|
||||||
background-color: var(--button); |
|
||||||
border-width: 0; |
|
||||||
color: var(--button-text, var(--text)); |
|
||||||
padding: .5rem 2.1rem; |
|
||||||
|
|
||||||
font-weight: bold; |
|
||||||
cursor: pointer; |
|
||||||
} |
|
||||||
|
|
||||||
.inputSearchButton:hover { |
|
||||||
background-color: var(--button-hover); |
|
||||||
} |
|
||||||
|
|
||||||
.inputTextBox:hover { |
|
||||||
background-color: var(--content-hover); |
|
||||||
} |
|
||||||
|
|
||||||
.inputSearchButton .text > .arrow { |
|
||||||
opacity: 0; |
|
||||||
transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out; |
|
||||||
position: absolute; |
|
||||||
right: -0.8rem; |
|
||||||
bottom: -0.2rem; |
|
||||||
} |
|
||||||
|
|
||||||
.inputSearchButton .text { |
|
||||||
display: flex; |
|
||||||
position: relative; |
|
||||||
transition: transform 0.2s ease-in-out; |
|
||||||
} |
|
||||||
|
|
||||||
.inputSearchButton:hover .text > .arrow { |
|
||||||
transform: translateX(8px); |
|
||||||
opacity: 1; |
|
||||||
} |
|
||||||
|
|
||||||
.inputSearchButton:hover .text { |
|
||||||
transform: translateX(-10px); |
|
||||||
} |
|
||||||
|
|
||||||
.inputSearchButton:active { |
|
||||||
background-color: var(--button-active); |
|
||||||
} |
|
||||||
|
|
||||||
@media screen and (max-width: 700px) { |
|
||||||
.inputBar { |
|
||||||
flex-direction: column; |
|
||||||
align-items: flex-start; |
|
||||||
height: auto; |
|
||||||
} |
|
||||||
|
|
||||||
.inputBar > *:nth-child(n) { |
|
||||||
border-radius: 10px !important; |
|
||||||
} |
|
||||||
|
|
||||||
.inputSearchButton { |
|
||||||
margin-top: .5rem; |
|
||||||
align-self: center; |
|
||||||
} |
|
||||||
|
|
||||||
.inputTextBox { |
|
||||||
margin-top: .5rem; |
|
||||||
width: 100%; |
|
||||||
} |
|
||||||
} |
|
@ -1,29 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { Arrow } from './Arrow'; |
|
||||||
import './InputBox.css' |
|
||||||
|
|
||||||
// props = { onSubmit: (str) => {}, placeholder: string}
|
|
||||||
export function InputBox({ onSubmit, placeholder }) { |
|
||||||
const [searchTerm, setSearchTerm] = React.useState(""); |
|
||||||
|
|
||||||
return ( |
|
||||||
<form className="inputBar" onSubmit={(e) => { |
|
||||||
e.preventDefault(); |
|
||||||
onSubmit(searchTerm) |
|
||||||
return false; |
|
||||||
}}> |
|
||||||
<input |
|
||||||
type='text' |
|
||||||
className="inputTextBox" |
|
||||||
id="inputTextBox" |
|
||||||
placeholder={placeholder} |
|
||||||
value={searchTerm} |
|
||||||
onChange={(e) => setSearchTerm(e.target.value)} |
|
||||||
required |
|
||||||
/> |
|
||||||
<button className="inputSearchButton"> |
|
||||||
<span className="text">Search<span className="arrow"><Arrow /></span></span> |
|
||||||
</button> |
|
||||||
</form> |
|
||||||
) |
|
||||||
} |
|
@ -1,97 +0,0 @@ |
|||||||
.movieRow { |
|
||||||
position: relative; |
|
||||||
display: flex; |
|
||||||
border-radius: 5px; |
|
||||||
background-color: var(--content); |
|
||||||
color: var(--text); |
|
||||||
padding: .8rem 1.5rem; |
|
||||||
margin-top: .5rem; |
|
||||||
cursor: pointer; |
|
||||||
transition: transform 50ms ease-in-out; |
|
||||||
user-select: none; |
|
||||||
overflow: hidden; |
|
||||||
} |
|
||||||
|
|
||||||
.movieRow p { |
|
||||||
margin: 0; |
|
||||||
} |
|
||||||
|
|
||||||
.movieRow .left { |
|
||||||
flex: 1; |
|
||||||
display: flex; |
|
||||||
flex-flow: row wrap; |
|
||||||
align-items: flex-start; |
|
||||||
margin-right: 0.5rem; |
|
||||||
} |
|
||||||
|
|
||||||
.movieRow .left .titleWrapper { |
|
||||||
height: 100%; |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
justify-content: center; |
|
||||||
} |
|
||||||
|
|
||||||
.movieRow .left .seasonEpisodeSubtitle, |
|
||||||
.movieRow .left .year { |
|
||||||
color: var(--text-secondary); |
|
||||||
} |
|
||||||
|
|
||||||
.movieRow .watch { |
|
||||||
color: var(--theme-color-text); |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
} |
|
||||||
|
|
||||||
.movieRow .watch .arrow { |
|
||||||
margin-left: .5rem; |
|
||||||
transition: transform 50ms ease-in-out; |
|
||||||
transform: translateY(.1rem); |
|
||||||
} |
|
||||||
|
|
||||||
.movieRow:active { |
|
||||||
transform: scale(1.02); |
|
||||||
} |
|
||||||
|
|
||||||
.movieRow:hover { |
|
||||||
background-color: var(--content-hover); |
|
||||||
} |
|
||||||
|
|
||||||
.movieRow:hover .watch .arrow { |
|
||||||
transform: translateX(.3rem) translateY(.1rem); |
|
||||||
} |
|
||||||
|
|
||||||
.movieRow:focus-visible { |
|
||||||
border: 1px solid #fff; |
|
||||||
background-color: var(--content-hover); |
|
||||||
} |
|
||||||
|
|
||||||
.movieRow:focus-visible .watch .arrow { |
|
||||||
transform: translateX(.3rem) translateY(.1rem); |
|
||||||
} |
|
||||||
|
|
||||||
.attribute { |
|
||||||
color: var(--text); |
|
||||||
background-color: var(--theme-color); |
|
||||||
font-size: .75rem; |
|
||||||
padding: .25rem; |
|
||||||
border-radius: 10px; |
|
||||||
margin-right: 10px; |
|
||||||
} |
|
||||||
|
|
||||||
.subtitleIcon { |
|
||||||
width: 30px; |
|
||||||
display: flex; |
|
||||||
justify-content: center; |
|
||||||
align-items: center; |
|
||||||
margin-right: 10px; |
|
||||||
} |
|
||||||
|
|
||||||
@media screen and (max-width: 400px) { |
|
||||||
.movieRow { |
|
||||||
flex-direction: column; |
|
||||||
} |
|
||||||
|
|
||||||
.movieRow .watch { |
|
||||||
margin-top: .5rem; |
|
||||||
} |
|
||||||
} |
|
@ -1,59 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
import { Arrow } from './Arrow' |
|
||||||
import { PercentageOverlay } from './PercentageOverlay' |
|
||||||
import { VideoProgressStore } from '../lib/storage/VideoProgress' |
|
||||||
import './MovieRow.css' |
|
||||||
|
|
||||||
// title: string
|
|
||||||
// onClick: () => void
|
|
||||||
export function MovieRow(props) { |
|
||||||
const progressData = VideoProgressStore.get(); |
|
||||||
let progress; |
|
||||||
let percentage = null; |
|
||||||
|
|
||||||
if (props.type === "movie") { |
|
||||||
progress = progressData?.[props.source]?.movie?.[props.slug]?.full |
|
||||||
|
|
||||||
if (progress) { |
|
||||||
percentage = Math.floor((progress.currentlyAt / progress.totalDuration) * 100) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function handleKeyPress(event){ |
|
||||||
if ((event.code === 'Enter' || event.code === 'Space') && props.onClick){ |
|
||||||
props.onClick(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<div className="movieRow" tabIndex={0} onKeyPress={handleKeyPress} onClick={() => props.onClick && props.onClick()}> |
|
||||||
|
|
||||||
{ (props.source === "lookmovie" || props.source === "xemovie") && ( |
|
||||||
<div className="subtitleIcon"> |
|
||||||
<svg id="subtitleIcon" width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> |
|
||||||
<path d="M20 4H4C2.897 4 2 4.897 2 6V18C2 19.103 2.897 20 4 20H20C21.103 20 22 19.103 22 18V6C22 4.897 21.103 4 20 4ZM11 10H8V14H11V16H8C6.897 16 6 15.103 6 14V10C6 8.897 6.897 8 8 8H11V10ZM18 10H15V14H18V16H15C13.897 16 13 15.103 13 14V10C13 8.897 13.897 8 15 8H18V10Z" fill="#EEEEEE"/> |
|
||||||
</svg> |
|
||||||
</div> |
|
||||||
) } |
|
||||||
|
|
||||||
<div className="left"> |
|
||||||
{/* <Cross /> */} |
|
||||||
<div className="titleWrapper"> |
|
||||||
<div className="titleText"> |
|
||||||
{props.title} |
|
||||||
|
|
||||||
<span className="year">({props.year})</span> |
|
||||||
<span className="seasonEpisodeSubtitle">{props.place ? ` - S${props.place.season}:E${props.place.episode}` : ''}</span> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
|
|
||||||
<div className="watch"> |
|
||||||
<p>Watch {props.type}</p> |
|
||||||
<Arrow/> |
|
||||||
</div> |
|
||||||
|
|
||||||
<PercentageOverlay percentage={props.percentage || percentage} /> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,55 +0,0 @@ |
|||||||
.numberSelector { |
|
||||||
display: grid; |
|
||||||
grid-template-columns: repeat(auto-fill, minmax(2.5rem, 1fr)); |
|
||||||
gap: 5px; |
|
||||||
position: relative; |
|
||||||
margin-bottom: 1.5rem; |
|
||||||
} |
|
||||||
|
|
||||||
.numberSelector .choiceWrapper { |
|
||||||
position: relative; |
|
||||||
border-radius: 10%; |
|
||||||
overflow: hidden; |
|
||||||
} |
|
||||||
|
|
||||||
.numberSelector .choiceWrapper::before { |
|
||||||
content: ''; |
|
||||||
display: block; |
|
||||||
width: 100%; |
|
||||||
padding-bottom: 100%; |
|
||||||
} |
|
||||||
|
|
||||||
.numberSelector .choice { |
|
||||||
position: absolute; |
|
||||||
top: 0; |
|
||||||
left: 0; |
|
||||||
width: 100%; |
|
||||||
height: 100%; |
|
||||||
background-color: var(--choice); |
|
||||||
margin-right: 5px; |
|
||||||
padding: .2rem; |
|
||||||
display: flex; |
|
||||||
justify-content: center; |
|
||||||
align-items: center; |
|
||||||
text-align: center; |
|
||||||
color: var(--text); |
|
||||||
font-weight: bold; |
|
||||||
cursor: pointer; |
|
||||||
user-select: none; |
|
||||||
box-sizing: border-box; |
|
||||||
} |
|
||||||
|
|
||||||
.numberSelector .choice:hover, |
|
||||||
.numberSelector .choiceWrapper:focus-visible .choice { |
|
||||||
background-color: var(--choice-hover); |
|
||||||
} |
|
||||||
|
|
||||||
.numberSelector .choiceWrapper:focus-visible { |
|
||||||
border: 1px solid #fff; |
|
||||||
} |
|
||||||
|
|
||||||
.numberSelector .choice.selected { |
|
||||||
color: var(--choice-active-text, var(--text)); |
|
||||||
background-color: var(--choice-active); |
|
||||||
} |
|
||||||
|
|
@ -1,27 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
// import { Arrow } from './Arrow';
|
|
||||||
import './NumberSelector.css' |
|
||||||
import { PercentageOverlay } from './PercentageOverlay'; |
|
||||||
|
|
||||||
// setType: (txt: string) => void
|
|
||||||
// choices: { label: string, value: string }[]
|
|
||||||
// selected: string
|
|
||||||
export function NumberSelector({ setType, choices, selected }) { |
|
||||||
const handleKeyPress = choice => event => { |
|
||||||
if (event.code === 'Space' || event.code === 'Enter'){ |
|
||||||
setType(choice); |
|
||||||
} |
|
||||||
} |
|
||||||
return ( |
|
||||||
<div className="numberSelector"> |
|
||||||
{choices.map(v=>( |
|
||||||
<div key={v.value} className="choiceWrapper" tabIndex={0} onKeyPress={handleKeyPress(v.value)}> |
|
||||||
<div className={`choice ${selected&&selected===v.value?'selected':''}`} onClick={() => setType(v.value)}> |
|
||||||
{v.label} |
|
||||||
<PercentageOverlay percentage={v.percentage} /> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,12 +0,0 @@ |
|||||||
.progressBar { |
|
||||||
position: absolute; |
|
||||||
bottom: 0; |
|
||||||
left: 0; |
|
||||||
width: 100%; |
|
||||||
height: 100%; |
|
||||||
opacity: 0.2; |
|
||||||
} |
|
||||||
.progressBarInner { |
|
||||||
background: var(--theme-color); |
|
||||||
height: 100%; |
|
||||||
} |
|
@ -1,13 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
import './PercentageOverlay.css' |
|
||||||
|
|
||||||
export function PercentageOverlay({ percentage }) { |
|
||||||
|
|
||||||
if(percentage && percentage > 3) percentage = Math.max(20, percentage < 90 ? percentage : 100) |
|
||||||
|
|
||||||
return percentage > 0 ? ( |
|
||||||
<div className="progressBar"> |
|
||||||
<div className="progressBarInner" style={{width: `${percentage}%`}}></div> |
|
||||||
</div> |
|
||||||
) : <React.Fragment></React.Fragment> |
|
||||||
} |
|
@ -1,43 +0,0 @@ |
|||||||
.progress { |
|
||||||
text-align: center; |
|
||||||
color: var(--text-secondary); |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
justify-content: center; |
|
||||||
flex-direction: column; |
|
||||||
height: 5rem; |
|
||||||
margin-top: 1rem; |
|
||||||
transition: height 800ms ease-in-out, opacity 800ms ease-in-out; |
|
||||||
opacity: 1; |
|
||||||
} |
|
||||||
|
|
||||||
.progress.hide { |
|
||||||
opacity: 0; |
|
||||||
height: 0rem; |
|
||||||
} |
|
||||||
|
|
||||||
.progress p { |
|
||||||
margin: 0; |
|
||||||
margin-bottom: 1rem; |
|
||||||
} |
|
||||||
|
|
||||||
.progress .bar { |
|
||||||
width: 13rem; |
|
||||||
max-width: 100%; |
|
||||||
background-color: var(--content); |
|
||||||
border-radius: 10px; |
|
||||||
height: 7px; |
|
||||||
display: inline-block; |
|
||||||
} |
|
||||||
|
|
||||||
.progress .bar .bar-inner { |
|
||||||
transition: width 400ms ease-in-out, background-color 100ms ease-in-out; |
|
||||||
background-color: var(--theme-color); |
|
||||||
border-radius: 10px; |
|
||||||
height: 100%; |
|
||||||
width: 0%; |
|
||||||
} |
|
||||||
|
|
||||||
.progress.failed .bar .bar-inner { |
|
||||||
background-color: var(--failed); |
|
||||||
} |
|
@ -1,21 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
import './Progress.css' |
|
||||||
|
|
||||||
// show: boolean
|
|
||||||
// progress: number
|
|
||||||
// steps: number
|
|
||||||
// text: string
|
|
||||||
// failed: boolean
|
|
||||||
export function Progress(props) { |
|
||||||
return ( |
|
||||||
<div className={`progress ${props.show ? '' : 'hide'} ${props.failed ? 'failed' : ''}`}> |
|
||||||
{ props.text && props.text.length > 0 ? ( |
|
||||||
<p>{props.text}</p>) : null} |
|
||||||
<div className="bar"> |
|
||||||
<div className="bar-inner" style={{ |
|
||||||
width: (props.progress / props.steps * 100).toFixed(0) + "%" |
|
||||||
}}/> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,111 +0,0 @@ |
|||||||
@import url('https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i,800,800i&display=swap'); |
|
||||||
|
|
||||||
/* select box styling */ |
|
||||||
.select-box { |
|
||||||
display: flex; |
|
||||||
width: 200px; |
|
||||||
flex-direction: column; |
|
||||||
position: relative; |
|
||||||
} |
|
||||||
|
|
||||||
.select-box:focus-visible .selected { |
|
||||||
border: 1px solid #fff; |
|
||||||
} |
|
||||||
|
|
||||||
.select-box > * { |
|
||||||
box-sizing: border-box; |
|
||||||
} |
|
||||||
|
|
||||||
.select-box .options-container { |
|
||||||
max-height: 0; |
|
||||||
width: calc( 100% - 12px); |
|
||||||
opacity: 0; |
|
||||||
transition: all 0.2s ease-in-out; |
|
||||||
overflow: hidden; |
|
||||||
border-radius: 5px; |
|
||||||
background-color: var(--choice); |
|
||||||
order: 1; |
|
||||||
position: absolute; |
|
||||||
z-index: 1; |
|
||||||
top: 50px; |
|
||||||
} |
|
||||||
|
|
||||||
.select-box .selected { |
|
||||||
margin-bottom: 8px; |
|
||||||
position: relative; |
|
||||||
width: 188px; |
|
||||||
height: 45px; |
|
||||||
border-radius: 5px; |
|
||||||
display: flex; |
|
||||||
align-items: center; |
|
||||||
background-color: var(--choice); |
|
||||||
color: white; |
|
||||||
order: 0; |
|
||||||
} |
|
||||||
|
|
||||||
.select-box .selected::after { |
|
||||||
content: ""; |
|
||||||
width: 1.2rem; |
|
||||||
height: 1.2rem; |
|
||||||
background: url(../assets/down-arrow.svg); |
|
||||||
position: absolute; |
|
||||||
right: 15px; |
|
||||||
top: 50%; |
|
||||||
transition: transform 150ms; |
|
||||||
transform: translateY(-50%); |
|
||||||
background-size: contain; |
|
||||||
background-position: center; |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
.select-box .option .item { |
|
||||||
color: var(--text); |
|
||||||
font-weight: bold; |
|
||||||
} |
|
||||||
|
|
||||||
.select-box .options-container.active { |
|
||||||
max-height: 240px; |
|
||||||
opacity: 1; |
|
||||||
overflow-y: scroll; |
|
||||||
} |
|
||||||
|
|
||||||
.select-box .options-container.active + .selected::after { |
|
||||||
transform: translateY(-50%) rotateX(180deg); |
|
||||||
} |
|
||||||
|
|
||||||
.select-box .options-container::-webkit-scrollbar { |
|
||||||
width: 8px; |
|
||||||
background: #0d141f; |
|
||||||
background: #81878f; |
|
||||||
background: #f1f2f3; |
|
||||||
border-radius: 0 5px 5px 0; |
|
||||||
} |
|
||||||
|
|
||||||
.select-box .options-container::-webkit-scrollbar-thumb { |
|
||||||
background: #525861; |
|
||||||
background: #81878f; |
|
||||||
border-radius: 0 5px 5px 0; |
|
||||||
} |
|
||||||
.select-box .option { |
|
||||||
padding: 12px 15px; |
|
||||||
} |
|
||||||
|
|
||||||
.select-box .option, |
|
||||||
.selected { |
|
||||||
cursor: pointer; |
|
||||||
} |
|
||||||
|
|
||||||
.select-box .options-container .option:hover { |
|
||||||
background: var(--choice-hover); |
|
||||||
} |
|
||||||
.select-box .options-container .option:hover .item { |
|
||||||
color: var(--choice-active-text, var(--text)); |
|
||||||
} |
|
||||||
|
|
||||||
.select-box label { |
|
||||||
cursor: pointer; |
|
||||||
} |
|
||||||
|
|
||||||
.select-box .option .radio { |
|
||||||
display: none; |
|
||||||
} |
|
@ -1,91 +0,0 @@ |
|||||||
import { useRef, useState, useEffect } from "react"; |
|
||||||
import "./SelectBox.css"; |
|
||||||
|
|
||||||
function Option({ option, ...props }) { |
|
||||||
return ( |
|
||||||
<div className="option" {...props}> |
|
||||||
<input type="radio" className="radio" id={option.id} /> |
|
||||||
<label htmlFor={option.id}> |
|
||||||
<div className="item">{option.name}</div> |
|
||||||
</label> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
||||||
|
|
||||||
export function SelectBox({ options, selectedItem, setSelectedItem }) { |
|
||||||
if (!Array.isArray(options)) { |
|
||||||
throw new Error("Items must be an array!"); |
|
||||||
} |
|
||||||
|
|
||||||
const [active, setActive] = useState(false); |
|
||||||
|
|
||||||
const containerRef = useRef(); |
|
||||||
|
|
||||||
const handleClick = (e) => { |
|
||||||
if (containerRef.current.contains(e.target)) { |
|
||||||
// inside click
|
|
||||||
return; |
|
||||||
} |
|
||||||
// outside click
|
|
||||||
closeDropdown(); |
|
||||||
}; |
|
||||||
|
|
||||||
const closeDropdown = () => { |
|
||||||
setActive(false); |
|
||||||
}; |
|
||||||
|
|
||||||
useEffect(() => { |
|
||||||
// add when mounted
|
|
||||||
document.addEventListener("mousedown", handleClick); |
|
||||||
// return function to be called when unmounted
|
|
||||||
return () => { |
|
||||||
document.removeEventListener("mousedown", handleClick); |
|
||||||
}; |
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []); |
|
||||||
|
|
||||||
const onOptionClick = (e, option, i) => { |
|
||||||
e.stopPropagation(); |
|
||||||
setSelectedItem(i); |
|
||||||
closeDropdown(); |
|
||||||
}; |
|
||||||
|
|
||||||
const handleSelectedKeyPress = (event) => { |
|
||||||
if (event.code === "Enter" || event.code === "Space") { |
|
||||||
setActive((a) => !a); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
const handleOptionKeyPress = (option, i) => (event) => { |
|
||||||
if (event.code === "Enter" || event.code === "Space") { |
|
||||||
onOptionClick(event, option, i); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
return ( |
|
||||||
<div |
|
||||||
className="select-box" |
|
||||||
ref={containerRef} |
|
||||||
onClick={() => setActive((a) => !a)} |
|
||||||
> |
|
||||||
<div |
|
||||||
className="selected" |
|
||||||
tabIndex={0} |
|
||||||
onKeyPress={handleSelectedKeyPress} |
|
||||||
> |
|
||||||
{options ? <Option option={options[selectedItem]} /> : null} |
|
||||||
</div> |
|
||||||
<div className={"options-container" + (active ? " active" : "")}> |
|
||||||
{options.map((opt, i) => ( |
|
||||||
<Option |
|
||||||
option={opt} |
|
||||||
key={i} |
|
||||||
onClick={(e) => onOptionClick(e, opt, i)} |
|
||||||
tabIndex={active ? 0 : undefined} |
|
||||||
onKeyPress={active ? handleOptionKeyPress(opt, i) : undefined} |
|
||||||
/> |
|
||||||
))} |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
@ -1,52 +0,0 @@ |
|||||||
.title { |
|
||||||
font-size: 2rem; |
|
||||||
color: var(--text); |
|
||||||
/* max-width: 20rem; */ |
|
||||||
margin: 0; |
|
||||||
padding: 0; |
|
||||||
margin-bottom: 3.5rem; |
|
||||||
} |
|
||||||
|
|
||||||
.title-size-medium { |
|
||||||
font-size: 1.5rem; |
|
||||||
} |
|
||||||
.title-size-small { |
|
||||||
font-size: 1.1rem; |
|
||||||
color: var(--text-secondary); |
|
||||||
} |
|
||||||
|
|
||||||
.title-accent { |
|
||||||
color: var(--theme-color); |
|
||||||
font-weight: 600; |
|
||||||
margin: 0; |
|
||||||
padding: 0; |
|
||||||
margin-bottom: 0.5rem; |
|
||||||
margin-top: 1rem; |
|
||||||
display: inline-block; |
|
||||||
} |
|
||||||
|
|
||||||
.title-accent.title-accent-link { |
|
||||||
cursor: pointer; |
|
||||||
} |
|
||||||
|
|
||||||
.title.accent.title-accent-link:focus-visible { |
|
||||||
border: 1px solid #ffffff; |
|
||||||
} |
|
||||||
|
|
||||||
.title.accent.title-accent-link:focus-visible .arrow { |
|
||||||
transform: translateY(.1rem) translateX(-.5rem); |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
.title-accent.title-accent-link .arrow { |
|
||||||
transition: transform 100ms ease-in-out; |
|
||||||
transform: translateY(.1rem); |
|
||||||
margin-right: .2rem; |
|
||||||
} |
|
||||||
|
|
||||||
.title-accent.title-accent-link:hover .arrow { |
|
||||||
transform: translateY(.1rem) translateX(-.5rem); |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,41 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { useHistory } from 'react-router-dom'; |
|
||||||
import { useMovie } from '../hooks/useMovie' |
|
||||||
import { Arrow } from '../components/Arrow' |
|
||||||
import './Title.css' |
|
||||||
|
|
||||||
// size: "big" | "medium" | "small" | null
|
|
||||||
// accent: string | null
|
|
||||||
// accentLink: string | null
|
|
||||||
export function Title(props) { |
|
||||||
const { streamData, resetStreamData } = useMovie(); |
|
||||||
const history = useHistory(); |
|
||||||
const size = props.size || "big"; |
|
||||||
|
|
||||||
const accentLink = props.accentLink || ""; |
|
||||||
const accent = props.accent || ""; |
|
||||||
|
|
||||||
function handleAccentClick(){ |
|
||||||
if (accentLink.length > 0) { |
|
||||||
history.push(`/${streamData.type}`); |
|
||||||
resetStreamData(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
function handleKeyPress(event){ |
|
||||||
if (event.code === 'Enter' || event.code === 'Space'){ |
|
||||||
handleAccentClick(); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<div> |
|
||||||
{accent.length > 0 ? ( |
|
||||||
<p onClick={handleAccentClick} className={`title-accent ${accentLink.length > 0 ? 'title-accent-link' : ''}`} tabIndex={accentLink.length > 0 ? 0 : undefined} onKeyPress={handleKeyPress}> |
|
||||||
{accentLink.length > 0 ? (<Arrow left/>) : null}{accent} |
|
||||||
</p> |
|
||||||
) : null} |
|
||||||
<h1 className={"title " + ( size ? `title-size-${size}` : '' )}>{props.children}</h1> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,65 +0,0 @@ |
|||||||
|
|
||||||
/* TODO better responsiveness, use dropdown if more than 5 options */ |
|
||||||
.typeSelector { |
|
||||||
display: inline-flex; |
|
||||||
position: relative; |
|
||||||
margin-bottom: 1.5rem; |
|
||||||
max-width: 100%; |
|
||||||
} |
|
||||||
.typeSelector:not(.nowrap) { |
|
||||||
flex-wrap: wrap; |
|
||||||
} |
|
||||||
|
|
||||||
.typeSelector::before { |
|
||||||
content: ""; |
|
||||||
position: absolute; |
|
||||||
width: 100%; |
|
||||||
bottom: 0; |
|
||||||
background-color: var(--content); |
|
||||||
height: 4px; |
|
||||||
border-radius: 2px; |
|
||||||
} |
|
||||||
|
|
||||||
.typeSelector .choice { |
|
||||||
width: 7rem; |
|
||||||
height: 3rem; |
|
||||||
padding: .3rem .2rem; |
|
||||||
display: flex; |
|
||||||
justify-content: center; |
|
||||||
align-items: center; |
|
||||||
text-align: center; |
|
||||||
box-sizing: border-box; |
|
||||||
color: var(--text-tertiary); |
|
||||||
font-weight: bold; |
|
||||||
cursor: pointer; |
|
||||||
user-select: none; |
|
||||||
} |
|
||||||
|
|
||||||
.typeSelector .choice:hover { |
|
||||||
color: var(--text-secondary); |
|
||||||
} |
|
||||||
|
|
||||||
.typeSelector .choice:focus-visible { |
|
||||||
border: 1px solid #fff; |
|
||||||
color: var(--text-secondary); |
|
||||||
} |
|
||||||
|
|
||||||
.typeSelector .choice.selected { |
|
||||||
color: var(--text); |
|
||||||
} |
|
||||||
|
|
||||||
.typeSelector .selectedBar { |
|
||||||
position: absolute; |
|
||||||
height: 4px; |
|
||||||
width: 7rem; |
|
||||||
background-color: var(--theme-color); |
|
||||||
border-radius: 2px; |
|
||||||
bottom: 0; |
|
||||||
transition: transform 150ms ease-in-out; |
|
||||||
} |
|
||||||
|
|
||||||
@media screen and (max-width: 700px) { |
|
||||||
.typeSelector:not(.nowrap) { |
|
||||||
display: block; |
|
||||||
} |
|
||||||
} |
|
@ -1,36 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import './TypeSelector.css'; |
|
||||||
|
|
||||||
// setType: (txt: string) => void
|
|
||||||
// choices: { label: string, value: string }[]
|
|
||||||
// selected: string
|
|
||||||
export function TypeSelector({ setType, choices, selected, noWrap = false }) { |
|
||||||
const selectedIndex = choices.findIndex(v => v.value === selected); |
|
||||||
const transformStyles = { |
|
||||||
opacity: selectedIndex !== -1 ? 1 : 0, |
|
||||||
transform: `translateX(${selectedIndex !== -1 ? selectedIndex * 7 : 0}rem)`, |
|
||||||
}; |
|
||||||
|
|
||||||
const handleKeyPress = choice => event => { |
|
||||||
if (event.code === 'Enter' || event.code === 'Space') { |
|
||||||
setType(choice); |
|
||||||
} |
|
||||||
}; |
|
||||||
|
|
||||||
return ( |
|
||||||
<div className={`typeSelector ${noWrap ? 'nowrap' : ''}`}> |
|
||||||
{choices.map(v => ( |
|
||||||
<div |
|
||||||
key={v.value} |
|
||||||
className={`choice ${selected === v.value ? 'selected' : ''}`} |
|
||||||
onClick={() => setType(v.value)} |
|
||||||
onKeyPress={handleKeyPress(v.value)} |
|
||||||
tabIndex={0} |
|
||||||
> |
|
||||||
{v.label} |
|
||||||
</div> |
|
||||||
))} |
|
||||||
<div className="selectedBar" style={transformStyles} /> |
|
||||||
</div> |
|
||||||
); |
|
||||||
} |
|
@ -1,10 +0,0 @@ |
|||||||
.videoElement { |
|
||||||
width: 100%; |
|
||||||
background-color: black; |
|
||||||
border-radius: 5px; |
|
||||||
} |
|
||||||
|
|
||||||
.videoElementText { |
|
||||||
color: var(--text); |
|
||||||
margin: 0; |
|
||||||
} |
|
@ -1,63 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
import Hls from 'hls.js' |
|
||||||
import { VideoPlaceholder } from './VideoPlaceholder' |
|
||||||
|
|
||||||
import './VideoElement.css' |
|
||||||
|
|
||||||
// streamUrl: string
|
|
||||||
// loading: boolean
|
|
||||||
// setProgress: (event: NativeEvent) => void
|
|
||||||
// videoRef: useRef
|
|
||||||
// startTime: number
|
|
||||||
export function VideoElement({ streamUrl, loading, setProgress, videoRef, startTime, streamData }) { |
|
||||||
const [error, setError] = React.useState(false); |
|
||||||
|
|
||||||
function onLoad() { |
|
||||||
if (startTime) |
|
||||||
videoRef.current.currentTime = startTime; |
|
||||||
} |
|
||||||
|
|
||||||
React.useEffect(() => { |
|
||||||
if (!streamUrl.includes('.mp4')) { |
|
||||||
setError(false) |
|
||||||
if (!videoRef || !videoRef.current || !streamUrl || streamUrl.length === 0 || loading) return; |
|
||||||
|
|
||||||
const hls = new Hls(); |
|
||||||
|
|
||||||
if (!Hls.isSupported() && videoRef.current.canPlayType('application/vnd.apple.mpegurl')) { |
|
||||||
videoRef.current.src = streamUrl; |
|
||||||
return; |
|
||||||
} else if (!Hls.isSupported()) { |
|
||||||
setError(true) |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
hls.attachMedia(videoRef.current); |
|
||||||
hls.loadSource(streamUrl); |
|
||||||
} |
|
||||||
}, [videoRef, streamUrl, loading]); |
|
||||||
|
|
||||||
if (error) |
|
||||||
return (<VideoPlaceholder>Your browser is not supported</VideoPlaceholder>) |
|
||||||
|
|
||||||
if (loading) |
|
||||||
return <VideoPlaceholder>Loading episode...</VideoPlaceholder> |
|
||||||
|
|
||||||
if (!streamUrl || streamUrl.length === 0) |
|
||||||
return <VideoPlaceholder>No video selected</VideoPlaceholder> |
|
||||||
|
|
||||||
if (!streamUrl.includes('.mp4')) { |
|
||||||
return ( |
|
||||||
<video className="videoElement" ref={videoRef} controls autoPlay onProgress={setProgress} onLoadedData={onLoad}> |
|
||||||
{ streamData.subtitles && streamData.subtitles.map((sub, index) => <track key={index} kind="captions" label={sub.language} src={sub.file} />) } |
|
||||||
</video> |
|
||||||
) |
|
||||||
} else { |
|
||||||
return ( |
|
||||||
<video className="videoElement" ref={videoRef} controls autoPlay onProgress={setProgress} onLoadedData={onLoad}> |
|
||||||
{ streamData.subtitles && streamData.subtitles.map((sub, index) => <track key={index} kind="captions" label={sub.language} src={sub.file} />) } |
|
||||||
<source src={streamUrl} type="video/mp4" /> |
|
||||||
</video> |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
@ -1,23 +0,0 @@ |
|||||||
.videoPlaceholder { |
|
||||||
width: 100%; |
|
||||||
position: relative; |
|
||||||
} |
|
||||||
.videoPlaceholder::before { |
|
||||||
content: ''; |
|
||||||
display: block; |
|
||||||
width: 100%; |
|
||||||
padding-bottom: 56.25%; |
|
||||||
} |
|
||||||
.videoPlaceholderBox { |
|
||||||
display: flex; |
|
||||||
justify-content: center; |
|
||||||
align-items: center; |
|
||||||
width: 100%; |
|
||||||
height: 100%; |
|
||||||
top: 0; |
|
||||||
left: 0; |
|
||||||
position: absolute; |
|
||||||
background: var(--choice); |
|
||||||
border-radius: 6px; |
|
||||||
color: var(--text); |
|
||||||
} |
|
@ -1,12 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
import './VideoPlaceholder.css' |
|
||||||
|
|
||||||
export function VideoPlaceholder(props) { |
|
||||||
return ( |
|
||||||
<div className="videoPlaceholder"> |
|
||||||
<div className="videoPlaceholderBox"> |
|
||||||
<p>{props.children}</p> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,30 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
const MovieContext = React.createContext(null) |
|
||||||
|
|
||||||
export function MovieProvider(props) { |
|
||||||
const [page, setPage] = React.useState("search"); |
|
||||||
const [stream, setStream] = React.useState(""); |
|
||||||
const [streamData, setStreamData] = React.useState(null); //{ title: "", slug: "", type: "", episodes: [], seasons: [] })
|
|
||||||
|
|
||||||
return ( |
|
||||||
<MovieContext.Provider value={{ |
|
||||||
navigate(str) { |
|
||||||
setPage(str) |
|
||||||
}, |
|
||||||
page, |
|
||||||
setStreamUrl: setStream, |
|
||||||
streamUrl: stream, |
|
||||||
streamData, |
|
||||||
setStreamData(d) { |
|
||||||
setStreamData(p => ({...p,...d})) |
|
||||||
}, |
|
||||||
resetStreamData() { setStreamData(null) } |
|
||||||
}}> |
|
||||||
{props.children} |
|
||||||
</MovieContext.Provider> |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
export function useMovie(props) { |
|
||||||
return React.useContext(MovieContext); |
|
||||||
} |
|
@ -1,28 +0,0 @@ |
|||||||
import { useEffect, useState } from "react"; |
|
||||||
|
|
||||||
// https://usehooks.com/useWindowSize/
|
|
||||||
export function useWindowSize() { |
|
||||||
// Initialize state with undefined width/height so server and client renders match
|
|
||||||
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
|
|
||||||
const [windowSize, setWindowSize] = useState({ |
|
||||||
width: undefined, |
|
||||||
height: undefined, |
|
||||||
}); |
|
||||||
useEffect(() => { |
|
||||||
// Handler to call on window resize
|
|
||||||
function handleResize() { |
|
||||||
// Set window width/height to state
|
|
||||||
setWindowSize({ |
|
||||||
width: window.innerWidth, |
|
||||||
height: window.innerHeight, |
|
||||||
}); |
|
||||||
} |
|
||||||
// Add event listener
|
|
||||||
window.addEventListener("resize", handleResize); |
|
||||||
// Call handler right away so state gets updated with initial window size
|
|
||||||
handleResize(); |
|
||||||
// Remove event listener on cleanup
|
|
||||||
return () => window.removeEventListener("resize", handleResize); |
|
||||||
}, []); // Empty array ensures that effect is only run on mount
|
|
||||||
return windowSize; |
|
||||||
} |
|
@ -1,70 +0,0 @@ |
|||||||
:root { |
|
||||||
--theme-color: #E880C5; |
|
||||||
--theme-color-text: var(--theme-color); |
|
||||||
|
|
||||||
--failed: #d85b66; |
|
||||||
|
|
||||||
--body: #16171D; |
|
||||||
--card: #22232A; |
|
||||||
|
|
||||||
--text: white; |
|
||||||
--text-secondary: #BCBECB; |
|
||||||
--text-tertiary: #585A67; |
|
||||||
|
|
||||||
--content: #36363e; |
|
||||||
--content-hover: #3C3D44; |
|
||||||
|
|
||||||
--button: #A73B83; |
|
||||||
--button-hover: #9C3179; |
|
||||||
--button-active: #8b286a; |
|
||||||
--button-text: var(--text); |
|
||||||
|
|
||||||
--choice: #2E2F37; |
|
||||||
--choice-hover: #45464D; |
|
||||||
--choice-active: #45464D; |
|
||||||
|
|
||||||
--source-headings: #5b5c63; |
|
||||||
} |
|
||||||
/* @media (prefers-color-scheme: light) { |
|
||||||
:root { |
|
||||||
--theme-color: #457461; |
|
||||||
|
|
||||||
--body: white; |
|
||||||
--card: #f8f9fa; |
|
||||||
|
|
||||||
--content: #eee; |
|
||||||
--content-hover: #e7e7e7; |
|
||||||
|
|
||||||
--text: #333; |
|
||||||
--text-secondary: #616161; |
|
||||||
--text-tertiary: #aaa; |
|
||||||
|
|
||||||
--button: #457461; |
|
||||||
--button-hover: #4e836e; |
|
||||||
--button-active: #437a64; |
|
||||||
--button-text: white; |
|
||||||
|
|
||||||
--choice: var(--content); |
|
||||||
--choice-hover: var(--content-hover); |
|
||||||
--choice-active: var(--content-hover); |
|
||||||
} |
|
||||||
} */ |
|
||||||
|
|
||||||
body, html { |
|
||||||
margin: 0; |
|
||||||
background-color: var(--body); |
|
||||||
min-height: 100vh; |
|
||||||
} |
|
||||||
|
|
||||||
body, html, input, button { |
|
||||||
font-family: 'Segoe UI', 'Roboto', 'Oxygen', |
|
||||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', |
|
||||||
sans-serif; |
|
||||||
-webkit-font-smoothing: antialiased; |
|
||||||
-moz-osx-font-smoothing: grayscale; |
|
||||||
font-size: 1rem; |
|
||||||
} |
|
||||||
|
|
||||||
*:focus { |
|
||||||
outline: none; |
|
||||||
} |
|
@ -1,14 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import ReactDOM from 'react-dom'; |
|
||||||
import { HashRouter } from 'react-router-dom'; |
|
||||||
import './index.css'; |
|
||||||
import App from './App'; |
|
||||||
|
|
||||||
ReactDOM.render( |
|
||||||
<React.StrictMode> |
|
||||||
<HashRouter> |
|
||||||
<App /> |
|
||||||
</HashRouter> |
|
||||||
</React.StrictMode>, |
|
||||||
document.getElementById('root') |
|
||||||
); |
|
@ -1,55 +0,0 @@ |
|||||||
import lookmovie from './scraper/lookmovie'; |
|
||||||
import xemovie from './scraper/xemovie'; |
|
||||||
import theflix from './scraper/theflix'; |
|
||||||
import vidzstore from './scraper/vidzstore'; |
|
||||||
|
|
||||||
async function findContent(searchTerm, type) { |
|
||||||
const results = { options: []}; |
|
||||||
const content = await Promise.all([ |
|
||||||
// lookmovie.findContent(searchTerm, type),
|
|
||||||
xemovie.findContent(searchTerm, type), |
|
||||||
theflix.findContent(searchTerm, type), |
|
||||||
vidzstore.findContent(searchTerm, type) |
|
||||||
]); |
|
||||||
|
|
||||||
content.forEach((o) => { |
|
||||||
if (!o || !o.options) return; |
|
||||||
|
|
||||||
o.options.forEach((i) => { |
|
||||||
if (!i) return; |
|
||||||
results.options.push(i) |
|
||||||
}) |
|
||||||
}); |
|
||||||
|
|
||||||
return results; |
|
||||||
} |
|
||||||
|
|
||||||
async function getStreamUrl(slug, type, source, season, episode) { |
|
||||||
switch (source) { |
|
||||||
case 'lookmovie': |
|
||||||
return await lookmovie.getStreamUrl(slug, type, season, episode); |
|
||||||
case 'theflix': |
|
||||||
return await theflix.getStreamUrl(slug, type, season, episode); |
|
||||||
case 'vidzstore': |
|
||||||
return await vidzstore.getStreamUrl(slug); |
|
||||||
case 'xemovie': |
|
||||||
return await xemovie.getStreamUrl(slug, type, season, episode); |
|
||||||
default: |
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function getEpisodes(slug, source) { |
|
||||||
switch (source) { |
|
||||||
case 'lookmovie': |
|
||||||
return await lookmovie.getEpisodes(slug); |
|
||||||
case 'theflix': |
|
||||||
return await theflix.getEpisodes(slug); |
|
||||||
case 'xemovie': |
|
||||||
return await xemovie.getEpisodes(slug); |
|
||||||
default: |
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
export { findContent, getStreamUrl, getEpisodes } |
|
@ -1,111 +0,0 @@ |
|||||||
// THIS SCRAPER DOES NOT CURRENTLY WORK AND IS NOT IN USE |
|
||||||
|
|
||||||
const BASE_URL = `${process.env.REACT_APP_CORS_PROXY_URL}https://database.gdriveplayer.us`; |
|
||||||
const MOVIE_URL = `${process.env.REACT_APP_CORS_PROXY_URL}https://database.gdriveplayer.us/player.php`; |
|
||||||
const SHOW_URL = `${process.env.REACT_APP_CORS_PROXY_URL}https://series.databasegdriveplayer.co/player.php`; |
|
||||||
|
|
||||||
async function findContent(searchTerm, type) { |
|
||||||
try { |
|
||||||
if (type !== 'movie') return; |
|
||||||
|
|
||||||
const term = searchTerm.toLowerCase() |
|
||||||
const tmdbRes = await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}https://www.themoviedb.org/search?query=${term}`).then(d => d.text()); |
|
||||||
|
|
||||||
const doc = new DOMParser().parseFromString(tmdbRes, 'text/html'); |
|
||||||
const nodes = Array.from(doc.querySelectorAll('div.results > div > div.wrapper')); |
|
||||||
const results = nodes.slice(0, 10).map((node) => { |
|
||||||
let type = node.querySelector('div.details > div.wrapper > div.title > div > a').getAttribute('data-media-type'); |
|
||||||
switch (type) { |
|
||||||
case 'movie': |
|
||||||
type = 'movie'; |
|
||||||
break; |
|
||||||
case 'tv': |
|
||||||
type = 'show'; |
|
||||||
// eslint-disable-next-line array-callback-return |
|
||||||
return; |
|
||||||
case 'collection': |
|
||||||
// eslint-disable-next-line array-callback-return |
|
||||||
return; |
|
||||||
default: |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
return { |
|
||||||
type: type, |
|
||||||
title: node.querySelector('div.details > div.wrapper > div.title > div > a').textContent, |
|
||||||
year: node.querySelector('div.details > div.wrapper > div.title > span').textContent.trim().split(' ')[2], |
|
||||||
slug: node.querySelector('div.details > div.wrapper > div.title > div > a').href.split('/')[4], |
|
||||||
source: 'gdriveplayer' |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
if (results.length > 1) { |
|
||||||
return { options: results }; |
|
||||||
} else { |
|
||||||
return { options: [ results[0] ] } |
|
||||||
} |
|
||||||
} catch (err) { |
|
||||||
console.error(err); |
|
||||||
throw new Error(err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function getStreamUrl(slug, type, season, episode) { |
|
||||||
if (type !== 'movie') return; |
|
||||||
|
|
||||||
// const tmdbRes = await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}https://www.themoviedb.org/search?query=${term}`).then(d => d.text()); |
|
||||||
|
|
||||||
console.log(`${MOVIE_URL}?tmdb=${slug}`) |
|
||||||
const res = await fetch(`${MOVIE_URL}?tmdb=${slug}`).then(d => d.text()); |
|
||||||
|
|
||||||
const embed = Array.from(new DOMParser().parseFromString(res, 'text/html').querySelectorAll('.list-server-items a')) |
|
||||||
.find((e) => e.textContent.includes("Mirror")) |
|
||||||
|
|
||||||
if (embed && embed.getAttribute('href')) { |
|
||||||
let href = embed.getAttribute('href'); |
|
||||||
if (href.startsWith('//')) href = `https:${href}`; |
|
||||||
|
|
||||||
const res1 = await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}${href}`.replace('streaming.php', 'download')).then(d => d.text()); |
|
||||||
const sb = Array.from(new DOMParser().parseFromString(res1, 'text/html').querySelectorAll('a')) |
|
||||||
.find((a) => a.textContent.includes("StreamSB")); |
|
||||||
|
|
||||||
console.log(sb); |
|
||||||
|
|
||||||
if (sb && sb.getAttribute('href')) { |
|
||||||
console.log(sb.getAttribute('href')) |
|
||||||
const src = await sbPlayGetLink(sb.getAttribute('href')); |
|
||||||
if (src) return { url: src }; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return { url: '' } |
|
||||||
} |
|
||||||
|
|
||||||
async function sbPlayGetLink(href) { |
|
||||||
if (href.startsWith("//")) href = `https:${href}`; |
|
||||||
|
|
||||||
const res = await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}${href}`).then(d => d.text()); |
|
||||||
const a = new DOMParser().parseFromString(res, 'text/html').querySelector('table tbody tr a'); |
|
||||||
|
|
||||||
if (a && a.getAttribute('onclick')) { |
|
||||||
let match = a.getAttribute("onclick").match(/'([^']+)'/gm); |
|
||||||
console.log(a.getAttribute("onclick")); |
|
||||||
|
|
||||||
if (match) { |
|
||||||
let [code, mode, hash] = match; |
|
||||||
|
|
||||||
const url = `https://sbplay2.com/dl?op=download_orig&id=${code.replace(/'/gm, "")}&mode=${mode.replace(/'/gm, "")}&hash=${hash.replace(/'/gm, "")}`; |
|
||||||
|
|
||||||
// https://sbplay2.com/dl?op=download_orig&id=glr78kyk21kd&mode=n&hash=1890245-0-0-1640889479-95e144cdfdbe0e9104a67b8e3eee0c2d |
|
||||||
// https://sbplay2.com/dl?op=download_orig&id=0hh6mxf5qqn0&mode=h&hash=2473604-78-149-1640889782-797bc207a16b2934c21ea6fdb1e97352 |
|
||||||
// https://proxy-1.movie-web.workers.dev/?destination=https://sbplay2.com/dl?op=download_orig&id=glr78kyk21kd&mode=n&hash=1890245-0-0-1640889479-95e144cdfdbe0e9104a67b8e3eee0c2d |
|
||||||
|
|
||||||
const text = await fetch(url).then((e) => e.text()); |
|
||||||
const a = new DOMParser().parseFromString(text, 'text/html').querySelector(".contentbox span a"); |
|
||||||
if (a && a.getAttribute("href")) return a.getAttribute("href"); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
const gdriveplayer = { findContent, getStreamUrl } |
|
||||||
export default gdriveplayer; |
|
@ -1,91 +0,0 @@ |
|||||||
// THIS SCRAPER DOES NOT CURRENTLY WORK AND IS NOT IN USE |
|
||||||
|
|
||||||
import { unpack } from '../util/unpacker'; |
|
||||||
|
|
||||||
const BASE_URL = `${process.env.REACT_APP_CORS_PROXY_URL}https://gomo.to`; |
|
||||||
const MOVIE_URL = `${BASE_URL}/movie` |
|
||||||
const DECODING_URL = `${BASE_URL}/decoding_v3.php` |
|
||||||
|
|
||||||
async function findContent(searchTerm, type) { |
|
||||||
try { |
|
||||||
if (type !== 'movie') return; |
|
||||||
|
|
||||||
const term = searchTerm.toLowerCase() |
|
||||||
const imdbRes = await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}https://v2.sg.media-imdb.com/suggestion/${term.slice(0, 1)}/${term}.json`).then(d => d.json()) |
|
||||||
|
|
||||||
const results = []; |
|
||||||
imdbRes.d.forEach((e) => { |
|
||||||
if (!e.id.startsWith('tt')) return; |
|
||||||
|
|
||||||
// Block tv shows |
|
||||||
if (e.q === "TV series") return; |
|
||||||
if (e.q === "TV mini-series") return; |
|
||||||
if (e.q === "video game") return; |
|
||||||
if (e.q === "TV movie") return; |
|
||||||
if (e.q === "TV special") return; |
|
||||||
|
|
||||||
results.push({ |
|
||||||
title: e.l, |
|
||||||
slug: e.id, |
|
||||||
type: 'movie', |
|
||||||
year: e.y, |
|
||||||
source: 'gomostream' |
|
||||||
}) |
|
||||||
}); |
|
||||||
|
|
||||||
if (results.length > 1) { |
|
||||||
return { options: results }; |
|
||||||
} else { |
|
||||||
return { options: [ results[0] ] } |
|
||||||
} |
|
||||||
} catch (err) { |
|
||||||
console.error(err); |
|
||||||
throw new Error(err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function getStreamUrl(slug, type, season, episode) { |
|
||||||
if (type !== 'movie') return; |
|
||||||
|
|
||||||
// Get stream to go with IMDB ID |
|
||||||
const site1 = await fetch(`${MOVIE_URL}/${slug}`).then((d) => d.text()); |
|
||||||
|
|
||||||
if (site1 === "Movie not available.") |
|
||||||
return { url: '' }; |
|
||||||
|
|
||||||
const tc = site1.match(/var tc = '(.+)';/)?.[1] |
|
||||||
const _token = site1.match(/"_token": "(.+)",/)?.[1] |
|
||||||
|
|
||||||
const fd = new FormData() |
|
||||||
fd.append('tokenCode', tc) |
|
||||||
fd.append('_token', _token) |
|
||||||
|
|
||||||
const src = await fetch(DECODING_URL, { |
|
||||||
method: "POST", |
|
||||||
body: fd, |
|
||||||
headers: { |
|
||||||
'x-token': tc.slice(5, 13).split("").reverse().join("") + "13574199" |
|
||||||
} |
|
||||||
}).then((d) => d.json()); |
|
||||||
|
|
||||||
const embedUrl = src.find(url => url.includes('gomo.to')); |
|
||||||
const site2 = await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}${embedUrl}`).then((d) => d.text()); |
|
||||||
|
|
||||||
const parser = new DOMParser(); |
|
||||||
const site2Dom = parser.parseFromString(site2, "text/html"); |
|
||||||
|
|
||||||
if (site2Dom.body.innerText === "File was deleted") |
|
||||||
return { url: '' } |
|
||||||
|
|
||||||
const script = site2Dom.querySelectorAll("script")[8].innerHTML; |
|
||||||
|
|
||||||
let unpacked = unpack(script).split(''); |
|
||||||
unpacked.splice(0, 43); |
|
||||||
let index = unpacked.findIndex((e) => e === '"'); |
|
||||||
const url = unpacked.slice(0, index).join(''); |
|
||||||
|
|
||||||
return { url } |
|
||||||
} |
|
||||||
|
|
||||||
const gomostream = { findContent, getStreamUrl } |
|
||||||
export default gomostream; |
|
@ -1,164 +0,0 @@ |
|||||||
import Fuse from 'fuse.js' |
|
||||||
import JSON5 from 'json5' |
|
||||||
|
|
||||||
const BASE_URL = `https://lookmovie.io`; |
|
||||||
const API_URL = `${process.env.REACT_APP_CORS_PROXY_URL}https://lookmovie125.xyz`; |
|
||||||
const CORS_URL = `${process.env.REACT_APP_CORS_PROXY_URL}${BASE_URL}`; |
|
||||||
let phpsessid; |
|
||||||
|
|
||||||
async function findContent(searchTerm, type) { |
|
||||||
try { |
|
||||||
const searchUrl = `${CORS_URL}/${type}s/search/?q=${encodeURIComponent(searchTerm)}`; |
|
||||||
const searchRes = await fetch(searchUrl).then((d) => d.text()); |
|
||||||
|
|
||||||
// Parse DOM to find search results on full search page
|
|
||||||
const parser = new DOMParser(); |
|
||||||
const doc = parser.parseFromString(searchRes, "text/html"); |
|
||||||
const nodes = Array.from(doc.querySelectorAll('.movie-item-style-1')); |
|
||||||
const results = nodes.map(node => { |
|
||||||
return { |
|
||||||
type, |
|
||||||
title: node.querySelector('h6 a').innerText.trim(), |
|
||||||
year: node.querySelector('.year').innerText.trim(), |
|
||||||
slug: node.querySelector('a').href.split('/').pop(), |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
const fuse = new Fuse(results, { threshold: 0.3, distance: 200, keys: ["title"] }); |
|
||||||
const matchedResults = fuse |
|
||||||
.search(searchTerm.toString()) |
|
||||||
.map((result) => result.item); |
|
||||||
|
|
||||||
if (matchedResults.length === 0) { |
|
||||||
return { options: [] } |
|
||||||
} |
|
||||||
|
|
||||||
if (matchedResults.length > 1) { |
|
||||||
const res = { options: [] }; |
|
||||||
|
|
||||||
matchedResults.forEach((r) => res.options.push({ |
|
||||||
title: r.title, |
|
||||||
slug: r.slug, |
|
||||||
type: r.type, |
|
||||||
year: r.year, |
|
||||||
source: 'lookmovie' |
|
||||||
})); |
|
||||||
|
|
||||||
return res; |
|
||||||
} else { |
|
||||||
const { title, slug, type, year } = matchedResults[0]; |
|
||||||
|
|
||||||
return { |
|
||||||
options: [{ title, slug, type, year, source: 'lookmovie' }] |
|
||||||
} |
|
||||||
} |
|
||||||
} catch (e) { |
|
||||||
return { options: [] } |
|
||||||
} |
|
||||||
} |
|
||||||
async function getVideoUrl(config) { |
|
||||||
let url = ''; |
|
||||||
|
|
||||||
if (config.type === 'movie') { |
|
||||||
url = `${API_URL}/api/v1/security/movie-access?id_movie=${config.id}&token=1&sk=&step=1`; |
|
||||||
} else if (config.type === 'show') { |
|
||||||
url = `${API_URL}/api/v1/security/episode-access?id_episode=${config.id}`; |
|
||||||
} |
|
||||||
|
|
||||||
const data = await fetch(url, { |
|
||||||
headers: { phpsessid }, |
|
||||||
}).then((d) => d.json()); |
|
||||||
|
|
||||||
const subs = data?.subtitles.filter((sub) => { |
|
||||||
if (typeof sub.file === 'object') return false; |
|
||||||
return true; |
|
||||||
}) |
|
||||||
|
|
||||||
// Find video URL and return it (with a check for a full url if needed)
|
|
||||||
const opts = ["1080p", "1080", "720p", "720", "480p", "480", "auto"]; |
|
||||||
|
|
||||||
let videoUrl = ""; |
|
||||||
for (let res of opts) { |
|
||||||
if (data.streams[res] && !data.streams[res].includes('dummy') && !data.streams[res].includes('earth-1984') && !videoUrl) { |
|
||||||
videoUrl = data.streams[res] |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return { |
|
||||||
videoUrl: videoUrl.startsWith("/") ? `${BASE_URL}${videoUrl}` : videoUrl, |
|
||||||
subs: subs, |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
async function getEpisodes(slug) { |
|
||||||
const url = `${CORS_URL}/shows/view/${slug}`; |
|
||||||
const pageReq = await fetch(url, { |
|
||||||
headers: { phpsessid }, |
|
||||||
}).then((d) => d.text()); |
|
||||||
|
|
||||||
const data = JSON5.parse("{" + |
|
||||||
pageReq |
|
||||||
.slice(pageReq.indexOf(`show_storage`)) |
|
||||||
.split("};")[0] |
|
||||||
.split("= {")[1] |
|
||||||
.trim() + |
|
||||||
"}" |
|
||||||
); |
|
||||||
|
|
||||||
let seasons = []; |
|
||||||
let episodes = []; |
|
||||||
data.seasons.forEach((e) => { |
|
||||||
if (!seasons.includes(e.season)) |
|
||||||
seasons.push(e.season); |
|
||||||
|
|
||||||
if (!episodes[e.season]) |
|
||||||
episodes[e.season] = [] |
|
||||||
episodes[e.season].push(e.episode) |
|
||||||
}) |
|
||||||
|
|
||||||
return { seasons, episodes } |
|
||||||
} |
|
||||||
|
|
||||||
async function getStreamUrl(slug, type, season, episode) { |
|
||||||
const url = `${CORS_URL}/${type}s/view/${slug}`; |
|
||||||
const pageRes = await fetch(url); |
|
||||||
if (pageRes.headers.get('phpsessid')) phpsessid = pageRes.headers.get('phpsessid'); |
|
||||||
const pageResText = await pageRes.text(); |
|
||||||
|
|
||||||
const data = JSON5.parse("{" + |
|
||||||
pageResText |
|
||||||
.slice(pageResText.indexOf(`${type}_storage`)) |
|
||||||
.split("};")[0] |
|
||||||
.split("= {")[1] |
|
||||||
.trim() + |
|
||||||
"}" |
|
||||||
); |
|
||||||
|
|
||||||
let id = ''; |
|
||||||
|
|
||||||
if (type === "movie") { |
|
||||||
id = data.id_movie; |
|
||||||
} else if (type === "show") { |
|
||||||
const episodeObj = data.seasons.find((v) => { return v.season === season && v.episode === episode; }); |
|
||||||
|
|
||||||
if (episodeObj) { |
|
||||||
id = episodeObj.id_episode; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
if (id === '') { |
|
||||||
return { url: '' } |
|
||||||
} |
|
||||||
|
|
||||||
const videoUrl = await getVideoUrl({ |
|
||||||
slug: slug, |
|
||||||
id: id, |
|
||||||
type: type, |
|
||||||
}); |
|
||||||
|
|
||||||
return { url: videoUrl.videoUrl, subtitles: videoUrl.subs }; |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
const lookMovie = { findContent, getStreamUrl, getEpisodes }; |
|
||||||
export default lookMovie; |
|
@ -1,120 +0,0 @@ |
|||||||
const BASE_URL = `${process.env.REACT_APP_CORS_PROXY_URL}https://theflix.to`; |
|
||||||
|
|
||||||
async function findContent(searchTerm, type) { |
|
||||||
try { |
|
||||||
const term = searchTerm.toLowerCase() |
|
||||||
const tmdbRes = await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}https://www.themoviedb.org/search/${type === 'show' ? 'tv' : type}?query=${term}`).then(d => d.text()); |
|
||||||
|
|
||||||
const doc = new DOMParser().parseFromString(tmdbRes, 'text/html'); |
|
||||||
const nodes = Array.from(doc.querySelectorAll('div.results > div > div.wrapper')); |
|
||||||
const results = nodes.slice(0, 10).map((node) => { |
|
||||||
let type = node.querySelector('div.details > div.wrapper > div.title > div > a').getAttribute('data-media-type'); |
|
||||||
type = type === 'tv' ? 'show' : type; |
|
||||||
|
|
||||||
let title; |
|
||||||
let year; |
|
||||||
let slug; |
|
||||||
|
|
||||||
if (type === 'movie') { |
|
||||||
try { |
|
||||||
title = node.querySelector('div.details > div.wrapper > div.title > div > a').textContent; |
|
||||||
year = node.querySelector('div.details > div.wrapper > div.title > span').textContent.trim().split(' ')[2]; |
|
||||||
slug = node.querySelector('div.details > div.wrapper > div.title > div > a').getAttribute('href').split('/')[2]; |
|
||||||
} catch (e) { |
|
||||||
// eslint-disable-next-line array-callback-return
|
|
||||||
return; |
|
||||||
} |
|
||||||
} else if (type === 'show') { |
|
||||||
try { |
|
||||||
title = node.querySelector('div.details > div.wrapper > div.title > div > a > h2').textContent; |
|
||||||
year = node.querySelector('div.details > div.wrapper > div.title > span').textContent.trim().split(' ')[2]; |
|
||||||
slug = node.querySelector('div.details > div.wrapper > div.title > div > a').getAttribute('href').split('/')[2]; |
|
||||||
} catch (e) { |
|
||||||
// eslint-disable-next-line array-callback-return
|
|
||||||
return; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return { |
|
||||||
type: type, |
|
||||||
title: title, |
|
||||||
year: year, |
|
||||||
slug: slug + '-' + title.replace(/[^a-z0-9]+|\s+/gmi, " ").replace(/\s+/g, '-').toLowerCase(), |
|
||||||
source: 'theflix' |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
if (results.length > 1) { |
|
||||||
return { options: results }; |
|
||||||
} else { |
|
||||||
return { options: [ results[0] ] } |
|
||||||
} |
|
||||||
} catch (err) { |
|
||||||
console.error(err); |
|
||||||
throw new Error(err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function getEpisodes(slug) { |
|
||||||
let tmdbRes; |
|
||||||
|
|
||||||
try { |
|
||||||
tmdbRes = await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}https://www.themoviedb.org/tv/${slug}/seasons`).then(d => d.text()); |
|
||||||
} catch (err) { |
|
||||||
tmdbRes = await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}https://www.themoviedb.org/tv/${slug.split('-')[0]}/seasons`).then(d => d.text()); |
|
||||||
|
|
||||||
if (tmdbRes) |
|
||||||
slug = slug.split('-')[0]; |
|
||||||
} |
|
||||||
|
|
||||||
const sNodes = Array.from(new DOMParser().parseFromString(tmdbRes, 'text/html').querySelectorAll('div.column_wrapper > div.flex > div')); |
|
||||||
|
|
||||||
let seasons = []; |
|
||||||
let episodes = []; |
|
||||||
|
|
||||||
for (let s of sNodes) { |
|
||||||
const text = s.querySelector('div > section > div > div > div > h2 > a').textContent; |
|
||||||
if (!text.includes('Season')) continue; |
|
||||||
|
|
||||||
const season = text.split(' ')[1]; |
|
||||||
|
|
||||||
if (!seasons.includes(season)) { |
|
||||||
seasons.push(season); |
|
||||||
} |
|
||||||
|
|
||||||
if (!episodes[season]) { |
|
||||||
episodes[season] = []; |
|
||||||
} |
|
||||||
|
|
||||||
const epRes = await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}https://www.themoviedb.org/tv/${slug}/season/${season}`).then(d => d.text()); |
|
||||||
const epNodes = Array.from(new DOMParser().parseFromString(epRes, 'text/html').querySelectorAll('div.episode_list > div.card')); |
|
||||||
epNodes.forEach((e, i) => episodes[season].push(++i)); |
|
||||||
} |
|
||||||
|
|
||||||
return { seasons, episodes }; |
|
||||||
} |
|
||||||
|
|
||||||
async function getStreamUrl(slug, type, season, episode) { |
|
||||||
let url; |
|
||||||
|
|
||||||
if (type === 'show') { |
|
||||||
url = `${BASE_URL}/tv-show/${slug}/season-${season}/episode-${episode}`; |
|
||||||
} else { |
|
||||||
url = `${BASE_URL}/movie/${slug}?movieInfo=${slug}`; |
|
||||||
} |
|
||||||
|
|
||||||
const res = await fetch(url).then(d => d.text()); |
|
||||||
|
|
||||||
const scripts = Array.from(new DOMParser().parseFromString(res, "text/html").querySelectorAll('script')); |
|
||||||
const prop = scripts.find((e) => e.textContent.includes("theflixvd.b-cdn")); |
|
||||||
|
|
||||||
if (prop) { |
|
||||||
const data = JSON.parse(prop.textContent); |
|
||||||
return { url: data.props.pageProps.videoUrl }; |
|
||||||
} |
|
||||||
|
|
||||||
return { url: '' } |
|
||||||
} |
|
||||||
|
|
||||||
const theflix = { findContent, getStreamUrl, getEpisodes } |
|
||||||
export default theflix; |
|
@ -1,41 +0,0 @@ |
|||||||
const BASE_URL = `${process.env.REACT_APP_CORS_PROXY_URL}https://stream.vidzstore.com`; |
|
||||||
|
|
||||||
async function findContent(searchTerm, type) { |
|
||||||
if (type === 'show') return { options: [] }; |
|
||||||
try { |
|
||||||
const searchUrl = `${BASE_URL}/search.php?sd=${searchTerm.replace(/ /g, "_")}`; |
|
||||||
const searchRes = await fetch(searchUrl).then((d) => d.text()); |
|
||||||
|
|
||||||
const parser = new DOMParser(); |
|
||||||
const doc = parser.parseFromString(searchRes, "text/html"); |
|
||||||
const nodes = [...doc.querySelectorAll(".post")]; |
|
||||||
const results = nodes.map(node => { |
|
||||||
const title = node.querySelector("a").title.replace(/-/g, " ").trim(); |
|
||||||
const titleArray = title.split(" "); |
|
||||||
titleArray.splice(-2); |
|
||||||
return { |
|
||||||
type, |
|
||||||
title: titleArray.join(" "), |
|
||||||
year: node.querySelector(".post-meta").innerText.split(" ").pop().split("-").shift(), |
|
||||||
slug: encodeURIComponent(node.querySelector("a").href.split('/').pop()), |
|
||||||
source: "vidzstore", |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
return { options: results }; |
|
||||||
} catch { |
|
||||||
return { options: [] }; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function getStreamUrl(slug) { |
|
||||||
const url = `${BASE_URL}/${decodeURIComponent(slug)}`; |
|
||||||
|
|
||||||
const res = await fetch(url).then(d => d.text()); |
|
||||||
const DOM = new DOMParser().parseFromString(res, "text/html"); |
|
||||||
|
|
||||||
return { url: DOM.querySelector("source").src }; |
|
||||||
} |
|
||||||
|
|
||||||
const vidzstore = { findContent, getStreamUrl } |
|
||||||
export default vidzstore; |
|
@ -1,84 +0,0 @@ |
|||||||
// THIS SCRAPER DOES NOT CURRENTLY WORK AND IS NOT IN USE |
|
||||||
|
|
||||||
import { unpack } from '../util/unpacker'; |
|
||||||
|
|
||||||
const BASE_URL = `https://www.vmovee.watch`; |
|
||||||
const CORS_URL = `${process.env.REACT_APP_CORS_PROXY_URL}${BASE_URL}`; |
|
||||||
const SHOW_URL = `${CORS_URL}/series` |
|
||||||
const MOVIE_URL = `${CORS_URL}/movies` |
|
||||||
const MOVIE_URL_NO_CORS = `${BASE_URL}/movies` |
|
||||||
|
|
||||||
async function findContent(searchTerm, type) { |
|
||||||
try { |
|
||||||
if (type !== 'movie') return; |
|
||||||
|
|
||||||
const searchUrl = `${CORS_URL}/?s=${encodeURIComponent(searchTerm)}`; |
|
||||||
const searchRes = await fetch(searchUrl).then((d) => d.text()); |
|
||||||
|
|
||||||
const parser = new DOMParser(); |
|
||||||
const doc = parser.parseFromString(searchRes, "text/html"); |
|
||||||
const nodes = Array.from(doc.querySelectorAll('div.search-page > div.result-item > article')); |
|
||||||
const results = nodes.map(node => { |
|
||||||
const imgHolder = node.querySelector('div.image > div.thumbnail > a'); |
|
||||||
const titleHolder = node.querySelector('div.title > a'); |
|
||||||
|
|
||||||
return { |
|
||||||
type: imgHolder.querySelector('span').textContent === 'TV' ? 'show' : 'movie', |
|
||||||
title: titleHolder.textContent, |
|
||||||
year: node.querySelector('div.details > div.meta > span.year').textContent, |
|
||||||
slug: titleHolder.href.split('/')[4], |
|
||||||
source: 'vmovee' |
|
||||||
} |
|
||||||
}); |
|
||||||
|
|
||||||
if (results.length > 1) { |
|
||||||
return { options: results }; |
|
||||||
} else { |
|
||||||
return { options: [ results[0] ] } |
|
||||||
} |
|
||||||
} catch (err) { |
|
||||||
throw new Error(err) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function getStreamUrl(slug, type, season, episode) { |
|
||||||
let url = ''; |
|
||||||
|
|
||||||
if (type === 'movie') { |
|
||||||
url = `${MOVIE_URL}/${slug}`; |
|
||||||
} else if (type === 'show') { |
|
||||||
url = `${SHOW_URL}/${slug}`; |
|
||||||
} |
|
||||||
|
|
||||||
const res1 = await fetch(url, { headers: new Headers().append('referer', `${BASE_URL}/dashboard/admin-ajax.php`) }); |
|
||||||
const id = res1.headers.get('link').split('>')[0].split('?p=')[1]; |
|
||||||
|
|
||||||
const res2Headers = new Headers().append('referer', `${BASE_URL}/dashboard/admin-ajax.php`); |
|
||||||
const form = new FormData(); |
|
||||||
form.append('action', 'doo_player_ajax') |
|
||||||
form.append('post', id) |
|
||||||
form.append('nume', '2') |
|
||||||
form.append('type', type) |
|
||||||
|
|
||||||
const res2 = await fetch(`${CORS_URL}/dashboard/admin-ajax.php`, { |
|
||||||
method: 'POST', |
|
||||||
headers: res2Headers, |
|
||||||
body: form |
|
||||||
}).then((res) => res.json()); |
|
||||||
let realUrl = res2.embed_url; |
|
||||||
|
|
||||||
console.log(res2) |
|
||||||
|
|
||||||
if (realUrl.startsWith('//')) { |
|
||||||
realUrl = `https:${realUrl}`; |
|
||||||
} |
|
||||||
|
|
||||||
const res3 = await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}${realUrl}`); |
|
||||||
res3.headers.forEach(console.log) |
|
||||||
|
|
||||||
return { url: '' } |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
const vmovee = { findContent, getStreamUrl } |
|
||||||
export default vmovee; |
|
@ -1,121 +0,0 @@ |
|||||||
import Fuse from 'fuse.js' |
|
||||||
|
|
||||||
const BASE_URL = `${process.env.REACT_APP_CORS_PROXY_URL}https://xemovie.co`; |
|
||||||
|
|
||||||
async function findContent(searchTerm, type) { |
|
||||||
try { |
|
||||||
let results; |
|
||||||
|
|
||||||
const searchUrl = `${BASE_URL}/search?q=${encodeURIComponent(searchTerm)}`; |
|
||||||
const searchRes = await fetch(searchUrl).then((d) => d.text()); |
|
||||||
|
|
||||||
const parser = new DOMParser(); |
|
||||||
const doc = parser.parseFromString(searchRes, "text/html"); |
|
||||||
switch (type) { |
|
||||||
case 'show': |
|
||||||
// const showContainer = doc.querySelectorAll(".py-10")[1].querySelector(".grid");
|
|
||||||
// const showNodes = [...showContainer.querySelectorAll("a")].filter(link => !link.className);
|
|
||||||
// results = showNodes.map(node => {
|
|
||||||
// node = node.parentElement
|
|
||||||
// return {
|
|
||||||
// type,
|
|
||||||
// title: [...new Set(node.innerText.split("\n"))][1].split("(")[0].trim(),
|
|
||||||
// year: [...new Set(node.innerText.split("\n"))][3],
|
|
||||||
// slug: node.querySelector("a").href.split('/').pop(),
|
|
||||||
// source: "xemovie"
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// break;
|
|
||||||
return { options: [] }; |
|
||||||
case 'movie': |
|
||||||
const movieContainer = doc.querySelectorAll(".py-10")[0].querySelector(".grid"); |
|
||||||
const movieNodes = [...movieContainer.querySelectorAll("a")].filter(link => !link.className); |
|
||||||
results = movieNodes.map(node => { |
|
||||||
node = node.parentElement |
|
||||||
return { |
|
||||||
type, |
|
||||||
title: [...new Set(node.innerText.split("\n"))][1].split("(")[0].trim(), |
|
||||||
year: [...new Set(node.innerText.split("\n"))][3], |
|
||||||
slug: node.querySelector("a").href.split('/').pop(), |
|
||||||
source: "xemovie" |
|
||||||
} |
|
||||||
}) |
|
||||||
break; |
|
||||||
default: |
|
||||||
results = []; |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
const fuse = new Fuse(results, { threshold: 0.3, keys: ["title"] }); |
|
||||||
const matchedResults = fuse |
|
||||||
.search(searchTerm) |
|
||||||
.map(result => result.item); |
|
||||||
|
|
||||||
if (matchedResults.length === 0) { |
|
||||||
return { options: [] }; |
|
||||||
} |
|
||||||
|
|
||||||
if (matchedResults.length > 1) { |
|
||||||
const res = { options: [] }; |
|
||||||
|
|
||||||
matchedResults.forEach((r) => res.options.push({ |
|
||||||
title: r.title, |
|
||||||
slug: r.slug, |
|
||||||
type: r.type, |
|
||||||
year: r.year, |
|
||||||
source: 'xemovie' |
|
||||||
})); |
|
||||||
|
|
||||||
return res; |
|
||||||
} else { |
|
||||||
const { title, slug, type, year } = matchedResults[0]; |
|
||||||
|
|
||||||
return { |
|
||||||
options: [{ title, slug, type, year, source: 'xemovie' }] |
|
||||||
} |
|
||||||
} |
|
||||||
} catch { |
|
||||||
return { options: [] }; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function getStreamUrl(slug, type, season, episode) { |
|
||||||
let url; |
|
||||||
|
|
||||||
if (type === "show") { |
|
||||||
|
|
||||||
} else { |
|
||||||
url = `${BASE_URL}/movies/${slug}/watch`; |
|
||||||
} |
|
||||||
|
|
||||||
let mediaUrl = ""; |
|
||||||
let subtitles = []; |
|
||||||
|
|
||||||
const res = await fetch(url).then(d => d.text()); |
|
||||||
const DOM = new DOMParser().parseFromString(res, "text/html"); |
|
||||||
|
|
||||||
for (const script of DOM.scripts) { |
|
||||||
if (script.textContent.match(/https:\/\/s[0-9]\.xemovie\.com/)) { |
|
||||||
// eslint-disable-next-line
|
|
||||||
let data = JSON.parse(JSON.stringify(eval(`(${script.textContent.replace("const data = ", "").split("};")[0]}})`))); |
|
||||||
// eslint-disable-next-line
|
|
||||||
mediaUrl = data.playlist[0].file; |
|
||||||
// eslint-disable-next-line
|
|
||||||
for (const subtitleTrack of data.playlist[0].tracks) { |
|
||||||
const subtitleBlob = URL.createObjectURL(await fetch(`${process.env.REACT_APP_CORS_PROXY_URL}${subtitleTrack.file}`).then(res => res.blob())); // do this so no need for CORS errors
|
|
||||||
subtitles.push({ |
|
||||||
file: subtitleBlob, |
|
||||||
language: subtitleTrack.label |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
return { url: mediaUrl, subtitles: subtitles } |
|
||||||
} |
|
||||||
|
|
||||||
async function getEpisodes(slug) { |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
const xemovie = { findContent, getStreamUrl, getEpisodes } |
|
||||||
export default xemovie; |
|
@ -1,43 +0,0 @@ |
|||||||
import { versionedStoreBuilder } from './base.js'; |
|
||||||
|
|
||||||
/* |
|
||||||
version 0 |
|
||||||
{ |
|
||||||
[{scraperid}]: { |
|
||||||
movie: { |
|
||||||
[{movie-id}]: { |
|
||||||
full: { |
|
||||||
currentlyAt: number, |
|
||||||
totalDuration: number, |
|
||||||
updatedAt: number, // unix timestamp in ms
|
|
||||||
meta: FullMetaObject, // no idea whats in here
|
|
||||||
} |
|
||||||
} |
|
||||||
}, |
|
||||||
show: { |
|
||||||
[{show-id}]: { |
|
||||||
[{season}-{episode}]: { |
|
||||||
currentlyAt: number, |
|
||||||
totalDuration: number, |
|
||||||
updatedAt: number, // unix timestamp in ms
|
|
||||||
show: { |
|
||||||
episode: string, |
|
||||||
season: string, |
|
||||||
}, |
|
||||||
meta: FullMetaObject, // no idea whats in here
|
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
*/ |
|
||||||
|
|
||||||
export const VideoProgressStore = versionedStoreBuilder() |
|
||||||
.setKey('video-progress') |
|
||||||
.addVersion({ |
|
||||||
version: 0, |
|
||||||
create() { |
|
||||||
return {} |
|
||||||
} |
|
||||||
}) |
|
||||||
.build() |
|
@ -1,230 +0,0 @@ |
|||||||
function buildStoreObject(d) { |
|
||||||
const data = { |
|
||||||
versions: d.versions, |
|
||||||
currentVersion: d.maxVersion, |
|
||||||
id: d.storageString, |
|
||||||
} |
|
||||||
|
|
||||||
function update(obj) { |
|
||||||
if (!obj) |
|
||||||
throw new Error("object to update is not an object"); |
|
||||||
|
|
||||||
// repeat until object fully updated
|
|
||||||
if (obj["--version"] === undefined) |
|
||||||
obj["--version"] = 0; |
|
||||||
while (obj["--version"] !== this.currentVersion) { |
|
||||||
// get version
|
|
||||||
let version = obj["--version"] || 0; |
|
||||||
if (version.constructor !== Number || version < 0) |
|
||||||
version = -42; // invalid on purpose so it will reset
|
|
||||||
else { |
|
||||||
version = (version+1).toString() |
|
||||||
} |
|
||||||
|
|
||||||
// check if version exists
|
|
||||||
if (!this.versions[version]) { |
|
||||||
console.error(`Version not found for storage item in store ${this.id}, resetting`); |
|
||||||
obj = null; |
|
||||||
break; |
|
||||||
} |
|
||||||
|
|
||||||
// update object
|
|
||||||
obj = this.versions[version].update(obj); |
|
||||||
} |
|
||||||
|
|
||||||
// if resulting obj is null, use latest version as init object
|
|
||||||
if (obj === null) { |
|
||||||
console.error(`Storage item for store ${this.id} has been reset due to faulty updates`); |
|
||||||
return this.versions[this.currentVersion.toString()].init(); |
|
||||||
} |
|
||||||
|
|
||||||
// updates succesful, return
|
|
||||||
return obj; |
|
||||||
} |
|
||||||
|
|
||||||
function get() { |
|
||||||
// get from storage api
|
|
||||||
const store = this; |
|
||||||
let data = localStorage.getItem(this.id); |
|
||||||
|
|
||||||
// parse json if item exists
|
|
||||||
if (data) { |
|
||||||
try { |
|
||||||
data = JSON.parse(data); |
|
||||||
if (!data.constructor) { |
|
||||||
console.error(`Storage item for store ${this.id} has not constructor`) |
|
||||||
throw new Error("storage item has no constructor") |
|
||||||
} |
|
||||||
if (data.constructor !== Object) { |
|
||||||
console.error(`Storage item for store ${this.id} is not an object`) |
|
||||||
throw new Error("storage item is not an object") |
|
||||||
} |
|
||||||
} catch (_) { |
|
||||||
// if errored, set to null so it generates new one, see below
|
|
||||||
console.error(`Failed to parse storage item for store ${this.id}`) |
|
||||||
data = null; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// if item doesnt exist, generate from version init
|
|
||||||
if (!data) { |
|
||||||
data = this.versions[this.currentVersion.toString()].init(); |
|
||||||
} |
|
||||||
|
|
||||||
// update the data if needed
|
|
||||||
data = this.update(data); |
|
||||||
|
|
||||||
// add a save object to return value
|
|
||||||
data.save = function save() { |
|
||||||
localStorage.setItem(store.id, JSON.stringify(data)); |
|
||||||
} |
|
||||||
|
|
||||||
// add instance helpers
|
|
||||||
Object.entries(d.instanceHelpers).forEach(([name, helper]) => { |
|
||||||
if (data[name] !== undefined) |
|
||||||
throw new Error(`helper name: ${name} on instance of store ${this.id} is reserved`) |
|
||||||
data[name] = helper.bind(data); |
|
||||||
}) |
|
||||||
|
|
||||||
// return data
|
|
||||||
return data; |
|
||||||
} |
|
||||||
|
|
||||||
// add functions to store
|
|
||||||
data.get = get.bind(data); |
|
||||||
data.update = update.bind(data); |
|
||||||
|
|
||||||
// add static helpers
|
|
||||||
Object.entries(d.staticHelpers).forEach(([name, helper]) => { |
|
||||||
if (data[name] !== undefined) |
|
||||||
throw new Error(`helper name: ${name} on store ${data.id} is reserved`) |
|
||||||
data[name] = helper.bind({}); |
|
||||||
}) |
|
||||||
|
|
||||||
return data; |
|
||||||
} |
|
||||||
|
|
||||||
/* |
|
||||||
* Builds a versioned store |
|
||||||
* |
|
||||||
* manages versioning of localstorage items |
|
||||||
*/ |
|
||||||
export function versionedStoreBuilder() { |
|
||||||
return { |
|
||||||
_data: { |
|
||||||
versionList: [], |
|
||||||
maxVersion: 0, |
|
||||||
versions: {}, |
|
||||||
storageString: null, |
|
||||||
instanceHelpers: {}, |
|
||||||
staticHelpers: {}, |
|
||||||
}, |
|
||||||
|
|
||||||
/* |
|
||||||
* set key of localstorage item, must be unique |
|
||||||
*/ |
|
||||||
setKey(str) { |
|
||||||
this._data.storageString = str; |
|
||||||
return this; |
|
||||||
}, |
|
||||||
|
|
||||||
/* |
|
||||||
* add a version to the store |
|
||||||
* |
|
||||||
* version: version number |
|
||||||
* migrate: function to update from previous version to this version |
|
||||||
* create: function to return an empty storage item from this version (in correct syntax) |
|
||||||
*/ |
|
||||||
addVersion({ version, migrate, create }) { |
|
||||||
// input checking
|
|
||||||
if (version < 0) |
|
||||||
throw new Error("Cannot add version below 0 in store"); |
|
||||||
if (version > 0 && !migrate) |
|
||||||
throw new Error(`Missing migration on version ${version} (needed for any version above 0)`); |
|
||||||
|
|
||||||
// update max version list
|
|
||||||
if (version > this._data.maxVersion) |
|
||||||
this._data.maxVersion = version; |
|
||||||
// add to version list
|
|
||||||
this._data.versionList.push(version); |
|
||||||
|
|
||||||
|
|
||||||
// register version
|
|
||||||
this._data.versions[version.toString()] = { |
|
||||||
version: version, // version number
|
|
||||||
update: migrate ? (data) => { // update function, and increment version
|
|
||||||
migrate(data); |
|
||||||
data["--version"] = version; |
|
||||||
return data; |
|
||||||
} : null, |
|
||||||
init: create ? () => { // return an initial object
|
|
||||||
const data = create(); |
|
||||||
data["--version"] = version; |
|
||||||
return data; |
|
||||||
} : null |
|
||||||
} |
|
||||||
return this; |
|
||||||
}, |
|
||||||
|
|
||||||
/* |
|
||||||
* register a instance or static helper to the store |
|
||||||
* |
|
||||||
* name: name of the helper function |
|
||||||
* helper: function to execute, the 'this' context is the current storage item (type is instance) |
|
||||||
* type: "instance" or "static". instance is put on the storage item when you store.get() it, static is on the store |
|
||||||
*/ |
|
||||||
registerHelper({ name, helper, type }) { |
|
||||||
// type
|
|
||||||
if (!type) |
|
||||||
type = "instance" |
|
||||||
|
|
||||||
// input checking
|
|
||||||
if (!name || name.constructor !== String) { |
|
||||||
throw new Error("helper name is not a string") |
|
||||||
} |
|
||||||
if (!helper || helper.constructor !== Function) { |
|
||||||
throw new Error("helper function is not a function") |
|
||||||
} |
|
||||||
if (!["instance", "static"].includes(type)) { |
|
||||||
throw new Error("helper type must be either 'instance' or 'static'") |
|
||||||
} |
|
||||||
|
|
||||||
// register helper
|
|
||||||
if (type === "instance") |
|
||||||
this._data.instanceHelpers[name] = helper |
|
||||||
else if (type === "static") |
|
||||||
this._data.staticHelpers[name] = helper |
|
||||||
|
|
||||||
return this; |
|
||||||
}, |
|
||||||
|
|
||||||
/* |
|
||||||
* returns function store based on what has been set |
|
||||||
*/ |
|
||||||
build() { |
|
||||||
// check if version list doesnt skip versions
|
|
||||||
const versionListSorted = this._data.versionList.sort((a,b)=>a-b); |
|
||||||
versionListSorted.forEach((v, i, arr) => { |
|
||||||
if (i === 0) |
|
||||||
return; |
|
||||||
if (v !== arr[i-1]+1) |
|
||||||
throw new Error("Version list of store is not incremental"); |
|
||||||
}) |
|
||||||
|
|
||||||
// version zero must exist
|
|
||||||
if (versionListSorted[0] !== 0) |
|
||||||
throw new Error("Version 0 doesn't exist in version list of store"); |
|
||||||
|
|
||||||
// max version must have init function
|
|
||||||
if (!this._data.versions[this._data.maxVersion.toString()].init) |
|
||||||
throw new Error(`Missing create function on version ${this._data.maxVersion} (needed for latest version of store)`); |
|
||||||
|
|
||||||
// check storage string
|
|
||||||
if (!this._data.storageString) |
|
||||||
throw new Error("storage key not set in store"); |
|
||||||
|
|
||||||
// build versioned store
|
|
||||||
return buildStoreObject(this._data); |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,53 +0,0 @@ |
|||||||
const alphabet = { |
|
||||||
62: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", |
|
||||||
95: '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~' |
|
||||||
}; |
|
||||||
|
|
||||||
function _filterargs(str) { |
|
||||||
var juicers = [ |
|
||||||
/}\('([\s\S]*)', *(\d+), *(\d+), *'([\s\S]*)'\.split\('\|'\), *(\d+), *([\s\S]*)\)\)/, |
|
||||||
/}\('([\s\S]*)', *(\d+), *(\d+), *'([\s\S]*)'\.split\('\|'\)/ |
|
||||||
]; |
|
||||||
|
|
||||||
for (var c = 0; c < juicers.length; ++c) { |
|
||||||
var m, juicer = juicers[c]; |
|
||||||
|
|
||||||
// eslint-disable-next-line no-cond-assign
|
|
||||||
if (m = juicer.exec(str)) { |
|
||||||
return [m[1], m[4].split('|'), parseInt(m[2]), parseInt(m[3])]; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
throw new Error("Could not make sense of p.a.c.k.e.r data (unexpected code structure)"); |
|
||||||
} |
|
||||||
|
|
||||||
function _unbaser(base) { |
|
||||||
if (2 <= base <= 36) return (str) => parseInt(str, base); |
|
||||||
|
|
||||||
const dictionary = {}; |
|
||||||
var alpha = alphabet[base]; |
|
||||||
if (!alpha) throw new Error("Unsupported encoding"); |
|
||||||
|
|
||||||
for (let c = 0; c < alpha.length; ++alpha) { |
|
||||||
dictionary[alpha[c]] = c; |
|
||||||
} |
|
||||||
|
|
||||||
return (str) => str.split("").reverse().reduce((cipher, ind) => Math.pow(base, ind) * dictionary[cipher]); |
|
||||||
} |
|
||||||
|
|
||||||
function unpack(str) { |
|
||||||
var params = _filterargs(str); |
|
||||||
var payload = params[0], symtab = params[1], radix = params[2], count = params[3]; |
|
||||||
|
|
||||||
if (count !== symtab.length) { |
|
||||||
throw new Error("Malformed p.a.c.k.e.r. symtab. (" + count + " != " + symtab.length + ")"); |
|
||||||
} |
|
||||||
|
|
||||||
var unbase = _unbaser(radix); |
|
||||||
var lookup = (word) => symtab[unbase(word)] || word; |
|
||||||
var source = payload.replace(/\b\w+\b/g, lookup); |
|
||||||
|
|
||||||
return source; |
|
||||||
} |
|
||||||
|
|
||||||
export { unpack }; |
|
@ -1,6 +0,0 @@ |
|||||||
.showType-show .title-size-big { |
|
||||||
margin-bottom: 10px; |
|
||||||
} |
|
||||||
.showType-show .title-size-small, .showType-movie .title-size-big { |
|
||||||
margin-bottom: 30px; |
|
||||||
} |
|
@ -1,156 +0,0 @@ |
|||||||
import React from 'react' |
|
||||||
import { useRouteMatch, useHistory } from 'react-router-dom' |
|
||||||
import { Helmet } from 'react-helmet'; |
|
||||||
import { Title } from '../components/Title' |
|
||||||
import { Card } from '../components/Card' |
|
||||||
import { useMovie } from '../hooks/useMovie' |
|
||||||
import { VideoElement } from '../components/VideoElement' |
|
||||||
import { EpisodeSelector } from '../components/EpisodeSelector' |
|
||||||
import { getStreamUrl } from '../lib/index' |
|
||||||
import { VideoProgressStore } from '../lib/storage/VideoProgress' |
|
||||||
|
|
||||||
import './Movie.css' |
|
||||||
|
|
||||||
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 [ 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 episode = showRouteMatch?.params.episode || "1"; |
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
function setEpisode({ season, episode }) { |
|
||||||
history.push(`${baseRouteMatch.url}/season/${season}/episode/${episode}`); |
|
||||||
isVideoTimeSet.current = false; |
|
||||||
} |
|
||||||
|
|
||||||
React.useEffect(() => { |
|
||||||
if (streamData.type === "show" && !showRouteMatch) history.replace(`${baseRouteMatch.url}/season/1/episode/1`); |
|
||||||
}, [streamData.type, showRouteMatch, history, baseRouteMatch.url]); |
|
||||||
|
|
||||||
React.useEffect(() => { |
|
||||||
if (streamData.type === "show" && showRouteMatch) setSelectedSeason(showRouteMatch.params.season.toString()); |
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []); |
|
||||||
|
|
||||||
React.useEffect(() => { |
|
||||||
let cancel = false; |
|
||||||
|
|
||||||
if (streamData.type !== "show") return () => { |
|
||||||
cancel = true; |
|
||||||
}; |
|
||||||
|
|
||||||
if (!episode) { |
|
||||||
setLoading(false); |
|
||||||
setStreamUrl(''); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
setLoading(true); |
|
||||||
getStreamUrl(streamData.slug, streamData.type, streamData.source, season, episode) |
|
||||||
.then(({ url, subtitles }) => { |
|
||||||
if (cancel) return; |
|
||||||
streamData.subtitles = subtitles; |
|
||||||
setStreamUrl(url) |
|
||||||
setLoading(false); |
|
||||||
}) |
|
||||||
.catch((e) => { |
|
||||||
if (cancel) return; |
|
||||||
console.error(e) |
|
||||||
}) |
|
||||||
|
|
||||||
return () => { |
|
||||||
cancel = true; |
|
||||||
} |
|
||||||
}, [episode, streamData, setStreamUrl, season]); |
|
||||||
|
|
||||||
React.useEffect(() => { |
|
||||||
if (streamData.type === "show") { |
|
||||||
setSeasonList(streamData.seasons); |
|
||||||
setEpisodeList(streamData.episodes[selectedSeason]); |
|
||||||
} |
|
||||||
}, [streamData.seasons, streamData.episodes, streamData.type, selectedSeason]) |
|
||||||
|
|
||||||
React.useEffect(() => { |
|
||||||
const progressData = VideoProgressStore.get(); |
|
||||||
let key = streamData.type === "show" ? `${season}-${episode}` : "full" |
|
||||||
let time = progressData?.[streamData.source]?.[streamData.type]?.[streamData.slug]?.[key]?.currentlyAt; |
|
||||||
setStartTime(time); |
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [baseRouteMatch, showRouteMatch]); |
|
||||||
|
|
||||||
const setProgress = (evt) => { |
|
||||||
let progressSave = VideoProgressStore.get(); |
|
||||||
|
|
||||||
if (!progressSave[streamData.source]) |
|
||||||
progressSave[streamData.source] = {} |
|
||||||
if (!progressSave[streamData.source][streamData.type]) |
|
||||||
progressSave[streamData.source][streamData.type] = {} |
|
||||||
if (!progressSave[streamData.source][streamData.type][streamData.slug]) |
|
||||||
progressSave[streamData.source][streamData.type][streamData.slug] = {} |
|
||||||
|
|
||||||
// Store real data
|
|
||||||
let key = streamData.type === "show" ? `${season}-${episode}` : "full" |
|
||||||
progressSave[streamData.source][streamData.type][streamData.slug][key] = { |
|
||||||
currentlyAt: Math.floor(evt.currentTarget.currentTime), |
|
||||||
totalDuration: Math.floor(evt.currentTarget.duration), |
|
||||||
updatedAt: Date.now(), |
|
||||||
meta: streamData |
|
||||||
} |
|
||||||
|
|
||||||
if(streamData.type === "show") { |
|
||||||
progressSave[streamData.source][streamData.type][streamData.slug][key].show = { |
|
||||||
season, |
|
||||||
episode |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
progressSave.save(); |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<div className={`cardView showType-${streamData.type}`}> |
|
||||||
<Helmet> |
|
||||||
<title>{streamData.title}{streamData.type === 'show' ? ` | S${season}E${episode}` : ''} | movie-web</title> |
|
||||||
</Helmet> |
|
||||||
|
|
||||||
<Card fullWidth> |
|
||||||
<Title accent="Return to home" accentLink="search"> |
|
||||||
{streamData.title} |
|
||||||
</Title> |
|
||||||
{streamData.type === "show" ? <Title size="small"> |
|
||||||
Season {season}: Episode {episode} |
|
||||||
</Title> : undefined} |
|
||||||
|
|
||||||
<VideoElement streamUrl={streamUrl} loading={loading} setProgress={setProgress} videoRef={videoRef} startTime={startTime} streamData={streamData} /> |
|
||||||
|
|
||||||
{streamData.type === "show" ? |
|
||||||
<EpisodeSelector |
|
||||||
setSelectedSeason={setSelectedSeason} |
|
||||||
selectedSeason={selectedSeason} |
|
||||||
|
|
||||||
setEpisode={setEpisode} |
|
||||||
|
|
||||||
seasons={seasonList} |
|
||||||
episodes={episodeLists} |
|
||||||
|
|
||||||
currentSeason={season} |
|
||||||
currentEpisode={episode} |
|
||||||
|
|
||||||
streamData={streamData} |
|
||||||
/> |
|
||||||
: ''} |
|
||||||
</Card> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
@ -1,76 +0,0 @@ |
|||||||
.cardView { |
|
||||||
display: flex; |
|
||||||
min-height: 100vh; |
|
||||||
justify-content: center; |
|
||||||
align-items: center; |
|
||||||
flex-direction: column; |
|
||||||
padding: 1rem; |
|
||||||
box-sizing: border-box; |
|
||||||
} |
|
||||||
|
|
||||||
.cardView nav { |
|
||||||
width: 100%; |
|
||||||
max-width: 624px; |
|
||||||
} |
|
||||||
.cardView nav span { |
|
||||||
padding: 8px 16px; |
|
||||||
margin-right: 10px; |
|
||||||
border-radius: 4px; |
|
||||||
color: var(--text); |
|
||||||
} |
|
||||||
|
|
||||||
.cardView nav span:focus-visible { |
|
||||||
border: 1px solid #fff; |
|
||||||
} |
|
||||||
|
|
||||||
.cardView nav span:not(.selected-link) { |
|
||||||
cursor: pointer; |
|
||||||
} |
|
||||||
.cardView nav span.selected-link { |
|
||||||
background: var(--card); |
|
||||||
color: var(--button-text); |
|
||||||
font-weight: bold; |
|
||||||
} |
|
||||||
|
|
||||||
.cardView > * { |
|
||||||
margin-top: 2rem; |
|
||||||
} |
|
||||||
|
|
||||||
.cardView > *:first-child { |
|
||||||
margin-top: 38px; |
|
||||||
} |
|
||||||
|
|
||||||
.topRightCredits { |
|
||||||
position: absolute; |
|
||||||
right: 1rem; |
|
||||||
top: 1rem; |
|
||||||
margin-top: 0 !important; |
|
||||||
text-align: right; |
|
||||||
} |
|
||||||
|
|
||||||
.topRightCredits a, .topRightCredits a:visited { |
|
||||||
color: var(--theme-color); |
|
||||||
text-decoration: none; |
|
||||||
margin: 0; |
|
||||||
} |
|
||||||
|
|
||||||
.topRightCredits a:hover, .topRightCredits a:active { |
|
||||||
color: var(--theme-color); |
|
||||||
text-decoration: none; |
|
||||||
} |
|
||||||
|
|
||||||
.topRightCredits a .arrow { |
|
||||||
transform: translateY(.1rem); |
|
||||||
} |
|
||||||
|
|
||||||
.topRightCredits a:hover .arrow { |
|
||||||
transform: translateY(.1rem) translateX(.2rem); |
|
||||||
} |
|
||||||
|
|
||||||
p.source { |
|
||||||
text-transform: uppercase; |
|
||||||
font-weight: bold; |
|
||||||
color: var(--source-headings); |
|
||||||
font-size: 0.8em; |
|
||||||
margin-top: 2rem; |
|
||||||
} |
|
@ -1,299 +0,0 @@ |
|||||||
import React from 'react'; |
|
||||||
import { Helmet } from 'react-helmet'; |
|
||||||
import { Redirect, useHistory, useRouteMatch } from 'react-router-dom'; |
|
||||||
import { Arrow } from '../components/Arrow'; |
|
||||||
import { Card } from '../components/Card'; |
|
||||||
import { ErrorBanner } from '../components/ErrorBanner'; |
|
||||||
import { InputBox } from '../components/InputBox'; |
|
||||||
import { MovieRow } from '../components/MovieRow'; |
|
||||||
import { Progress } from '../components/Progress'; |
|
||||||
import { Title } from '../components/Title'; |
|
||||||
import { TypeSelector } from '../components/TypeSelector'; |
|
||||||
import { useMovie } from '../hooks/useMovie'; |
|
||||||
import { findContent, getEpisodes, getStreamUrl } from '../lib/index'; |
|
||||||
import { VideoProgressStore } from '../lib/storage/VideoProgress' |
|
||||||
|
|
||||||
import './Search.css'; |
|
||||||
|
|
||||||
export function SearchView() { |
|
||||||
const { navigate, setStreamUrl, setStreamData } = useMovie(); |
|
||||||
|
|
||||||
const history = useHistory(); |
|
||||||
const routeMatch = useRouteMatch('/:type'); |
|
||||||
const type = routeMatch?.params?.type; |
|
||||||
const streamRouteMatch = useRouteMatch('/:type/:source/:title/:slug'); |
|
||||||
|
|
||||||
const maxSteps = 4; |
|
||||||
const [options, setOptions] = React.useState([]); |
|
||||||
const [progress, setProgress] = React.useState(0); |
|
||||||
const [text, setText] = React.useState(""); |
|
||||||
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); |
|
||||||
setText(str) |
|
||||||
setFailed(true) |
|
||||||
} |
|
||||||
|
|
||||||
async function getStream(title, slug, type, source, year) { |
|
||||||
setStreamUrl(""); |
|
||||||
|
|
||||||
try { |
|
||||||
setProgress(2); |
|
||||||
setText(`Getting stream for "${title}"`); |
|
||||||
|
|
||||||
let seasons = []; |
|
||||||
let episodes = []; |
|
||||||
if (type === "show") { |
|
||||||
const data = await getEpisodes(slug, source); |
|
||||||
seasons = data.seasons; |
|
||||||
episodes = data.episodes; |
|
||||||
} |
|
||||||
|
|
||||||
let realUrl = ''; |
|
||||||
let subtitles = [] |
|
||||||
|
|
||||||
if (type === "movie") { |
|
||||||
const { url, subtitles: subs } = await getStreamUrl(slug, type, source); |
|
||||||
|
|
||||||
if (url === '') { |
|
||||||
return fail(`Not found: ${title}`) |
|
||||||
} |
|
||||||
|
|
||||||
realUrl = url; |
|
||||||
subtitles = subs |
|
||||||
} |
|
||||||
|
|
||||||
setProgress(maxSteps); |
|
||||||
setStreamUrl(realUrl); |
|
||||||
setStreamData({ |
|
||||||
title, |
|
||||||
type, |
|
||||||
seasons, |
|
||||||
episodes, |
|
||||||
slug, |
|
||||||
source, |
|
||||||
year, |
|
||||||
subtitles |
|
||||||
}) |
|
||||||
setText(`Streaming...`) |
|
||||||
navigate("movie") |
|
||||||
} catch (err) { |
|
||||||
console.error(err); |
|
||||||
fail("Failed to get stream") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
async function searchMovie(query, contentType) { |
|
||||||
setFailed(false); |
|
||||||
setText(`Searching for ${contentType} "${query}"`); |
|
||||||
setProgress(1) |
|
||||||
setShowingOptions(false) |
|
||||||
|
|
||||||
try { |
|
||||||
const { options } = await findContent(query, contentType); |
|
||||||
|
|
||||||
if (options.length === 0) { |
|
||||||
return fail(`Could not find that ${contentType}`) |
|
||||||
} else if (options.length > 1) { |
|
||||||
setProgress(2); |
|
||||||
setText(`Choose your ${contentType}`); |
|
||||||
setOptions(options); |
|
||||||
setShowingOptions(true); |
|
||||||
return; |
|
||||||
} |
|
||||||
|
|
||||||
const { title, slug, type, source, year } = options[0]; |
|
||||||
history.push(`${routeMatch.url}/${source}/${title}/${slug}`); |
|
||||||
getStream(title, slug, type, source, year); |
|
||||||
} catch (err) { |
|
||||||
console.error(err); |
|
||||||
fail(`Failed to watch ${contentType}`) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
React.useEffect(() => { |
|
||||||
async function fetchHealth() { |
|
||||||
await fetch(process.env.REACT_APP_CORS_PROXY_URL).catch(() => { |
|
||||||
// Request failed; source likely offline
|
|
||||||
setErrorStatus(`Our content provider is currently offline, apologies.`) |
|
||||||
}) |
|
||||||
} |
|
||||||
fetchHealth() |
|
||||||
}, []); |
|
||||||
|
|
||||||
React.useEffect(() => { |
|
||||||
if (streamRouteMatch) { |
|
||||||
if (streamRouteMatch?.params.type === 'movie' || streamRouteMatch.params.type === 'show') getStream(streamRouteMatch.params.title, streamRouteMatch.params.slug, streamRouteMatch.params.type, streamRouteMatch.params.source); |
|
||||||
else return setErrorStatus("Failed to find movie. Please try searching below."); |
|
||||||
} |
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []); |
|
||||||
|
|
||||||
React.useEffect(() => { |
|
||||||
const progressData = VideoProgressStore.get(); |
|
||||||
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" /> |
|
||||||
} |
|
||||||
|
|
||||||
const handleKeyPress = page => event => { |
|
||||||
if (event.code === 'Enter' || event.code === 'Space'){ |
|
||||||
setPage(page); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
return ( |
|
||||||
<div className="cardView"> |
|
||||||
<Helmet> |
|
||||||
<title>{type === 'movie' ? 'movies' : 'shows'} | movie-web</title> |
|
||||||
</Helmet> |
|
||||||
|
|
||||||
{/* Nav */} |
|
||||||
<nav> |
|
||||||
<span className={page === 'search' ? 'selected-link' : ''} onClick={() => setPage('search')} onKeyPress={handleKeyPress('search')} tabIndex={0}>Search</span> |
|
||||||
{continueWatching.length > 0 ? |
|
||||||
<span className={page === 'watching' ? 'selected-link' : ''} onClick={() => setPage('watching')} onKeyPress={handleKeyPress('watching')} tabIndex={0}>Continue watching</span> |
|
||||||
: ''} |
|
||||||
</nav> |
|
||||||
|
|
||||||
{/* Search */} |
|
||||||
{page === 'search' ? |
|
||||||
<React.Fragment> |
|
||||||
<Card> |
|
||||||
{errorStatus ? <ErrorBanner>{errorStatus}</ErrorBanner> : ''} |
|
||||||
<Title accent="Because watching content legally is boring"> |
|
||||||
What do you wanna watch? |
|
||||||
</Title> |
|
||||||
<TypeSelector |
|
||||||
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> |
|
||||||
<Progress show={progress > 0} failed={failed} progress={progress} steps={maxSteps} text={text} /> |
|
||||||
{continueWatching?.map((v, i) => ( |
|
||||||
<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(`/show/${v.source}/${v.data.meta.title}/${v.slug}/season/${v.data.show.season}/episode/${v.data.show.episode}`) |
|
||||||
} else { |
|
||||||
history.push(`/movie/${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) |
|
||||||
}} /> |
|
||||||
))} |
|
||||||
</Card> : <React.Fragment></React.Fragment>} |
|
||||||
|
|
||||||
<div className="topRightCredits"> |
|
||||||
<a href="https://github.com/JamesHawkinss/movie-web" target="_blank" rel="noreferrer">Check it out on GitHub <Arrow /></a> |
|
||||||
<br /> |
|
||||||
<a href="https://discord.gg/vXsRvye8BS" target="_blank" rel="noreferrer">Join the Discord <Arrow /></a> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
) |
|
||||||
} |
|
Loading…
Reference in new issue