feat: Add new provider implementations for Joya9tv and skyMovieHD

This commit is contained in:
himanshu8443
2025-10-02 14:19:47 +05:30
parent 56713a0be7
commit 0784a88fa7
20 changed files with 992 additions and 0 deletions

View File

@@ -0,0 +1,10 @@
export const catalog = [
{
title: "Latest",
filter: "", // baseUrl se latest page fetch hoga
},
{
title: "Bangali-Movies",
filter: "genre/bengali-movies/",
},
];

View File

@@ -0,0 +1,58 @@
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);
// Target the container that holds the episode links (based on the provided sample)
const container = $("ul:has(p.font-bold:contains('Episode'))").first();
const episodes: EpisodeLink[] = [];
// Find all bold episode link headings (e.g., 'Episode 38 Links 480p')
container.find("p.font-bold").each((_, element) => {
const el = $(element);
let title = el.text().trim(); // e.g., "Episode 38 Links 480p"
if (!title) return;
// Use a selector for the direct links that follow this title (in the next siblings)
// The episode links are in <li> elements directly following the <p class="font-bold">
let currentElement = el.parent(); // Get the parent <li> of the <p>
// Loop through the siblings until the next <p class="font-bold"> (the start of the next episode)
while (currentElement.next().length && !currentElement.next().find("p.font-bold").length) {
currentElement = currentElement.next();
// Find all anchor tags (links) in the current <li> sibling
currentElement.find("a[href]").each((_, a) => {
const anchor = $(a);
const href = anchor.attr("href")?.trim();
// Only include links for hubcloud and gdflix as requested
if (href && (href.includes("hubcloud.one") || href.includes("gdflix.dev"))) {
// Clean up the title to be just "Episode X 480p"
episodes.push({
title: title.replace(/ Links$/i, ''),
link: href
});
}
});
}
});
return episodes;
})
.catch((err) => {
console.log("getEpisodeLinks error:", err);
return [];
});
};

160
providers/Joya9tv/meta.ts Normal file
View File

@@ -0,0 +1,160 @@
import { Info, Link, ProviderContext } from "../types";
// Headers
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 const getMeta = async function ({
link,
providerContext,
}: {
link: string;
providerContext: ProviderContext;
}): Promise<Info> {
const { cheerio } = providerContext;
const url = link;
const baseUrl = url.split("/").slice(0, 3).join("/");
const emptyResult: Info = {
title: "",
synopsis: "",
image: "",
imdbId: "",
type: "movie",
linkList: [],
};
try {
const response = await fetch(url, {
headers: { ...headers, Referer: baseUrl },
});
const data = await response.text();
const $ = cheerio.load(data);
// Use the main container from the new HTML structure
const infoContainer = $(".content.right").first();
const result: Info = {
title: "",
synopsis: "",
image: "",
imdbId: "",
type: "movie",
linkList: [],
};
// --- Type determination (Based on content, the HTML is for a Series) ---
// Check for 'S' or 'Season' in the main heading
if (
/S\d+|Season \d+|TV Series\/Shows/i.test(
infoContainer.find("h1").text() + $(".sgeneros").text()
)
) {
result.type = "series";
} else {
result.type = "movie";
}
// --- Title ---
const rawTitle = $("h1").first().text().trim();
// Clean up title (remove 'Download', site name, quality/episode tags)
let finalTitle = rawTitle
.replace(/ Download.*|\[Episode \d+ Added\]/g, "")
.trim();
// Extract base title before S19, (2025), etc.
finalTitle =
finalTitle.split(/\(2025\)| S\d+/i)[0].trim() || "Unknown Title";
result.title = finalTitle;
// --- IMDb ID ---
// The new HTML doesn't explicitly show an IMDb ID, so we'll rely on a more generic search.
const imdbMatch = infoContainer.html()?.match(/tt\d+/);
result.imdbId = imdbMatch ? imdbMatch[0] : "";
// --- Image ---
let image =
infoContainer.find(".poster img[src]").first().attr("src") || "";
if (image.startsWith("//")) image = "https:" + image;
// Check for "no-thumbnail" or "placeholder" in the filename
if (image.includes("no-thumbnail") || image.includes("placeholder"))
image = "";
result.image = image;
// --- Synopsis ---
// The synopsis is directly in the <div itemprop="description" class="wp-content"> inside #info
result.synopsis = $("#info .wp-content").text().trim() || "";
// --- LinkList extraction (Updated for the <table> structure in #download) ---
const links: Link[] = [];
const downloadTable = $("#download .links_table table tbody");
// The entire season/series batch links are in the table
downloadTable.find("tr").each((index, element) => {
const row = $(element);
const quality = row.find("strong.quality").text().trim();
// Get the size from the fourth <td> in the row
const size = row.find("td:nth-child(4)").text().trim();
const directLinkAnchor = row.find("td a").first();
const directLink = directLinkAnchor.attr("href");
const linkTitle = directLinkAnchor.text().trim();
if (quality && directLink) {
// FIX: Assert the type to satisfy the Link interface's literal type requirement
const assertedType = result.type as "movie" | "series";
// Assuming the table links are for the entire batch/season
const directLinks = [
{
title: linkTitle || "Download Link",
link: directLink,
type: assertedType, // Use the asserted type
},
];
// Combine title, quality, and size for the LinkList entry
const seasonMatch = rawTitle.match(/S(\d+)/)?.[1];
let fullTitle = `${result.title}`;
if (seasonMatch) fullTitle += ` Season ${seasonMatch}`;
fullTitle += ` - ${quality}`;
if (size) fullTitle += ` (${size})`; // ADDED: Append size to the link title
links.push({
title: fullTitle,
quality: quality.replace(/[^0-9p]/g, ""), // Clean to just 480p, 720p, 1080p
// The direct link is to a page that lists all episodes, so it acts as the episodesLink
episodesLink: directLink,
directLinks,
});
}
});
result.linkList = links;
return result;
} catch (err) {
console.log("getMeta error:", err);
return emptyResult;
}
};

148
providers/Joya9tv/posts.ts Normal file
View File

@@ -0,0 +1,148 @@
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 = await providerContext.getBaseUrl("joya9tv");
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 { cheerio } = providerContext;
const res = await fetch(url, { headers: defaultHeaders, signal });
const data = await res.text();
const $ = cheerio.load(data || "");
const resolveUrl = (href: string) =>
href?.startsWith("http") ? href : new URL(href, baseUrl).href;
const seen = new Set<string>();
const catalog: Post[] = [];
// ✅ Case 1: Normal catalog listing
$("article.item.movies").each((_, el) => {
const card = $(el);
let link = card.find("div.data h3 a").attr("href") || "";
if (!link) return;
link = resolveUrl(link);
if (seen.has(link)) return;
let title = card.find("div.data h3 a").text().trim();
if (!title) return;
let img = card.find("div.poster img").attr("src") || "";
const image = img ? resolveUrl(img) : "";
seen.add(link);
catalog.push({ title, link, image });
});
// ✅ Case 2: Search results
$(".result-item article").each((_, el) => {
const card = $(el);
let link = card.find("a").attr("href") || "";
if (!link) return;
link = resolveUrl(link);
if (seen.has(link)) return;
let title =
card.find("a").attr("title") || card.find("img").attr("alt") || "";
title = title.trim();
if (!title) return;
let img = card.find("img").attr("src") || "";
const image = img ? resolveUrl(img) : "";
seen.add(link);
catalog.push({ title, link, image });
});
console.log(`fetchPosts: Fetched ${catalog.length} posts from ${url}`);
return catalog.slice(0, 100);
} catch (err) {
console.error(
"fetchPosts error:",
err instanceof Error ? err.message : String(err)
);
return [];
}
}

114
providers/Joya9tv/stream.ts Normal file
View File

@@ -0,0 +1,114 @@
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",
"accept-language": "en-US,en;q=0.9,en-IN;q=0.8",
"cache-control": "no-cache",
pragma: "no-cache",
priority: "u=0, i",
"sec-ch-ua":
'"Chromium";v="140", "Not=A?Brand";v="24", "Microsoft Edge";v="140"',
"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",
"upgrade-insecure-requests": "1",
};
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);
if (type === "movie") {
// vlink
const dotlinkRes = await fetch(`${link}`, { headers });
const dotlinkText = await dotlinkRes.text();
// console.log('dotlinkText', dotlinkText);
const vlink = dotlinkText.match(/<a\s+href="([^"]*cloud\.[^"]*)"/i) || [];
// console.log('vLink', vlink[1]);
link = vlink[1];
// filepress link
try {
const $ = cheerio.load(dotlinkText);
const filepressLink = $(
'.btn.btn-sm.btn-outline[style="background:linear-gradient(135deg,rgb(252,185,0) 0%,rgb(0,0,0)); color: #fdf8f2;"]'
)
.parent()
.attr("href");
// console.log('filepressLink', filepressLink);
const filepressID = filepressLink?.split("/").pop();
const filepressBaseUrl = filepressLink
?.split("/")
.slice(0, -2)
.join("/");
// console.log('filepressID', filepressID);
// console.log('filepressBaseUrl', filepressBaseUrl);
const filepressTokenRes = await axios.post(
filepressBaseUrl + "/api/file/downlaod/",
{
id: filepressID,
method: "indexDownlaod",
captchaValue: null,
},
{
headers: {
"Content-Type": "application/json",
Referer: filepressBaseUrl,
},
}
);
// console.log('filepressTokenRes', filepressTokenRes.data);
if (filepressTokenRes.data?.status) {
const filepressToken = filepressTokenRes.data?.data;
const filepressStreamLink = await axios.post(
filepressBaseUrl + "/api/file/downlaod2/",
{
id: filepressToken,
method: "indexDownlaod",
captchaValue: null,
},
{
headers: {
"Content-Type": "application/json",
Referer: filepressBaseUrl,
},
}
);
// console.log('filepressStreamLink', filepressStreamLink.data);
streamLinks.push({
server: "filepress",
link: filepressStreamLink.data?.data?.[0],
type: "mkv",
});
}
} catch (error) {
console.log("filepress error: ");
// console.error(error);
}
}
return await hubcloudExtracter(link, signal);
} catch (error: any) {
console.log("getStream error: ", error);
if (error.message.includes("Aborted")) {
} else {
}
return [];
}
}

View File

@@ -0,0 +1,10 @@
export const catalog = [
{
title: "Trending",
filter: "",
},
{
title: "JIo-Studios",
filter: "category/jio-studios/",
},
];

View 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 [];
});
};

View 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: [],
};
}
};

View 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 [];
}
}

View 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 [];
}
}