simple_magnet2direct.py 43 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264
  1. from flask import Flask, request, render_template_string, jsonify, redirect, send_file
  2. from seedrcc import Seedr
  3. import time
  4. import os
  5. import threading
  6. import requests
  7. app = Flask(__name__)
  8. # Default credentials (can be overridden by user)
  9. DEFAULT_EMAIL = ""
  10. DEFAULT_PASSWORD = ""
  11. # Download folder (for local development only)
  12. DOWNLOAD_FOLDER = os.path.join(os.path.expanduser("~"), "Downloads", "Magnet2Direct") if os.path.expanduser("~") != "~" else "/tmp"
  13. # Global variables
  14. download_status = {"progress": 0, "status": "idle", "filename": "", "download_url": "", "file_size": "", "error": ""}
  15. # Create download folder (only if not in serverless environment)
  16. try:
  17. if not os.path.exists(DOWNLOAD_FOLDER) and os.path.expanduser("~") != "~":
  18. os.makedirs(DOWNLOAD_FOLDER)
  19. except:
  20. pass # Skip folder creation in serverless environments
  21. HTML_TEMPLATE = """
  22. <!DOCTYPE html>
  23. <html lang="en">
  24. <head>
  25. <meta charset="UTF-8">
  26. <link rel="icon" href="/favicon.ico" type="image/x-icon">
  27. <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
  28. <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
  29. <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
  30. <link rel="manifest" href="/site.webmanifest">
  31. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  32. <title>Magnet 2 Direct</title>
  33. <style>
  34. * {
  35. margin: 0;
  36. padding: 0;
  37. box-sizing: border-box;
  38. }
  39. body {
  40. font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
  41. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  42. min-height: 100vh;
  43. padding: 15px;
  44. line-height: 1.6;
  45. }
  46. .container {
  47. max-width: 650px;
  48. margin: 0 auto;
  49. background: white;
  50. border-radius: 20px;
  51. box-shadow: 0 20px 40px rgba(0,0,0,0.15);
  52. padding: 40px;
  53. margin-top: 20px;
  54. position: relative;
  55. }
  56. @media (max-width: 768px) {
  57. .container {
  58. padding: 25px;
  59. margin-top: 60px;
  60. border-radius: 15px;
  61. }
  62. body {
  63. padding: 10px;
  64. }
  65. }
  66. h1 {
  67. text-align: center;
  68. color: #2d3748;
  69. margin-bottom: 15px;
  70. font-size: 2.8em;
  71. font-weight: 700;
  72. }
  73. @media (max-width: 768px) {
  74. h1 {
  75. font-size: 2.2em;
  76. margin-bottom: 10px;
  77. }
  78. }
  79. .subtitle {
  80. text-align: center;
  81. color: #718096;
  82. margin-bottom: 35px;
  83. font-size: 1.1em;
  84. font-weight: 400;
  85. }
  86. @media (max-width: 768px) {
  87. .subtitle {
  88. font-size: 1em;
  89. margin-bottom: 25px;
  90. }
  91. }
  92. .form-group {
  93. margin: 25px 0;
  94. }
  95. label {
  96. display: block;
  97. margin-bottom: 10px;
  98. font-weight: 600;
  99. color: #4a5568;
  100. font-size: 0.95em;
  101. }
  102. input[type="text"], input[type="email"], input[type="password"] {
  103. width: 100%;
  104. padding: 16px 20px;
  105. border: 2px solid #e2e8f0;
  106. border-radius: 12px;
  107. font-size: 16px;
  108. transition: all 0.3s ease;
  109. background: #f8fafc;
  110. color: #2d3748;
  111. }
  112. input[type="text"]:focus, input[type="email"]:focus, input[type="password"]:focus {
  113. outline: none;
  114. border-color: #667eea;
  115. background: white;
  116. box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
  117. transform: translateY(-1px);
  118. }
  119. input::placeholder {
  120. color: #a0aec0;
  121. font-style: italic;
  122. }
  123. .btn {
  124. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  125. color: white;
  126. border: none;
  127. padding: 16px 32px;
  128. border-radius: 12px;
  129. font-size: 16px;
  130. font-weight: 600;
  131. cursor: pointer;
  132. transition: all 0.3s ease;
  133. text-decoration: none;
  134. display: inline-block;
  135. margin: 8px;
  136. min-width: 140px;
  137. text-align: center;
  138. box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
  139. }
  140. .btn:hover {
  141. transform: translateY(-2px);
  142. box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
  143. }
  144. .btn:active {
  145. transform: translateY(0);
  146. }
  147. .btn:disabled {
  148. opacity: 0.6;
  149. cursor: not-allowed;
  150. transform: none;
  151. box-shadow: 0 4px 15px rgba(102, 126, 234, 0.2);
  152. }
  153. .btn-copy {
  154. background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
  155. box-shadow: 0 4px 15px rgba(72, 187, 120, 0.3);
  156. }
  157. .btn-copy:hover {
  158. box-shadow: 0 8px 25px rgba(72, 187, 120, 0.4);
  159. }
  160. .btn-download {
  161. background: linear-gradient(135deg, #4299e1 0%, #3182ce 100%);
  162. box-shadow: 0 4px 15px rgba(66, 153, 225, 0.3);
  163. }
  164. .btn-download:hover {
  165. box-shadow: 0 8px 25px rgba(66, 153, 225, 0.4);
  166. }
  167. .btn-danger {
  168. background: linear-gradient(135deg, #f56565 0%, #e53e3e 100%);
  169. box-shadow: 0 4px 15px rgba(245, 101, 101, 0.3);
  170. }
  171. .btn-danger:hover {
  172. box-shadow: 0 8px 25px rgba(245, 101, 101, 0.4);
  173. }
  174. @media (max-width: 768px) {
  175. .btn {
  176. width: 100%;
  177. margin: 5px 0;
  178. padding: 14px 24px;
  179. font-size: 15px;
  180. }
  181. }
  182. .progress-container {
  183. margin: 30px 0;
  184. padding: 25px;
  185. background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
  186. border-radius: 15px;
  187. border: 1px solid #e2e8f0;
  188. }
  189. .account-info {
  190. margin: 30px 0;
  191. padding: 25px;
  192. background: linear-gradient(135deg, #f0fff4 0%, #e6fffa 100%);
  193. border-radius: 15px;
  194. border: 1px solid #9ae6b4;
  195. font-size: 14px;
  196. }
  197. .account-info h4 {
  198. margin: 0 0 15px 0;
  199. color: #22543d;
  200. font-size: 1.1em;
  201. }
  202. .storage-bar {
  203. width: 100%;
  204. height: 24px;
  205. background: #e2e8f0;
  206. border-radius: 12px;
  207. overflow: hidden;
  208. margin: 12px 0;
  209. }
  210. .storage-fill {
  211. height: 100%;
  212. background: linear-gradient(90deg, #48bb78, #ed8936, #f56565);
  213. transition: width 0.5s ease;
  214. }
  215. .progress-bar {
  216. width: 100%;
  217. height: 24px;
  218. background: #e2e8f0;
  219. border-radius: 12px;
  220. overflow: hidden;
  221. margin: 15px 0;
  222. }
  223. .progress-fill {
  224. height: 100%;
  225. background: linear-gradient(90deg, #667eea, #48bb78);
  226. transition: width 0.5s ease;
  227. animation: shimmer 2s infinite;
  228. }
  229. .status-filename {
  230. word-break: break-word;
  231. overflow-wrap: anywhere;
  232. font-size: 0.9em;
  233. color: #4a5568;
  234. margin-top: 10px;
  235. padding: 8px 12px;
  236. background: rgba(255,255,255,0.8);
  237. border-radius: 6px;
  238. font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
  239. }
  240. @keyframes shimmer {
  241. 0% { opacity: 1; }
  242. 50% { opacity: 0.8; }
  243. 100% { opacity: 1; }
  244. }
  245. .result {
  246. margin: 30px 0;
  247. padding: 25px;
  248. background: linear-gradient(135deg, #f0fff4 0%, #e6fffa 100%);
  249. border-radius: 15px;
  250. border: 1px solid #9ae6b4;
  251. }
  252. .result.error {
  253. background: linear-gradient(135deg, #fed7d7 0%, #fbb6ce 100%);
  254. border: 1px solid #fc8181;
  255. color: #742a2a;
  256. }
  257. .file-info {
  258. background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
  259. padding: 20px;
  260. border-radius: 12px;
  261. margin: 15px 0;
  262. border: 1px solid #e2e8f0;
  263. word-break: break-word;
  264. overflow-wrap: break-word;
  265. }
  266. .file-info p {
  267. margin: 8px 0;
  268. line-height: 1.5;
  269. }
  270. .filename-text {
  271. word-break: break-all;
  272. overflow-wrap: anywhere;
  273. max-width: 100%;
  274. font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
  275. font-size: 13px;
  276. background: rgba(0,0,0,0.05);
  277. padding: 8px 10px;
  278. border-radius: 6px;
  279. margin-top: 5px;
  280. }
  281. .url-box {
  282. background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
  283. padding: 15px;
  284. border-radius: 10px;
  285. font-family: 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
  286. word-break: break-all;
  287. overflow-wrap: anywhere;
  288. border: 1px solid #e2e8f0;
  289. margin: 15px 0;
  290. font-size: 13px;
  291. color: #4a5568;
  292. max-width: 100%;
  293. overflow-x: auto;
  294. }
  295. .settings-btn {
  296. position: absolute;
  297. top: 15px;
  298. left: 15px;
  299. background: linear-gradient(135deg, #667eea 0%, #38a169 100%);
  300. color: white;
  301. border: none;
  302. border-radius: 50%;
  303. width: 50px;
  304. height: 50px;
  305. font-size: 20px;
  306. cursor: pointer;
  307. box-shadow: 0 6px 15px rgba(72, 187, 120, 0.4);
  308. transition: all 0.3s ease;
  309. z-index: 100;
  310. }
  311. .settings-btn:hover {
  312. background: linear-gradient(135deg, #38a169 0%, #2f855a 100%);
  313. transform: scale(1.1);
  314. box-shadow: 0 12px 30px rgba(72, 187, 120, 0.5);
  315. }
  316. @media (max-width: 768px) {
  317. .settings-btn {
  318. width: 45px;
  319. height: 45px;
  320. font-size: 18px;
  321. top: 12px;
  322. left: 12px;
  323. }
  324. }
  325. .modal {
  326. display: none;
  327. position: fixed;
  328. z-index: 2000;
  329. left: 0;
  330. top: 0;
  331. width: 100%;
  332. height: 100%;
  333. background-color: rgba(0,0,0,0.6);
  334. backdrop-filter: blur(5px);
  335. overflow-y: auto;
  336. align-items: center;
  337. justify-content: center;
  338. padding: 20px;
  339. box-sizing: border-box;
  340. }
  341. .modal.show {
  342. display: flex;
  343. }
  344. .modal-content {
  345. background: white;
  346. padding: 40px;
  347. border-radius: 20px;
  348. width: 100%;
  349. max-width: 550px;
  350. max-height: 90vh;
  351. overflow-y: auto;
  352. box-shadow: 0 25px 50px rgba(0,0,0,0.2);
  353. position: relative;
  354. animation: modalSlideIn 0.3s ease;
  355. margin: auto;
  356. }
  357. @keyframes modalSlideIn {
  358. from {
  359. opacity: 0;
  360. transform: translateY(-30px);
  361. }
  362. to {
  363. opacity: 1;
  364. transform: translateY(0);
  365. }
  366. }
  367. @media (max-width: 768px) {
  368. .modal {
  369. padding: 15px;
  370. }
  371. .modal-content {
  372. padding: 25px;
  373. border-radius: 15px;
  374. max-height: 85vh;
  375. }
  376. }
  377. .close {
  378. color: #a0aec0;
  379. float: right;
  380. font-size: 32px;
  381. font-weight: bold;
  382. cursor: pointer;
  383. line-height: 1;
  384. transition: color 0.3s ease;
  385. }
  386. .close:hover {
  387. color: #4a5568;
  388. }
  389. .credentials-status {
  390. padding: 16px 20px;
  391. border-radius: 12px;
  392. margin: 20px 0;
  393. font-weight: 600;
  394. font-size: 0.95em;
  395. }
  396. .credentials-status.saved {
  397. background: linear-gradient(135deg, #c6f6d5 0%, #9ae6b4 100%);
  398. color: #22543d;
  399. border: 1px solid #9ae6b4;
  400. }
  401. .credentials-status.not-saved {
  402. background: linear-gradient(135deg, #fed7d7 0%, #fbb6ce 100%);
  403. color: #742a2a;
  404. border: 1px solid #fc8181;
  405. }
  406. .privacy-note {
  407. margin-top: 25px;
  408. padding: 20px;
  409. background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
  410. border-radius: 12px;
  411. font-size: 14px;
  412. color: #4a5568;
  413. border: 1px solid #e2e8f0;
  414. }
  415. .button-group {
  416. display: flex;
  417. gap: 10px;
  418. justify-content: center;
  419. flex-wrap: wrap;
  420. margin-top: 25px;
  421. }
  422. @media (max-width: 768px) {
  423. .button-group {
  424. flex-direction: column;
  425. gap: 8px;
  426. }
  427. }
  428. /* Loading states */
  429. .loading {
  430. opacity: 0.7;
  431. pointer-events: none;
  432. }
  433. /* Improved responsive typography */
  434. @media (max-width: 480px) {
  435. html {
  436. font-size: 14px;
  437. }
  438. h1 {
  439. font-size: 2em;
  440. }
  441. .subtitle {
  442. font-size: 0.95em;
  443. }
  444. input[type="text"], input[type="email"], input[type="password"] {
  445. padding: 14px 16px;
  446. font-size: 16px; /* Prevents zoom on iOS */
  447. }
  448. .filename-text {
  449. font-size: 11px;
  450. padding: 6px 8px;
  451. line-height: 1.4;
  452. }
  453. .url-box {
  454. font-size: 11px;
  455. padding: 12px;
  456. line-height: 1.4;
  457. }
  458. .status-filename {
  459. font-size: 0.8em;
  460. padding: 6px 10px;
  461. }
  462. .file-info {
  463. padding: 15px;
  464. }
  465. }
  466. /* Footer */
  467. .footer {
  468. margin-top: 50px;
  469. padding: 30px 20px;
  470. text-align: center;
  471. background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
  472. border-radius: 15px;
  473. border: 1px solid #e2e8f0;
  474. color: #6c757d;
  475. font-size: 14px;
  476. line-height: 1.6;
  477. }
  478. .footer a {
  479. color: #007bff;
  480. text-decoration: none;
  481. font-weight: 600;
  482. transition: color 0.3s ease;
  483. }
  484. .footer a:hover {
  485. color: #0056b3;
  486. text-decoration: underline;
  487. }
  488. .footer .disclaimer {
  489. font-size: 12px;
  490. color: #868e96;
  491. margin-top: 15px;
  492. font-style: italic;
  493. }
  494. @media (max-width: 768px) {
  495. .footer {
  496. margin-top: 30px;
  497. padding: 20px 15px;
  498. font-size: 13px;
  499. }
  500. }
  501. </style>
  502. <script>
  503. function checkStatus() {
  504. fetch('/status')
  505. .then(response => response.json())
  506. .then(data => {
  507. const statusEl = document.getElementById('status');
  508. const resultEl = document.getElementById('result');
  509. const submitBtn = document.getElementById('submitBtn');
  510. if (data.status === 'processing') {
  511. statusEl.style.display = 'block';
  512. statusEl.innerHTML = `
  513. <div><strong>Status:</strong> ${data.status}</div>
  514. <div>Progress: ${data.progress}%</div>
  515. <div class="progress-bar">
  516. <div class="progress-fill" style="width: ${data.progress}%"></div>
  517. </div>
  518. ${data.filename ? `<div class="status-filename">${data.filename}</div>` : ''}
  519. `;
  520. submitBtn.disabled = true;
  521. setTimeout(checkStatus, 2000);
  522. } else if (data.status === 'completed') {
  523. statusEl.style.display = 'none';
  524. resultEl.innerHTML = `
  525. <h3>✅ Video Found!</h3>
  526. <div class="file-info">
  527. <p><strong>Filename:</strong></p>
  528. <div class="filename-text">${data.filename}</div>
  529. <p style="margin-top: 15px;"><strong>Size:</strong> ${data.file_size}</p>
  530. </div>
  531. <div class="url-box" id="downloadUrl">${data.download_url}</div>
  532. <div class="button-group" style="margin-top: 20px;">
  533. <button class="btn btn-copy" onclick="copyUrl()">📋 Copy Download URL</button>
  534. <a href="/download-file" target="_blank" class="btn btn-download">📥 Download File</a>
  535. </div>
  536. `;
  537. resultEl.className = 'result';
  538. resultEl.style.display = 'block';
  539. submitBtn.disabled = false;
  540. // Auto-scroll to results section
  541. setTimeout(() => {
  542. resultEl.scrollIntoView({
  543. behavior: 'smooth',
  544. block: 'center'
  545. });
  546. }, 100);
  547. } else if (data.status === 'error') {
  548. statusEl.style.display = 'none';
  549. resultEl.innerHTML = `<h3>❌ Error</h3><p>${data.error}</p>`;
  550. resultEl.className = 'result error';
  551. resultEl.style.display = 'block';
  552. submitBtn.disabled = false;
  553. // Auto-scroll to error message
  554. setTimeout(() => {
  555. resultEl.scrollIntoView({
  556. behavior: 'smooth',
  557. block: 'center'
  558. });
  559. }, 100);
  560. } else {
  561. submitBtn.disabled = false;
  562. }
  563. });
  564. }
  565. function addMagnet() {
  566. const magnet = document.getElementById('magnet').value.trim();
  567. if (!magnet) {
  568. alert('Please enter a magnet link');
  569. return;
  570. }
  571. const credentials = getSavedCredentials();
  572. if (!credentials.email || !credentials.password) {
  573. alert('Please set up your Seedr credentials first (click the ⚙️ button)');
  574. openSettings();
  575. return;
  576. }
  577. const formData = new FormData();
  578. formData.append('magnet', magnet);
  579. formData.append('email', credentials.email);
  580. formData.append('password', credentials.password);
  581. fetch('/add-magnet', {
  582. method: 'POST',
  583. body: formData
  584. })
  585. .then(response => response.json())
  586. .then(data => {
  587. if (data.status === 'started') {
  588. checkStatus();
  589. } else if (data.error) {
  590. alert('Error: ' + data.error);
  591. }
  592. });
  593. }
  594. function copyUrl() {
  595. const urlText = document.getElementById('downloadUrl').textContent;
  596. navigator.clipboard.writeText(urlText).then(() => {
  597. alert('Download URL copied to clipboard!');
  598. });
  599. }
  600. // Auto-check status on page load
  601. window.onload = function() {
  602. checkStatus();
  603. loadAccountInfo();
  604. loadSavedCredentials();
  605. };
  606. // Credential management functions
  607. function loadSavedCredentials() {
  608. const email = localStorage.getItem('seedr_email');
  609. const password = localStorage.getItem('seedr_password');
  610. const statusEl = document.getElementById('credentialsStatus');
  611. if (email && password) {
  612. statusEl.className = 'credentials-status saved';
  613. statusEl.innerHTML = '✅ Credentials saved for: ' + email;
  614. document.getElementById('seedrEmail').value = email;
  615. document.getElementById('seedrPassword').value = password;
  616. } else {
  617. statusEl.className = 'credentials-status not-saved';
  618. statusEl.innerHTML = '❌ No credentials saved - Please add your Seedr account';
  619. }
  620. }
  621. function saveCredentials() {
  622. const email = document.getElementById('seedrEmail').value.trim();
  623. const password = document.getElementById('seedrPassword').value.trim();
  624. if (!email || !password) {
  625. alert('Please enter both email and password');
  626. return;
  627. }
  628. // Test credentials first
  629. testCredentials(email, password).then(valid => {
  630. if (valid) {
  631. localStorage.setItem('seedr_email', email);
  632. localStorage.setItem('seedr_password', password);
  633. loadSavedCredentials();
  634. alert('✅ Credentials saved successfully!');
  635. closeSettings();
  636. loadAccountInfo(); // Refresh account info
  637. } else {
  638. alert('❌ Invalid credentials. Please check your email and password.');
  639. }
  640. });
  641. }
  642. function clearCredentials() {
  643. if (confirm('Are you sure you want to clear saved credentials?')) {
  644. localStorage.removeItem('seedr_email');
  645. localStorage.removeItem('seedr_password');
  646. document.getElementById('seedrEmail').value = '';
  647. document.getElementById('seedrPassword').value = '';
  648. loadSavedCredentials();
  649. alert('🗑️ Credentials cleared');
  650. }
  651. }
  652. function testCredentials(email, password) {
  653. return fetch('/test-credentials', {
  654. method: 'POST',
  655. headers: {
  656. 'Content-Type': 'application/json',
  657. },
  658. body: JSON.stringify({
  659. email: email,
  660. password: password
  661. })
  662. })
  663. .then(response => response.json())
  664. .then(data => data.valid)
  665. .catch(() => false);
  666. }
  667. function openSettings() {
  668. const modal = document.getElementById('settingsModal');
  669. modal.style.display = 'flex';
  670. modal.classList.add('show');
  671. }
  672. function closeSettings() {
  673. const modal = document.getElementById('settingsModal');
  674. modal.style.display = 'none';
  675. modal.classList.remove('show');
  676. }
  677. // Close modal when clicking outside
  678. window.onclick = function(event) {
  679. const modal = document.getElementById('settingsModal');
  680. if (event.target == modal) {
  681. closeSettings();
  682. }
  683. }
  684. // Modified functions to use saved credentials
  685. function getSavedCredentials() {
  686. return {
  687. email: localStorage.getItem('seedr_email') || '',
  688. password: localStorage.getItem('seedr_password') || ''
  689. };
  690. }
  691. function loadAccountInfo() {
  692. const credentials = getSavedCredentials();
  693. if (!credentials.email || !credentials.password) {
  694. const accountInfoEl = document.getElementById('account-info');
  695. const accountDetailsEl = document.getElementById('account-details');
  696. accountDetailsEl.innerHTML = `
  697. <div style="color: #dc3545;">
  698. <strong>⚠️ No Seedr credentials set</strong><br>
  699. <small>Click the ⚙️ button to add your account</small>
  700. </div>
  701. `;
  702. accountInfoEl.style.display = 'block';
  703. return;
  704. }
  705. fetch('/account-info', {
  706. method: 'POST',
  707. headers: {
  708. 'Content-Type': 'application/json',
  709. },
  710. body: JSON.stringify(credentials)
  711. })
  712. .then(response => response.json())
  713. .then(data => {
  714. const accountInfoEl = document.getElementById('account-info');
  715. const accountDetailsEl = document.getElementById('account-details');
  716. if (data.max_file_size !== 'Connection Error') {
  717. accountDetailsEl.innerHTML = `
  718. <div style="background: #fff3cd; padding: 8px; border-radius: 4px; border: 1px solid #ffeaa7;">
  719. <strong>⚠️ Max file size you can download:</strong> <span style="color: #856404; font-weight: bold;">${data.max_file_size}</span>
  720. </div>
  721. `;
  722. accountInfoEl.style.display = 'block';
  723. } else {
  724. accountDetailsEl.innerHTML = `
  725. <div style="color: #dc3545;">
  726. <strong>⚠️ Could not load account info</strong><br>
  727. <small>Check your Seedr credentials</small>
  728. </div>
  729. `;
  730. accountInfoEl.style.display = 'block';
  731. }
  732. })
  733. .catch(error => {
  734. console.error('Error loading account info:', error);
  735. const accountInfoEl = document.getElementById('account-info');
  736. const accountDetailsEl = document.getElementById('account-details');
  737. accountDetailsEl.innerHTML = `
  738. <div style="color: #dc3545;">
  739. <strong>⚠️ Connection error</strong><br>
  740. <small>Could not load account information</small>
  741. </div>
  742. `;
  743. accountInfoEl.style.display = 'block';
  744. });
  745. }
  746. </script>
  747. </head>
  748. <body>
  749. <!-- Settings Modal -->
  750. <div id="settingsModal" class="modal">
  751. <div class="modal-content">
  752. <span class="close" onclick="closeSettings()">&times;</span>
  753. <h2>🔐 Seedr Account Settings</h2>
  754. <p style="color: #718096; margin-bottom: 25px; font-size: 1.05em;">
  755. Connect your Seedr account to start downloading. Your credentials are stored securely in your browser only.
  756. </p>
  757. <div id="credentialsStatus" class="credentials-status not-saved">
  758. ❌ No credentials saved - Please add your Seedr account
  759. </div>
  760. <div class="form-group">
  761. <label for="seedrEmail">📧 Seedr Email Address:</label>
  762. <input type="email" id="seedrEmail"
  763. placeholder="Enter your Seedr email (e.g., yourname@gmail.com)"
  764. autocomplete="email" required>
  765. </div>
  766. <div class="form-group">
  767. <label for="seedrPassword">🔒 Seedr Password:</label>
  768. <input type="password" id="seedrPassword"
  769. placeholder="Enter your Seedr account password"
  770. autocomplete="current-password" required>
  771. </div>
  772. <div class="button-group">
  773. <button type="button" class="btn" onclick="saveCredentials()">
  774. 💾 Save & Test Connection
  775. </button>
  776. <button type="button" class="btn btn-danger" onclick="clearCredentials()">
  777. 🗑️ Clear Saved Data
  778. </button>
  779. </div>
  780. <div class="privacy-note">
  781. <strong>🔒 Privacy & Security:</strong><br>
  782. • Your credentials are stored only in your browser's local storage<br>
  783. • No data is sent to our servers - only directly to Seedr<br>
  784. • You can clear your data anytime using the button above<br>
  785. • Your credentials are tested before saving to ensure they work
  786. </div>
  787. </div>
  788. </div>
  789. <div class="container">
  790. <!-- Settings Button -->
  791. <button class="settings-btn" onclick="openSettings()" title="Seedr Account Settings">⚙️</button>
  792. <h1>🧲 Magnet2Direct</h1>
  793. <p class="subtitle">
  794. No more Seedr login marathons - Just paste, wait & enjoy! 🍿
  795. </p>
  796. <div id="account-info" class="account-info" style="display: none;">
  797. <h4>📊 Your Seedr Account</h4>
  798. <div id="account-details">Loading account info...</div>
  799. </div>
  800. <div class="form-group">
  801. <label for="magnet">🔗 Magnet Link:</label>
  802. <input type="text" id="magnet" name="magnet"
  803. placeholder="Paste your magnet link here... (e.g., magnet:?xt=urn:btih:...)"
  804. required>
  805. </div>
  806. <div style="text-align: center;">
  807. <button type="button" class="btn" id="submitBtn" onclick="addMagnet()">
  808. 🚀 Add Magnet & Download
  809. </button>
  810. </div>
  811. <div id="status" class="progress-container" style="display: none;"></div>
  812. <div id="result" class="result" style="display: none;"></div>
  813. <!-- Footer -->
  814. <div class="footer">
  815. <div>
  816. 🍿 <strong>Need magnet links?</strong></br><a href="https://github.com/sh13y" target="_blank" rel="noopener">sh13y's</a> favorite spot:
  817. <a href="https://watchsomuch.to/" target="_blank" rel="noopener">WatchSoMuch</a>
  818. <br>
  819. 🎬 <em>"It's like Netflix, but with more... freedom!"</em> 😉
  820. </div>
  821. <div class="disclaimer">
  822. ⚖️ For educational purposes only. We're just converting links here - what you download is between you and your conscience!
  823. <br>
  824. Made with ❤️ (and lots of backpain) for fellow movie lovers who hate repetitive clicking.
  825. </div>
  826. </div>
  827. </div>
  828. </body>
  829. </html>
  830. """
  831. def get_account_info(client):
  832. """Get Seedr account information including storage limits."""
  833. try:
  834. # Get account settings/info
  835. settings = client.get_settings()
  836. if settings and hasattr(settings, 'account'):
  837. account = settings.account
  838. space_max = getattr(account, 'space_max', 0)
  839. if space_max > 0:
  840. # Since we clear Seedr each time, max file size = total space
  841. return {
  842. 'max_file_size': format_file_size(space_max)
  843. }
  844. # Fallback for free accounts
  845. return {
  846. 'max_file_size': '2.0 GB (Free Account Limit)'
  847. }
  848. except Exception as e:
  849. print(f"Error getting account info: {e}")
  850. # Return default values for free account
  851. return {
  852. 'max_file_size': '2.0 GB (Free Account Limit)'
  853. }
  854. def connect_to_seedr(email, password, retries=3):
  855. """Connect to Seedr with retry logic."""
  856. for attempt in range(retries):
  857. try:
  858. print(f"Connecting to Seedr (attempt {attempt + 1}/{retries})...")
  859. client = Seedr.from_password(email, password)
  860. print("Connected to Seedr successfully")
  861. return client
  862. except Exception as e:
  863. print(f"Connection attempt {attempt + 1} failed: {e}")
  864. if attempt < retries - 1:
  865. time.sleep(2) # Wait before retry
  866. else:
  867. raise Exception(f"Could not connect to Seedr after {retries} attempts: {e}")
  868. def clear_seedr_account(client):
  869. """Delete all files and folders from Seedr account."""
  870. try:
  871. contents = client.list_contents()
  872. deleted_count = 0
  873. # Delete all files
  874. for file in contents.files:
  875. try:
  876. client.delete_file(file.folder_file_id)
  877. print(f"Deleted file: {file.name}")
  878. deleted_count += 1
  879. except Exception as e:
  880. print(f"Error deleting file {file.name}: {e}")
  881. # Delete all folders
  882. for folder in contents.folders:
  883. try:
  884. client.delete_folder(folder.id)
  885. print(f"Deleted folder: {folder.name}")
  886. deleted_count += 1
  887. except Exception as e:
  888. print(f"Error deleting folder {folder.name}: {e}")
  889. print(f"Cleared Seedr account: {deleted_count} items deleted")
  890. return True
  891. except Exception as e:
  892. print(f"Error clearing Seedr account: {e}")
  893. return False
  894. def process_magnet(magnet_link, email, password):
  895. """Process magnet link and find video."""
  896. global download_status
  897. try:
  898. download_status = {"progress": 10, "status": "processing", "filename": "", "download_url": "", "file_size": "", "error": ""}
  899. client = connect_to_seedr(email, password)
  900. with client:
  901. print("Connected to Seedr successfully")
  902. # First clear the Seedr account
  903. download_status["progress"] = 15
  904. download_status["filename"] = "Clearing Seedr account..."
  905. clear_success = clear_seedr_account(client)
  906. if not clear_success:
  907. print("Warning: Could not fully clear Seedr account, continuing anyway...")
  908. # Add the new magnet
  909. download_status["progress"] = 30
  910. download_status["filename"] = "Adding magnet to Seedr..."
  911. try:
  912. result = client.add_torrent(magnet_link)
  913. print(f"Magnet added successfully: {type(result)}")
  914. except Exception as e:
  915. raise Exception(f"Failed to add magnet: {e}")
  916. # Wait for files to appear
  917. download_status["progress"] = 40
  918. download_status["filename"] = "Waiting for download..."
  919. max_wait = 300 # 5 minutes
  920. wait_time = 0
  921. while wait_time < max_wait:
  922. try:
  923. video_file = find_biggest_video(client)
  924. if video_file:
  925. print(f"Found video file: {video_file.name}")
  926. break
  927. except Exception as e:
  928. print(f"Error checking for files: {e}")
  929. progress = 40 + int((wait_time / max_wait) * 50) # 40-90%
  930. download_status["progress"] = progress
  931. download_status["filename"] = f"Downloading... ({wait_time}s)"
  932. time.sleep(10)
  933. wait_time += 10
  934. if not video_file:
  935. raise Exception("No video file found after 5 minutes. The torrent might be slow or invalid.")
  936. # Get download URL
  937. download_status["progress"] = 95
  938. download_status["filename"] = video_file.name
  939. try:
  940. fetch_result = client.fetch_file(video_file.folder_file_id)
  941. download_url = fetch_result.url if hasattr(fetch_result, 'url') else str(fetch_result)
  942. except Exception as e:
  943. raise Exception(f"Could not get download URL: {e}")
  944. # Success
  945. download_status = {
  946. "progress": 100,
  947. "status": "completed",
  948. "filename": video_file.name,
  949. "download_url": download_url,
  950. "file_size": format_file_size(video_file.size),
  951. "error": ""
  952. }
  953. print(f"Success! Video: {video_file.name} ({download_status['file_size']})")
  954. except Exception as e:
  955. print(f"Process error: {e}")
  956. import traceback
  957. traceback.print_exc()
  958. download_status = {
  959. "progress": 0,
  960. "status": "error",
  961. "filename": "",
  962. "download_url": "",
  963. "file_size": "",
  964. "error": f"Error: {str(e)}. Try again with a different magnet link."
  965. }
  966. def find_biggest_video(client, folder_id=None):
  967. """Find the biggest video file."""
  968. biggest_video = None
  969. biggest_size = 0
  970. try:
  971. contents = client.list_contents(folder_id)
  972. folder_name = "root" if folder_id is None else f"folder_{folder_id}"
  973. print(f"Searching {folder_name}: {len(contents.files)} files, {len(contents.folders)} folders")
  974. # Check files in current folder
  975. for file in contents.files:
  976. print(f"Found file: {file.name} ({format_file_size(file.size)})")
  977. if file.name.lower().endswith(('.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.m4v', '.webm', '.mpg', '.mpeg')):
  978. print(f"Video file: {file.name}")
  979. if file.size > biggest_size:
  980. biggest_video = file
  981. biggest_size = file.size
  982. print(f"New biggest video: {file.name} ({format_file_size(file.size)})")
  983. # Check subfolders
  984. for subfolder in contents.folders:
  985. print(f"Checking subfolder: {subfolder.name}")
  986. result = find_biggest_video(client, subfolder.id)
  987. if result and result.size > biggest_size:
  988. biggest_video = result
  989. biggest_size = result.size
  990. print(f"New biggest from subfolder: {result.name} ({format_file_size(result.size)})")
  991. except Exception as e:
  992. print(f"Error searching folder {folder_id}: {e}")
  993. import traceback
  994. traceback.print_exc()
  995. if biggest_video:
  996. print(f"Returning biggest video: {biggest_video.name} ({format_file_size(biggest_video.size)})")
  997. else:
  998. print("No video files found in this search")
  999. return biggest_video
  1000. def format_file_size(size_bytes):
  1001. """Convert bytes to human readable format."""
  1002. if size_bytes == 0:
  1003. return "0 B"
  1004. for unit in ['B', 'KB', 'MB', 'GB']:
  1005. if size_bytes < 1024.0:
  1006. return f"{size_bytes:.1f} {unit}"
  1007. size_bytes /= 1024.0
  1008. @app.route('/')
  1009. def index():
  1010. return render_template_string(HTML_TEMPLATE)
  1011. @app.route('/add-magnet', methods=['POST'])
  1012. def add_magnet():
  1013. magnet = request.form['magnet']
  1014. email = request.form.get('email', DEFAULT_EMAIL)
  1015. password = request.form.get('password', DEFAULT_PASSWORD)
  1016. if not email or not password:
  1017. return jsonify({"status": "error", "error": "Seedr credentials required"})
  1018. # Start processing in background
  1019. thread = threading.Thread(target=process_magnet, args=(magnet, email, password))
  1020. thread.daemon = True
  1021. thread.start()
  1022. return jsonify({"status": "started"})
  1023. @app.route('/test-credentials', methods=['POST'])
  1024. def test_credentials():
  1025. """Test if Seedr credentials are valid."""
  1026. data = request.get_json()
  1027. email = data.get('email', '')
  1028. password = data.get('password', '')
  1029. try:
  1030. client = connect_to_seedr(email, password, retries=1)
  1031. with client:
  1032. # Try to get account info to verify credentials
  1033. settings = client.get_settings()
  1034. return jsonify({"valid": True})
  1035. except Exception as e:
  1036. print(f"Credential test failed: {e}")
  1037. return jsonify({"valid": False})
  1038. @app.route('/account-info', methods=['POST'])
  1039. def get_account_info_route():
  1040. """Get Seedr account information."""
  1041. try:
  1042. data = request.get_json()
  1043. email = data.get('email', DEFAULT_EMAIL)
  1044. password = data.get('password', DEFAULT_PASSWORD)
  1045. if not email or not password:
  1046. return jsonify({'max_file_size': 'No credentials provided'})
  1047. client = connect_to_seedr(email, password)
  1048. with client:
  1049. account_info = get_account_info(client)
  1050. return jsonify(account_info)
  1051. except Exception as e:
  1052. return jsonify({
  1053. 'max_file_size': 'Connection Error'
  1054. })
  1055. @app.route('/status')
  1056. def get_status():
  1057. return jsonify(download_status)
  1058. @app.route('/download-file', methods=['GET'])
  1059. def download_file():
  1060. """Redirect to direct download URL for browser download."""
  1061. global download_status
  1062. if download_status["status"] != "completed" or not download_status["download_url"]:
  1063. return "No file ready for download", 400
  1064. # Redirect browser to the direct download URL
  1065. return redirect(download_status["download_url"])
  1066. # Favicon and manifest routes
  1067. @app.route('/favicon.ico')
  1068. def favicon():
  1069. return send_file('favicon.ico')
  1070. @app.route('/apple-touch-icon.png')
  1071. def apple_touch_icon():
  1072. return send_file('apple-touch-icon.png')
  1073. @app.route('/favicon-32x32.png')
  1074. def favicon_32():
  1075. return send_file('favicon-32x32.png')
  1076. @app.route('/favicon-16x16.png')
  1077. def favicon_16():
  1078. return send_file('favicon-16x16.png')
  1079. @app.route('/site.webmanifest')
  1080. def site_manifest():
  1081. return send_file('site.webmanifest')
  1082. @app.route('/android-chrome-192x192.png')
  1083. def android_chrome_192():
  1084. return send_file('android-chrome-192x192.png')
  1085. @app.route('/android-chrome-512x512.png')
  1086. def android_chrome_512():
  1087. return send_file('android-chrome-512x512.png')
  1088. if __name__ == '__main__':
  1089. print("🧲 Starting Simple Magnet2Direct...")
  1090. print(f"📁 Downloads folder: {DOWNLOAD_FOLDER}")
  1091. print("🌐 Open http://localhost:5000")
  1092. app.run(debug=True, host='0.0.0.0', port=5000)