diff --git a/package.json b/package.json
index 29118a46..b0bec93e 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
+ "crypto-js": "^4.0.0",
"fuse.js": "^6.4.6",
"hls.js": "^1.0.7",
"json5": "^2.2.0",
diff --git a/src/components/EpisodeSelector.js b/src/components/EpisodeSelector.js
index aac45e27..e1f98117 100644
--- a/src/components/EpisodeSelector.js
+++ b/src/components/EpisodeSelector.js
@@ -3,7 +3,7 @@ import { TypeSelector } from './TypeSelector';
import { NumberSelector } from './NumberSelector';
import './EpisodeSelector.css'
-export function EpisodeSelector({ setSeason, setEpisode, seasons, season, episodes, currentSeason, currentEpisode, slug }) {
+export function EpisodeSelector({ setSeason, setEpisode, seasons, season, episodes, currentSeason, currentEpisode, slug, source }) {
const choices = episodes.map(v => {
@@ -12,7 +12,7 @@ export function EpisodeSelector({ setSeason, setEpisode, seasons, season, episod
let currentlyAt = 0;
let totalDuration = 0;
- const progress = progressData?.lookmovie?.show?.slug?.[`${season}-${v}`]
+ const progress = progressData?.[source]?.show?.slug?.[`${season}-${v}`]
if(progress) {
currentlyAt = progress.currentlyAt
totalDuration = progress.totalDuration
diff --git a/src/components/MovieRow.js b/src/components/MovieRow.js
index 9473dfa0..00219e19 100644
--- a/src/components/MovieRow.js
+++ b/src/components/MovieRow.js
@@ -11,7 +11,7 @@ export function MovieRow(props) {
let progress;
let percentage = null;
if(props.type === "movie") {
- progress = progressData?.lookmovie?.movie?.[props.slug]?.full
+ progress = progressData?.[props.source]?.movie?.[props.slug]?.full
if(progress) {
percentage = Math.floor((progress.currentlyAt / progress.totalDuration) * 100)
}
@@ -24,6 +24,7 @@ export function MovieRow(props) {
({props.year})
+
{props.source}
Watch {props.type}
diff --git a/src/components/TypeSelector.js b/src/components/TypeSelector.js
index 05f545e3..2af819f4 100644
--- a/src/components/TypeSelector.js
+++ b/src/components/TypeSelector.js
@@ -1,5 +1,4 @@
import React from 'react';
-// import { Arrow } from './Arrow';
import './TypeSelector.css'
// setType: (txt: string) => void
diff --git a/src/components/VideoElement.js b/src/components/VideoElement.js
index 35ec011a..2f3863fc 100644
--- a/src/components/VideoElement.js
+++ b/src/components/VideoElement.js
@@ -1,8 +1,9 @@
import React from 'react'
import Hls from 'hls.js'
-import './VideoElement.css'
import { VideoPlaceholder } from './VideoPlaceholder'
+import './VideoElement.css'
+
// streamUrl: string
// loading: boolean
export function VideoElement({ streamUrl, loading, setProgress }) {
@@ -10,21 +11,23 @@ export function VideoElement({ streamUrl, loading, setProgress }) {
const [error, setError] = React.useState(false);
React.useEffect(() => {
- 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;
+ if (!streamUrl.endsWith('.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);
}
-
- hls.attachMedia(videoRef.current);
- hls.loadSource(streamUrl);
}, [videoRef, streamUrl, loading])
if (error)
@@ -36,7 +39,15 @@ export function VideoElement({ streamUrl, loading, setProgress }) {
if (!streamUrl || streamUrl.length === 0)
return No video selected
- return (
-
- )
+ if (!streamUrl.endsWith('.mp4')) {
+ return (
+
+ )
+ } else {
+ return (
+
+ )
+ }
}
diff --git a/src/lib/gomostream.js b/src/lib/gomostream.js
new file mode 100644
index 00000000..fcd5ee97
--- /dev/null
+++ b/src/lib/gomostream.js
@@ -0,0 +1,92 @@
+import { unpack } from './util/unpacker';
+
+const CORS_URL = 'https://hidden-inlet-27205.herokuapp.com/';
+const BASE_URL = `${CORS_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(`${CORS_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], source: 'gomostream' } ] }
+ }
+ } 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(`${CORS_URL}${embedUrl}`).then((d) => d.text());
+
+ const parser = new DOMParser();
+ const site2Dom = parser.parseFromString(site2, "text/html");
+
+ console.log(site2Dom.body)
+
+ 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;
\ No newline at end of file
diff --git a/src/lib/index.js b/src/lib/index.js
new file mode 100644
index 00000000..27915eca
--- /dev/null
+++ b/src/lib/index.js
@@ -0,0 +1,44 @@
+import lookMovie from './lookMovie';
+import gomostream from './gomostream';
+
+async function findContent(searchTerm, type) {
+ const results = { options: []};
+ const content = await Promise.all([
+ lookMovie.findContent(searchTerm, type),
+ gomostream.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 'gomostream':
+ return await gomostream.getStreamUrl(slug, type, season, episode);
+ default:
+ return;
+ }
+}
+
+async function getEpisodes(slug, source) {
+ switch (source) {
+ case 'lookmovie':
+ return await lookMovie.getEpisodes(slug);
+ case 'gomostream':
+ default:
+ return;
+ }
+}
+
+export { findContent, getStreamUrl, getEpisodes }
\ No newline at end of file
diff --git a/src/lib/lookMovie.js b/src/lib/lookMovie.js
index 6166c7dc..0aa8ab9d 100644
--- a/src/lib/lookMovie.js
+++ b/src/lib/lookMovie.js
@@ -17,23 +17,19 @@ async function getVideoUrl(config) {
url = getCorsUrl(`https://lookmovie.io/manifests/shows/json/${accessToken}/${now}/${config.id}/master.m3u8`);
}
- if (url) {
- const videoOpts = await fetch(url).then((d) => d.json());
+ const videoOpts = await fetch(url).then((d) => d.json());
- // Find video URL and return it (with a check for a full url if needed)
- const opts = ["1080p", "1080", "720p", "720", "480p", "480", "auto"]
+ // 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 (videoOpts[res] && !videoOpts[res].includes('dummy') && !videoOpts[res].includes('earth-1984') && !videoUrl) {
- videoUrl = videoOpts[res]
- }
+ let videoUrl = "";
+ for (let res of opts) {
+ if (videoOpts[res] && !videoOpts[res].includes('dummy') && !videoOpts[res].includes('earth-1984') && !videoUrl) {
+ videoUrl = videoOpts[res]
}
-
- return videoUrl.startsWith("/") ? getCorsUrl(`https://lookmovie.io/${videoUrl}`) : getCorsUrl(videoUrl);
}
- return "Invalid type.";
+ return videoUrl.startsWith("/") ? `https://lookmovie.io${videoUrl}` : videoUrl;
}
async function getAccessToken(config) {
@@ -66,7 +62,18 @@ async function getEpisodes(slug) {
"}"
);
- return data.seasons
+ 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) {
@@ -85,14 +92,14 @@ async function getStreamUrl(slug, type, season, episode) {
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; });
+ 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;
- }
- }
+ id = episodeObj.id_episode;
+ }
+ }
if (id === '') {
return { url: '' }
@@ -107,11 +114,10 @@ async function getStreamUrl(slug, type, season, episode) {
return { url: videoUrl }
}
-async function findContent(searchTerm, type) {
- // const searchUrl = getCorsUrl(`https://lookmovie.io/api/v1/${type}s/search/?q=${encodeURIComponent(searchTerm)}`);
+async function findContent(searchTerm, type) {
const searchUrl = getCorsUrl(`https://lookmovie.io/${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");
@@ -136,22 +142,24 @@ async function findContent(searchTerm, type) {
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
+ year: r.year,
+ source: 'lookmovie'
}));
return res;
} else {
const { title, slug, type, year } = matchedResults[0];
-
+
return {
- options: [{ title, slug, type, year }]
+ options: [{ title, slug, type, year, source: 'lookmovie' }]
}
}
}
-export { findContent, getStreamUrl, getEpisodes };
\ No newline at end of file
+const lookMovie = { findContent, getStreamUrl, getEpisodes };
+export default lookMovie;
\ No newline at end of file
diff --git a/src/lib/util/unpacker.js b/src/lib/util/unpacker.js
new file mode 100644
index 00000000..da7e1e51
--- /dev/null
+++ b/src/lib/util/unpacker.js
@@ -0,0 +1,53 @@
+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 };
\ No newline at end of file
diff --git a/src/views/Movie.js b/src/views/Movie.js
index ac8db275..4ad94786 100644
--- a/src/views/Movie.js
+++ b/src/views/Movie.js
@@ -4,8 +4,9 @@ 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 './Movie.css'
-import { getStreamUrl } from '../lib/lookMovie'
export function MovieView(props) {
const { streamUrl, streamData, setStreamUrl } = useMovie();
@@ -42,7 +43,7 @@ export function MovieView(props) {
}
setLoading(true);
- getStreamUrl(streamData.slug, streamData.type, episode.season, episode.episode)
+ getStreamUrl(streamData.slug, streamData.type, streamData.source, episode.season, episode.episode)
.then(({url}) => {
if (cancel) return;
setStreamUrl(url)
@@ -105,6 +106,7 @@ export function MovieView(props) {
slug={streamData.slug}
currentSeason={season}
currentEpisode={episode}
+ source={streamData.source}
/>
: ''}
diff --git a/src/views/Search.js b/src/views/Search.js
index c0232aea..b2a0da8a 100644
--- a/src/views/Search.js
+++ b/src/views/Search.js
@@ -1,16 +1,16 @@
import React from 'react';
-import { InputBox } from '../components/InputBox'
-import { Title } from '../components/Title'
-import { Card } from '../components/Card'
-import { ErrorBanner } from '../components/ErrorBanner'
-import { MovieRow } from '../components/MovieRow'
-import { Arrow } from '../components/Arrow'
-import { Progress } from '../components/Progress'
-import { findContent, getStreamUrl, getEpisodes } from '../lib/lookMovie'
+import { InputBox } from '../components/InputBox';
+import { Title } from '../components/Title';
+import { Card } from '../components/Card';
+import { ErrorBanner } from '../components/ErrorBanner';
+import { MovieRow } from '../components/MovieRow';
+import { Arrow } from '../components/Arrow';
+import { Progress } from '../components/Progress';
+import { findContent, getStreamUrl, getEpisodes } from '../lib/index';
import { useMovie } from '../hooks/useMovie';
-import { TypeSelector } from '../components/TypeSelector'
+import { TypeSelector } from '../components/TypeSelector';
-import './Search.css'
+import './Search.css';
export function SearchView() {
const { navigate, setStreamUrl, setStreamData } = useMovie();
@@ -30,7 +30,7 @@ export function SearchView() {
setFailed(true)
}
- async function getStream(title, slug, type) {
+ async function getStream(title, slug, type, source) {
setStreamUrl("");
try {
@@ -40,21 +40,15 @@ export function SearchView() {
let seasons = [];
let episodes = [];
if (type === "show") {
- const episodeData = await getEpisodes(slug);
- episodeData.forEach((e) => {
- if (!seasons.includes(e.season))
- seasons.push(e.season);
-
- if (!episodes[e.season])
- episodes[e.season] = []
- episodes[e.season].push(e.episode)
-
- })
+ const data = await getEpisodes(slug, source);
+ seasons = data.seasons;
+ episodes = data.episodes;
}
let realUrl = '';
if (type === "movie") {
- const { url } = await getStreamUrl(slug, type);
+ // getStreamUrl(slug, type, source, season, episode)
+ const { url } = await getStreamUrl(slug, type, source);
if (url === '') {
return fail(`Not found: ${title}`)
@@ -69,11 +63,13 @@ export function SearchView() {
type,
seasons,
episodes,
- slug
+ slug,
+ source
})
setText(`Streaming...`)
navigate("movie")
} catch (err) {
+ console.error(err);
fail("Failed to get stream")
}
}
@@ -85,7 +81,7 @@ export function SearchView() {
setShowingOptions(false)
try {
- const { options } = await findContent(query, contentType)
+ const { options } = await findContent(query, contentType);
if (options.length === 0) {
return fail(`Could not find that ${contentType}`)
@@ -97,9 +93,10 @@ export function SearchView() {
return;
}
- const { title, slug, type } = options[0];
- getStream(title, slug, type);
+ const { title, slug, type, source } = options[0];
+ getStream(title, slug, type, source);
} catch (err) {
+ console.error(err);
fail(`Failed to watch ${contentType}`)
}
}
@@ -140,9 +137,9 @@ export function SearchView() {
Whoops, there are a few {type}s like that
{options?.map((v, i) => (
- {
+ {
setShowingOptions(false)
- getStream(v.title, v.slug, v.type, v.season, v.episode)
+ getStream(v.title, v.slug, v.type, v.source)
}}/>
))}
diff --git a/yarn.lock b/yarn.lock
index 7b138a93..786af3aa 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3654,6 +3654,11 @@ crypto-browserify@^3.11.0:
randombytes "^2.0.0"
randomfill "^1.0.3"
+crypto-js@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.0.0.tgz#2904ab2677a9d042856a2ea2ef80de92e4a36dcc"
+ integrity sha512-bzHZN8Pn+gS7DQA6n+iUmBfl0hO5DJq++QP3U6uTucDtk/0iGpXd/Gg7CGR0p8tJhofJyaKoWBuJI4eAO00BBg==
+
crypto-random-string@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e"