mirror of
https://github.com/sussy-code/smov.git
synced 2024-12-21 14:47:41 +01:00
start on jons providers
Co-authored-by: Jip Frijlink <JipFr@users.noreply.github.com> Co-authored-by: Jonathan Barrow <jonbarrow@users.noreply.github.com>
This commit is contained in:
parent
b43f39b007
commit
4a35287975
6 changed files with 313 additions and 11 deletions
20
src/backend/embeds/playm4u.ts
Normal file
20
src/backend/embeds/playm4u.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { MWEmbedType } from "@/backend/helpers/embed";
|
||||||
|
import { MWMediaType } from "../metadata/types";
|
||||||
|
import { registerEmbedScraper } from "@/backend/helpers/register";
|
||||||
|
import { MWStreamQuality, MWStreamType } from "@/backend/helpers/streams";
|
||||||
|
|
||||||
|
registerEmbedScraper({
|
||||||
|
id: "playm4u",
|
||||||
|
displayName: "playm4u",
|
||||||
|
for: MWEmbedType.PLAYM4U,
|
||||||
|
rank: 0,
|
||||||
|
async getStream(ctx) {
|
||||||
|
throw new Error("Oh well 2")
|
||||||
|
return {
|
||||||
|
streamUrl: '',
|
||||||
|
quality: MWStreamQuality.Q1080P,
|
||||||
|
captions: [],
|
||||||
|
type: MWStreamType.MP4,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
71
src/backend/embeds/streamm4u.ts
Normal file
71
src/backend/embeds/streamm4u.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import { MWEmbedType } from "@/backend/helpers/embed";
|
||||||
|
import { MWMediaType } from "../metadata/types";
|
||||||
|
import { registerEmbedScraper } from "@/backend/helpers/register";
|
||||||
|
import { MWStreamQuality, MWStreamType, MWStream } from "@/backend/helpers/streams";
|
||||||
|
import { proxiedFetch } from "@/backend/helpers/fetch";
|
||||||
|
|
||||||
|
const HOST = 'streamm4u.club';
|
||||||
|
const URL_BASE = `https://${HOST}`;
|
||||||
|
const URL_API = `${URL_BASE}/api`;
|
||||||
|
const URL_API_SOURCE = `${URL_API}/source`;
|
||||||
|
|
||||||
|
// TODO check out 403 / 404 on successfully returned video stream URLs
|
||||||
|
registerEmbedScraper({
|
||||||
|
id: "streamm4u",
|
||||||
|
displayName: "streamm4u",
|
||||||
|
for: MWEmbedType.STREAMM4U,
|
||||||
|
rank: 100,
|
||||||
|
async getStream({ progress, url }) {
|
||||||
|
|
||||||
|
const scrapingThreads = [];
|
||||||
|
let streams = [];
|
||||||
|
|
||||||
|
const sources = (await scrape(url)).sort((a, b) => Number(b.quality.replace("p", "")) - Number(a.quality.replace("p", "")));
|
||||||
|
let preferredSourceIndex = 0;
|
||||||
|
let preferredSource;
|
||||||
|
|
||||||
|
while (!preferredSource && sources[preferredSourceIndex]) {
|
||||||
|
console.log('Testing', preferredSourceIndex)
|
||||||
|
console.log(sources[preferredSourceIndex]?.streamUrl)
|
||||||
|
// try {
|
||||||
|
// await proxiedFetch(sources[preferredSourceIndex]?.streamUrl)
|
||||||
|
// } catch (err) { }
|
||||||
|
preferredSource = sources[0]
|
||||||
|
preferredSourceIndex++
|
||||||
|
}
|
||||||
|
console.log(preferredSource)
|
||||||
|
|
||||||
|
if (!preferredSource) throw new Error("No source found")
|
||||||
|
|
||||||
|
progress(100)
|
||||||
|
|
||||||
|
return preferredSource
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
async function scrape(embed: string) {
|
||||||
|
const sources: MWStream[] = [];
|
||||||
|
|
||||||
|
const embedID = embed.split('/').pop();
|
||||||
|
|
||||||
|
console.log(`${URL_API_SOURCE}/${embedID}`)
|
||||||
|
const json = await proxiedFetch<any>(`${URL_API_SOURCE}/${embedID}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: `r=&d=${HOST}`
|
||||||
|
});
|
||||||
|
|
||||||
|
if (json.success) {
|
||||||
|
const streams = json.data;
|
||||||
|
|
||||||
|
for (const stream of streams) {
|
||||||
|
sources.push({
|
||||||
|
streamUrl: stream.file as string,
|
||||||
|
quality: stream.label as MWStreamQuality,
|
||||||
|
type: stream.type as MWStreamType,
|
||||||
|
captions: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sources;
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
import { MWStream } from "./streams";
|
import { MWStream } from "./streams";
|
||||||
|
|
||||||
export enum MWEmbedType {
|
export enum MWEmbedType {
|
||||||
OPENLOAD = "openload",
|
M4UFREE = "m4ufree",
|
||||||
|
STREAMM4U = "streamm4u",
|
||||||
|
PLAYM4U = "playm4u"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MWEmbed = {
|
export type MWEmbed = {
|
||||||
|
|
|
@ -25,15 +25,15 @@ type MWProviderRunContextBase = {
|
||||||
};
|
};
|
||||||
type MWProviderRunContextTypeSpecific =
|
type MWProviderRunContextTypeSpecific =
|
||||||
| {
|
| {
|
||||||
type: MWMediaType.MOVIE | MWMediaType.ANIME;
|
type: MWMediaType.MOVIE | MWMediaType.ANIME;
|
||||||
episode: undefined;
|
episode: undefined;
|
||||||
season: undefined;
|
season: undefined;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: MWMediaType.SERIES;
|
type: MWMediaType.SERIES;
|
||||||
episode: string;
|
episode: string;
|
||||||
season: string;
|
season: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MWProviderRunContext = MWProviderRunContextBase &
|
export type MWProviderRunContext = MWProviderRunContextBase &
|
||||||
MWProviderRunContextTypeSpecific;
|
MWProviderRunContextTypeSpecific;
|
||||||
|
@ -50,7 +50,7 @@ async function findBestEmbedStream(
|
||||||
embedNum += 1;
|
embedNum += 1;
|
||||||
if (!embed.type) continue;
|
if (!embed.type) continue;
|
||||||
const scraper = getEmbedScraperByType(embed.type);
|
const scraper = getEmbedScraperByType(embed.type);
|
||||||
if (!scraper) throw new Error("Type for embed not found");
|
if (!scraper) throw new Error("Type for embed not found: " + embed.type);
|
||||||
|
|
||||||
const eventId = [providerId, scraper.id, embedNum].join("|");
|
const eventId = [providerId, scraper.id, embedNum].join("|");
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,10 @@ import "./providers/gdriveplayer";
|
||||||
import "./providers/flixhq";
|
import "./providers/flixhq";
|
||||||
import "./providers/superstream";
|
import "./providers/superstream";
|
||||||
import "./providers/netfilm";
|
import "./providers/netfilm";
|
||||||
|
import "./providers/m4ufree";
|
||||||
|
|
||||||
// embeds
|
// embeds
|
||||||
// -- nothing here yet
|
import "./embeds/streamm4u"
|
||||||
|
import "./embeds/playm4u"
|
||||||
|
|
||||||
initializeScraperStore();
|
initializeScraperStore();
|
||||||
|
|
207
src/backend/providers/m4ufree.ts
Normal file
207
src/backend/providers/m4ufree.ts
Normal file
|
@ -0,0 +1,207 @@
|
||||||
|
import { compareTitle } from "@/utils/titleMatch";
|
||||||
|
import { MWEmbedType } from "../helpers/embed";
|
||||||
|
import { proxiedFetch } from "../helpers/fetch";
|
||||||
|
import { registerProvider } from "../helpers/register";
|
||||||
|
import { MWMediaType } from "../metadata/types";
|
||||||
|
import { MWEmbed } from "@/backend/helpers/embed";
|
||||||
|
|
||||||
|
const HOST = 'm4ufree.com';
|
||||||
|
const URL_BASE = `https://${HOST}`;
|
||||||
|
const URL_SEARCH = `${URL_BASE}/search`;
|
||||||
|
const URL_AJAX = `${URL_BASE}/ajax`;
|
||||||
|
const URL_AJAX_TV = `${URL_BASE}/ajaxtv`;
|
||||||
|
|
||||||
|
// * Years can be in one of 4 formats:
|
||||||
|
// * - "startyear" (for movies, EX: 2022)
|
||||||
|
// * - "startyear-" (for TV series which has not ended, EX: 2022-)
|
||||||
|
// * - "startyear-endyear" (for TV series which has ended, EX: 2022-2023)
|
||||||
|
// * - "startyearendyear" (for TV series which has ended, EX: 20222023)
|
||||||
|
const REGEX_TITLE_AND_YEAR = /(.*) \(?(\d*|\d*-|\d*-\d*)\)?$/;
|
||||||
|
const REGEX_TYPE = /.*-(movie|tvshow)-online-free-m4ufree\.html/;
|
||||||
|
const REGEX_COOKIES = /XSRF-TOKEN=(.*?);.*laravel_session=(.*?);/;
|
||||||
|
const REGEX_SEASON_EPISODE = /S(\d*)-E(\d*)/;
|
||||||
|
|
||||||
|
function toDom(html: string) {
|
||||||
|
return new DOMParser().parseFromString(html, "text/html")
|
||||||
|
}
|
||||||
|
|
||||||
|
registerProvider({
|
||||||
|
id: "m4ufree",
|
||||||
|
displayName: "m4ufree",
|
||||||
|
rank: -1,
|
||||||
|
type: [MWMediaType.MOVIE, MWMediaType.SERIES],
|
||||||
|
|
||||||
|
async scrape({ media, progress, type, episode: episodeId, season: seasonId }) {
|
||||||
|
const season = media.meta.seasons?.find(s => s.id === seasonId)?.number || 1
|
||||||
|
const episode = media.meta.type === MWMediaType.SERIES ? media.meta.seasonData.episodes.find(ep => ep.id === episodeId)?.number || 1 : undefined
|
||||||
|
|
||||||
|
const embeds: MWEmbed[] = [];
|
||||||
|
|
||||||
|
/*
|
||||||
|
, {
|
||||||
|
responseType: "text" as any,
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let responseText = await proxiedFetch<string>(`${URL_SEARCH}/${encodeURIComponent(media.meta.title)}.html`);
|
||||||
|
let dom = toDom(responseText);
|
||||||
|
|
||||||
|
const searchResults = [...dom.querySelectorAll('.item')].map(element => {
|
||||||
|
const tooltipText = element.querySelector('.tiptitle p')?.innerHTML;
|
||||||
|
if (!tooltipText) return;
|
||||||
|
|
||||||
|
let regexResult = REGEX_TITLE_AND_YEAR.exec(tooltipText);
|
||||||
|
|
||||||
|
if (!regexResult || !regexResult[1] || !regexResult[2]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = regexResult[1];
|
||||||
|
const year = Number(regexResult[2].slice(0, 4)); // * Some media stores the start AND end year. Only need start year
|
||||||
|
const a = element.querySelector('a');
|
||||||
|
if (!a) return;
|
||||||
|
const href = a.href;
|
||||||
|
|
||||||
|
regexResult = REGEX_TYPE.exec(href);
|
||||||
|
|
||||||
|
if (!regexResult || !regexResult[1]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scraperDeterminedType = regexResult[1];
|
||||||
|
|
||||||
|
scraperDeterminedType = scraperDeterminedType === 'tvshow' ? 'show' : 'movie'; // * Map to Trakt type
|
||||||
|
|
||||||
|
return { type: scraperDeterminedType, title, year, href };
|
||||||
|
}).filter(item => item);
|
||||||
|
|
||||||
|
const mediaInResults = searchResults.find(item => item && item.title === media.meta.title && item.year.toString() === media.meta.year);
|
||||||
|
|
||||||
|
if (!mediaInResults) {
|
||||||
|
// * Nothing found
|
||||||
|
return {
|
||||||
|
embeds,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let cookies: string | null = '';
|
||||||
|
const responseTextFromMedia = await proxiedFetch<string>(mediaInResults.href, {
|
||||||
|
onResponse(context) {
|
||||||
|
cookies = context.response.headers.get('X-Set-Cookie')
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dom = toDom(responseTextFromMedia);
|
||||||
|
|
||||||
|
let regexResult = REGEX_COOKIES.exec(cookies);
|
||||||
|
|
||||||
|
if (!regexResult || !regexResult[1] || !regexResult[2]) {
|
||||||
|
// * DO SOMETHING?
|
||||||
|
throw new Error("No regexResults, yikesssssss kinda gross idk")
|
||||||
|
}
|
||||||
|
|
||||||
|
const cookieHeader = `XSRF-TOKEN=${regexResult[1]}; laravel_session=${regexResult[2]}`;
|
||||||
|
|
||||||
|
const token = dom.querySelector('meta[name="csrf-token"]')?.getAttribute("content");
|
||||||
|
if (!token) return { embeds };
|
||||||
|
|
||||||
|
if (type === MWMediaType.SERIES) {
|
||||||
|
// * Get the season/episode data
|
||||||
|
const episodes = [...dom.querySelectorAll('.episode')].map(element => {
|
||||||
|
regexResult = REGEX_SEASON_EPISODE.exec(element.innerHTML);
|
||||||
|
|
||||||
|
if (!regexResult || !regexResult[1] || !regexResult[2]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const episode = Number(regexResult[1]);
|
||||||
|
const season = Number(regexResult[2]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: element.getAttribute('idepisode'),
|
||||||
|
episode: episode,
|
||||||
|
season: season
|
||||||
|
};
|
||||||
|
}).filter(item => item);
|
||||||
|
|
||||||
|
const ep = episodes.find(ep => ep && ep.episode === episode && ep.season === season);
|
||||||
|
if (!ep) return { embeds }
|
||||||
|
|
||||||
|
const form = `idepisode=${ep.id}&_token=${token}`;
|
||||||
|
|
||||||
|
let response = await proxiedFetch<string>(URL_AJAX_TV, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': '*/*',
|
||||||
|
'Accept-Encoding': 'gzip, deflate, br',
|
||||||
|
'Accept-Language': "en-US,en;q=0.9",
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Sec-CH-UA': '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"',
|
||||||
|
'Sec-CH-UA-Mobile': '?0',
|
||||||
|
'Sec-CH-UA-Platform': '"Linux"',
|
||||||
|
'Sec-Fetch-Site': 'same-origin',
|
||||||
|
'Sec-Fetch-Mode': 'cors',
|
||||||
|
'Sec-Fetch-Dest': 'empty',
|
||||||
|
'X-Cookie': cookieHeader,
|
||||||
|
'X-Origin': URL_BASE,
|
||||||
|
'X-Referer': mediaInResults.href
|
||||||
|
},
|
||||||
|
body: form
|
||||||
|
});
|
||||||
|
|
||||||
|
dom = toDom(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
const servers = [...dom.querySelectorAll('.singlemv')].map(element => element.getAttribute('data'));
|
||||||
|
|
||||||
|
for (const server of servers) {
|
||||||
|
const form = `m4u=${server}&_token=${token}`;
|
||||||
|
|
||||||
|
const response = await proxiedFetch<string>(URL_AJAX, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Accept': '*/*',
|
||||||
|
'Accept-Encoding': 'gzip, deflate, br',
|
||||||
|
'Accept-Language': "en-US,en;q=0.9",
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
|
'Sec-CH-UA': '"Not?A_Brand";v="8", "Chromium";v="108", "Microsoft Edge";v="108"',
|
||||||
|
'Sec-CH-UA-Mobile': '?0',
|
||||||
|
'Sec-CH-UA-Platform': '"Linux"',
|
||||||
|
'Sec-Fetch-Site': 'same-origin',
|
||||||
|
'Sec-Fetch-Mode': 'cors',
|
||||||
|
'Sec-Fetch-Dest': 'empty',
|
||||||
|
'X-Cookie': cookieHeader,
|
||||||
|
'X-Origin': URL_BASE,
|
||||||
|
'X-Referer': mediaInResults.href
|
||||||
|
},
|
||||||
|
body: form
|
||||||
|
});
|
||||||
|
|
||||||
|
const dom = toDom(response);
|
||||||
|
|
||||||
|
const link = dom.querySelector('iframe')?.src;
|
||||||
|
|
||||||
|
const getEmbedType = (url: string) => {
|
||||||
|
if (url.startsWith("https://streamm4u.club")) return MWEmbedType.STREAMM4U
|
||||||
|
if (url.startsWith("https://play.playm4u.xyz")) return MWEmbedType.PLAYM4U
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!link) continue;
|
||||||
|
|
||||||
|
const embedType = getEmbedType(link);
|
||||||
|
if (embedType) {
|
||||||
|
embeds.push({
|
||||||
|
url: link,
|
||||||
|
type: embedType
|
||||||
|
})
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(embeds);
|
||||||
|
return {
|
||||||
|
embeds,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
Loading…
Reference in a new issue