multporn-api-sdk 0.1.1 → 0.1.3
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/README.md +83 -135
- package/dist/browser/index.js +513 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/index.cjs +75 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +69 -44
- package/dist/index.js.map +1 -1
- package/dist/node/index.cjs +533 -0
- package/dist/node/index.cjs.map +1 -0
- package/dist/node/index.js +508 -0
- package/dist/node/index.js.map +1 -0
- package/dist/rn/index.js +513 -0
- package/dist/rn/index.js.map +1 -0
- package/dist/types/index.d.ts +228 -0
- package/dist/types/index.js +452 -0
- package/package.json +42 -13
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
type ListingItem = {
|
|
2
|
+
title: string;
|
|
3
|
+
url: string;
|
|
4
|
+
thumb?: string;
|
|
5
|
+
proxiedThumb?: string;
|
|
6
|
+
};
|
|
7
|
+
type ExposedOption = {
|
|
8
|
+
value: string;
|
|
9
|
+
label: string;
|
|
10
|
+
selected?: boolean;
|
|
11
|
+
};
|
|
12
|
+
type ExposedSelect = {
|
|
13
|
+
name: string;
|
|
14
|
+
label?: string;
|
|
15
|
+
options: ExposedOption[];
|
|
16
|
+
};
|
|
17
|
+
interface SortingUI {
|
|
18
|
+
actionPath: string;
|
|
19
|
+
selects: ExposedSelect[];
|
|
20
|
+
appliedParams: Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
type Page<T> = {
|
|
23
|
+
page: number;
|
|
24
|
+
items: T[];
|
|
25
|
+
hasNext: boolean;
|
|
26
|
+
totalPages?: number;
|
|
27
|
+
pageSize?: number;
|
|
28
|
+
alphabet?: AlphabetBlock;
|
|
29
|
+
sorting?: SortingUI;
|
|
30
|
+
};
|
|
31
|
+
type Post = {
|
|
32
|
+
title: string;
|
|
33
|
+
url: string;
|
|
34
|
+
images: string[];
|
|
35
|
+
tags: string[];
|
|
36
|
+
author: string | null;
|
|
37
|
+
};
|
|
38
|
+
type ViewName = 'new_mini' | 'user_upload_front' | 'updated_manga' | 'updated_manga_promoted' | 'updated_games' | 'random_top_comics' | 'top_random_characters';
|
|
39
|
+
type UpdatesResult = {
|
|
40
|
+
items: ListingItem[];
|
|
41
|
+
first: number;
|
|
42
|
+
last: number;
|
|
43
|
+
html: string;
|
|
44
|
+
viewName: string;
|
|
45
|
+
};
|
|
46
|
+
type MultpornUpdatesParams = {
|
|
47
|
+
first?: number;
|
|
48
|
+
last?: number;
|
|
49
|
+
view_args?: string;
|
|
50
|
+
view_path?: string;
|
|
51
|
+
view_base_path?: string;
|
|
52
|
+
view_display_id?: string;
|
|
53
|
+
view_name?: ViewName | string;
|
|
54
|
+
jcarousel_dom_id?: string | number;
|
|
55
|
+
};
|
|
56
|
+
type ResolvedListingRoute = {
|
|
57
|
+
route: 'listing';
|
|
58
|
+
data: Page<ListingItem> & {
|
|
59
|
+
absoluteUrl: string;
|
|
60
|
+
path: string;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
type ResolvedViewerRoute = {
|
|
64
|
+
route: 'viewer';
|
|
65
|
+
data: {
|
|
66
|
+
absoluteUrl: string;
|
|
67
|
+
viewer: {
|
|
68
|
+
kind: 'images' | 'video' | 'other';
|
|
69
|
+
images?: Array<{
|
|
70
|
+
original?: string;
|
|
71
|
+
large?: string;
|
|
72
|
+
medium?: string;
|
|
73
|
+
small?: string;
|
|
74
|
+
thumb?: string;
|
|
75
|
+
proxied?: string;
|
|
76
|
+
}>;
|
|
77
|
+
video?: {
|
|
78
|
+
poster?: string;
|
|
79
|
+
sources: Array<{
|
|
80
|
+
url?: string;
|
|
81
|
+
proxied?: string;
|
|
82
|
+
type?: string;
|
|
83
|
+
label?: string;
|
|
84
|
+
}>;
|
|
85
|
+
};
|
|
86
|
+
meta?: Record<string, unknown>;
|
|
87
|
+
};
|
|
88
|
+
recommendations?: ListingItem[];
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
type ResolvedRoute = ResolvedListingRoute | ResolvedViewerRoute;
|
|
92
|
+
type AlphabetSection = 'comics' | 'category_comic' | 'characters' | 'authors_comics' | 'pipictures' | 'porn_gifs' | 'manga' | 'authors_hentai';
|
|
93
|
+
type AlphabetLetter = {
|
|
94
|
+
label: string;
|
|
95
|
+
value: string;
|
|
96
|
+
href: string;
|
|
97
|
+
count?: number;
|
|
98
|
+
active?: boolean;
|
|
99
|
+
};
|
|
100
|
+
interface AlphabetBlock {
|
|
101
|
+
section: string;
|
|
102
|
+
letters: AlphabetLetter[];
|
|
103
|
+
}
|
|
104
|
+
type LinkItem = {
|
|
105
|
+
title: string;
|
|
106
|
+
url: string;
|
|
107
|
+
};
|
|
108
|
+
type ViewerKind = 'manga' | 'comics' | 'pictures' | 'humor' | 'video' | 'game' | 'other' | 'images';
|
|
109
|
+
interface ViewerImage {
|
|
110
|
+
original: string;
|
|
111
|
+
large?: string;
|
|
112
|
+
medium?: string;
|
|
113
|
+
small?: string;
|
|
114
|
+
thumb?: string;
|
|
115
|
+
proxied?: string;
|
|
116
|
+
}
|
|
117
|
+
interface ViewerVideoSource {
|
|
118
|
+
url: string;
|
|
119
|
+
type?: string;
|
|
120
|
+
label?: string;
|
|
121
|
+
proxied?: string;
|
|
122
|
+
}
|
|
123
|
+
interface ViewerVideo {
|
|
124
|
+
poster?: string;
|
|
125
|
+
sources: ViewerVideoSource[];
|
|
126
|
+
}
|
|
127
|
+
interface ViewerMeta {
|
|
128
|
+
nodeId: number | null;
|
|
129
|
+
fieldSys: string | null;
|
|
130
|
+
title: string;
|
|
131
|
+
kind: ViewerKind;
|
|
132
|
+
breadcrumbs: LinkItem[];
|
|
133
|
+
authors: LinkItem[];
|
|
134
|
+
sections: LinkItem[];
|
|
135
|
+
tags: LinkItem[];
|
|
136
|
+
characters: LinkItem[];
|
|
137
|
+
userTags: LinkItem[];
|
|
138
|
+
rating?: number;
|
|
139
|
+
votes?: number;
|
|
140
|
+
views?: number;
|
|
141
|
+
related?: LinkItem[];
|
|
142
|
+
}
|
|
143
|
+
interface ViewerResult {
|
|
144
|
+
kind: ViewerKind;
|
|
145
|
+
meta: ViewerMeta;
|
|
146
|
+
images?: ViewerImage[];
|
|
147
|
+
video?: ViewerVideo;
|
|
148
|
+
}
|
|
149
|
+
type ListingPayload = {
|
|
150
|
+
page: Page<ListingItem>;
|
|
151
|
+
absoluteUrl: string;
|
|
152
|
+
path: string;
|
|
153
|
+
title?: string;
|
|
154
|
+
breadcrumbs?: LinkItem[];
|
|
155
|
+
};
|
|
156
|
+
type ViewerPayload = {
|
|
157
|
+
viewer: ViewerResult;
|
|
158
|
+
absoluteUrl: string;
|
|
159
|
+
path: string;
|
|
160
|
+
recommendations?: ListingItem[];
|
|
161
|
+
};
|
|
162
|
+
interface ResolveOptions {
|
|
163
|
+
proxyImage?: (url: string) => string;
|
|
164
|
+
proxyVideo?: (url: string) => string;
|
|
165
|
+
signal?: AbortSignal;
|
|
166
|
+
}
|
|
167
|
+
type ListingQuery = Record<string, string | number | boolean | undefined>;
|
|
168
|
+
|
|
169
|
+
type RetryPolicy = {
|
|
170
|
+
retries: number;
|
|
171
|
+
factor: number;
|
|
172
|
+
minDelayMs: number;
|
|
173
|
+
maxDelayMs: number;
|
|
174
|
+
retryOn: (status?: number) => boolean;
|
|
175
|
+
};
|
|
176
|
+
type HttpClientOptions = {
|
|
177
|
+
baseURL: string;
|
|
178
|
+
headers?: Record<string, string>;
|
|
179
|
+
timeoutMs?: number;
|
|
180
|
+
retry?: Partial<RetryPolicy>;
|
|
181
|
+
userAgent?: string;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
type DomDocument = any;
|
|
185
|
+
type DomElement = any;
|
|
186
|
+
interface DomApi {
|
|
187
|
+
parse(html: string): DomDocument;
|
|
188
|
+
qsa(ctx: DomDocument | DomElement, selector: string): DomElement[];
|
|
189
|
+
qs(ctx: DomDocument | DomElement, selector: string): DomElement | null;
|
|
190
|
+
text(el?: DomElement | null): string;
|
|
191
|
+
attr(el: DomElement | null | undefined, name: string): string | undefined;
|
|
192
|
+
closest(el: DomElement | null | undefined, selector: string): DomElement | null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
type MultpornClientOptions = Omit<HttpClientOptions, 'baseURL'> & {
|
|
196
|
+
baseURL?: string;
|
|
197
|
+
dom: DomApi;
|
|
198
|
+
};
|
|
199
|
+
declare class MultpornClientCore {
|
|
200
|
+
private http;
|
|
201
|
+
private baseURL;
|
|
202
|
+
private dom;
|
|
203
|
+
constructor(opts: MultpornClientOptions);
|
|
204
|
+
private text;
|
|
205
|
+
private attr;
|
|
206
|
+
private pickImg;
|
|
207
|
+
private parseListing;
|
|
208
|
+
private parseHasNext;
|
|
209
|
+
private buildListURL;
|
|
210
|
+
latest(page?: number, params?: ListingQuery): Promise<Page<ListingItem>>;
|
|
211
|
+
listByPath(path: string | undefined, page?: number, params?: ListingQuery & {
|
|
212
|
+
letter?: string;
|
|
213
|
+
}): Promise<Page<ListingItem>>;
|
|
214
|
+
search(q: string, page?: number): Promise<Page<ListingItem>>;
|
|
215
|
+
private parsePost;
|
|
216
|
+
getPost(urlOrSlug: string): Promise<Post>;
|
|
217
|
+
resolveSmart(urlOrSlug: string, _opts?: ResolveOptions): Promise<ResolvedRoute>;
|
|
218
|
+
alphabetLetters(section: AlphabetSection): Promise<AlphabetLetter[]>;
|
|
219
|
+
alphabet(section: AlphabetSection, letter: string, page?: number): Promise<Page<ListingItem>>;
|
|
220
|
+
updates(params?: Partial<Record<string, string | number>> & {
|
|
221
|
+
view_name?: string;
|
|
222
|
+
}): Promise<UpdatesResult>;
|
|
223
|
+
viewUpdates(viewName: string, params?: Omit<{
|
|
224
|
+
[k: string]: string | number;
|
|
225
|
+
}, 'view_name'>): Promise<UpdatesResult>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export { type AlphabetBlock, type AlphabetLetter, type AlphabetSection, type ExposedOption, type ExposedSelect, type LinkItem, type ListingItem, type ListingPayload, type ListingQuery, MultpornClientCore as MultpornClient, type MultpornUpdatesParams, type Page, type Post, type ResolveOptions, type ResolvedListingRoute, type ResolvedRoute, type ResolvedViewerRoute, type SortingUI, type UpdatesResult, type ViewName, type ViewerImage, type ViewerKind, type ViewerMeta, type ViewerPayload, type ViewerResult, type ViewerVideo, type ViewerVideoSource };
|
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// src/http.ts
|
|
2
|
+
var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
|
+
function hasAbortController() {
|
|
4
|
+
return typeof AbortController !== "undefined";
|
|
5
|
+
}
|
|
6
|
+
async function fetchWithTimeout(url, init, timeoutMs) {
|
|
7
|
+
if (hasAbortController()) {
|
|
8
|
+
const ctrl = new AbortController();
|
|
9
|
+
const timer = setTimeout(() => {
|
|
10
|
+
try {
|
|
11
|
+
ctrl.abort();
|
|
12
|
+
} catch {
|
|
13
|
+
}
|
|
14
|
+
}, timeoutMs);
|
|
15
|
+
try {
|
|
16
|
+
const res = await fetch(url, { ...init, signal: ctrl.signal });
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
return res;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
throw e;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return await Promise.race([
|
|
25
|
+
fetch(url, init),
|
|
26
|
+
new Promise(
|
|
27
|
+
(_, reject) => setTimeout(() => reject(new HttpError("Request timeout")), timeoutMs)
|
|
28
|
+
)
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
var defaultRetryPolicy = {
|
|
32
|
+
retries: 2,
|
|
33
|
+
factor: 2,
|
|
34
|
+
minDelayMs: 300,
|
|
35
|
+
maxDelayMs: 3e3,
|
|
36
|
+
retryOn: (status) => {
|
|
37
|
+
if (status == null) return true;
|
|
38
|
+
return status >= 500 && status <= 599;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var HttpError = class extends Error {
|
|
42
|
+
constructor(message, status) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.status = status;
|
|
45
|
+
this.name = "HttpError";
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
var HttpClient = class {
|
|
49
|
+
baseURL;
|
|
50
|
+
headers;
|
|
51
|
+
timeoutMs;
|
|
52
|
+
retry;
|
|
53
|
+
userAgent;
|
|
54
|
+
constructor(opts) {
|
|
55
|
+
this.baseURL = opts.baseURL.replace(/\/+$/, "");
|
|
56
|
+
this.headers = opts.headers ?? {};
|
|
57
|
+
this.timeoutMs = opts.timeoutMs ?? 15e3;
|
|
58
|
+
this.retry = { ...defaultRetryPolicy, ...opts.retry ?? {} };
|
|
59
|
+
this.userAgent = opts.userAgent ?? "Mozilla/5.0 (Windows NT 10.0; Win32; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118 Safari/537.36";
|
|
60
|
+
}
|
|
61
|
+
buildURL(pathOrUrl) {
|
|
62
|
+
try {
|
|
63
|
+
return new URL(pathOrUrl).toString();
|
|
64
|
+
} catch {
|
|
65
|
+
return new URL(pathOrUrl.replace(/^\//, ""), this.baseURL + "/").toString();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async getHtml(pathOrUrl, attempt = 0) {
|
|
69
|
+
const url = this.buildURL(pathOrUrl);
|
|
70
|
+
try {
|
|
71
|
+
const res = await fetchWithTimeout(
|
|
72
|
+
url,
|
|
73
|
+
{
|
|
74
|
+
method: "GET",
|
|
75
|
+
headers: {
|
|
76
|
+
"User-Agent": this.userAgent,
|
|
77
|
+
Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
78
|
+
...this.headers
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
this.timeoutMs
|
|
82
|
+
);
|
|
83
|
+
if (!res.ok) {
|
|
84
|
+
if (this.retry.retryOn(res.status) && attempt < this.retry.retries) {
|
|
85
|
+
const delay = Math.min(
|
|
86
|
+
this.retry.maxDelayMs,
|
|
87
|
+
this.retry.minDelayMs * this.retry.factor ** attempt
|
|
88
|
+
);
|
|
89
|
+
await sleep(delay);
|
|
90
|
+
return this.getHtml(pathOrUrl, attempt + 1);
|
|
91
|
+
}
|
|
92
|
+
throw new HttpError(`HTTP ${res.status}`, res.status);
|
|
93
|
+
}
|
|
94
|
+
return await res.text();
|
|
95
|
+
} catch (e) {
|
|
96
|
+
if (attempt < this.retry.retries) {
|
|
97
|
+
const delay = Math.min(
|
|
98
|
+
this.retry.maxDelayMs,
|
|
99
|
+
this.retry.minDelayMs * this.retry.factor ** attempt
|
|
100
|
+
);
|
|
101
|
+
await sleep(delay);
|
|
102
|
+
return this.getHtml(pathOrUrl, attempt + 1);
|
|
103
|
+
}
|
|
104
|
+
if (e?.name === "AbortError") throw new HttpError("Request timeout");
|
|
105
|
+
throw new HttpError(e?.message ?? "Network error");
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async getJson(pathOrUrl, attempt = 0) {
|
|
109
|
+
const url = this.buildURL(pathOrUrl);
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetchWithTimeout(
|
|
112
|
+
url,
|
|
113
|
+
{
|
|
114
|
+
method: "GET",
|
|
115
|
+
headers: {
|
|
116
|
+
"User-Agent": this.userAgent,
|
|
117
|
+
Accept: "application/json,text/plain;q=0.9,*/*;q=0.8",
|
|
118
|
+
...this.headers
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
this.timeoutMs
|
|
122
|
+
);
|
|
123
|
+
if (!res.ok) {
|
|
124
|
+
if (this.retry.retryOn(res.status) && attempt < this.retry.retries) {
|
|
125
|
+
const delay = Math.min(
|
|
126
|
+
this.retry.maxDelayMs,
|
|
127
|
+
this.retry.minDelayMs * this.retry.factor ** attempt
|
|
128
|
+
);
|
|
129
|
+
await sleep(delay);
|
|
130
|
+
return this.getJson(pathOrUrl, attempt + 1);
|
|
131
|
+
}
|
|
132
|
+
throw new HttpError(`HTTP ${res.status}`, res.status);
|
|
133
|
+
}
|
|
134
|
+
return await res.json();
|
|
135
|
+
} catch (e) {
|
|
136
|
+
if (attempt < this.retry.retries) {
|
|
137
|
+
const delay = Math.min(
|
|
138
|
+
this.retry.maxDelayMs,
|
|
139
|
+
this.retry.minDelayMs * this.retry.factor ** attempt
|
|
140
|
+
);
|
|
141
|
+
await sleep(delay);
|
|
142
|
+
return this.getJson(pathOrUrl, attempt + 1);
|
|
143
|
+
}
|
|
144
|
+
if (e?.name === "AbortError") throw new HttpError("Request timeout");
|
|
145
|
+
throw new HttpError(e?.message ?? "Network error");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async postForm(pathOrUrl, form, attempt = 0) {
|
|
149
|
+
const url = this.buildURL(pathOrUrl);
|
|
150
|
+
const usp = new URLSearchParams();
|
|
151
|
+
for (const [k, v] of Object.entries(form)) {
|
|
152
|
+
if (Array.isArray(v)) {
|
|
153
|
+
for (const item of v) usp.append(k, item);
|
|
154
|
+
} else if (v != null) {
|
|
155
|
+
usp.append(k, v);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const body = usp.toString();
|
|
159
|
+
try {
|
|
160
|
+
const res = await fetchWithTimeout(
|
|
161
|
+
url,
|
|
162
|
+
{
|
|
163
|
+
method: "POST",
|
|
164
|
+
headers: {
|
|
165
|
+
"User-Agent": this.userAgent,
|
|
166
|
+
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
|
|
167
|
+
"X-Requested-With": "XMLHttpRequest",
|
|
168
|
+
Accept: "application/json, text/javascript, */*; q=0.01",
|
|
169
|
+
...this.headers
|
|
170
|
+
},
|
|
171
|
+
body
|
|
172
|
+
},
|
|
173
|
+
this.timeoutMs
|
|
174
|
+
);
|
|
175
|
+
if (!res.ok) {
|
|
176
|
+
if (this.retry.retryOn(res.status) && attempt < this.retry.retries) {
|
|
177
|
+
const delay = Math.min(
|
|
178
|
+
this.retry.maxDelayMs,
|
|
179
|
+
this.retry.minDelayMs * this.retry.factor ** attempt
|
|
180
|
+
);
|
|
181
|
+
await sleep(delay);
|
|
182
|
+
return this.postForm(pathOrUrl, form, attempt + 1);
|
|
183
|
+
}
|
|
184
|
+
throw new HttpError(`HTTP ${res.status}`, res.status);
|
|
185
|
+
}
|
|
186
|
+
const ct = res.headers.get("content-type") || "";
|
|
187
|
+
if (ct.includes("application/json") || ct.includes("text/javascript")) {
|
|
188
|
+
return await res.json();
|
|
189
|
+
}
|
|
190
|
+
const text = await res.text();
|
|
191
|
+
try {
|
|
192
|
+
return JSON.parse(text);
|
|
193
|
+
} catch {
|
|
194
|
+
return text;
|
|
195
|
+
}
|
|
196
|
+
} catch (e) {
|
|
197
|
+
if (attempt < this.retry.retries) {
|
|
198
|
+
const delay = Math.min(
|
|
199
|
+
this.retry.maxDelayMs,
|
|
200
|
+
this.retry.minDelayMs * this.retry.factor ** attempt
|
|
201
|
+
);
|
|
202
|
+
await sleep(delay);
|
|
203
|
+
return this.postForm(pathOrUrl, form, attempt + 1);
|
|
204
|
+
}
|
|
205
|
+
if (e?.name === "AbortError") throw new HttpError("Request timeout");
|
|
206
|
+
throw new HttpError(e?.message ?? "Network error");
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// src/client-core.ts
|
|
212
|
+
var DEFAULT_HEADERS = {
|
|
213
|
+
Referer: "https://multporn.net",
|
|
214
|
+
Origin: "https://multporn.net",
|
|
215
|
+
Accept: "*/*",
|
|
216
|
+
"User-Agent": "Mozilla/5.0 (Linux; Android 12; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0 Mobile Safari/537.36"
|
|
217
|
+
};
|
|
218
|
+
function absolutize(base, href) {
|
|
219
|
+
if (!href) return void 0;
|
|
220
|
+
try {
|
|
221
|
+
if (href.startsWith("//")) return new URL("https:" + href).href;
|
|
222
|
+
return new URL(href, base).href;
|
|
223
|
+
} catch {
|
|
224
|
+
return void 0;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
function looksLikeListingUrl(u) {
|
|
228
|
+
return /\/(comics|manga|munga|pictures|video|gay_porn_comics|hentai_manga)\//i.test(u);
|
|
229
|
+
}
|
|
230
|
+
function shouldSkipThumb(u) {
|
|
231
|
+
if (!u) return true;
|
|
232
|
+
return /(logo|avatar|sprite|icon|favicon)/i.test(u);
|
|
233
|
+
}
|
|
234
|
+
var MultpornClientCore = class {
|
|
235
|
+
http;
|
|
236
|
+
baseURL;
|
|
237
|
+
dom;
|
|
238
|
+
constructor(opts) {
|
|
239
|
+
this.baseURL = (opts.baseURL ?? "https://multporn.net").replace(/\/+$/, "");
|
|
240
|
+
this.http = new HttpClient({
|
|
241
|
+
baseURL: this.baseURL,
|
|
242
|
+
headers: { ...DEFAULT_HEADERS, ...opts.headers ?? {} },
|
|
243
|
+
timeoutMs: opts.timeoutMs ?? 15e3,
|
|
244
|
+
retry: opts.retry,
|
|
245
|
+
userAgent: opts.userAgent
|
|
246
|
+
});
|
|
247
|
+
this.dom = opts.dom;
|
|
248
|
+
}
|
|
249
|
+
text(el) {
|
|
250
|
+
return this.dom.text(el);
|
|
251
|
+
}
|
|
252
|
+
attr(el, n) {
|
|
253
|
+
return this.dom.attr(el, n) ?? "";
|
|
254
|
+
}
|
|
255
|
+
pickImg(el) {
|
|
256
|
+
if (!el) return void 0;
|
|
257
|
+
const inNode = el.querySelector?.("img") ?? null;
|
|
258
|
+
const imgEl = inNode || el;
|
|
259
|
+
const src = this.attr(imgEl, "data-src") || this.attr(imgEl, "data-original") || this.attr(imgEl, "src");
|
|
260
|
+
return absolutize(this.baseURL, src);
|
|
261
|
+
}
|
|
262
|
+
parseListing(html) {
|
|
263
|
+
const doc = this.dom.parse(html);
|
|
264
|
+
const root = doc.documentElement ?? doc;
|
|
265
|
+
const anchors = this.dom.qsa(root, "a[href]");
|
|
266
|
+
const seen = /* @__PURE__ */ new Set();
|
|
267
|
+
const out = [];
|
|
268
|
+
for (const a of anchors) {
|
|
269
|
+
const href = this.attr(a, "href");
|
|
270
|
+
const url = absolutize(this.baseURL, href);
|
|
271
|
+
if (!url || !looksLikeListingUrl(url)) continue;
|
|
272
|
+
if (seen.has(url)) continue;
|
|
273
|
+
const img = this.pickImg(a) || this.pickImg(this.dom.closest(a, "figure")) || this.pickImg(this.dom.closest(a, ".thumb")) || this.pickImg(a.parentNode);
|
|
274
|
+
const title = this.attr(a, "title") || this.text(a);
|
|
275
|
+
if (!title) continue;
|
|
276
|
+
const thumb = shouldSkipThumb(img) ? void 0 : img;
|
|
277
|
+
out.push({ title, url, thumb });
|
|
278
|
+
seen.add(url);
|
|
279
|
+
}
|
|
280
|
+
return out;
|
|
281
|
+
}
|
|
282
|
+
parseHasNext(html) {
|
|
283
|
+
const doc = this.dom.parse(html);
|
|
284
|
+
const root = doc.documentElement ?? doc;
|
|
285
|
+
if (this.dom.qs(root, 'a[rel="next"]')) return true;
|
|
286
|
+
const pager = this.dom.qs(root, ".pager, .pagination, nav[role='navigation']");
|
|
287
|
+
if (pager) {
|
|
288
|
+
const next = this.dom.qs(
|
|
289
|
+
pager,
|
|
290
|
+
'a[rel="next"], a.next, li.next a, a[title*="\u0421\u043B\u0435\u0434"], a[aria-label*="Next"]'
|
|
291
|
+
);
|
|
292
|
+
if (next) return true;
|
|
293
|
+
}
|
|
294
|
+
return /\bpage=\d+\b/i.test(html);
|
|
295
|
+
}
|
|
296
|
+
buildListURL(path, page = 0, letter) {
|
|
297
|
+
if (!path) {
|
|
298
|
+
const u2 = new URL(this.baseURL);
|
|
299
|
+
if (page > 0) u2.searchParams.set("page", String(page));
|
|
300
|
+
return u2.href;
|
|
301
|
+
}
|
|
302
|
+
const u = new URL(path.startsWith("/") ? path : "/" + path, this.baseURL);
|
|
303
|
+
if (page > 0) u.searchParams.set("page", String(page));
|
|
304
|
+
if (letter) u.searchParams.set("letter", letter);
|
|
305
|
+
return u.href;
|
|
306
|
+
}
|
|
307
|
+
// -------- Listing
|
|
308
|
+
async latest(page = 0, params) {
|
|
309
|
+
return this.listByPath(void 0, page, params);
|
|
310
|
+
}
|
|
311
|
+
async listByPath(path, page = 0, params) {
|
|
312
|
+
const url = this.buildListURL(path, page, params?.letter);
|
|
313
|
+
const html = await this.http.getHtml(url);
|
|
314
|
+
const items = this.parseListing(html);
|
|
315
|
+
const hasNext = this.parseHasNext(html);
|
|
316
|
+
return { items, page, hasNext, totalPages: hasNext ? page + 2 : page + 1 };
|
|
317
|
+
}
|
|
318
|
+
// -------- Search
|
|
319
|
+
async search(q, page = 0) {
|
|
320
|
+
const u = new URL("/search", this.baseURL);
|
|
321
|
+
u.searchParams.set("search", q);
|
|
322
|
+
if (page > 0) u.searchParams.set("page", String(page));
|
|
323
|
+
const html = await this.http.getHtml(u.href);
|
|
324
|
+
const items = this.parseListing(html);
|
|
325
|
+
const hasNext = this.parseHasNext(html);
|
|
326
|
+
return { items, page, hasNext, totalPages: hasNext ? page + 2 : page + 1 };
|
|
327
|
+
}
|
|
328
|
+
// -------- Post
|
|
329
|
+
parsePost(html, url) {
|
|
330
|
+
const doc = this.dom.parse(html);
|
|
331
|
+
const root = doc.documentElement ?? doc;
|
|
332
|
+
const title = this.text(this.dom.qs(root, "h1")) || this.attr(this.dom.qs(root, 'meta[property="og:title"]'), "content") || "\u0411\u0435\u0437 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u044F";
|
|
333
|
+
const imgEls = this.dom.qsa(root, "img");
|
|
334
|
+
const imgSet = /* @__PURE__ */ new Set();
|
|
335
|
+
for (const el of imgEls) {
|
|
336
|
+
const src = absolutize(
|
|
337
|
+
this.baseURL,
|
|
338
|
+
this.attr(el, "data-src") || this.attr(el, "data-original") || this.attr(el, "src")
|
|
339
|
+
);
|
|
340
|
+
if (!src || /\b(logo|sprite|icon|favicon)\b/i.test(src)) continue;
|
|
341
|
+
imgSet.add(src);
|
|
342
|
+
}
|
|
343
|
+
const images = Array.from(imgSet);
|
|
344
|
+
const tags = [];
|
|
345
|
+
const tagEls = this.dom.qsa(root, 'a[href*="/tags/"], .tags a, .field-name-field-tags a');
|
|
346
|
+
for (const t of tagEls) {
|
|
347
|
+
const txt = this.text(t);
|
|
348
|
+
if (txt) tags.push(txt);
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
url,
|
|
352
|
+
title,
|
|
353
|
+
images,
|
|
354
|
+
tags,
|
|
355
|
+
author: null
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
async getPost(urlOrSlug) {
|
|
359
|
+
const url = absolutize(this.baseURL, urlOrSlug) ?? new URL(urlOrSlug, this.baseURL).href;
|
|
360
|
+
const html = await this.http.getHtml(url);
|
|
361
|
+
return this.parsePost(html, url);
|
|
362
|
+
}
|
|
363
|
+
// -------- Smart resolve
|
|
364
|
+
async resolveSmart(urlOrSlug, _opts) {
|
|
365
|
+
const url = absolutize(this.baseURL, urlOrSlug) ?? new URL(urlOrSlug, this.baseURL).href;
|
|
366
|
+
const html = await this.http.getHtml(url);
|
|
367
|
+
const doc = this.dom.parse(html);
|
|
368
|
+
const articleImgs = this.dom.qsa(doc.documentElement ?? doc, "article img");
|
|
369
|
+
if (articleImgs.length >= 2) {
|
|
370
|
+
const post = this.parsePost(html, url);
|
|
371
|
+
return {
|
|
372
|
+
route: "viewer",
|
|
373
|
+
data: {
|
|
374
|
+
absoluteUrl: url,
|
|
375
|
+
viewer: {
|
|
376
|
+
kind: "images",
|
|
377
|
+
meta: {
|
|
378
|
+
nodeId: null,
|
|
379
|
+
fieldSys: null,
|
|
380
|
+
title: post.title,
|
|
381
|
+
kind: "images",
|
|
382
|
+
breadcrumbs: [],
|
|
383
|
+
authors: [],
|
|
384
|
+
sections: [],
|
|
385
|
+
tags: [],
|
|
386
|
+
characters: [],
|
|
387
|
+
userTags: []
|
|
388
|
+
},
|
|
389
|
+
images: post.images.map((u) => ({ original: u }))
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
const items = this.parseListing(html);
|
|
395
|
+
const hasNext = this.parseHasNext(html);
|
|
396
|
+
return {
|
|
397
|
+
route: "listing",
|
|
398
|
+
data: {
|
|
399
|
+
page: 0,
|
|
400
|
+
items,
|
|
401
|
+
hasNext,
|
|
402
|
+
absoluteUrl: url,
|
|
403
|
+
path: new URL(url).pathname
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
// -------- Alphabet (best effort)
|
|
408
|
+
async alphabetLetters(section) {
|
|
409
|
+
const path = section === "manga" ? "/munga" : `/${section}`;
|
|
410
|
+
const html = await this.http.getHtml(path);
|
|
411
|
+
const doc = this.dom.parse(html);
|
|
412
|
+
const root = doc.documentElement ?? doc;
|
|
413
|
+
const letters = [];
|
|
414
|
+
const els = this.dom.qsa(
|
|
415
|
+
root,
|
|
416
|
+
".alphabet a, .alphabet__item a, .letters a, a[href*='letter=']"
|
|
417
|
+
);
|
|
418
|
+
for (const a of els) {
|
|
419
|
+
const label = this.text(a) || this.attr(a, "data-letter");
|
|
420
|
+
if (!label) continue;
|
|
421
|
+
const href = this.attr(a, "href");
|
|
422
|
+
letters.push({ label, value: label, href });
|
|
423
|
+
}
|
|
424
|
+
if (!letters.length) {
|
|
425
|
+
return "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").map((c) => ({ label: c, value: c, href: "" }));
|
|
426
|
+
}
|
|
427
|
+
return letters;
|
|
428
|
+
}
|
|
429
|
+
async alphabet(section, letter, page = 0) {
|
|
430
|
+
const path = section === "manga" ? "/munga" : `/${section}`;
|
|
431
|
+
return this.listByPath(path, page, { letter });
|
|
432
|
+
}
|
|
433
|
+
// -------- Updates (views/ajax) — best effort
|
|
434
|
+
// ВАЖНО: чтобы не ловить TS2411, не используем индексную сигнатуру с строгим 'string|number'.
|
|
435
|
+
// Вход — Partial (значит, значения могут быть undefined), а отправляем — отфильтрованный Record.
|
|
436
|
+
async updates(params) {
|
|
437
|
+
const filtered = Object.fromEntries(
|
|
438
|
+
Object.entries(params ?? {}).filter(([, v]) => v !== void 0)
|
|
439
|
+
);
|
|
440
|
+
const p = { view_name: params?.view_name ?? "new_mini", ...params };
|
|
441
|
+
const json = await this.http.postForm("/views/ajax", p);
|
|
442
|
+
const raw = typeof json === "string" ? json : JSON.stringify(json);
|
|
443
|
+
const items = this.parseListing(raw);
|
|
444
|
+
return { items, first: 0, last: items.length, html: raw, viewName: String(p.view_name) };
|
|
445
|
+
}
|
|
446
|
+
async viewUpdates(viewName, params) {
|
|
447
|
+
return this.updates({ view_name: viewName, ...params || {} });
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
export {
|
|
451
|
+
MultpornClientCore as MultpornClient
|
|
452
|
+
};
|