docs4you commited on
Commit
635422a
·
verified ·
1 Parent(s): 4e50fa4

Create Dockerfile

Browse files
Files changed (1) hide show
  1. Dockerfile +429 -0
Dockerfile ADDED
@@ -0,0 +1,429 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ addEventListener('fetch', event => {
2
+ event.respondWith(handleRequest(event.request));
3
+ });
4
+
5
+ async function handleRequest(request) {
6
+ const url = new URL(request.url);
7
+ const pathname = url.pathname;
8
+
9
+ // Handle the home page route
10
+ if (pathname === '/' && !url.search) {
11
+ const html = `<!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="UTF-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ <title>M3U Playlist Proxy</title>
17
+ <style>
18
+
19
+ body{
20
+ color: #626262;
21
+ }
22
+ /* General Form Styling */
23
+ form {
24
+ max-width: 600px;
25
+ margin: 0 auto;
26
+ padding: 20px;
27
+ background-color: #f7f7f7;
28
+ border: 1px solid #ddd;
29
+ border-radius: 8px;
30
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
31
+ }
32
+
33
+ /* Styling for Input Fields */
34
+ input[type="text"] {
35
+ width: calc(50% - 10px);
36
+ padding: 10px;
37
+ font-size: 16px;
38
+ border: 1px solid #ccc;
39
+ border-radius: 4px;
40
+ margin-bottom: 10px;
41
+ color: #626262;
42
+ }
43
+
44
+ input[type="text"]:focus {
45
+ border-color: #007bff;
46
+ outline: none;
47
+ box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
48
+ }
49
+
50
+ /* Styling for Add More and Submit Buttons */
51
+ button {
52
+ padding: 10px 15px;
53
+ font-size: 16px;
54
+ border: none;
55
+ border-radius: 4px;
56
+ cursor: pointer;
57
+ margin-top: 10px;
58
+ width: 45%;
59
+ margin-left: 20px;
60
+ align-content: center;
61
+ }
62
+
63
+ button#add-more {
64
+ background-color: #007bff;
65
+ color: white;
66
+ }
67
+
68
+ button[type="submit"] {
69
+ background-color: #28a745;
70
+ color: white;
71
+ }
72
+
73
+ button:hover {
74
+ opacity: 0.9;
75
+ }
76
+
77
+ button:focus {
78
+ outline: none;
79
+ }
80
+
81
+ /* Styling for Text Area */
82
+ textarea {
83
+ width: 100%;
84
+ padding: 10px;
85
+ font-size: 16px;
86
+ border: 1px solid #ccc;
87
+ border-radius: 4px;
88
+ margin-top: 10px;
89
+ resize: none;
90
+ color: #626262;
91
+ }
92
+
93
+ /* Styling for Header Pair Div */
94
+ .header-pair {
95
+ display: flex;
96
+ justify-content: space-between;
97
+ margin-bottom: 10px;
98
+ }
99
+
100
+ h3,h4 {
101
+ text-align: center;
102
+ font-size: 24px;
103
+ font-weight: bold;
104
+ margin-bottom: 20px;
105
+ color: #626262;
106
+ }
107
+ form {
108
+ max-width: 600px;
109
+ margin: 0 auto;
110
+ padding: 20px;
111
+ background-color: #f7f7f7;
112
+ border: 1px solid #ddd;
113
+ border-radius: 8px;
114
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
115
+ }
116
+
117
+ .container {
118
+ max-width: 600px;
119
+ margin: 0 auto;
120
+ }
121
+
122
+ .footer {
123
+ max-width: 600px;
124
+ margin: 0 auto;
125
+ padding-top:20px;
126
+ padding-bottom:10px;
127
+ text-align:center;
128
+ }
129
+
130
+
131
+ textarea#result {
132
+ width: 100%;
133
+ max-width: 600px;
134
+ display: block;
135
+ margin: 20px auto;
136
+ padding: 10px;
137
+ font-size: 16px;
138
+ border
139
+
140
+
141
+ /* Label Styling */
142
+ label {
143
+ font-weight: bold;
144
+ margin-bottom: 5px;
145
+ display: block;
146
+ }
147
+
148
+ /* Basic Responsive Design */
149
+ @media (max-width: 600px) {
150
+ input[type="text"] {
151
+ width: 100%;
152
+ margin-bottom: 10px;
153
+ }
154
+
155
+ .header-pair {
156
+ flex-direction: column;
157
+ }
158
+ }
159
+
160
+ </style>
161
+ </head>
162
+ <body><h3>M3U Playlist Proxy</h3>
163
+ <div class="container"><p>Use the form below to generate a playlist with the necessary headers. For example, if the server requires a "Referer: http://example.com" header to allow streaming, you would enter "Referer" as the Header Name and "http://example.com" as the Header Value.</p></div>
164
+
165
+ <form id="headerForm">
166
+ <div>
167
+ <label for="playlistUrl">Playlist URL:</label><br>
168
+ <input type="text" id="playlistUrl" name="playlistUrl" placeholder="Enter playlist URL" style="width: 96%; margin-bottom: 20px;">
169
+ </div>
170
+
171
+ <div class="header-pair">
172
+ <input type="text" name="headerName" placeholder="Header Name">
173
+ <input type="text" name="headerValue" placeholder="Header Value">
174
+ </div>
175
+ <button type="button" id="add-more">Add More Headers</button>
176
+ <button type="submit">Generate Playlist URL</button>
177
+ </form>
178
+
179
+ <h4>Generated Playlist URL:</h4>
180
+ <textarea id="result" rows="4" cols="80" readonly></textarea>
181
+ <div class="container"><p>Once the URL has been generated, you can use a URL shortener like <a href="https://tinyurl.com" target=_blank">TinyURL.com</a> to shorten it. This will make it much easier to add the URL as a M3U playlist within your IPTV application.</p></div>
182
+
183
+ <div class="footer">Created by <a href="https://github.com/dtankdempse/m3u-playlist-proxy">Tank Dempse</a></div>
184
+ <script>
185
+ // Function to add more header input fields
186
+ document.getElementById('add-more').addEventListener('click', function () {
187
+ const headerPair = document.createElement('div');
188
+ headerPair.classList.add('header-pair');
189
+ headerPair.innerHTML =
190
+ "<input type='text' name='headerName' placeholder='Header Name'>" +
191
+ "<input type='text' name='headerValue' placeholder='Header Value'>";
192
+ document.getElementById('headerForm').insertBefore(headerPair, document.getElementById('add-more'));
193
+ });
194
+
195
+ // Function to handle form submission and generate the full URL
196
+ document.getElementById('headerForm').addEventListener('submit', function (event) {
197
+ event.preventDefault();
198
+
199
+ const playlistUrl = document.getElementById('playlistUrl').value.trim();
200
+ if (!playlistUrl) {
201
+ alert('Please enter a Playlist URL.');
202
+ return;
203
+ }
204
+
205
+ let headers = [];
206
+ const headerPairs = document.querySelectorAll('.header-pair');
207
+
208
+ headerPairs.forEach(pair => {
209
+ const headerName = pair.querySelector('input[name="headerName"]').value;
210
+ const headerValue = pair.querySelector('input[name="headerValue"]').value;
211
+ if (headerName && headerValue) {
212
+ headers.push(headerName + "=" + headerValue);
213
+ }
214
+ });
215
+
216
+ if (headers.length > 0) {
217
+ const headerString = headers.join('|');
218
+ const base64Encoded = btoa(headerString);
219
+ const urlEncodedData = encodeURIComponent(base64Encoded);
220
+ const baseUrl = window.location.origin;
221
+ const fullUrl = baseUrl + "/playlist?url=" + encodeURIComponent(playlistUrl) + "&data=" + urlEncodedData;
222
+
223
+ document.getElementById('result').value = fullUrl;
224
+ } else {
225
+ alert('Please add at least one header.');
226
+ }
227
+ });
228
+ </script>
229
+
230
+ </body>
231
+ </html>
232
+ `;
233
+ return new Response(html, { headers: { 'Content-Type': 'text/html' } });
234
+ }
235
+
236
+ // Handle the /playlist route
237
+ if (pathname === '/playlist') {
238
+ const urlParam = url.searchParams.get('url');
239
+ const dataParam = url.searchParams.get('data');
240
+
241
+ if (!urlParam) {
242
+ return new Response('URL parameter missing', { status: 400 });
243
+ }
244
+
245
+ return handlePlaylistRequest(request, urlParam, dataParam);
246
+ }
247
+
248
+ // Handle proxied URL requests (same logic as before)
249
+ const params = url.searchParams;
250
+ const requestUrl = params.has('url') ? decodeURIComponent(params.get('url')) : null;
251
+ const secondaryUrl = params.has('url2') ? decodeURIComponent(params.get('url2')) : null;
252
+ const data = params.get('data') ? atob(params.get('data')) : null; // Decode base64
253
+
254
+ const isMaster = !params.has('url2'); // Check if it's a master playlist
255
+ const finalRequestUrl = isMaster ? requestUrl : secondaryUrl;
256
+
257
+ if (finalRequestUrl) {
258
+ // Handle encryption key requests
259
+ if (params.has('key') && params.get('key') === 'true') {
260
+ return fetchEncryptionKey(finalRequestUrl, data);
261
+ }
262
+
263
+ // Set dataType based on whether it's a master playlist (text) or a segment (binary)
264
+ const dataType = isMaster ? 'text' : 'binary';
265
+
266
+ // Fetch content from the URL with appropriate headers and dataType
267
+ const result = await fetchContent(finalRequestUrl, data, dataType);
268
+
269
+ if (result.status >= 400) {
270
+ return new Response(`Error: ${result.status}`, { status: result.status });
271
+ }
272
+
273
+ let content = result.content;
274
+
275
+ // If it's a master playlist, rewrite the URLs and treat as text
276
+ if (isMaster) {
277
+ const baseUrl = new URL(result.finalUrl).origin;
278
+ const proxyUrl = `${url.origin}`;
279
+ content = rewriteUrls(content, baseUrl, proxyUrl, params.get('data'));
280
+ }
281
+
282
+ // Send the response with content and headers
283
+ return new Response(content, { headers: result.headers, status: result.status });
284
+ }
285
+
286
+ return new Response('Bad Request', { status: 400 });
287
+ }
288
+
289
+ // Function to fetch content from a URL using fetch
290
+ async function fetchContent(url, data, dataType = 'text') {
291
+ const headers = new Headers();
292
+
293
+ if (data) {
294
+ const headersArray = data.split('|');
295
+ headersArray.forEach(header => {
296
+ const [key, value] = header.split('=');
297
+ headers.append(key.trim(), value.trim().replace(/['"]/g, ''));
298
+ });
299
+ }
300
+
301
+ const response = await fetch(url, { headers });
302
+ const buffer = await response.arrayBuffer();
303
+ let content;
304
+
305
+ if (dataType === 'binary') {
306
+ content = buffer; // Treat as binary
307
+ } else {
308
+ content = new TextDecoder('utf-8').decode(buffer); // Default to text
309
+ }
310
+
311
+ return {
312
+ content,
313
+ finalUrl: url,
314
+ status: response.status,
315
+ headers: response.headers,
316
+ };
317
+ }
318
+
319
+ // Function to handle playlist requests
320
+ async function handlePlaylistRequest(request, playlistUrl, data) {
321
+
322
+ const result = await fetchContent(playlistUrl);
323
+
324
+ if (result.status !== 200) {
325
+ return new Response('Failed to fetch playlist', { status: 500 });
326
+ }
327
+
328
+ let playlistContent = result.content;
329
+ const baseUrl = new URL(request.url).origin;
330
+ playlistContent = rewritePlaylistUrls(playlistContent, baseUrl, data);
331
+
332
+ return new Response(playlistContent, { headers: { 'Content-Type': 'text/plain' } });
333
+ }
334
+
335
+ // Function to fetch encryption key
336
+ async function fetchEncryptionKey(url, data) {
337
+ const updatedUrl = url.includes('videohls.ru') ? url.replace('videohls.ru', 'onlinehdhls.ru') : url;
338
+ const result = await fetchContent(updatedUrl, data, 'binary'); // Pass 'binary' as the dataType
339
+
340
+ if (result.status >= 400) {
341
+ return new Response(`Failed to fetch encryption key: ${result.status}`, { status: 500 });
342
+ }
343
+
344
+ return new Response(result.content, { headers: result.headers });
345
+ }
346
+
347
+ // Rewrite URLs in the M3U8 playlist
348
+ function rewriteUrls(content, baseUrl, proxyUrl, data) {
349
+ const lines = content.split('\n');
350
+ const rewrittenLines = [];
351
+ let isNextLineUri = false;
352
+
353
+ lines.forEach(line => {
354
+ if (line.startsWith('#')) {
355
+ if (line.includes('URI="')) {
356
+ const uriMatch = line.match(/URI="([^"]+)"/i);
357
+ let uri = uriMatch[1];
358
+
359
+ if (!uri.startsWith('http')) {
360
+ uri = new URL(uri, baseUrl).href;
361
+ }
362
+
363
+ const rewrittenUri = `${proxyUrl}?url=${encodeURIComponent(uri)}&data=${encodeURIComponent(data)}${line.includes('#EXT-X-KEY') ? '&key=true' : ''}`;
364
+ line = line.replace(uriMatch[1], rewrittenUri);
365
+ }
366
+
367
+ rewrittenLines.push(line);
368
+
369
+ if (line.includes('#EXT-X-STREAM-INF')) {
370
+ isNextLineUri = true;
371
+ }
372
+
373
+ } else if (line.startsWith('http') || isNextLineUri) {
374
+ const urlParam = isNextLineUri ? 'url' : 'url2';
375
+ let lineUrl = line;
376
+
377
+ if (!lineUrl.startsWith('http')) {
378
+ lineUrl = new URL(lineUrl, baseUrl).href;
379
+ }
380
+
381
+ const fullUrl = `${proxyUrl}?${urlParam}=${encodeURIComponent(lineUrl)}&data=${encodeURIComponent(data)}${urlParam === 'url' ? '&type=/index.m3u8' : '&type=/index.ts'}`;
382
+ rewrittenLines.push(fullUrl);
383
+
384
+ isNextLineUri = false;
385
+ } else {
386
+ rewrittenLines.push(line);
387
+ }
388
+ });
389
+
390
+ return rewrittenLines.join('\n');
391
+ }
392
+
393
+ // Rewrite playlist URLs and encode headers
394
+ function rewritePlaylistUrls(content, baseUrl, data) {
395
+ const lines = content.split('\n');
396
+ const rewrittenLines = [];
397
+
398
+ lines.forEach(line => {
399
+ if (line.startsWith('#EXTINF')) {
400
+ rewrittenLines.push(line);
401
+ } else if (line.startsWith('http')) {
402
+ const headerSeparatorIndex = line.indexOf('|');
403
+ const streamUrl = headerSeparatorIndex !== -1 ? line.substring(0, headerSeparatorIndex) : line;
404
+
405
+ let base64Data = '';
406
+
407
+ if (data) {
408
+ base64Data = data; // Use provided data directly if it's passed
409
+ } else if (headerSeparatorIndex !== -1) {
410
+ const headersString = line.substring(headerSeparatorIndex + 1);
411
+ const headers = headersString
412
+ ? headersString.split('|').map(header => {
413
+ const [key, value] = header.split('=');
414
+ const cleanValue = value ? value.replace(/^['"]|['"]$/g, '').trim() : '';
415
+ return `${key.trim()}=${cleanValue}`;
416
+ })
417
+ : [];
418
+ base64Data = headers.length > 0 ? btoa(headers.join('|')) : '';
419
+ }
420
+
421
+ const newUrl = `${baseUrl}?url=${encodeURIComponent(streamUrl)}&data=${encodeURIComponent(base64Data)}`;
422
+ rewrittenLines.push(newUrl);
423
+ } else {
424
+ rewrittenLines.push(line);
425
+ }
426
+ });
427
+
428
+ return rewrittenLines.join('\n');
429
+ }