Merge pull request #3 from JamesHawkinss/v2/migration

Final v2 changes
This commit is contained in:
mrjvs 2022-05-01 16:06:17 +02:00 committed by GitHub
commit 39d2d79f67
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 6311 additions and 9336 deletions

1
.env
View File

@ -1 +0,0 @@
REACT_APP_CORS_PROXY_URL=https://proxy-1.movie-web.workers.dev/?destination=

View File

@ -63,6 +63,6 @@
"prettier-plugin-tailwindcss": "^0.1.7",
"tailwind-scrollbar": "^1.3.1",
"tailwindcss": "^3.0.20",
"typescript": "^4.6.2"
"typescript": "^4.6.4"
}
}

View File

@ -52,11 +52,11 @@ export function SearchBarInput(props: SearchBarProps) {
name: "Series",
icon: Icons.CLAPPER_BOARD,
},
{
id: MWMediaType.ANIME,
name: "Anime",
icon: Icons.DRAGON,
},
// {
// id: MWMediaType.ANIME,
// name: "Anime",
// icon: Icons.DRAGON,
// },
]}
onClick={() => setDropdownOpen((old) => !old)}
>

View File

@ -3,6 +3,7 @@ import { Icons } from "components/Icon";
import { Loading } from "components/layout/Loading";
import { MWMediaCaption, MWMediaStream } from "providers";
import { ReactElement, useEffect, useRef, useState } from "react";
import Hls from "hls.js";
export interface VideoPlayerProps {
source: MWMediaStream;
@ -40,7 +41,34 @@ export function VideoPlayer(props: VideoPlayerProps) {
useEffect(() => {
setLoading(true);
setErrored(false);
}, [props.source.url]);
// hls support
if (mustUseHls) {
if (!videoRef.current)
return;
if (!Hls.isSupported()) {
setLoading(false);
setErrored(true);
return;
}
const hls = new Hls();
if (videoRef.current.canPlayType('application/vnd.apple.mpegurl')) {
videoRef.current.src = props.source.url;
return;
}
hls.attachMedia(videoRef.current);
hls.loadSource(props.source.url);
hls.on(Hls.Events.ERROR, (event, data) => {
setErrored(true);
console.error(data);
});
}
}, [props.source.url, videoRef, mustUseHls]);
let skeletonUi: null | ReactElement = null;
if (hasErrored) {
@ -53,9 +81,8 @@ export function VideoPlayer(props: VideoPlayerProps) {
<>
{skeletonUi}
<video
className={`bg-denim-500 w-full rounded-xl ${
!showVideo ? "hidden" : ""
}`}
className={`bg-denim-500 w-full rounded-xl ${!showVideo ? "hidden" : ""
}`}
ref={videoRef}
onProgress={(e) =>
props.onProgress && props.onProgress(e.nativeEvent as ProgressEvent)

View File

@ -4,7 +4,6 @@ import {
MWPortableMedia,
MWMediaStream,
MWQuery,
MWMediaSeasons,
MWProviderMediaResult
} from "providers/types";
@ -57,14 +56,13 @@ export const gDrivePlayerScraper: MWMediaProvider = {
async searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]> {
const searchRes = await fetch(`${CORS_PROXY_URL}https://api.gdriveplayer.us/v1/movie/search?title=${query.searchQuery}`).then((d) => d.json());
const results: MWProviderMediaResult[] = searchRes.map((item: any) => ({
const results: MWProviderMediaResult[] = (searchRes || []).map((item: any) => ({
title: item.title,
year: item.year,
mediaId: item.imdb,
}));
return results;
},
async getStream(media: MWPortableMedia): Promise<MWMediaStream> {
@ -89,8 +87,4 @@ export const gDrivePlayerScraper: MWMediaProvider = {
return { url: `https:${source.file}`, type: source.type, captions: [] };
},
async getSeasonDataFromMedia(media: MWPortableMedia): Promise<MWMediaSeasons> {
return {} as MWMediaSeasons;
}
};

View File

@ -4,7 +4,6 @@ import {
MWPortableMedia,
MWMediaStream,
MWQuery,
MWMediaSeasons,
MWProviderMediaResult
} from "providers/types";
@ -47,7 +46,7 @@ export const gomostreamScraper: MWMediaProvider = {
`${CORS_PROXY_URL}http://www.omdbapi.com/?${encodeURIComponent(params.toString())}`,
).then(d => d.json())
const results: MWProviderMediaResult[] = searchRes.Search.map((d: any) => ({
const results: MWProviderMediaResult[] = (searchRes.Search || []).map((d: any) => ({
title: d.Title,
year: d.Year,
mediaId: d.imdbID
@ -92,9 +91,5 @@ export const gomostreamScraper: MWMediaProvider = {
if (streamType !== "mp4" && streamType !== "m3u8") throw new Error("Unsupported stream type");
return { url: streamUrl, type: streamType, captions: [] };
},
async getSeasonDataFromMedia(media: MWPortableMedia): Promise<MWMediaSeasons> {
return {} as MWMediaSeasons;
}
};

View File

@ -1,6 +1,6 @@
import { SimpleCache } from "utils/cache";
import { MWPortableMedia } from "providers";
import { MWMediaSeasons } from "providers/types";
import { MWMediaSeasons, MWMediaType, MWMediaProviderSeries } from "providers/types";
import { getProviderFromId } from "./helpers";
// cache
@ -16,13 +16,19 @@ seasonCache.initialize();
export async function getSeasonDataFromMedia(
media: MWPortableMedia
): Promise<MWMediaSeasons> {
const provider = getProviderFromId(media.providerId);
const provider = getProviderFromId(media.providerId) as MWMediaProviderSeries;
if (!provider) {
return {
seasons: [],
};
}
if (!provider.type.includes(MWMediaType.SERIES) && !provider.type.includes(MWMediaType.ANIME)) {
return {
seasons: [],
};
}
if (seasonCache.has(media)) {
return seasonCache.get(media) as MWMediaSeasons;
}

View File

@ -57,7 +57,7 @@ export interface MWQuery {
type: MWMediaType;
}
export interface MWMediaProvider {
export interface MWMediaProviderBase {
id: string; // id of provider, must be unique
enabled: boolean;
type: MWMediaType[];
@ -66,9 +66,15 @@ export interface MWMediaProvider {
getMediaFromPortable(media: MWPortableMedia): Promise<MWProviderMediaResult>;
searchForMedia(query: MWQuery): Promise<MWProviderMediaResult[]>;
getStream(media: MWPortableMedia): Promise<MWMediaStream>;
getSeasonDataFromMedia(media: MWPortableMedia): Promise<MWMediaSeasons>;
getSeasonDataFromMedia?: (media: MWPortableMedia) => Promise<MWMediaSeasons>;
}
export type MWMediaProviderSeries = MWMediaProviderBase & {
getSeasonDataFromMedia: (media: MWPortableMedia) => Promise<MWMediaSeasons>;
};
export type MWMediaProvider = MWMediaProviderBase;
export interface MWMediaProviderMetadata {
exists: boolean;
id?: string;

View File

@ -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;

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-down"><polyline points="6 9 12 15 18 9"></polyline></svg>

Before

Width:  |  Height:  |  Size: 261 B

View File

@ -1,7 +0,0 @@
.feather.left {
transform: rotate(180deg);
}
.arrow {
display: inline-block;
}

View File

@ -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>
)
}

View File

@ -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;
}
}

View File

@ -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>
)
}

View File

@ -1,3 +0,0 @@
.episodeSelector {
margin-top: 20px;
}

View File

@ -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>
)
}

View File

@ -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);
}

View File

@ -1,10 +0,0 @@
import React from 'react';
import './ErrorBanner.css';
export function ErrorBanner({children}) {
return (
<div className="errorBanner">
{children}
</div>
)
}

View File

@ -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%;
}
}

View File

@ -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>
)
}

View File

@ -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;
}
}

View File

@ -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}
&nbsp;
<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>
)
}

View File

@ -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);
}

View File

@ -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>
)
}

View File

@ -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%;
}

View File

@ -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>
}

View File

@ -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);
}

View File

@ -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>
)
}

View File

@ -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;
}

View File

@ -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>
);
}

View File

@ -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);
}

View File

@ -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>
)
}

View File

@ -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;
}
}

View File

@ -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>
);
}

View File

@ -1,10 +0,0 @@
.videoElement {
width: 100%;
background-color: black;
border-radius: 5px;
}
.videoElementText {
color: var(--text);
margin: 0;
}

View File

@ -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>
)
}
}

View File

@ -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);
}

View File

@ -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>
)
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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')
);

View File

@ -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 }

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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()

View File

@ -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);
}
}
}

View File

@ -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 };

View File

@ -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;
}

View File

@ -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>
)
}

View File

@ -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;
}

View File

@ -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>
)
}

12642
yarn.lock

File diff suppressed because it is too large Load Diff