Skip to content
This repository was archived by the owner on Dec 15, 2025. It is now read-only.

Commit a958b6c

Browse files
committed
added support for thumbnails
1 parent e1948fe commit a958b6c

File tree

6 files changed

+86
-62
lines changed

6 files changed

+86
-62
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ This application serves as a proxy for HLS streams, Images and enabling secure a
2424

2525
- `/proxy` - for HLS
2626
- `/cors` - for Images/Web pages
27+
- `/image` - for Images/Web pages
28+
- `/thumbnail` - for thumbnail images
2729

2830
Use the following format to access the proxy:
2931

src/cors.js

Lines changed: 1 addition & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,4 @@
1-
async function handleCorsRequest(request) {
2-
const urlParams = new URL(request.url).searchParams;
3-
const encodedUrl = urlParams.get('url');
4-
const headersBase64 = urlParams.get('headers');
5-
6-
if (!encodedUrl) {
7-
return new Response('Invalid URL format. Must be a valid base64-encoded URL.', {
8-
status: 400,
9-
});
10-
}
11-
12-
// Decode the base64-encoded URL
13-
let targetUrl;
14-
try {
15-
targetUrl = atob(encodedUrl);
16-
} catch (error) {
17-
return new Response('Failed to decode URL. Ensure it is base64-encoded.', {
18-
status: 400,
19-
});
20-
}
21-
22-
let decodedHeaders = {};
23-
if (headersBase64) {
24-
try {
25-
const decodedString = atob(headersBase64);
26-
decodedHeaders = JSON.parse(decodedString);
27-
} catch (error) {
28-
return new Response('Invalid headers format. Must be valid base64-encoded JSON.', {
29-
status: 400,
30-
});
31-
}
32-
}
33-
34-
// Convert the plain JSON headers object to a Headers instance
35-
const headers = new Headers();
36-
headers.append(
37-
'User-Agent',
38-
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/237.84.2.178 Safari/537.36'
39-
);
40-
for (const [key, value] of Object.entries(decodedHeaders)) {
41-
headers.append(key, value);
42-
}
43-
1+
async function handleCorsRequest(targetUrl, headers) {
442
try {
453
// Fetch the target URL with the specified headers
464
const response = await fetch(targetUrl, {

src/index.js

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
import handleCorsRequest from './cors.js';
1414
import proxy from './proxy.js';
15+
import { thumbnailHandler } from './thumbnails.js';
16+
import { handleRequest } from './utils/handler.js';
1517

1618
export default {
1719
async fetch(request) {
@@ -20,15 +22,26 @@ export default {
2022
if (url.pathname === '/proxy') {
2123
return proxy(request);
2224
} else if (url.pathname === '/cors') {
23-
return handleCorsRequest(request);
25+
const [url, headers] = handleRequest(request);
26+
return handleCorsRequest(url, headers);
2427
} else if (url.pathname === '/image') {
25-
return handleCorsRequest(request);
28+
const [url, headers, origin] = handleRequest(request);
29+
return handleCorsRequest(url, headers, origin);
30+
} else if (url.pathname === '/thumbnail') {
31+
const [url, headers, origin] = handleRequest(request);
32+
return thumbnailHandler(url, headers, origin);
2633
} else if (url.pathname === '/') {
2734
return new Response(
2835
JSON.stringify({
2936
message: 'Welcome to Roxy',
30-
Endpoints: ['/proxy', '/cors'],
37+
Endpoints: [
38+
{ '/proxy': 'For HLS' },
39+
{ '/cors': 'For CORS' },
40+
{ '/image': 'For Manga Images' },
41+
{ '/thumbnail': 'For Thumbnails' },
42+
],
3143
params: '?url=<Base64-encoded-m3u8-url>&headers=<Base64-encoded-headers>',
44+
tip: 'Base64Encoding is optional for /cors, /thumbnail, /image. Encode the url if it gives error. For /proxy, encoding is required.',
3245
}),
3346
{
3447
status: 200,

src/proxy.js

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { decodeHeaders } from './utils/handler';
2+
13
const m3u8ContentTypes = [
24
'application/vnd.apple.mpegurl', // Standard HLS playlist
35
'application/x-mpegurl', // Common alias
@@ -28,22 +30,6 @@ const CACHE_CONTROL_SETTINGS = {
2830
ERROR: 'no-store',
2931
};
3032

31-
function decodeHeaders(base64Headers) {
32-
const headers = new Headers();
33-
if (!base64Headers) return headers;
34-
try {
35-
const decodedString = atob(base64Headers);
36-
const headersObj = JSON.parse(decodedString);
37-
38-
Object.entries(headersObj).forEach(([key, value]) => {
39-
headers.append(key, value);
40-
});
41-
return headers;
42-
} catch (error) {
43-
return null;
44-
}
45-
}
46-
4733
function getCacheSettings(url, content) {
4834
if (url.includes('.ts') || url.includes('.m4s')) {
4935
return CACHE_CONTROL_SETTINGS.SEGMENT;

src/thumbnails.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export const thumbnailHandler = async (url, headers, origin) => {
2+
const resp = await fetch(url, {
3+
...headers,
4+
redirect: 'follow',
5+
'User-Agent':
6+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/237.84.2.178 Safari/537.36',
7+
});
8+
9+
if (resp.status !== 200) {
10+
return new Response(resp.status, resp);
11+
}
12+
const timestampRegex = /(?<=\d{2}:\d{2}:\d{2}\.\d{3} --> \d{2}:\d{2}:\d{2}\.\d{3}\s)(.*)/gm;
13+
const responseBody = await resp.text();
14+
const baseUrl = url.substring(0, url.lastIndexOf('/'));
15+
const modifiedBody = responseBody.replace(timestampRegex, (match) => {
16+
const fullUrl = match.startsWith('http') ? match : match.startsWith('/') ? `${baseUrl}/${match}` : `${baseUrl}/${match}`;
17+
return `${origin}/cors?url=${encodeURIComponent(btoa(fullUrl))}`;
18+
});
19+
return new Response(modifiedBody, {
20+
headers: {
21+
...resp.headers,
22+
'Access-Control-Allow-Origin': '*',
23+
},
24+
});
25+
};

src/utils/handler.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
export const decodeHeaders = (base64Headers) => {
2+
const headers = new Headers();
3+
if (!base64Headers) return headers;
4+
try {
5+
const decodedString = atob(base64Headers);
6+
const headersObj = JSON.parse(decodedString);
7+
8+
Object.entries(headersObj).forEach(([key, value]) => {
9+
headers.append(key, value);
10+
});
11+
headers.append(
12+
'User-Agent',
13+
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/237.84.2.178 Safari/537.36'
14+
);
15+
return headers;
16+
} catch (error) {
17+
return headers;
18+
}
19+
};
20+
21+
export const handleRequest = (request) => {
22+
const url = new URL(request.url);
23+
const urlParams = url.searchParams;
24+
const encodedUrl = urlParams.get('url');
25+
const headersBase64 = urlParams.get('headers');
26+
if (!encodedUrl) {
27+
return new Response('Url is required!', {
28+
status: 400,
29+
});
30+
}
31+
let targetUrl;
32+
try {
33+
targetUrl = atob(encodedUrl);
34+
} catch (error) {
35+
targetUrl = encodedUrl;
36+
}
37+
38+
const headers = decodeHeaders(headersBase64);
39+
return [targetUrl, headers, url.origin];
40+
};

0 commit comments

Comments
 (0)