mirror of
https://gitlab.com/vyla-entertainment/sdk.git
synced 2026-11-14 06:07:34 -05:00
added manga and added sdk source
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@vyla-entertainment/sdk",
|
||||
"version": "1.15.0",
|
||||
"version": "1.16.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@vyla-entertainment/sdk",
|
||||
"version": "1.15.0",
|
||||
"version": "1.16.0",
|
||||
"license": "CC-BY-NC-4.0",
|
||||
"dependencies": {
|
||||
"node-tls-client": "^2.1.0"
|
||||
|
||||
@@ -279,6 +279,25 @@ export const SOURCES = [
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'peestream',
|
||||
label: 'PeeStream',
|
||||
sourceFile: 'peestream',
|
||||
proxyParam: 'pee',
|
||||
timeout: 25000,
|
||||
jitter: 500,
|
||||
retries: 2,
|
||||
multiUrl: true,
|
||||
skipProxy: true,
|
||||
cdnHeaders: [{
|
||||
pattern: /peestream\.in/i,
|
||||
headers: {
|
||||
Referer: 'https://peestream.in/',
|
||||
Origin: 'https://peestream.in',
|
||||
},
|
||||
}],
|
||||
},
|
||||
|
||||
{
|
||||
key: 'pengu',
|
||||
label: 'Pengu',
|
||||
|
||||
168
src/manga/atsu.js
Normal file
168
src/manga/atsu.js
Normal file
@@ -0,0 +1,168 @@
|
||||
import { Session, ClientIdentifier, initTLS, destroyTLS } from "node-tls-client";
|
||||
|
||||
const BASE_URL = "https://atsu.moe";
|
||||
const CDN_URL = "https://cdn.atsu.moe";
|
||||
|
||||
const UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
||||
|
||||
const HEADERS = {
|
||||
"User-Agent": UA,
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Cache-Control": "no-cache",
|
||||
"Pragma": "no-cache",
|
||||
"Referer": `${BASE_URL}/`,
|
||||
"Origin": BASE_URL,
|
||||
"Sec-Ch-Ua": '"Chromium";v="131", "Not_A Brand";v="24"',
|
||||
"Sec-Ch-Ua-Mobile": "?0",
|
||||
"Sec-Ch-Ua-Platform": '"Windows"',
|
||||
"Sec-Fetch-Dest": "empty",
|
||||
"Sec-Fetch-Mode": "cors",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"Priority": "u=1, i"
|
||||
};
|
||||
|
||||
let tlsInitialized = false;
|
||||
let session = null;
|
||||
|
||||
async function ensureSession() {
|
||||
if (!tlsInitialized) {
|
||||
await initTLS();
|
||||
tlsInitialized = true;
|
||||
}
|
||||
if (!session) {
|
||||
session = new Session({
|
||||
clientIdentifier: ClientIdentifier.chrome_131,
|
||||
timeout: 15000,
|
||||
insecureSkipVerify: false,
|
||||
});
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
const formatAssetUrl = (url) => {
|
||||
if (!url) return null;
|
||||
if (url.startsWith("http://") || url.startsWith("https://")) return url;
|
||||
if (url.startsWith("/static/")) return `${CDN_URL}${url}`;
|
||||
if (url.startsWith("/")) return `${CDN_URL}${url}`;
|
||||
return `${CDN_URL}/static/${url}`;
|
||||
};
|
||||
|
||||
async function atsuFetch(url) {
|
||||
const sess = await ensureSession();
|
||||
const res = await sess.get(url, { headers: HEADERS });
|
||||
if (res.status !== 200) {
|
||||
throw new Error(`Atsu request failed: ${res.status}`);
|
||||
}
|
||||
const text = await res.text();
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
export async function searchManga(query, page = 1, perPage = 12) {
|
||||
const params = new URLSearchParams({
|
||||
q: query,
|
||||
query_by: "title,englishTitle,otherNames,authors,acronyms",
|
||||
query_by_weights: "4,3,2,2,1",
|
||||
num_typos: "4,3,2,1,0",
|
||||
prefix: "true,true,true,true,false",
|
||||
include_fields: "id,title,englishTitle,authors,poster,posterSmall,posterMedium,type,medium,isAdult,status,year,mbRating,popularity,dateAdded",
|
||||
filter_by: "isAdult:=false && views:>0 && hidden:!=true",
|
||||
page: String(page),
|
||||
per_page: String(perPage),
|
||||
infix: "off,off,fallback,off,off"
|
||||
});
|
||||
|
||||
const data = await atsuFetch(`${BASE_URL}/collections/manga/documents/search?${params.toString()}`);
|
||||
const results = (data?.hits || []).map(hit => {
|
||||
const doc = hit.document || {};
|
||||
return {
|
||||
id: doc.id,
|
||||
title: doc.title,
|
||||
englishTitle: doc.englishTitle || null,
|
||||
authors: Array.isArray(doc.authors) ? doc.authors : [],
|
||||
type: doc.type,
|
||||
medium: doc.medium,
|
||||
status: doc.status,
|
||||
year: doc.year,
|
||||
rating: doc.mbRating ?? null,
|
||||
popularity: doc.popularity ?? null,
|
||||
poster: formatAssetUrl(doc.poster),
|
||||
posterSmall: formatAssetUrl(doc.posterSmall),
|
||||
posterMedium: formatAssetUrl(doc.posterMedium)
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
page: data.page,
|
||||
total: data.found,
|
||||
results
|
||||
};
|
||||
}
|
||||
|
||||
export async function getMangaDetails(id) {
|
||||
const data = await atsuFetch(`${BASE_URL}/api/manga/page?id=${encodeURIComponent(id)}`);
|
||||
const page = data?.mangaPage;
|
||||
if (!page) throw new Error(`Manga with ID "${id}" not found`);
|
||||
|
||||
return {
|
||||
id: page.id,
|
||||
title: page.title,
|
||||
englishTitle: page.englishTitle || null,
|
||||
synopsis: page.synopsis || null,
|
||||
status: page.status,
|
||||
type: page.type,
|
||||
medium: page.medium,
|
||||
rating: page.avgRating ?? null,
|
||||
views: page.views ?? null,
|
||||
authors: Array.isArray(page.authors) ? page.authors.map(a => a.name) : [],
|
||||
genres: Array.isArray(page.genres) ? page.genres.map(g => g.name) : [],
|
||||
tags: Array.isArray(page.tags) ? page.tags.map(t => t.name) : [],
|
||||
poster: formatAssetUrl(page.poster?.largeImage || page.poster?.image),
|
||||
totalChapters: page.totalChapterCount || page.chapters?.length || 0,
|
||||
chapters: (page.chapters || []).map(ch => ({
|
||||
id: ch.id,
|
||||
number: ch.number,
|
||||
title: ch.title,
|
||||
pageCount: ch.pageCount,
|
||||
createdAt: ch.createdAt ? new Date(ch.createdAt).toISOString() : null
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
export async function getChapterPages(mangaId, chapterId) {
|
||||
const data = await atsuFetch(
|
||||
`${BASE_URL}/api/read/chapter?mangaId=${encodeURIComponent(mangaId)}&chapterId=${encodeURIComponent(chapterId)}`
|
||||
);
|
||||
const chapter = data?.readChapter;
|
||||
if (!chapter) throw new Error(`Chapter "${chapterId}" not found for manga "${mangaId}"`);
|
||||
|
||||
return {
|
||||
id: chapter.id,
|
||||
number: chapter.number,
|
||||
title: chapter.title,
|
||||
scanlatorId: chapter.scanlationMangaId || null,
|
||||
pages: (chapter.pages || []).map(p => ({
|
||||
number: p.number,
|
||||
url: formatAssetUrl(p.image),
|
||||
width: p.width,
|
||||
height: p.height,
|
||||
aspectRatio: p.aspectRatio
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
export async function cleanup() {
|
||||
if (session) {
|
||||
try {
|
||||
await session.close();
|
||||
} catch { }
|
||||
session = null;
|
||||
}
|
||||
if (tlsInitialized) {
|
||||
try {
|
||||
await destroyTLS();
|
||||
} catch { }
|
||||
tlsInitialized = false;
|
||||
}
|
||||
}
|
||||
10
src/sdk.js
10
src/sdk.js
@@ -1,6 +1,7 @@
|
||||
import SOURCES, { SOURCE_MAP } from "./config.js";
|
||||
import { fetchDownloads } from "./downloads/downloads.js";
|
||||
import { probeSource } from "./health.js";
|
||||
import { getChapterPages, getMangaDetails, searchManga } from "./manga/atsu.js";
|
||||
import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from "./subtitles.js";
|
||||
import { createStreamArgs } from "./utils/helpers.js";
|
||||
|
||||
@@ -65,4 +66,13 @@ export default class VylaSDK {
|
||||
async getDownloads(id, s = null, e = null) {
|
||||
return await fetchDownloads(this, id, s, e);
|
||||
}
|
||||
async searchManga(query, page = 1, perPage = 12) {
|
||||
return await searchManga(query, page, perPage);
|
||||
}
|
||||
async getManga(id) {
|
||||
return await getMangaDetails(id);
|
||||
}
|
||||
async getMangaChapter(mangaId, chapterId) {
|
||||
return await getChapterPages(mangaId, chapterId);
|
||||
}
|
||||
}
|
||||
139
src/sources/peestream.js
Normal file
139
src/sources/peestream.js
Normal file
@@ -0,0 +1,139 @@
|
||||
import { USER_AGENT, fetchJson, getTmdbInfo } from '../utils/helpers.js';
|
||||
|
||||
const BASE_URL = 'https://providers.peestream.in';
|
||||
|
||||
const PROVIDERS = {
|
||||
purstream: 'Athena',
|
||||
vidrock: 'Thor',
|
||||
vaplayer: 'Poseidon',
|
||||
netmirror: 'Ra'
|
||||
};
|
||||
|
||||
export async function getStream(args) {
|
||||
const { id, s, e, server, tmdbApiKey } = args;
|
||||
const isTv = s != null && e != null;
|
||||
const type = isTv ? 'tv' : 'movie';
|
||||
|
||||
let title = args.title || '';
|
||||
let releaseYear = args.year || '';
|
||||
let imdbId = args.imdbId || '';
|
||||
let seasonTmdbId = args.seasonTmdbId || '';
|
||||
let episodeTmdbId = args.episodeTmdbId || '';
|
||||
|
||||
try {
|
||||
const info = await getTmdbInfo(id, type, s, tmdbApiKey);
|
||||
if (info) {
|
||||
title = title || info.name || info.title || '';
|
||||
|
||||
if (!releaseYear) {
|
||||
const date = info.release_date || info.first_air_date || '';
|
||||
releaseYear = date ? date.split('-')[0] : (info.year || '');
|
||||
}
|
||||
|
||||
if (!imdbId && info.external_ids?.imdb_id) {
|
||||
imdbId = info.external_ids.imdb_id;
|
||||
} else if (!imdbId && info.imdb_id) {
|
||||
imdbId = info.imdb_id;
|
||||
}
|
||||
|
||||
if (isTv) {
|
||||
if (!seasonTmdbId && Array.isArray(info.seasons)) {
|
||||
const matchedSeason = info.seasons.find(
|
||||
item => item.season_number === Number(s)
|
||||
);
|
||||
if (matchedSeason) {
|
||||
seasonTmdbId = String(matchedSeason.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!episodeTmdbId && tmdbApiKey) {
|
||||
try {
|
||||
const seasonData = await fetchJson(
|
||||
`https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${tmdbApiKey}`
|
||||
);
|
||||
if (seasonData?.episodes) {
|
||||
const matchedEp = seasonData.episodes.find(
|
||||
ep => ep.episode_number === Number(e)
|
||||
);
|
||||
if (matchedEp) {
|
||||
episodeTmdbId = String(matchedEp.id);
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
type,
|
||||
tmdbId: String(id),
|
||||
title: title || String(id)
|
||||
});
|
||||
|
||||
if (releaseYear) queryParams.set('releaseYear', String(releaseYear));
|
||||
if (imdbId) queryParams.set('imdbId', String(imdbId));
|
||||
|
||||
if (isTv) {
|
||||
queryParams.set('seasonNumber', String(s));
|
||||
queryParams.set('episodeNumber', String(e));
|
||||
if (seasonTmdbId) queryParams.set('seasonTmdbId', String(seasonTmdbId));
|
||||
if (episodeTmdbId) queryParams.set('episodeTmdbId', String(episodeTmdbId));
|
||||
}
|
||||
|
||||
const allUrls = [];
|
||||
|
||||
try {
|
||||
const scrapeUrl = `${BASE_URL}/scrape?${queryParams.toString()}`;
|
||||
const res = await fetch(scrapeUrl, {
|
||||
headers: {
|
||||
'User-Agent': USER_AGENT,
|
||||
'Accept': 'text/event-stream',
|
||||
'Referer': `${BASE_URL}/`
|
||||
},
|
||||
signal: AbortSignal.timeout(20000)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const text = await res.text();
|
||||
const events = text.split('\n\n');
|
||||
|
||||
for (const ev of events) {
|
||||
if (ev.includes('event: completed')) {
|
||||
const dataMatch = ev.match(/data:\s*(.+)/);
|
||||
if (dataMatch) {
|
||||
try {
|
||||
const parsed = JSON.parse(dataMatch[1].trim());
|
||||
const stream = parsed.stream;
|
||||
const streamUrl = stream?.playlist || stream?.url || stream?.file;
|
||||
if (streamUrl) {
|
||||
const sourceName = PROVIDERS[parsed.sourceId] || parsed.sourceId || 'Poseidon';
|
||||
allUrls.push({
|
||||
url: streamUrl,
|
||||
server: `PeeStream - ${sourceName}`,
|
||||
quality: 'Auto',
|
||||
type: stream.type === 'hls' || streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
||||
headers: stream.headers || {
|
||||
'User-Agent': USER_AGENT
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
if (server && server !== 'all') {
|
||||
const clean = server.toLowerCase().replace('peestream - ', '').trim();
|
||||
const filtered = allUrls.filter(u => u.server.toLowerCase().includes(clean));
|
||||
if (filtered.length) return { allUrls: filtered };
|
||||
}
|
||||
|
||||
return allUrls.length ? { allUrls } : null;
|
||||
}
|
||||
|
||||
export async function getSources() {
|
||||
return Object.values(PROVIDERS).map(name => `PeeStream - ${name}`);
|
||||
}
|
||||
@@ -13,17 +13,20 @@ const CONFIG = {
|
||||
TV_ID: "37854", // TV TMDB ID to test, for anime I recommend using a random ID like 37854
|
||||
TV_SEASON: "1", // TV Season to test
|
||||
TV_EPISODE: "1", // TV Episode to test
|
||||
STREAM_SOURCE: "", // Source key to test
|
||||
STREAM_SOURCE: "atsu", // Source key to test
|
||||
|
||||
TEST_SUBTITLE: false, // Toggle subtitle testing
|
||||
TEST_DOWNLOAD: false, // Toggle download testing
|
||||
TEST_MOVIE: true, // Toggle movie testing
|
||||
TEST_TV: true, // Toggle TV testing
|
||||
TEST_SUBTITLE: false,
|
||||
TEST_DOWNLOAD: false,
|
||||
TEST_MOVIE: false,
|
||||
TEST_TV: false,
|
||||
TEST_MANGA: true,
|
||||
MANGA_QUERY: "Steel Ball Run",
|
||||
|
||||
EXTRA_SDK_PARAMS: { // Additional parameters for the SDK constructor
|
||||
EXTRA_SDK_PARAMS: {
|
||||
// penguManifest: ""
|
||||
}
|
||||
};
|
||||
|
||||
dotenv.config({
|
||||
debug: CONFIG.DEBUG
|
||||
});
|
||||
@@ -189,4 +192,33 @@ if (CONFIG.TEST_TV) {
|
||||
}
|
||||
} else {
|
||||
console.log("\nSkipped Stream Test");
|
||||
}
|
||||
|
||||
if (CONFIG.TEST_MANGA) {
|
||||
try {
|
||||
console.log(`\nSearching Manga for: "${CONFIG.MANGA_QUERY}"`);
|
||||
const searchRes = await sdk.searchManga(CONFIG.MANGA_QUERY, 1, 5);
|
||||
console.log("Manga Search Found:", searchRes.total);
|
||||
console.log("Top Hit:", searchRes.results[0]);
|
||||
|
||||
if (searchRes.results.length > 0) {
|
||||
const mangaId = searchRes.results[0].id;
|
||||
console.log(`\nFetching Manga Details for ID: ${mangaId}`);
|
||||
const mangaDetails = await sdk.getManga(mangaId);
|
||||
console.log("Manga Title:", mangaDetails.title);
|
||||
console.log("Total Chapters:", mangaDetails.totalChapters);
|
||||
|
||||
const firstChapter = mangaDetails.chapters[mangaDetails.chapters.length - 1] || mangaDetails.chapters[0];
|
||||
if (firstChapter) {
|
||||
console.log(`\nFetching Pages for Chapter: ${firstChapter.title} (${firstChapter.id})`);
|
||||
const chapterPages = await sdk.getMangaChapter(mangaId, firstChapter.id);
|
||||
console.log("Page Count:", chapterPages.pages.length);
|
||||
console.log("Sample First Page:", chapterPages.pages[0]);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Manga Test Error:", err.message);
|
||||
}
|
||||
} else {
|
||||
console.log("\nSkipped Manga Test");
|
||||
}
|
||||
Reference in New Issue
Block a user