diff --git a/package-lock.json b/package-lock.json index 87a5e4f..46747b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,18 @@ { "name": "@movie-web/providers", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@movie-web/providers", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { "cheerio": "^1.0.0-rc.12", "crypto-js": "^4.1.1", "form-data": "^4.0.0", + "json5": "^2.2.3", "nanoid": "^3.3.6", "node-fetch": "^2.7.0", "unpacker": "^1.0.1" @@ -4434,7 +4435,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "bin": { "json5": "lib/cli.js" }, diff --git a/package.json b/package.json index 04d1619..a0d31de 100644 --- a/package.json +++ b/package.json @@ -79,6 +79,7 @@ "cheerio": "^1.0.0-rc.12", "crypto-js": "^4.1.1", "form-data": "^4.0.0", + "json5": "^2.2.3", "nanoid": "^3.3.6", "node-fetch": "^2.7.0", "unpacker": "^1.0.1" diff --git a/src/providers/all.ts b/src/providers/all.ts index ef26094..e2c17f2 100644 --- a/src/providers/all.ts +++ b/src/providers/all.ts @@ -7,13 +7,22 @@ import { upstreamScraper } from '@/providers/embeds/upstream'; import { flixhqScraper } from '@/providers/sources/flixhq/index'; import { goMoviesScraper } from '@/providers/sources/gomovies/index'; import { kissAsianScraper } from '@/providers/sources/kissasian/index'; +import { lookmovieScraper } from '@/providers/sources/lookmovie'; import { remotestreamScraper } from '@/providers/sources/remotestream'; import { superStreamScraper } from '@/providers/sources/superstream/index'; import { zoechipScraper } from '@/providers/sources/zoechip'; export function gatherAllSources(): Array { // all sources are gathered here - return [flixhqScraper, remotestreamScraper, kissAsianScraper, superStreamScraper, goMoviesScraper, zoechipScraper]; + return [ + flixhqScraper, + remotestreamScraper, + kissAsianScraper, + superStreamScraper, + goMoviesScraper, + zoechipScraper, + lookmovieScraper, + ]; } export function gatherAllEmbeds(): Array { diff --git a/src/providers/sources/lookmovie/index.ts b/src/providers/sources/lookmovie/index.ts new file mode 100644 index 0000000..85b9d1d --- /dev/null +++ b/src/providers/sources/lookmovie/index.ts @@ -0,0 +1,31 @@ +import { SourcererOutput, makeSourcerer } from '@/providers/base'; +import { NotFoundError } from '@/utils/errors'; + +import { scrape, searchAndFindMedia } from './util'; +import { MovieContext, ShowContext } from '../zoechip/common'; + +async function universalScraper(ctx: ShowContext | MovieContext): Promise { + const lookmovieData = await searchAndFindMedia(ctx.media); + if (!lookmovieData) throw new NotFoundError('Media not found'); + + const videoUrl = await scrape(ctx.media, lookmovieData); + if (!videoUrl) throw new NotFoundError('No video found'); + + return { + embeds: [], + stream: { + playlist: videoUrl, + type: 'hls', + flags: [], + }, + }; +} + +export const lookmovieScraper = makeSourcerer({ + id: 'lookmovie', + name: 'LookMovie', + rank: 1, + flags: [], + scrapeShow: universalScraper, + scrapeMovie: universalScraper, +}); diff --git a/src/providers/sources/lookmovie/type.ts b/src/providers/sources/lookmovie/type.ts new file mode 100644 index 0000000..ddac5ca --- /dev/null +++ b/src/providers/sources/lookmovie/type.ts @@ -0,0 +1,31 @@ +// ! Types +interface BaseConfig { + /** The website's slug. Formatted as `1839578-person-of-interest-2011` */ + slug: string; + /** Type of request */ + type: 'show' | 'movie'; + /** Hash */ + hash: string; + /** Hash expiry */ + expires: number; +} +interface TvConfig extends BaseConfig { + /** Type of request */ + type: 'show'; + /** The episode ID for a TV show. Given in search and URL */ + episodeId: string; +} +interface MovieConfig extends BaseConfig { + /** Type of request */ + type: 'movie'; + /** Movie's id */ + id_movie: string; +} +export type Config = MovieConfig | TvConfig; + +export interface Result { + title: string; + slug: string; + year: string; + id_movie?: string; +} diff --git a/src/providers/sources/lookmovie/util.ts b/src/providers/sources/lookmovie/util.ts new file mode 100644 index 0000000..4bde2ea --- /dev/null +++ b/src/providers/sources/lookmovie/util.ts @@ -0,0 +1,68 @@ +import json5 from 'json5'; + +import { MovieMedia, ShowMedia } from '@/main/media'; +import { compareMedia } from '@/utils/compare'; +import { NotFoundError } from '@/utils/errors'; + +import { Result } from './type'; +import { getVideoUrl } from './video'; + +export async function searchAndFindMedia(media: MovieMedia | ShowMedia): Promise { + const searchRes = await fetch( + `https://lookmovie2.to/api/v1/${media.type}s/do-search/?q=${encodeURIComponent(media.title)}`, + ).then((d) => d.json()); + const results = searchRes.result; + const result = results.find((res: Result) => compareMedia(media, res.title, Number(res.year))); + return result; +} + +export async function scrape(media: MovieMedia | ShowMedia, result: Result) { + const url = `https://www.lookmovie2.to/${media.type}s/play/${result.slug}`; + const pageReq = await fetch(url).then((d) => d.text()); + + // Extract and parse JSON + const scriptJson = `{${pageReq + .slice(pageReq.indexOf(`${media.type}_storage`)) + .split('};')[0] + .split('= {')[1] + .trim()}}`; + const data = json5.parse(scriptJson); + + // Find the relevant id + let id = null; + if (media.type === 'movie') { + id = result.id_movie; + } else if (media.type === 'show') { + const episodeObj = data.seasons.find((v: any) => { + return Number(v.season) === Number(media.season.number) && Number(v.episode) === Number(media.episode.number); + }); + + if (episodeObj) id = episodeObj.id_episode; + } + + // Check ID + if (id === null) throw new NotFoundError('Not found'); + + // Generate object to send over to scraper + let reqObj = null; + if (media.type === 'show') { + reqObj = { + slug: result.slug, + episodeId: id, + type: 'tv', + ...data, + }; + } else if (media.type === 'movie') { + reqObj = { + slug: result.slug, + movieId: id, + type: 'movie', + ...data, + }; + } + + if (!reqObj) throw new NotFoundError('Invalid media type'); + + const videoUrl = await getVideoUrl(reqObj); + return videoUrl; +} diff --git a/src/providers/sources/lookmovie/video.ts b/src/providers/sources/lookmovie/video.ts new file mode 100644 index 0000000..c5e71c4 --- /dev/null +++ b/src/providers/sources/lookmovie/video.ts @@ -0,0 +1,31 @@ +import { Config } from './type'; + +export async function getVideoSources(config: Config): Promise { + // Fetch video sources + let url = ''; + if (config.type === 'show') { + url = `https://www.lookmovie2.to/api/v1/security/episode-access?id_episode=${config.episodeId}&hash=${config.hash}&expires=${config.expires}`; + } else if (config.type === 'movie') { + url = `https://www.lookmovie2.to/api/v1/security/movie-access?id_movie=${config.id_movie}&hash=${config.hash}&expires=${config.expires}`; + } + const data = await fetch(url).then((d) => d.json()); + return data; +} + +export async function getVideoUrl(config: Config): Promise { + // Get sources + const data = await getVideoSources(config); + const videoSources = data.streams; + + // Find video URL and return it + const opts = ['1080p', '1080', '720p', '720', '480p', '480', 'auto']; + + let videoUrl: string | null = null; + for (const res of opts) { + if (videoSources[res] && !videoUrl) { + videoUrl = videoSources[res]; + } + } + + return videoUrl; +}