Arkm20 commited on
Commit
05717bc
·
verified ·
1 Parent(s): 671dd1e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +388 -42
app.py CHANGED
@@ -196,105 +196,451 @@ DOWNLOAD_UI_HTML = """
196
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
197
  <title>Download Anime Series</title>
198
  <style>
199
- @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;700&display=swap');
200
- body{font-family:'Roboto',sans-serif;background-color:#141414;color:#fff;margin:0;padding:2rem;display:flex;justify-content:center;align-items:center;min-height:100vh}
201
- .container{background:#1c1c1c;padding:2rem;border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,0.5);width:100%;max-width:600px}
202
- h1{color:#E50914;text-align:center;margin-bottom:2rem;font-weight:700}
203
- .series-list{list-style:none;padding:0;max-height:40vh;overflow-y:auto;border:1px solid #333;border-radius:8px}
204
- .series-item{display:flex;align-items:center;padding:1rem;border-bottom:1px solid #333;cursor:pointer;transition:background-color 0.2s ease}
205
- .series-item:last-child{border-bottom:none}
206
- .series-item:hover{background-color:#2a2a2a}
207
- .series-item input[type="checkbox"]{margin-right:1rem;width:20px;height:20px;accent-color:#E50914}
208
- .series-item label{flex-grow:1;font-size:1.1rem}
209
- .button-container{text-align:center;margin-top:2rem}
210
- .btn{background-color:#E50914;color:white;border:none;padding:1rem 2rem;font-size:1.2rem;border-radius:8px;cursor:pointer;transition:background-color 0.2s ease;font-weight:700}
211
- .btn:hover{background-color:#f6121d}
212
- .btn:disabled{background-color:#555;cursor:not-allowed}
213
- #loading-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);display:none;flex-direction:column;justify-content:center;align-items:center;z-index:1000}
214
- .loader{border:8px solid #f3f3f3;border-top:8px solid #E50914;border-radius:50%;width:80px;height:80px;animation:spin 1s linear infinite}
215
- #loading-text{color:#fff;margin-top:20px;font-size:1.2rem}
216
- @keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  </style>
218
  </head>
219
  <body>
220
- <div id="loading-overlay"><div class="loader"></div><p id="loading-text">Generating your files...</p></div>
 
 
 
 
221
  <div class="container">
222
  <h1>Select Anime to Download</h1>
223
  <form id="downloadForm">
224
  <ul id="seriesList" class="series-list"></ul>
225
- <div class="button-container"><button type="submit" class="btn" id="generateBtn" disabled>Generate Zip</button></div>
 
 
226
  </form>
227
  </div>
228
-
229
  <script>
230
  document.addEventListener('DOMContentLoaded', async () => {
231
  const seriesList = document.getElementById('seriesList');
232
  const generateBtn = document.getElementById('generateBtn');
233
  const loadingOverlay = document.getElementById('loading-overlay');
234
  const apiBaseUrl = ''; // API is at the same origin
235
-
236
- // Get the token from the URL parameters instead of localStorage.
237
  const urlParams = new URLSearchParams(window.location.search);
238
  const token = urlParams.get('token');
239
-
240
  if (!token) {
241
- seriesList.innerHTML = '<li class="series-item" style="justify-content: center; color: #E50914;">Authentication token not found in URL.</li>';
242
  generateBtn.disabled = true;
243
  return;
244
  }
245
-
246
  try {
247
- // Use the token from the URL for the Bearer authentication header.
248
  const response = await fetch(`${apiBaseUrl}/users/me`, {
249
  headers: { 'Authorization': `Bearer ${token}` }
250
  });
251
-
252
  if (!response.ok) {
253
  throw new Error('Failed to fetch user data. Your session may have expired.');
254
  }
255
-
256
  const userData = await response.json();
257
  const history = userData.watch_history_detailed;
258
  const uniqueSeries = [...new Set(Object.values(history).map(show => show.title))].sort();
259
-
260
  if (uniqueSeries.length === 0) {
261
- seriesList.innerHTML = '<li class="series-item" style="justify-content: center;">No watched series found.</li>';
262
  return;
263
  }
264
-
265
  uniqueSeries.forEach(title => {
266
  const listItem = document.createElement('li');
267
  listItem.className = 'series-item';
268
- listItem.innerHTML = `<input type="checkbox" id="${title}" name="series" value="${title}"><label for="${title}">${title}</label>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  seriesList.appendChild(listItem);
270
  });
271
-
272
  generateBtn.disabled = false;
273
-
274
  } catch (error) {
275
- seriesList.innerHTML = `<li class="series-item" style="justify-content: center; color: #E50914;">${error.message}</li>`;
276
  }
277
-
278
  document.getElementById('downloadForm').addEventListener('submit', (e) => {
279
  e.preventDefault();
280
  loadingOverlay.style.display = 'flex';
281
-
282
  const selectedSeries = Array.from(document.querySelectorAll('input[name="series"]:checked')).map(cb => cb.value);
283
-
284
  if (selectedSeries.length === 0) {
285
  alert('Please select at least one series.');
286
  loadingOverlay.style.display = 'none';
287
  return;
288
  }
289
-
290
  const queryString = new URLSearchParams({ series_titles: selectedSeries.join(',') }).toString();
291
 
292
- // Construct the download URL, passing the token as a query parameter as required by the /generate-zip endpoint.
293
  const downloadUrl = `${apiBaseUrl}/generate-zip?${queryString}&token=${token}`;
294
 
295
  window.open(downloadUrl, '_blank');
296
 
297
- setTimeout(() => { loadingOverlay.style.display = 'none'; }, 3000);
 
 
298
  });
299
  });
300
  </script>
 
196
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
197
  <title>Download Anime Series</title>
198
  <style>
199
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
200
+
201
+ * {
202
+ margin: 0;
203
+ padding: 0;
204
+ box-sizing: border-box;
205
+ }
206
+
207
+ :root {
208
+ --primary-color: #FF9500;
209
+ --primary-dark: #E68600;
210
+ --primary-light: #FFB84D;
211
+ --bg-dark: #0F0F0F;
212
+ --bg-secondary: #1A1A1A;
213
+ --bg-tertiary: #252525;
214
+ --text-primary: #FFFFFF;
215
+ --text-secondary: #B3B3B3;
216
+ --text-muted: #808080;
217
+ --border-color: #333333;
218
+ --border-light: #404040;
219
+ --success-color: #10B981;
220
+ --error-color: #FF9500;
221
+ --glass-bg: rgba(255, 255, 255, 0.05);
222
+ --glass-border: rgba(255, 255, 255, 0.1);
223
+ }
224
+
225
+ body {
226
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
227
+ background: linear-gradient(135deg, #0F0F0F 0%, #1A1A1A 100%);
228
+ color: var(--text-primary);
229
+ margin: 0;
230
+ padding: 2rem;
231
+ min-height: 100vh;
232
+ display: flex;
233
+ justify-content: center;
234
+ align-items: center;
235
+ }
236
+
237
+ .container {
238
+ background: var(--glass-bg);
239
+ backdrop-filter: blur(20px);
240
+ border: 1px solid var(--glass-border);
241
+ padding: 2.5rem;
242
+ border-radius: 24px;
243
+ box-shadow: 0 25px 50px rgba(0, 0, 0, 0.4);
244
+ width: 100%;
245
+ max-width: 650px;
246
+ position: relative;
247
+ overflow: hidden;
248
+ }
249
+
250
+ .container::before {
251
+ content: '';
252
+ position: absolute;
253
+ top: 0;
254
+ left: 0;
255
+ right: 0;
256
+ height: 1px;
257
+ background: linear-gradient(90deg, transparent, var(--primary-color), transparent);
258
+ }
259
+
260
+ h1 {
261
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-light));
262
+ -webkit-background-clip: text;
263
+ -webkit-text-fill-color: transparent;
264
+ background-clip: text;
265
+ text-align: center;
266
+ margin-bottom: 2rem;
267
+ font-weight: 700;
268
+ font-size: 2rem;
269
+ letter-spacing: -0.02em;
270
+ }
271
+
272
+ .series-list {
273
+ list-style: none;
274
+ padding: 0;
275
+ max-height: 45vh;
276
+ overflow-y: auto;
277
+ border: 1px solid var(--border-color);
278
+ border-radius: 16px;
279
+ background: var(--bg-secondary);
280
+ position: relative;
281
+ }
282
+
283
+ .series-list::-webkit-scrollbar {
284
+ width: 8px;
285
+ }
286
+
287
+ .series-list::-webkit-scrollbar-track {
288
+ background: var(--bg-tertiary);
289
+ border-radius: 8px;
290
+ }
291
+
292
+ .series-list::-webkit-scrollbar-thumb {
293
+ background: var(--primary-color);
294
+ border-radius: 8px;
295
+ border: 2px solid var(--bg-tertiary);
296
+ }
297
+
298
+ .series-list::-webkit-scrollbar-thumb:hover {
299
+ background: var(--primary-light);
300
+ }
301
+
302
+ .series-item {
303
+ display: flex;
304
+ align-items: center;
305
+ padding: 1.25rem;
306
+ border-bottom: 1px solid var(--border-color);
307
+ cursor: pointer;
308
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
309
+ position: relative;
310
+ }
311
+
312
+ .series-item:last-child {
313
+ border-bottom: none;
314
+ }
315
+
316
+
317
+ .series-item.selected {
318
+ background: linear-gradient(135deg, rgba(255, 149, 0, 0.15), rgba(255, 149, 0, 0.08));
319
+ border-left: 3px solid var(--primary-color);
320
+ }
321
+
322
+ /* Custom Checkbox */
323
+ .custom-checkbox {
324
+ position: relative;
325
+ display: inline-block;
326
+ width: 24px;
327
+ height: 24px;
328
+ margin-right: 1rem;
329
+ cursor: pointer;
330
+ }
331
+
332
+ .custom-checkbox input[type="checkbox"] {
333
+ opacity: 0;
334
+ width: 100%;
335
+ height: 100%;
336
+ position: absolute;
337
+ cursor: pointer;
338
+ }
339
+
340
+ .checkbox-visual {
341
+ position: absolute;
342
+ top: 0;
343
+ left: 0;
344
+ width: 24px;
345
+ height: 24px;
346
+ border: 2px solid var(--border-light);
347
+ border-radius: 8px;
348
+ background: var(--bg-tertiary);
349
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
350
+ display: flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ }
354
+
355
+ .checkbox-visual::after {
356
+ content: '';
357
+ width: 8px;
358
+ height: 8px;
359
+ border: 2px solid var(--text-primary);
360
+ border-top: none;
361
+ border-right: none;
362
+ transform: rotate(-45deg) scale(0);
363
+ transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
364
+ }
365
+
366
+ .custom-checkbox input[type="checkbox"]:checked + .checkbox-visual {
367
+ background: var(--primary-color);
368
+ border-color: var(--primary-color);
369
+ box-shadow: 0 0 0 3px rgba(255, 149, 0, 0.2);
370
+ }
371
+
372
+ .custom-checkbox input[type="checkbox"]:checked + .checkbox-visual::after {
373
+ transform: rotate(-45deg) scale(1);
374
+ border-color: var(--text-primary);
375
+ }
376
+
377
+ .custom-checkbox:hover .checkbox-visual {
378
+ border-color: var(--primary-color);
379
+ box-shadow: 0 0 0 3px rgba(255, 149, 0, 0.1);
380
+ }
381
+
382
+ .series-item label {
383
+ flex-grow: 1;
384
+ font-size: 1.1rem;
385
+ font-weight: 500;
386
+ color: var(--text-primary);
387
+ cursor: pointer;
388
+ transition: color 0.2s ease;
389
+ line-height: 1.4;
390
+ }
391
+
392
+ .series-item:hover label {
393
+ color: var(--primary-light);
394
+ }
395
+
396
+ .button-container {
397
+ text-align: center;
398
+ margin-top: 2rem;
399
+ }
400
+
401
+ .btn {
402
+ background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
403
+ color: var(--text-primary);
404
+ border: none;
405
+ padding: 1rem 2.5rem;
406
+ font-size: 1.1rem;
407
+ font-weight: 600;
408
+ border-radius: 16px;
409
+ cursor: pointer;
410
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
411
+ box-shadow: 0 8px 25px rgba(255, 149, 0, 0.3);
412
+ position: relative;
413
+ overflow: hidden;
414
+ }
415
+
416
+ .btn::before {
417
+ content: '';
418
+ position: absolute;
419
+ top: 0;
420
+ left: -100%;
421
+ width: 100%;
422
+ height: 100%;
423
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
424
+ transition: left 0.5s ease;
425
+ }
426
+
427
+ .btn:hover {
428
+ transform: translateY(-2px);
429
+ box-shadow: 0 12px 35px rgba(255, 149, 0, 0.4);
430
+ }
431
+
432
+ .btn:hover::before {
433
+ left: 100%;
434
+ }
435
+
436
+ .btn:active {
437
+ transform: translateY(0);
438
+ }
439
+
440
+ .btn:disabled {
441
+ background: linear-gradient(135deg, var(--text-muted), #666);
442
+ cursor: not-allowed;
443
+ transform: none;
444
+ box-shadow: none;
445
+ }
446
+
447
+ .btn:disabled::before {
448
+ display: none;
449
+ }
450
+
451
+ #loading-overlay {
452
+ position: fixed;
453
+ top: 0;
454
+ left: 0;
455
+ width: 100%;
456
+ height: 100%;
457
+ background: rgba(0, 0, 0, 0.9);
458
+ backdrop-filter: blur(10px);
459
+ display: none;
460
+ flex-direction: column;
461
+ justify-content: center;
462
+ align-items: center;
463
+ z-index: 1000;
464
+ }
465
+
466
+ .loader {
467
+ width: 80px;
468
+ height: 80px;
469
+ border: 4px solid var(--border-color);
470
+ border-top: 4px solid var(--primary-color);
471
+ border-radius: 50%;
472
+ animation: spin 1s linear infinite;
473
+ box-shadow: 0 0 20px rgba(255, 149, 0, 0.3);
474
+ }
475
+
476
+ #loading-text {
477
+ color: var(--text-primary);
478
+ margin-top: 24px;
479
+ font-size: 1.2rem;
480
+ font-weight: 500;
481
+ text-align: center;
482
+ opacity: 0.9;
483
+ }
484
+
485
+ @keyframes spin {
486
+ 0% { transform: rotate(0deg); }
487
+ 100% { transform: rotate(360deg); }
488
+ }
489
+
490
+ .error-message {
491
+ color: var(--error-color);
492
+ text-align: center;
493
+ font-weight: 500;
494
+ }
495
+
496
+ .empty-state {
497
+ text-align: center;
498
+ color: var(--text-secondary);
499
+ font-style: italic;
500
+ }
501
+
502
+ @media (max-width: 768px) {
503
+ body {
504
+ padding: 1rem;
505
+ }
506
+
507
+ .container {
508
+ padding: 1.5rem;
509
+ border-radius: 16px;
510
+ }
511
+
512
+ h1 {
513
+ font-size: 1.75rem;
514
+ }
515
+
516
+ .series-list {
517
+ max-height: 35vh;
518
+ }
519
+
520
+ .series-item {
521
+ padding: 1rem;
522
+ }
523
+
524
+ .btn {
525
+ padding: 0.875rem 2rem;
526
+ font-size: 1rem;
527
+ }
528
+ }
529
  </style>
530
  </head>
531
  <body>
532
+ <div id="loading-overlay">
533
+ <div class="loader"></div>
534
+ <p id="loading-text">Generating your files...</p>
535
+ </div>
536
+
537
  <div class="container">
538
  <h1>Select Anime to Download</h1>
539
  <form id="downloadForm">
540
  <ul id="seriesList" class="series-list"></ul>
541
+ <div class="button-container">
542
+ <button type="submit" class="btn" id="generateBtn" disabled>Generate Zip</button>
543
+ </div>
544
  </form>
545
  </div>
546
+
547
  <script>
548
  document.addEventListener('DOMContentLoaded', async () => {
549
  const seriesList = document.getElementById('seriesList');
550
  const generateBtn = document.getElementById('generateBtn');
551
  const loadingOverlay = document.getElementById('loading-overlay');
552
  const apiBaseUrl = ''; // API is at the same origin
553
+
554
+ // Get the token from the URL parameters
555
  const urlParams = new URLSearchParams(window.location.search);
556
  const token = urlParams.get('token');
557
+
558
  if (!token) {
559
+ seriesList.innerHTML = '<li class="series-item" style="justify-content: center;"><span class="error-message">Authentication token not found in URL.</span></li>';
560
  generateBtn.disabled = true;
561
  return;
562
  }
563
+
564
  try {
565
+ // Use the token from the URL for the Bearer authentication header
566
  const response = await fetch(`${apiBaseUrl}/users/me`, {
567
  headers: { 'Authorization': `Bearer ${token}` }
568
  });
569
+
570
  if (!response.ok) {
571
  throw new Error('Failed to fetch user data. Your session may have expired.');
572
  }
573
+
574
  const userData = await response.json();
575
  const history = userData.watch_history_detailed;
576
  const uniqueSeries = [...new Set(Object.values(history).map(show => show.title))].sort();
577
+
578
  if (uniqueSeries.length === 0) {
579
+ seriesList.innerHTML = '<li class="series-item" style="justify-content: center;"><span class="empty-state">No watched series found.</span></li>';
580
  return;
581
  }
582
+
583
  uniqueSeries.forEach(title => {
584
  const listItem = document.createElement('li');
585
  listItem.className = 'series-item';
586
+ listItem.innerHTML = `
587
+ <div class="custom-checkbox">
588
+ <input type="checkbox" id="${title}" name="series" value="${title}">
589
+ <div class="checkbox-visual"></div>
590
+ </div>
591
+ <label for="${title}">${title}</label>
592
+ `;
593
+
594
+ // Add click handler for the entire item
595
+ listItem.addEventListener('click', (e) => {
596
+ if (e.target.tagName !== 'INPUT') {
597
+ const checkbox = listItem.querySelector('input[type="checkbox"]');
598
+ checkbox.checked = !checkbox.checked;
599
+ checkbox.dispatchEvent(new Event('change'));
600
+ }
601
+ });
602
+
603
+ // Add change handler for checkbox
604
+ const checkbox = listItem.querySelector('input[type="checkbox"]');
605
+ checkbox.addEventListener('change', () => {
606
+ if (checkbox.checked) {
607
+ listItem.classList.add('selected');
608
+ } else {
609
+ listItem.classList.remove('selected');
610
+ }
611
+ });
612
+
613
  seriesList.appendChild(listItem);
614
  });
615
+
616
  generateBtn.disabled = false;
617
+
618
  } catch (error) {
619
+ seriesList.innerHTML = `<li class="series-item" style="justify-content: center;"><span class="error-message">${error.message}</span></li>`;
620
  }
621
+
622
  document.getElementById('downloadForm').addEventListener('submit', (e) => {
623
  e.preventDefault();
624
  loadingOverlay.style.display = 'flex';
625
+
626
  const selectedSeries = Array.from(document.querySelectorAll('input[name="series"]:checked')).map(cb => cb.value);
627
+
628
  if (selectedSeries.length === 0) {
629
  alert('Please select at least one series.');
630
  loadingOverlay.style.display = 'none';
631
  return;
632
  }
633
+
634
  const queryString = new URLSearchParams({ series_titles: selectedSeries.join(',') }).toString();
635
 
636
+ // Construct the download URL, passing the token as a query parameter
637
  const downloadUrl = `${apiBaseUrl}/generate-zip?${queryString}&token=${token}`;
638
 
639
  window.open(downloadUrl, '_blank');
640
 
641
+ setTimeout(() => {
642
+ loadingOverlay.style.display = 'none';
643
+ }, 3000);
644
  });
645
  });
646
  </script>