mirror of
https://github.com/vega-org/vega-providers.git
synced 2026-04-17 23:51:44 +00:00
feat: Add new provider implementations for Joya9tv and skyMovieHD
This commit is contained in:
10
providers/skyMovieHD/catalog.ts
Normal file
10
providers/skyMovieHD/catalog.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export const catalog = [
|
||||
{
|
||||
title: "Trending",
|
||||
filter: "",
|
||||
},
|
||||
{
|
||||
title: "JIo-Studios",
|
||||
filter: "category/jio-studios/",
|
||||
},
|
||||
];
|
||||
47
providers/skyMovieHD/episodes.ts
Normal file
47
providers/skyMovieHD/episodes.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { EpisodeLink, ProviderContext } from "../types";
|
||||
|
||||
export const getEpisodes = function ({
|
||||
url,
|
||||
providerContext,
|
||||
}: {
|
||||
url: string;
|
||||
providerContext: ProviderContext;
|
||||
}): Promise<EpisodeLink[]> {
|
||||
const { axios, cheerio, commonHeaders: headers } = providerContext;
|
||||
console.log("getEpisodeLinks", url);
|
||||
|
||||
return axios
|
||||
.get(url, { headers })
|
||||
.then((res) => {
|
||||
const $ = cheerio.load(res.data);
|
||||
const container = $(".entry-content, .entry-inner");
|
||||
|
||||
// Remove unnecessary elements
|
||||
$(".unili-content, .code-block-1").remove();
|
||||
|
||||
const episodes: EpisodeLink[] = [];
|
||||
|
||||
container.find("h4, h3").each((_, element) => {
|
||||
const el = $(element);
|
||||
let title = el.text().replace(/[-:]/g, "").trim();
|
||||
if (!title) return;
|
||||
|
||||
// Saare V-Cloud links fetch
|
||||
el.next("p")
|
||||
.find("a[href*='vcloud.lol']")
|
||||
.each((_, a) => {
|
||||
const anchor = $(a);
|
||||
const href = anchor.attr("href")?.trim();
|
||||
if (href) {
|
||||
episodes.push({ title, link: href });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return episodes;
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log("getEpisodeLinks error:", err);
|
||||
return [];
|
||||
});
|
||||
};
|
||||
268
providers/skyMovieHD/meta.ts
Normal file
268
providers/skyMovieHD/meta.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import { Info, Link, ProviderContext } from "../types";
|
||||
|
||||
interface DirectLink {
|
||||
link: string;
|
||||
title: string;
|
||||
quality: string;
|
||||
type: "movie" | "episode";
|
||||
}
|
||||
|
||||
interface Episode {
|
||||
title: string;
|
||||
directLinks: DirectLink[];
|
||||
}
|
||||
|
||||
const headers = {
|
||||
Referer: "https://google.com",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||
};
|
||||
|
||||
export async function fetchEpisodesFromSelectedLink(
|
||||
url: string,
|
||||
providerContext: ProviderContext
|
||||
): Promise<Episode[]> {
|
||||
const { axios, cheerio } = providerContext;
|
||||
const res = await axios.get(url, { headers });
|
||||
const $ = cheerio.load(res.data);
|
||||
|
||||
const episodes: Episode[] = [];
|
||||
|
||||
$("h4").each((_, h4El) => {
|
||||
const epTitle = $(h4El).text().trim();
|
||||
if (!epTitle) return;
|
||||
|
||||
const directLinks: DirectLink[] = [];
|
||||
|
||||
$(h4El)
|
||||
.nextUntil("h4, hr")
|
||||
.find("a[href]")
|
||||
.each((_, linkEl) => {
|
||||
let href = ($(linkEl).attr("href") || "").trim();
|
||||
if (!href) return;
|
||||
if (!href.startsWith("http")) href = new URL(href, url).href;
|
||||
|
||||
const btnText = $(linkEl).text().trim() || "Watch Episode";
|
||||
directLinks.push({
|
||||
link: href,
|
||||
title: btnText,
|
||||
quality: "AUTO",
|
||||
type: "episode",
|
||||
});
|
||||
});
|
||||
|
||||
if (directLinks.length > 0) {
|
||||
episodes.push({
|
||||
title: epTitle,
|
||||
directLinks,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return episodes;
|
||||
}
|
||||
|
||||
// --- Main getMeta function
|
||||
export const getMeta = async function ({
|
||||
link,
|
||||
providerContext,
|
||||
}: {
|
||||
link: string;
|
||||
providerContext: ProviderContext;
|
||||
}): Promise<
|
||||
Info & { extraInfo: Record<string, string>; episodeList: Episode[] }
|
||||
> {
|
||||
const { axios, cheerio } = providerContext;
|
||||
if (!link.startsWith("http"))
|
||||
link = new URL(link, "https://vgmlinks.click").href;
|
||||
|
||||
try {
|
||||
const res = await axios.get(link, { headers });
|
||||
const $ = cheerio.load(res.data);
|
||||
const content = $(".entry-content, .post-inner").length
|
||||
? $(".entry-content, .post-inner")
|
||||
: $("body");
|
||||
|
||||
const title =
|
||||
$("h1.entry-title").first().text().trim() ||
|
||||
$("meta[property='og:title']").attr("content")?.trim() ||
|
||||
"Unknown"; // --- Type Detect ---
|
||||
|
||||
const pageText = content.text();
|
||||
const type =
|
||||
/Season\s*\d+/i.test(pageText) || /Episode\s*\d+/i.test(pageText)
|
||||
? "series"
|
||||
: "movie";
|
||||
|
||||
let image =
|
||||
$(".poster img").attr("src") ||
|
||||
$("meta[property='og:image']").attr("content") ||
|
||||
$("meta[name='twitter:image']").attr("content") ||
|
||||
"";
|
||||
if (image && !image.startsWith("http")) image = new URL(image, link).href;
|
||||
|
||||
let synopsis = "";
|
||||
$(".entry-content p").each((_, el) => {
|
||||
const txt = $(el).text().trim();
|
||||
if (txt.length > 40 && !txt.toLowerCase().includes("download")) {
|
||||
synopsis = txt;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
const imdbLink = $("a[href*='imdb.com']").attr("href") || "";
|
||||
const imdbId = imdbLink
|
||||
? "tt" + (imdbLink.split("/tt")[1]?.split("/")[0] || "")
|
||||
: "";
|
||||
|
||||
const tags: string[] = [];
|
||||
$(".entry-content p strong").each((_, el) => {
|
||||
const txt = $(el).text().trim();
|
||||
if (
|
||||
txt.match(
|
||||
/drama|biography|action|thriller|romance|adventure|animation/i
|
||||
)
|
||||
)
|
||||
tags.push(txt);
|
||||
});
|
||||
|
||||
const extra: Record<string, string> = {};
|
||||
$("p").each((_, el) => {
|
||||
const html = $(el).html() || "";
|
||||
if (html.includes("Series Name"))
|
||||
extra.name = $(el).text().split(":")[1]?.trim();
|
||||
if (html.includes("Language"))
|
||||
extra.language = $(el).text().split(":")[1]?.trim();
|
||||
if (html.includes("Released Year"))
|
||||
extra.year = $(el).text().split(":")[1]?.trim();
|
||||
if (html.includes("Quality"))
|
||||
extra.quality = $(el).text().split(":")[1]?.trim();
|
||||
if (html.includes("Episode Size"))
|
||||
extra.size = $(el).text().split(":")[1]?.trim();
|
||||
if (html.includes("Format"))
|
||||
extra.format = $(el).text().split(":")[1]?.trim();
|
||||
});
|
||||
|
||||
const links: Link[] = [];
|
||||
const episodeList: Episode[] = [];
|
||||
|
||||
const isInformationalHeading = (text: string) => {
|
||||
const lowerText = text.toLowerCase();
|
||||
return (
|
||||
lowerText.includes("series info") ||
|
||||
lowerText.includes("series name") ||
|
||||
lowerText.includes("language") ||
|
||||
lowerText.includes("released year") ||
|
||||
lowerText.includes("episode size") ||
|
||||
lowerText.includes("format") ||
|
||||
lowerText.includes("imdb rating") ||
|
||||
lowerText.includes("winding up") ||
|
||||
(lowerText.length < 5 && !/\d/.test(lowerText))
|
||||
);
|
||||
}; // --- Download Links Extraction ---
|
||||
|
||||
if (type === "series") {
|
||||
// Series case: h3 text as title + episode link button (V-Cloud)
|
||||
content.find("h3").each((_, h3) => {
|
||||
const h3Text = $(h3).text().trim();
|
||||
|
||||
if (isInformationalHeading(h3Text)) return;
|
||||
|
||||
const qualityMatch = h3Text.match(/\d+p/)?.[0] || "AUTO";
|
||||
|
||||
const vcloudLink = $(h3)
|
||||
.nextUntil("h3, hr")
|
||||
.find("a")
|
||||
.filter((_, a) => /v-cloud|mega|gdrive|download/i.test($(a).text()))
|
||||
.first();
|
||||
|
||||
const href = vcloudLink.attr("href");
|
||||
if (href) {
|
||||
// Hide unwanted texts
|
||||
const btnText = vcloudLink.text().trim() || "Link";
|
||||
if (
|
||||
btnText.toLowerCase().includes("imdb rating") ||
|
||||
btnText.toLowerCase().includes("winding up")
|
||||
)
|
||||
return;
|
||||
|
||||
links.push({
|
||||
title: h3Text,
|
||||
quality: qualityMatch,
|
||||
episodesLink: href,
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Movie case: h5/h3 text as title + direct download link
|
||||
content.find("h3, h5").each((_, heading) => {
|
||||
const headingText = $(heading).text().trim();
|
||||
|
||||
if (isInformationalHeading(headingText)) return;
|
||||
|
||||
const qualityMatch = headingText.match(/\d+p/)?.[0] || "AUTO";
|
||||
const linkEl = $(heading)
|
||||
.nextUntil("h3, h5, hr")
|
||||
.find("a[href]")
|
||||
.first();
|
||||
|
||||
const href = linkEl.attr("href");
|
||||
if (href) {
|
||||
let finalHref = href.trim();
|
||||
if (!finalHref.startsWith("http"))
|
||||
finalHref = new URL(finalHref, link).href;
|
||||
|
||||
const btnText = linkEl.text().trim() || "Download Link"; // Hide unwanted texts
|
||||
|
||||
if (
|
||||
btnText.toLowerCase().includes("imdb rating") ||
|
||||
btnText.toLowerCase().includes("winding up")
|
||||
)
|
||||
return;
|
||||
|
||||
links.push({
|
||||
title: headingText,
|
||||
quality: qualityMatch,
|
||||
episodesLink: "",
|
||||
directLinks: [
|
||||
{
|
||||
title: btnText,
|
||||
link: finalHref,
|
||||
type: "movie",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
title,
|
||||
synopsis,
|
||||
image,
|
||||
imdbId,
|
||||
type: type as "movie" | "series",
|
||||
tags,
|
||||
cast: [],
|
||||
rating: $(".entry-meta .entry-date").text().trim() || "",
|
||||
linkList: links,
|
||||
extraInfo: extra,
|
||||
episodeList,
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("getMeta error:", err);
|
||||
return {
|
||||
title: "",
|
||||
synopsis: "",
|
||||
image: "",
|
||||
imdbId: "",
|
||||
type: "movie",
|
||||
tags: [],
|
||||
cast: [],
|
||||
rating: "",
|
||||
linkList: [],
|
||||
extraInfo: {},
|
||||
episodeList: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
118
providers/skyMovieHD/posts.ts
Normal file
118
providers/skyMovieHD/posts.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { Post, ProviderContext } from "../types";
|
||||
|
||||
const defaultHeaders = {
|
||||
Referer: "https://www.google.com",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
||||
"(KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36",
|
||||
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
Pragma: "no-cache",
|
||||
"Cache-Control": "no-cache",
|
||||
};
|
||||
|
||||
// --- Normal catalog posts ---
|
||||
export async function getPosts({
|
||||
filter,
|
||||
page = 1,
|
||||
signal,
|
||||
providerContext,
|
||||
}: {
|
||||
filter?: string;
|
||||
page?: number;
|
||||
signal?: AbortSignal;
|
||||
providerContext: ProviderContext;
|
||||
}): Promise<Post[]> {
|
||||
return fetchPosts({ filter, page, query: "", signal, providerContext });
|
||||
}
|
||||
|
||||
// --- Search posts ---
|
||||
export async function getSearchPosts({
|
||||
searchQuery,
|
||||
page = 1,
|
||||
signal,
|
||||
providerContext,
|
||||
}: {
|
||||
searchQuery: string;
|
||||
page?: number;
|
||||
signal?: AbortSignal;
|
||||
providerContext: ProviderContext;
|
||||
}): Promise<Post[]> {
|
||||
return fetchPosts({ filter: "", page, query: searchQuery, signal, providerContext });
|
||||
}
|
||||
|
||||
// --- Core fetch function ---
|
||||
async function fetchPosts({
|
||||
filter,
|
||||
query,
|
||||
page = 1,
|
||||
signal,
|
||||
providerContext,
|
||||
}: {
|
||||
filter?: string;
|
||||
query?: string;
|
||||
page?: number;
|
||||
signal?: AbortSignal;
|
||||
providerContext: ProviderContext;
|
||||
}): Promise<Post[]> {
|
||||
try {
|
||||
const baseUrl = "https://skymovieshd.tattoo";
|
||||
let url: string;
|
||||
|
||||
if (query && query.trim() && query.trim().toLowerCase() !== "what are you looking for?") {
|
||||
const params = new URLSearchParams();
|
||||
params.append("s", query.trim());
|
||||
if (page > 1) params.append("paged", page.toString());
|
||||
url = `${baseUrl}/?${params.toString()}`;
|
||||
} else if (filter) {
|
||||
url = filter.startsWith("/")
|
||||
? `${baseUrl}${filter.replace(/\/$/, "")}${page > 1 ? `/page/${page}` : ""}`
|
||||
: `${baseUrl}/${filter}${page > 1 ? `/page/${page}` : ""}`;
|
||||
} else {
|
||||
url = `${baseUrl}${page > 1 ? `/page/${page}` : ""}`;
|
||||
}
|
||||
|
||||
const { axios, cheerio } = providerContext;
|
||||
const res = await axios.get(url, { headers: defaultHeaders, signal });
|
||||
const $ = cheerio.load(res.data || "");
|
||||
|
||||
const resolveUrl = (href: string) =>
|
||||
href?.startsWith("http") ? href : new URL(href, baseUrl).href;
|
||||
|
||||
const seen = new Set<string>();
|
||||
const catalog: Post[] = [];
|
||||
|
||||
// ✅ Scrape posts
|
||||
$("article.latestpost").each((_, el) => {
|
||||
const card = $(el);
|
||||
|
||||
// Link
|
||||
let link = card.find("header.entry-header h2.entry-title a, header.entry-header h1.entry-title a").attr("href") || "";
|
||||
if (!link) return;
|
||||
link = resolveUrl(link);
|
||||
if (seen.has(link)) return;
|
||||
|
||||
// Title: remove "Download"
|
||||
let title = card.find("header.entry-header h2.entry-title a, header.entry-header h1.entry-title a")
|
||||
.text()
|
||||
.replace(/^Download\s*/i, "")
|
||||
.trim();
|
||||
if (!title) return;
|
||||
|
||||
// Image
|
||||
let img =
|
||||
card.find("a#featured-thumbnail img").attr("data-src") ||
|
||||
card.find("a#featured-thumbnail img").attr("src") ||
|
||||
"";
|
||||
const image = img ? resolveUrl(img) : "";
|
||||
|
||||
seen.add(link);
|
||||
catalog.push({ title, link, image });
|
||||
});
|
||||
|
||||
return catalog.slice(0, 100);
|
||||
} catch (err) {
|
||||
console.error("fetchPosts error:", err instanceof Error ? err.message : String(err));
|
||||
return [];
|
||||
}
|
||||
}
|
||||
49
providers/skyMovieHD/stream.ts
Normal file
49
providers/skyMovieHD/stream.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ProviderContext, Stream } from "../types";
|
||||
|
||||
const headers = {
|
||||
Accept:
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||
"Cache-Control": "no-store",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
DNT: "1",
|
||||
"sec-ch-ua":
|
||||
'"Not_A Brand";v="8", "Chromium";v="120", "Microsoft Edge";v="120"',
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '"Windows"',
|
||||
"Sec-Fetch-Dest": "document",
|
||||
"Sec-Fetch-Mode": "navigate",
|
||||
"Sec-Fetch-Site": "none",
|
||||
"Sec-Fetch-User": "?1",
|
||||
Cookie:
|
||||
"xla=s4t; _ga=GA1.1.1081149560.1756378968; _ga_BLZGKYN5PF=GS2.1.s1756378968$o1$g1$t1756378984$j44$l0$h0",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
|
||||
};
|
||||
|
||||
export async function getStream({
|
||||
link,
|
||||
type,
|
||||
signal,
|
||||
providerContext,
|
||||
}: {
|
||||
link: string;
|
||||
type: string;
|
||||
signal: AbortSignal;
|
||||
providerContext: ProviderContext;
|
||||
}) {
|
||||
const { axios, cheerio, extractors } = providerContext;
|
||||
const { hubcloudExtracter } = extractors;
|
||||
try {
|
||||
const streamLinks: Stream[] = [];
|
||||
console.log("dotlink", link);
|
||||
|
||||
return await hubcloudExtracter(link, signal);
|
||||
} catch (error: any) {
|
||||
console.log("getStream error: ", error);
|
||||
if (error.message.includes("Aborted")) {
|
||||
} else {
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user