Skip to content

Commit f3be2f3

Browse files
committed
Update metadata API
1 parent 46340dc commit f3be2f3

11 files changed

Lines changed: 298 additions & 55 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# DownloadLib
22

3-
#### Браузерное расширение для загрузки манги | v1.0.2
3+
#### Браузерное расширение для загрузки манги | v1.0.3
44

55
[![Test Extension](https://github.com/ivanvit100/DownloadLib/actions/workflows/test.yaml/badge.svg)](https://github.com/ivanvit/DownloadLib/actions/workflows/test.yaml)
66
![Code Coverage](https://img.shields.io/badge/Coverage-100%25-brightgreen)

background/Background.js

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ function detectServiceByUrl(url) {
4242

4343
function detectServiceByReferer(details) {
4444
const headers = details.requestHeaders || [];
45+
const serviceHeader = headers.find(h => h.name.toLowerCase() === 'x-dl-service');
46+
if (serviceHeader) {
47+
const serviceValue = String(serviceHeader.value || '').toLowerCase();
48+
if (serviceValue === 'mangalib') return 'mangalib';
49+
else if (serviceValue === 'ranobelib') return 'ranobelib';
50+
}
51+
52+
const siteIdHeader = headers.find(h => h.name.toLowerCase() === 'site-id');
53+
if (siteIdHeader) {
54+
const siteId = String(siteIdHeader.value || '').trim();
55+
if (siteId === '1') return 'mangalib';
56+
else if (siteId === '3') return 'ranobelib';
57+
}
58+
4559
const refererHeader = headers.find(h => h.name.toLowerCase() === 'referer');
4660
const referer = refererHeader ? refererHeader.value : '';
4761

@@ -80,10 +94,7 @@ if (isFirefox && browserAPI && browserAPI.webRequest) {
8094
const fromExtension = isFromExtension(details);
8195
const serviceName = detectServiceByReferer(details);
8296

83-
if (!fromExtension) {
84-
if (serviceName) rateLimiter.recordRequest(serviceName);
85-
return {};
86-
}
97+
if (!fromExtension) return {};
8798

8899
if (details.url.includes('ranobelib.me') && isImageRequest(details.url))
89100
return {};
@@ -138,8 +149,6 @@ if (isChrome && browserAPI && browserAPI.webRequest) {
138149

139150
if (isFromExtension(details))
140151
await rateLimiter.trackRequest(serviceName);
141-
else
142-
rateLimiter.recordRequest(serviceName);
143152
},
144153
{ urls: ['<all_urls>'] },
145154
['requestHeaders']

manifest.chrome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "DownloadLib",
4-
"version": "1.0.2",
4+
"version": "1.0.3",
55
"description": "Скачивает мангу с порталов MangaLib в удобном для чтения формате.",
66
"author": "https://ivanvit.ru",
77
"homepage_url": "https://github.com/ivanvit100/DownloadLib",

manifest.firefox.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "DownloadLib",
4-
"version": "1.0.2",
4+
"version": "1.0.3",
55
"description": "Скачивает мангу с порталов MangaLib в удобном для чтения формате.",
66
"author": "https://ivanvit.ru",
77
"homepage_url": "https://github.com/ivanvit100/DownloadLib",

services/mangalib/MangaLibService.js

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,42 @@
3030

3131
async fetchMangaMetadata(slug) {
3232
const fields = this.config.fields;
33-
const query = fields.map(f => `fields[]=${f}`).join('&');
34-
const url = `${this.baseUrl}/api/manga/${slug}?${query}`;
35-
36-
console.log('[MangaLibService] Fetching metadata:', url);
37-
38-
const response = await this.fetchWithRateLimitRetry(url, {
39-
method: 'GET',
40-
headers: this.config.headers,
41-
mode: 'cors',
42-
credentials: 'include',
43-
cache: 'no-store'
44-
});
45-
46-
if (!response.ok) {
33+
const query = Array.isArray(fields) && fields.length
34+
? fields.map(f => `fields[]=${f}`).join('&')
35+
: '';
36+
const urls = [];
37+
38+
if (query) urls.push(`${this.baseUrl}/api/manga/${slug}?${query}`);
39+
urls.push(`${this.baseUrl}/api/manga/${slug}`);
40+
41+
for (let i = 0; i < urls.length; i++) {
42+
const url = urls[i];
43+
console.log('[MangaLibService] Fetching metadata:', url);
44+
45+
const response = await this.fetchWithRateLimitRetry(url, {
46+
method: 'GET',
47+
headers: this.config.headers,
48+
mode: 'cors',
49+
credentials: 'include',
50+
cache: 'no-store'
51+
});
52+
53+
if (!response.ok) {
54+
const text = await response.text().catch(() => '');
55+
if (response.status === 403) {
56+
if (i < urls.length - 1)
57+
console.warn('[MangaLibService] Metadata endpoint rejected, retrying with fallback URL');
58+
continue;
59+
}
60+
console.error('[MangaLibService] Error response:', text);
61+
throw new Error(`Failed to fetch manga: ${response.status}`);
62+
}
63+
4764
const text = await response.text().catch(() => '');
48-
console.error('[MangaLibService] Error response:', text);
49-
throw new Error(`Failed to fetch manga: ${response.status}`);
65+
return text ? JSON.parse(text) : null;
5066
}
51-
52-
const text = await response.text().catch(() => '');
53-
return text ? JSON.parse(text) : null;
67+
68+
return null;
5469
}
5570

5671
async fetchChaptersList(slug) {

services/mangalib/config.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@
1717
],
1818

1919
headers: {
20-
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:146.0) Gecko/20100101 Firefox/146.0',
20+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0',
2121
'Accept': '*/*',
22-
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
22+
'Accept-Language': 'ru,en-US;q=0.9,en;q=0.8',
2323
'Site-Id': '1',
24+
'X-DL-Service': 'mangalib',
2425
'Content-Type': 'application/json',
2526
'Client-Time-Zone': 'Europe/Moscow',
2627
'Referer': 'https://mangalib.me/',
28+
'Origin': 'https://mangalib.me',
2729
'Sec-GPC': '1',
2830
'Sec-Fetch-Dest': 'empty',
2931
'Sec-Fetch-Mode': 'cors',
30-
'Sec-Fetch-Site': 'same-site',
32+
'Sec-Fetch-Site': 'cross-site',
3133
'Connection': 'keep-alive'
3234
},
3335

services/ranobelib/RanobeLibService.js

Lines changed: 33 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,43 @@
3030

3131
async fetchMangaMetadata(slug) {
3232
const fields = this.config.fields;
33-
const query = fields.map(f => `fields[]=${f}`).join('&');
34-
const url = `${this.baseUrl}/api/manga/${slug}?${query}`;
35-
36-
console.log('[RanobeLibService] Fetching metadata:', url);
37-
38-
const response = await this.fetchWithRateLimitRetry(url, {
39-
method: 'GET',
40-
headers: this.config.headers,
41-
mode: 'cors',
42-
credentials: 'include',
43-
cache: 'no-store'
44-
});
33+
const query = Array.isArray(fields) && fields.length
34+
? fields.map(f => `fields[]=${f}`).join('&')
35+
: '';
36+
const urls = [];
37+
38+
if (query) urls.push(`${this.baseUrl}/api/manga/${slug}?${query}`);
39+
urls.push(`${this.baseUrl}/api/manga/${slug}`);
40+
41+
let result = null;
42+
43+
for (let i = 0; i < urls.length; i++) {
44+
const url = urls[i];
45+
console.log('[RanobeLibService] Fetching metadata:', url);
46+
47+
const response = await this.fetchWithRateLimitRetry(url, {
48+
method: 'GET',
49+
headers: this.config.headers,
50+
mode: 'cors',
51+
credentials: 'include',
52+
cache: 'no-store'
53+
});
54+
55+
if (!response.ok) {
56+
const text = await response.text().catch(() => '');
57+
if (response.status === 403 && i < urls.length - 1) {
58+
console.warn('[RanobeLibService] Metadata endpoint rejected, retrying with fallback URL');
59+
continue;
60+
}
61+
console.error('[RanobeLibService] Error response:', text);
62+
throw new Error(`Failed to fetch manga: ${response.status}`);
63+
}
4564

46-
if (!response.ok) {
4765
const text = await response.text().catch(() => '');
48-
console.error('[RanobeLibService] Error response:', text);
49-
throw new Error(`Failed to fetch manga: ${response.status}`);
66+
result = text ? JSON.parse(text) : null;
67+
break;
5068
}
5169

52-
const text = await response.text().catch(() => '');
53-
const result = text ? JSON.parse(text) : null;
54-
5570
if (result && result.data && result.data.id)
5671
this._mangaIdCache = result.data.id;
5772

services/ranobelib/config.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,19 @@
1717
],
1818

1919
headers: {
20-
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:146.0) Gecko/20100101 Firefox/146.0',
20+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:147.0) Gecko/20100101 Firefox/147.0',
2121
'Accept': '*/*',
22-
'Accept-Language': 'ru-RU,ru;q=0.8,en-US;q=0.5,en;q=0.3',
22+
'Accept-Language': 'ru,en-US;q=0.9,en;q=0.8',
2323
'Site-Id': '3',
24+
'X-DL-Service': 'ranobelib',
2425
'Content-Type': 'application/json',
2526
'Client-Time-Zone': 'Europe/Moscow',
2627
'Referer': 'https://ranobelib.me/',
28+
'Origin': 'https://ranobelib.me',
2729
'Sec-GPC': '1',
2830
'Sec-Fetch-Dest': 'empty',
2931
'Sec-Fetch-Mode': 'cors',
30-
'Sec-Fetch-Site': 'same-site',
32+
'Sec-Fetch-Site': 'cross-site',
3133
'Connection': 'keep-alive'
3234
},
3335

tests/background/Background.test.js

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,7 @@ describe('Background', () => {
366366
expect(mockTrackRequest).toHaveBeenCalledWith('mangalib');
367367
});
368368

369-
it('Calls recordRequest for non-extension service request', async () => {
369+
it('Does not track non-extension service request', async () => {
370370
const details = {
371371
tabId: 1,
372372
frameId: 0,
@@ -376,8 +376,98 @@ describe('Background', () => {
376376
],
377377
};
378378
await capturedBeforeSendHeadersCb(details);
379-
expect(globalThis.globalRateLimiter.recordRequest).toHaveBeenCalledWith('mangalib');
380-
})
379+
expect(globalThis.globalRateLimiter.recordRequest).not.toHaveBeenCalled();
380+
expect(mockTrackRequest).not.toHaveBeenCalled();
381+
});
382+
383+
it('Detects service by X-DL-Service header', async () => {
384+
await capturedBeforeSendHeadersCb({
385+
tabId: -1,
386+
frameId: 0,
387+
url: 'https://api.cdnlibs.org/api/manga/slug',
388+
requestHeaders: [
389+
{ name: 'X-DL-Service', value: 'ranobelib' },
390+
],
391+
});
392+
expect(mockTrackRequest).toHaveBeenCalledWith('ranobelib');
393+
});
394+
395+
it('Detects mangalib by X-DL-Service header', async () => {
396+
await capturedBeforeSendHeadersCb({
397+
tabId: -1,
398+
frameId: 0,
399+
url: 'https://api.cdnlibs.org/api/manga/slug',
400+
requestHeaders: [
401+
{ name: 'X-DL-Service', value: 'mangalib' },
402+
{ name: 'Site-Id', value: '3' },
403+
],
404+
});
405+
expect(mockTrackRequest).toHaveBeenCalledWith('mangalib');
406+
});
407+
408+
it('Detects service by Site-Id header', async () => {
409+
await capturedBeforeSendHeadersCb({
410+
tabId: -1,
411+
frameId: 0,
412+
url: 'https://api.cdnlibs.org/api/manga/slug',
413+
requestHeaders: [
414+
{ name: 'Site-Id', value: '1' },
415+
],
416+
});
417+
expect(mockTrackRequest).toHaveBeenCalledWith('mangalib');
418+
});
419+
420+
it('Detects ranobelib by Site-Id 3 when service header value is empty', async () => {
421+
await capturedBeforeSendHeadersCb({
422+
tabId: -1,
423+
frameId: 0,
424+
url: 'https://api.cdnlibs.org/api/manga/slug',
425+
requestHeaders: [
426+
{ name: 'X-DL-Service', value: undefined },
427+
{ name: 'Site-Id', value: '3' },
428+
],
429+
});
430+
expect(mockTrackRequest).toHaveBeenCalledWith('ranobelib');
431+
});
432+
433+
it('Falls back from unknown service header to Site-Id mapping', async () => {
434+
await capturedBeforeSendHeadersCb({
435+
tabId: -1,
436+
frameId: 0,
437+
url: 'https://api.cdnlibs.org/api/manga/slug',
438+
requestHeaders: [
439+
{ name: 'X-DL-Service', value: 'unknown-service' },
440+
{ name: 'Site-Id', value: '1' },
441+
],
442+
});
443+
expect(mockTrackRequest).toHaveBeenCalledWith('mangalib');
444+
});
445+
446+
it('Falls back from unknown Site-Id to referer mapping', async () => {
447+
await capturedBeforeSendHeadersCb({
448+
tabId: -1,
449+
frameId: 0,
450+
url: 'https://api.cdnlibs.org/api/manga/slug',
451+
requestHeaders: [
452+
{ name: 'Site-Id', value: '2' },
453+
{ name: 'Referer', value: 'https://ranobelib.me/title' },
454+
],
455+
});
456+
expect(mockTrackRequest).toHaveBeenCalledWith('ranobelib');
457+
});
458+
459+
it('Falls back from empty Site-Id value to referer mapping', async () => {
460+
await capturedBeforeSendHeadersCb({
461+
tabId: -1,
462+
frameId: 0,
463+
url: 'https://api.cdnlibs.org/api/manga/slug',
464+
requestHeaders: [
465+
{ name: 'Site-Id', value: undefined },
466+
{ name: 'Referer', value: 'https://mangalib.me/title' },
467+
],
468+
});
469+
expect(mockTrackRequest).toHaveBeenCalledWith('mangalib');
470+
});
381471
});
382472

383473
describe('Chrome mode', () => {

0 commit comments

Comments
 (0)