From e52b29a1a1809f6ffd451cbb20a75c4061d8b1f9 Mon Sep 17 00:00:00 2001 From: JORDAAR <69628820+Jordaar@users.noreply.github.com> Date: Wed, 19 Apr 2023 15:44:20 +0530 Subject: [PATCH 1/4] add hdwatched provider --- src/backend/index.ts | 1 + src/backend/providers/hdwatched.ts | 105 +++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) create mode 100644 src/backend/providers/hdwatched.ts diff --git a/src/backend/index.ts b/src/backend/index.ts index 7a13a445..5cf50906 100644 --- a/src/backend/index.ts +++ b/src/backend/index.ts @@ -6,6 +6,7 @@ import "./providers/flixhq"; import "./providers/superstream"; import "./providers/netfilm"; import "./providers/m4ufree"; +import "./providers/hdwatched"; // embeds import "./embeds/streamm4u"; diff --git a/src/backend/providers/hdwatched.ts b/src/backend/providers/hdwatched.ts new file mode 100644 index 00000000..08a3935b --- /dev/null +++ b/src/backend/providers/hdwatched.ts @@ -0,0 +1,105 @@ +import { proxiedFetch } from "../helpers/fetch"; +import { registerProvider } from "../helpers/register"; +import { MWStreamQuality, MWStreamType } from "../helpers/streams"; +import { MWMediaType } from "../metadata/types"; + +const hdwatchedBase = "https://www.hdwatched.xyz"; + +const qualityMap: Record = { + 360: MWStreamQuality.Q360P, + 540: MWStreamQuality.Q540P, + 480: MWStreamQuality.Q480P, + 720: MWStreamQuality.Q720P, + 1080: MWStreamQuality.Q1080P, +}; + +interface MovieSearchList { + title: string; + id: string; + year: number; +} + +registerProvider({ + id: "hdwatched", + displayName: "HDwatched", + rank: 50, + type: [MWMediaType.MOVIE], + async scrape({ media, progress }) { + if (!this.type.includes(media.meta.type)) { + throw new Error("Unsupported type"); + } + + progress(20); + + const search = await proxiedFetch(`/search/${media.imdbId}`, { + baseURL: hdwatchedBase, + }); + + const searchPage = new DOMParser().parseFromString(search, "text/html"); + const movieElements = searchPage.querySelectorAll("div.i-container"); + + const movieSearchList: MovieSearchList[] = []; + movieElements.forEach((movieElement) => { + const href = movieElement.querySelector("a")?.getAttribute("href") || ""; + const title = + movieElement?.querySelector("span.content-title")?.textContent || ""; + const year = + parseInt( + movieElement + ?.querySelector("div.duration") + ?.textContent?.trim() + ?.split(" ") + ?.pop() || "", + 10 + ) || 0; + + movieSearchList.push({ + title, + year, + id: href.split("/")[2], // Format: /free/{id}}/{movie-slug} | Example: /free/18804/iron-man-231 + }); + }); + + progress(50); + + const targetMovie = movieSearchList.find( + (movie) => movie.year === (media.meta.year ? +media.meta.year : 0) // Compare year to make the search more robust + ); + + if (!targetMovie) { + throw new Error("Could not find stream"); + } + + const stream = await proxiedFetch(`/embed/${targetMovie.id}`, { + baseURL: hdwatchedBase, + }); + + progress(80); + + const embedPage = new DOMParser().parseFromString(stream, "text/html"); + const source = embedPage.querySelector("#vjsplayer > source"); + if (!source) { + throw new Error("Could not find stream"); + } + + const streamSrc = source.getAttribute("src"); + const streamRes = source.getAttribute("res"); + + if (!streamSrc) { + throw new Error("Could not find stream"); + } + + return { + embeds: [], + stream: { + streamUrl: streamSrc, + quality: + streamRes && typeof +streamRes === "number" + ? qualityMap[+streamRes] + : MWStreamQuality.QUNKNOWN, + type: MWStreamType.MP4, + captions: [], + }, + }; + }, +}); From 7b75c36d2149a977058b2df820eb04ff9d68419d Mon Sep 17 00:00:00 2001 From: JORDAAR <69628820+Jordaar@users.noreply.github.com> Date: Thu, 20 Apr 2023 15:53:28 +0530 Subject: [PATCH 2/4] add series support & improvements --- src/backend/providers/hdwatched.ts | 169 ++++++++++++++++++++++------- 1 file changed, 130 insertions(+), 39 deletions(-) diff --git a/src/backend/providers/hdwatched.ts b/src/backend/providers/hdwatched.ts index 08a3935b..65e0c356 100644 --- a/src/backend/providers/hdwatched.ts +++ b/src/backend/providers/hdwatched.ts @@ -1,4 +1,5 @@ import { proxiedFetch } from "../helpers/fetch"; +import { MWProviderContext } from "../helpers/provider"; import { registerProvider } from "../helpers/register"; import { MWStreamQuality, MWStreamType } from "../helpers/streams"; import { MWMediaType } from "../metadata/types"; @@ -13,39 +14,134 @@ const qualityMap: Record = { 1080: MWStreamQuality.Q1080P, }; -interface MovieSearchList { +interface SearchRes { title: string; + year?: number; + href: string; id: string; - year: number; +} + +function getStreamFromEmbed(stream: string) { + const embedPage = new DOMParser().parseFromString(stream, "text/html"); + const source = embedPage.querySelector("#vjsplayer > source"); + if (!source) { + throw new Error("Unable to fetch stream"); + } + + const streamSrc = source.getAttribute("src"); + const streamRes = source.getAttribute("res"); + + if (!streamSrc || !streamRes) throw new Error("Unable to find stream"); + + return { + streamUrl: streamSrc, + quality: + streamRes && typeof +streamRes === "number" + ? qualityMap[+streamRes] + : MWStreamQuality.QUNKNOWN, + }; +} + +async function fetchMovie(targetSource: SearchRes) { + const stream = await proxiedFetch(`/embed/${targetSource.id}`, { + baseURL: hdwatchedBase, + }); + + const embedPage = new DOMParser().parseFromString(stream, "text/html"); + const source = embedPage.querySelector("#vjsplayer > source"); + if (!source) { + throw new Error("Unable to fetch movie stream"); + } + + return getStreamFromEmbed(stream); +} + +async function fetchSeries( + targetSource: SearchRes, + { media, episode, progress }: MWProviderContext +) { + if (media.meta.type !== MWMediaType.SERIES) + throw new Error("Media type mismatch"); + + const seasonNumber = media.meta.seasonData.number; + const episodeNumber = media.meta.seasonData.episodes.find( + (e) => e.id === episode + )?.number; + + if (!seasonNumber || !episodeNumber) + throw new Error("Unable to get season or episode number"); + + const seriesPage = await proxiedFetch( + `${targetSource.href}?season=${media.meta.seasonData.number}`, + { + baseURL: hdwatchedBase, + } + ); + + const seasonPage = new DOMParser().parseFromString(seriesPage, "text/html"); + const pageElements = seasonPage.querySelectorAll("div.i-container"); + + const seriesList: SearchRes[] = []; + pageElements.forEach((pageElement) => { + const href = pageElement.querySelector("a")?.getAttribute("href") || ""; + const title = + pageElement?.querySelector("span.content-title")?.textContent || ""; + + seriesList.push({ + title, + href, + id: href.split("/")[2], // Format: /free/{id}/{series-slug}-season-{season-number}-episode-{episode-number} + }); + }); + + const targetEpisode = seriesList.find( + (episodeEl) => + episodeEl.title.trim().toLowerCase() === `episode ${episodeNumber}` + ); + + if (!targetEpisode) throw new Error("Unable to find episode"); + + progress(70); + + const stream = await proxiedFetch(`/embed/${targetEpisode.id}`, { + baseURL: hdwatchedBase, + }); + + const embedPage = new DOMParser().parseFromString(stream, "text/html"); + const source = embedPage.querySelector("#vjsplayer > source"); + if (!source) { + throw new Error("Unable to fetch movie stream"); + } + + return getStreamFromEmbed(stream); } registerProvider({ id: "hdwatched", displayName: "HDwatched", rank: 50, - type: [MWMediaType.MOVIE], - async scrape({ media, progress }) { + type: [MWMediaType.MOVIE, MWMediaType.SERIES], + async scrape(options) { + const { media, progress } = options; if (!this.type.includes(media.meta.type)) { throw new Error("Unsupported type"); } - progress(20); - const search = await proxiedFetch(`/search/${media.imdbId}`, { baseURL: hdwatchedBase, }); const searchPage = new DOMParser().parseFromString(search, "text/html"); - const movieElements = searchPage.querySelectorAll("div.i-container"); + const pageElements = searchPage.querySelectorAll("div.i-container"); - const movieSearchList: MovieSearchList[] = []; - movieElements.forEach((movieElement) => { - const href = movieElement.querySelector("a")?.getAttribute("href") || ""; + const searchList: SearchRes[] = []; + pageElements.forEach((pageElement) => { + const href = pageElement.querySelector("a")?.getAttribute("href") || ""; const title = - movieElement?.querySelector("span.content-title")?.textContent || ""; + pageElement?.querySelector("span.content-title")?.textContent || ""; const year = parseInt( - movieElement + pageElement ?.querySelector("div.duration") ?.textContent?.trim() ?.split(" ") @@ -53,50 +149,45 @@ registerProvider({ 10 ) || 0; - movieSearchList.push({ + searchList.push({ title, year, - id: href.split("/")[2], // Format: /free/{id}}/{movie-slug} | Example: /free/18804/iron-man-231 + href, + id: href.split("/")[2], // Format: /free/{id}/{movie-slug} or /series/{id}/{series-slug} }); }); - progress(50); + progress(20); - const targetMovie = movieSearchList.find( - (movie) => movie.year === (media.meta.year ? +media.meta.year : 0) // Compare year to make the search more robust + const targetSource = searchList.find( + (source) => source.year === (media.meta.year ? +media.meta.year : 0) // Compare year to make the search more robust ); - if (!targetMovie) { + if (!targetSource) { throw new Error("Could not find stream"); } - const stream = await proxiedFetch(`/embed/${targetMovie.id}`, { - baseURL: hdwatchedBase, - }); + progress(40); - progress(80); - - const embedPage = new DOMParser().parseFromString(stream, "text/html"); - const source = embedPage.querySelector("#vjsplayer > source"); - if (!source) { - throw new Error("Could not find stream"); - } - - const streamSrc = source.getAttribute("src"); - const streamRes = source.getAttribute("res"); - - if (!streamSrc) { - throw new Error("Could not find stream"); + if (media.meta.type === MWMediaType.SERIES) { + const series = await fetchSeries(targetSource, options); + return { + embeds: [], + stream: { + streamUrl: series.streamUrl, + quality: series.quality, + type: MWStreamType.MP4, + captions: [], + }, + }; } + const movie = await fetchMovie(targetSource); return { embeds: [], stream: { - streamUrl: streamSrc, - quality: - streamRes && typeof +streamRes === "number" - ? qualityMap[+streamRes] - : MWStreamQuality.QUNKNOWN, + streamUrl: movie.streamUrl, + quality: movie.quality, type: MWStreamType.MP4, captions: [], }, From b26b0715bd913e144cdb4a463e17d9fc1933b4c2 Mon Sep 17 00:00:00 2001 From: JORDAAR <69628820+Jordaar@users.noreply.github.com> Date: Thu, 20 Apr 2023 22:26:54 +0530 Subject: [PATCH 3/4] increase rank --- src/backend/providers/hdwatched.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/backend/providers/hdwatched.ts b/src/backend/providers/hdwatched.ts index 65e0c356..cacaec41 100644 --- a/src/backend/providers/hdwatched.ts +++ b/src/backend/providers/hdwatched.ts @@ -119,7 +119,7 @@ async function fetchSeries( registerProvider({ id: "hdwatched", displayName: "HDwatched", - rank: 50, + rank: 150, type: [MWMediaType.MOVIE, MWMediaType.SERIES], async scrape(options) { const { media, progress } = options; From d6def996bf1b4384de81b351ef3e879b464224e2 Mon Sep 17 00:00:00 2001 From: mrjvs Date: Thu, 20 Apr 2023 21:26:37 +0200 Subject: [PATCH 4/4] bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 097149c6..2223761f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "movie-web", - "version": "3.0.10", + "version": "3.0.11", "private": true, "homepage": "https://movie.squeezebox.dev", "dependencies": {