@vyla-entertainment/sdk 1.16.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/config.js +19 -0
- package/src/manga/atsu.js +168 -0
- package/src/sdk.js +10 -0
- package/src/sources/peestream.js +139 -0
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -279,6 +279,25 @@ export const SOURCES = [
|
|
|
279
279
|
}],
|
|
280
280
|
},
|
|
281
281
|
|
|
282
|
+
{
|
|
283
|
+
key: 'peestream',
|
|
284
|
+
label: 'PeeStream',
|
|
285
|
+
sourceFile: 'peestream',
|
|
286
|
+
proxyParam: 'pee',
|
|
287
|
+
timeout: 25000,
|
|
288
|
+
jitter: 500,
|
|
289
|
+
retries: 2,
|
|
290
|
+
multiUrl: true,
|
|
291
|
+
skipProxy: true,
|
|
292
|
+
cdnHeaders: [{
|
|
293
|
+
pattern: /peestream\.in/i,
|
|
294
|
+
headers: {
|
|
295
|
+
Referer: 'https://peestream.in/',
|
|
296
|
+
Origin: 'https://peestream.in',
|
|
297
|
+
},
|
|
298
|
+
}],
|
|
299
|
+
},
|
|
300
|
+
|
|
282
301
|
{
|
|
283
302
|
key: 'pengu',
|
|
284
303
|
label: 'Pengu',
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { Session, ClientIdentifier, initTLS, destroyTLS } from "node-tls-client";
|
|
2
|
+
|
|
3
|
+
const BASE_URL = "https://atsu.moe";
|
|
4
|
+
const CDN_URL = "https://cdn.atsu.moe";
|
|
5
|
+
|
|
6
|
+
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";
|
|
7
|
+
|
|
8
|
+
const HEADERS = {
|
|
9
|
+
"User-Agent": UA,
|
|
10
|
+
"Accept": "*/*",
|
|
11
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
12
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
13
|
+
"Cache-Control": "no-cache",
|
|
14
|
+
"Pragma": "no-cache",
|
|
15
|
+
"Referer": `${BASE_URL}/`,
|
|
16
|
+
"Origin": BASE_URL,
|
|
17
|
+
"Sec-Ch-Ua": '"Chromium";v="131", "Not_A Brand";v="24"',
|
|
18
|
+
"Sec-Ch-Ua-Mobile": "?0",
|
|
19
|
+
"Sec-Ch-Ua-Platform": '"Windows"',
|
|
20
|
+
"Sec-Fetch-Dest": "empty",
|
|
21
|
+
"Sec-Fetch-Mode": "cors",
|
|
22
|
+
"Sec-Fetch-Site": "same-origin",
|
|
23
|
+
"Priority": "u=1, i"
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let tlsInitialized = false;
|
|
27
|
+
let session = null;
|
|
28
|
+
|
|
29
|
+
async function ensureSession() {
|
|
30
|
+
if (!tlsInitialized) {
|
|
31
|
+
await initTLS();
|
|
32
|
+
tlsInitialized = true;
|
|
33
|
+
}
|
|
34
|
+
if (!session) {
|
|
35
|
+
session = new Session({
|
|
36
|
+
clientIdentifier: ClientIdentifier.chrome_131,
|
|
37
|
+
timeout: 15000,
|
|
38
|
+
insecureSkipVerify: false,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return session;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const formatAssetUrl = (url) => {
|
|
45
|
+
if (!url) return null;
|
|
46
|
+
if (url.startsWith("http://") || url.startsWith("https://")) return url;
|
|
47
|
+
if (url.startsWith("/static/")) return `${CDN_URL}${url}`;
|
|
48
|
+
if (url.startsWith("/")) return `${CDN_URL}${url}`;
|
|
49
|
+
return `${CDN_URL}/static/${url}`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
async function atsuFetch(url) {
|
|
53
|
+
const sess = await ensureSession();
|
|
54
|
+
const res = await sess.get(url, { headers: HEADERS });
|
|
55
|
+
if (res.status !== 200) {
|
|
56
|
+
throw new Error(`Atsu request failed: ${res.status}`);
|
|
57
|
+
}
|
|
58
|
+
const text = await res.text();
|
|
59
|
+
return JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function searchManga(query, page = 1, perPage = 12) {
|
|
63
|
+
const params = new URLSearchParams({
|
|
64
|
+
q: query,
|
|
65
|
+
query_by: "title,englishTitle,otherNames,authors,acronyms",
|
|
66
|
+
query_by_weights: "4,3,2,2,1",
|
|
67
|
+
num_typos: "4,3,2,1,0",
|
|
68
|
+
prefix: "true,true,true,true,false",
|
|
69
|
+
include_fields: "id,title,englishTitle,authors,poster,posterSmall,posterMedium,type,medium,isAdult,status,year,mbRating,popularity,dateAdded",
|
|
70
|
+
filter_by: "isAdult:=false && views:>0 && hidden:!=true",
|
|
71
|
+
page: String(page),
|
|
72
|
+
per_page: String(perPage),
|
|
73
|
+
infix: "off,off,fallback,off,off"
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const data = await atsuFetch(`${BASE_URL}/collections/manga/documents/search?${params.toString()}`);
|
|
77
|
+
const results = (data?.hits || []).map(hit => {
|
|
78
|
+
const doc = hit.document || {};
|
|
79
|
+
return {
|
|
80
|
+
id: doc.id,
|
|
81
|
+
title: doc.title,
|
|
82
|
+
englishTitle: doc.englishTitle || null,
|
|
83
|
+
authors: Array.isArray(doc.authors) ? doc.authors : [],
|
|
84
|
+
type: doc.type,
|
|
85
|
+
medium: doc.medium,
|
|
86
|
+
status: doc.status,
|
|
87
|
+
year: doc.year,
|
|
88
|
+
rating: doc.mbRating ?? null,
|
|
89
|
+
popularity: doc.popularity ?? null,
|
|
90
|
+
poster: formatAssetUrl(doc.poster),
|
|
91
|
+
posterSmall: formatAssetUrl(doc.posterSmall),
|
|
92
|
+
posterMedium: formatAssetUrl(doc.posterMedium)
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
page: data.page,
|
|
98
|
+
total: data.found,
|
|
99
|
+
results
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function getMangaDetails(id) {
|
|
104
|
+
const data = await atsuFetch(`${BASE_URL}/api/manga/page?id=${encodeURIComponent(id)}`);
|
|
105
|
+
const page = data?.mangaPage;
|
|
106
|
+
if (!page) throw new Error(`Manga with ID "${id}" not found`);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
id: page.id,
|
|
110
|
+
title: page.title,
|
|
111
|
+
englishTitle: page.englishTitle || null,
|
|
112
|
+
synopsis: page.synopsis || null,
|
|
113
|
+
status: page.status,
|
|
114
|
+
type: page.type,
|
|
115
|
+
medium: page.medium,
|
|
116
|
+
rating: page.avgRating ?? null,
|
|
117
|
+
views: page.views ?? null,
|
|
118
|
+
authors: Array.isArray(page.authors) ? page.authors.map(a => a.name) : [],
|
|
119
|
+
genres: Array.isArray(page.genres) ? page.genres.map(g => g.name) : [],
|
|
120
|
+
tags: Array.isArray(page.tags) ? page.tags.map(t => t.name) : [],
|
|
121
|
+
poster: formatAssetUrl(page.poster?.largeImage || page.poster?.image),
|
|
122
|
+
totalChapters: page.totalChapterCount || page.chapters?.length || 0,
|
|
123
|
+
chapters: (page.chapters || []).map(ch => ({
|
|
124
|
+
id: ch.id,
|
|
125
|
+
number: ch.number,
|
|
126
|
+
title: ch.title,
|
|
127
|
+
pageCount: ch.pageCount,
|
|
128
|
+
createdAt: ch.createdAt ? new Date(ch.createdAt).toISOString() : null
|
|
129
|
+
}))
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function getChapterPages(mangaId, chapterId) {
|
|
134
|
+
const data = await atsuFetch(
|
|
135
|
+
`${BASE_URL}/api/read/chapter?mangaId=${encodeURIComponent(mangaId)}&chapterId=${encodeURIComponent(chapterId)}`
|
|
136
|
+
);
|
|
137
|
+
const chapter = data?.readChapter;
|
|
138
|
+
if (!chapter) throw new Error(`Chapter "${chapterId}" not found for manga "${mangaId}"`);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
id: chapter.id,
|
|
142
|
+
number: chapter.number,
|
|
143
|
+
title: chapter.title,
|
|
144
|
+
scanlatorId: chapter.scanlationMangaId || null,
|
|
145
|
+
pages: (chapter.pages || []).map(p => ({
|
|
146
|
+
number: p.number,
|
|
147
|
+
url: formatAssetUrl(p.image),
|
|
148
|
+
width: p.width,
|
|
149
|
+
height: p.height,
|
|
150
|
+
aspectRatio: p.aspectRatio
|
|
151
|
+
}))
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function cleanup() {
|
|
156
|
+
if (session) {
|
|
157
|
+
try {
|
|
158
|
+
await session.close();
|
|
159
|
+
} catch { }
|
|
160
|
+
session = null;
|
|
161
|
+
}
|
|
162
|
+
if (tlsInitialized) {
|
|
163
|
+
try {
|
|
164
|
+
await destroyTLS();
|
|
165
|
+
} catch { }
|
|
166
|
+
tlsInitialized = false;
|
|
167
|
+
}
|
|
168
|
+
}
|
package/src/sdk.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import SOURCES, { SOURCE_MAP } from "./config.js";
|
|
2
2
|
import { fetchDownloads } from "./downloads/downloads.js";
|
|
3
3
|
import { probeSource } from "./health.js";
|
|
4
|
+
import { getChapterPages, getMangaDetails, searchManga } from "./manga/atsu.js";
|
|
4
5
|
import { fetchSubtitles, getSubPathsMovie, getSubPathsTv } from "./subtitles.js";
|
|
5
6
|
import { createStreamArgs } from "./utils/helpers.js";
|
|
6
7
|
|
|
@@ -65,4 +66,13 @@ export default class VylaSDK {
|
|
|
65
66
|
async getDownloads(id, s = null, e = null) {
|
|
66
67
|
return await fetchDownloads(this, id, s, e);
|
|
67
68
|
}
|
|
69
|
+
async searchManga(query, page = 1, perPage = 12) {
|
|
70
|
+
return await searchManga(query, page, perPage);
|
|
71
|
+
}
|
|
72
|
+
async getManga(id) {
|
|
73
|
+
return await getMangaDetails(id);
|
|
74
|
+
}
|
|
75
|
+
async getMangaChapter(mangaId, chapterId) {
|
|
76
|
+
return await getChapterPages(mangaId, chapterId);
|
|
77
|
+
}
|
|
68
78
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { USER_AGENT, fetchJson, getTmdbInfo } from '../utils/helpers.js';
|
|
2
|
+
|
|
3
|
+
const BASE_URL = 'https://providers.peestream.in';
|
|
4
|
+
|
|
5
|
+
const PROVIDERS = {
|
|
6
|
+
purstream: 'Athena',
|
|
7
|
+
vidrock: 'Thor',
|
|
8
|
+
vaplayer: 'Poseidon',
|
|
9
|
+
netmirror: 'Ra'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export async function getStream(args) {
|
|
13
|
+
const { id, s, e, server, tmdbApiKey } = args;
|
|
14
|
+
const isTv = s != null && e != null;
|
|
15
|
+
const type = isTv ? 'tv' : 'movie';
|
|
16
|
+
|
|
17
|
+
let title = args.title || '';
|
|
18
|
+
let releaseYear = args.year || '';
|
|
19
|
+
let imdbId = args.imdbId || '';
|
|
20
|
+
let seasonTmdbId = args.seasonTmdbId || '';
|
|
21
|
+
let episodeTmdbId = args.episodeTmdbId || '';
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const info = await getTmdbInfo(id, type, s, tmdbApiKey);
|
|
25
|
+
if (info) {
|
|
26
|
+
title = title || info.name || info.title || '';
|
|
27
|
+
|
|
28
|
+
if (!releaseYear) {
|
|
29
|
+
const date = info.release_date || info.first_air_date || '';
|
|
30
|
+
releaseYear = date ? date.split('-')[0] : (info.year || '');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!imdbId && info.external_ids?.imdb_id) {
|
|
34
|
+
imdbId = info.external_ids.imdb_id;
|
|
35
|
+
} else if (!imdbId && info.imdb_id) {
|
|
36
|
+
imdbId = info.imdb_id;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isTv) {
|
|
40
|
+
if (!seasonTmdbId && Array.isArray(info.seasons)) {
|
|
41
|
+
const matchedSeason = info.seasons.find(
|
|
42
|
+
item => item.season_number === Number(s)
|
|
43
|
+
);
|
|
44
|
+
if (matchedSeason) {
|
|
45
|
+
seasonTmdbId = String(matchedSeason.id);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!episodeTmdbId && tmdbApiKey) {
|
|
50
|
+
try {
|
|
51
|
+
const seasonData = await fetchJson(
|
|
52
|
+
`https://api.themoviedb.org/3/tv/${id}/season/${s}?api_key=${tmdbApiKey}`
|
|
53
|
+
);
|
|
54
|
+
if (seasonData?.episodes) {
|
|
55
|
+
const matchedEp = seasonData.episodes.find(
|
|
56
|
+
ep => ep.episode_number === Number(e)
|
|
57
|
+
);
|
|
58
|
+
if (matchedEp) {
|
|
59
|
+
episodeTmdbId = String(matchedEp.id);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch { }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} catch { }
|
|
67
|
+
|
|
68
|
+
const queryParams = new URLSearchParams({
|
|
69
|
+
type,
|
|
70
|
+
tmdbId: String(id),
|
|
71
|
+
title: title || String(id)
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (releaseYear) queryParams.set('releaseYear', String(releaseYear));
|
|
75
|
+
if (imdbId) queryParams.set('imdbId', String(imdbId));
|
|
76
|
+
|
|
77
|
+
if (isTv) {
|
|
78
|
+
queryParams.set('seasonNumber', String(s));
|
|
79
|
+
queryParams.set('episodeNumber', String(e));
|
|
80
|
+
if (seasonTmdbId) queryParams.set('seasonTmdbId', String(seasonTmdbId));
|
|
81
|
+
if (episodeTmdbId) queryParams.set('episodeTmdbId', String(episodeTmdbId));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const allUrls = [];
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const scrapeUrl = `${BASE_URL}/scrape?${queryParams.toString()}`;
|
|
88
|
+
const res = await fetch(scrapeUrl, {
|
|
89
|
+
headers: {
|
|
90
|
+
'User-Agent': USER_AGENT,
|
|
91
|
+
'Accept': 'text/event-stream',
|
|
92
|
+
'Referer': `${BASE_URL}/`
|
|
93
|
+
},
|
|
94
|
+
signal: AbortSignal.timeout(20000)
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (res.ok) {
|
|
98
|
+
const text = await res.text();
|
|
99
|
+
const events = text.split('\n\n');
|
|
100
|
+
|
|
101
|
+
for (const ev of events) {
|
|
102
|
+
if (ev.includes('event: completed')) {
|
|
103
|
+
const dataMatch = ev.match(/data:\s*(.+)/);
|
|
104
|
+
if (dataMatch) {
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(dataMatch[1].trim());
|
|
107
|
+
const stream = parsed.stream;
|
|
108
|
+
const streamUrl = stream?.playlist || stream?.url || stream?.file;
|
|
109
|
+
if (streamUrl) {
|
|
110
|
+
const sourceName = PROVIDERS[parsed.sourceId] || parsed.sourceId || 'Poseidon';
|
|
111
|
+
allUrls.push({
|
|
112
|
+
url: streamUrl,
|
|
113
|
+
server: `PeeStream - ${sourceName}`,
|
|
114
|
+
quality: 'Auto',
|
|
115
|
+
type: stream.type === 'hls' || streamUrl.includes('.m3u8') ? 'hls' : 'mp4',
|
|
116
|
+
headers: stream.headers || {
|
|
117
|
+
'User-Agent': USER_AGENT
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
} catch { }
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch { }
|
|
127
|
+
|
|
128
|
+
if (server && server !== 'all') {
|
|
129
|
+
const clean = server.toLowerCase().replace('peestream - ', '').trim();
|
|
130
|
+
const filtered = allUrls.filter(u => u.server.toLowerCase().includes(clean));
|
|
131
|
+
if (filtered.length) return { allUrls: filtered };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return allUrls.length ? { allUrls } : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function getSources() {
|
|
138
|
+
return Object.values(PROVIDERS).map(name => `PeeStream - ${name}`);
|
|
139
|
+
}
|