Twitter/X Media Batch Downloader

Batch download all images and videos from a Twitter/X account, including withheld accounts, in original quality.

  1. // ==UserScript==
  2. // @name Twitter/X Media Batch Downloader
  3. // @description Batch download all images and videos from a Twitter/X account, including withheld accounts, in original quality.
  4. // @icon https://raw.githubusercontent.com/afkarxyz/Twitter-X-Media-Batch-Downloader/refs/heads/main/Archived/icon.svg
  5. // @version 3.6
  6. // @author afkarxyz
  7. // @namespace https://github.com/afkarxyz/userscripts/
  8. // @supportURL https://github.com/afkarxyz/userscripts/issues
  9. // @license MIT
  10. // @match https://twitter.com/*
  11. // @match https://x.com/*
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM_setValue
  14. // @grant GM_getValue
  15. // @grant GM_download
  16. // @connect api.gallerydl.web.id
  17. // @connect backup.gallerydl.web.id
  18. // @connect pbs.twimg.com
  19. // @connect video.twimg.com
  20. // @require https://cdn.jsdelivr.net/npm/jszip@3.7.1/dist/jszip.min.js
  21. // ==/UserScript==
  22.  
  23. ;(() => {
  24. const defaultSettings = {
  25. patreonAuth: "",
  26. authToken: "",
  27. batchEnabled: false,
  28. batchSize: 100,
  29. timelineType: "media",
  30. mediaType: "all",
  31. concurrentDownloads: 25,
  32. cacheDuration: 360,
  33. apiServer: "default"
  34. }
  35.  
  36. const batchSizes = [25, 50, 100, 200]
  37. const concurrentSizes = [5, 10, 20, 25, 50]
  38. const cacheDurations = [60, 120, 180, 240, 300, 360, 720, 1440]
  39.  
  40. function getSettings() {
  41. return {
  42. patreonAuth: GM_getValue("patreonAuth", defaultSettings.patreonAuth),
  43. authToken: GM_getValue("authToken", defaultSettings.authToken),
  44. batchEnabled: GM_getValue("batchEnabled", defaultSettings.batchEnabled),
  45. batchSize: GM_getValue("batchSize", defaultSettings.batchSize),
  46. timelineType: GM_getValue("timelineType", defaultSettings.timelineType),
  47. mediaType: GM_getValue("mediaType", defaultSettings.mediaType),
  48. concurrentDownloads: GM_getValue("concurrentDownloads", defaultSettings.concurrentDownloads),
  49. cacheDuration: GM_getValue("cacheDuration", defaultSettings.cacheDuration),
  50. apiServer: GM_getValue("apiServer", defaultSettings.apiServer),
  51. }
  52. }
  53.  
  54. function saveSettings(settings) {
  55. GM_setValue("patreonAuth", settings.patreonAuth)
  56. GM_setValue("authToken", settings.authToken)
  57. GM_setValue("batchEnabled", settings.batchEnabled)
  58. GM_setValue("batchSize", settings.batchSize)
  59. GM_setValue("timelineType", settings.timelineType)
  60. GM_setValue("mediaType", settings.mediaType)
  61. GM_setValue("concurrentDownloads", settings.concurrentDownloads)
  62. GM_setValue("cacheDuration", settings.cacheDuration)
  63. GM_setValue("apiServer", settings.apiServer)
  64. }
  65.  
  66. function getServiceBaseUrl() {
  67. const settings = getSettings()
  68. return settings.apiServer === "default"
  69. ? "https://api.gallerydl.web.id"
  70. : "https://backup.gallerydl.web.id"
  71. }
  72.  
  73. function formatNumber(num) {
  74. return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
  75. }
  76.  
  77. const cacheManager = {
  78. set: (key, data, success = true) => {
  79. if (!success) return;
  80. const settings = getSettings()
  81. const cacheItem = {
  82. data: data,
  83. timestamp: Date.now(),
  84. expiry: Date.now() + settings.cacheDuration * 60 * 1000,
  85. }
  86. localStorage.setItem(`twitter_dl_${key}`, JSON.stringify(cacheItem))
  87. },
  88.  
  89. get: (key) => {
  90. const cacheItem = localStorage.getItem(`twitter_dl_${key}`)
  91. if (!cacheItem) return null
  92.  
  93. try {
  94. const parsed = JSON.parse(cacheItem)
  95. if (Date.now() > parsed.expiry) {
  96. localStorage.removeItem(`twitter_dl_${key}`)
  97. return null
  98. }
  99. return parsed.data
  100. } catch (e) {
  101. localStorage.removeItem(`twitter_dl_${key}`)
  102. return null
  103. }
  104. },
  105.  
  106. clear: () => {
  107. const keysToRemove = []
  108. for (let i = 0; i < localStorage.length; i++) {
  109. const key = localStorage.key(i)
  110. if (key.startsWith("twitter_dl_")) {
  111. keysToRemove.push(key)
  112. }
  113. }
  114.  
  115. keysToRemove.forEach((key) => localStorage.removeItem(key))
  116. },
  117. }
  118.  
  119. function createDownloadIcon() {
  120. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  121. svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
  122. svg.setAttribute("viewBox", "0 0 512 512")
  123. svg.setAttribute("width", "18")
  124. svg.setAttribute("height", "18")
  125. svg.style.verticalAlign = "middle"
  126. svg.style.cursor = "pointer"
  127.  
  128. const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs")
  129. const style = document.createElementNS("http://www.w3.org/2000/svg", "style")
  130. style.textContent = ".fa-secondary{opacity:.4}"
  131. defs.appendChild(style)
  132. svg.appendChild(defs)
  133.  
  134. const secondaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
  135. secondaryPath.setAttribute("class", "fa-secondary")
  136. secondaryPath.setAttribute("fill", "currentColor")
  137. secondaryPath.setAttribute(
  138. "d",
  139. "M0 256C0 397.4 114.6 512 256 512s256-114.6 256-256c0-17.7-14.3-32-32-32s-32 14.3-32 32c0 106-86 192-192 192S64 362 64 256c0-17.7-14.3-32-32-32s-32 14.3-32 32z",
  140. )
  141. svg.appendChild(secondaryPath)
  142.  
  143. const primaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
  144. primaryPath.setAttribute("class", "fa-primary")
  145. primaryPath.setAttribute("fill", "currentColor")
  146. primaryPath.setAttribute(
  147. "d",
  148. "M390.6 185.4c12.5 12.5 12.5 32.8 0 45.3l-112 112c-12.5 12.5-32.8 12.5-45.3 0l-112-112c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L224 242.7 224 32c0-17.7 14.3-32 32-32s32 14.3 32 32l0 210.7 57.4-57.4c12.5-12.5 32.8-12.5 45.3 0z",
  149. )
  150. svg.appendChild(primaryPath)
  151.  
  152. return svg
  153. }
  154.  
  155. function createPatreonIcon() {
  156. const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  157. svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
  158. svg.setAttribute("viewBox", "0 0 512 512")
  159. svg.setAttribute("width", "18")
  160. svg.setAttribute("height", "18")
  161. svg.style.verticalAlign = "middle"
  162. svg.style.marginRight = "8px"
  163.  
  164. const path = document.createElementNS("http://www.w3.org/2000/svg", "path")
  165. path.setAttribute("fill", "currentColor")
  166. path.setAttribute(
  167. "d",
  168. "M489.7 153.8c-.1-65.4-51-119-110.7-138.3C304.8-8.5 207-5 136.1 28.4C50.3 68.9 23.3 157.7 22.3 246.2C21.5 319 28.7 510.6 136.9 512c80.3 1 92.3-102.5 129.5-152.3c26.4-35.5 60.5-45.5 102.4-55.9c72-17.8 121.1-74.7 121-150z",
  169. )
  170. svg.appendChild(path)
  171.  
  172. return svg
  173. }
  174.  
  175. function createAuthTokenPopup() {
  176. const overlay = document.createElement("div")
  177. overlay.style.cssText = `
  178. position: fixed;
  179. top: 0;
  180. left: 0;
  181. width: 100%;
  182. height: 100%;
  183. background-color: rgba(0, 0, 0, 0.35);
  184. backdrop-filter: blur(2.5px);
  185. display: flex;
  186. justify-content: center;
  187. align-items: center;
  188. z-index: 10001;
  189. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  190. `
  191.  
  192. const popup = document.createElement("div")
  193. popup.style.cssText = `
  194. background-color: #ffffff;
  195. color: #0f172a;
  196. border-radius: 16px;
  197. width: 300px;
  198. max-width: 90%;
  199. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  200. overflow: hidden;
  201. `
  202.  
  203. const header = document.createElement("div")
  204. header.style.cssText = `
  205. padding: 16px;
  206. border-bottom: 1px solid #e2e8f0;
  207. font-weight: bold;
  208. font-size: 16px;
  209. text-align: center;
  210. `
  211. header.textContent = "Authentication Required"
  212.  
  213. const content = document.createElement("div")
  214. content.style.cssText = `
  215. padding: 16px;
  216. text-align: center;
  217. `
  218.  
  219. const authLink = document.createElement("a")
  220. authLink.href = "https://www.patreon.com/posts/127206894"
  221. authLink.target = "_blank"
  222. authLink.textContent = "How to Obtain Auth Token"
  223. authLink.style.cssText = `
  224. color: #0ea5e9;
  225. text-decoration: none;
  226. cursor: pointer;
  227. `
  228. content.appendChild(authLink)
  229.  
  230. const buttonContainer = document.createElement("div")
  231. buttonContainer.style.cssText = `
  232. padding: 16px;
  233. display: flex;
  234. justify-content: center;
  235. border-top: 1px solid #e2e8f0;
  236. `
  237.  
  238. const okButton = document.createElement("button")
  239. okButton.style.cssText = `
  240. background-color: #0ea5e9;
  241. color: white;
  242. border: none;
  243. border-radius: 9999px;
  244. padding: 8px 24px;
  245. font-weight: bold;
  246. cursor: pointer;
  247. transition: background-color 0.2s;
  248. `
  249. okButton.textContent = "OK"
  250. okButton.addEventListener("mouseenter", () => {
  251. okButton.style.backgroundColor = "#0284c7"
  252. })
  253. okButton.addEventListener("mouseleave", () => {
  254. okButton.style.backgroundColor = "#0ea5e9"
  255. })
  256. okButton.onclick = () => {
  257. document.body.removeChild(overlay)
  258. settingsTab.click()
  259. }
  260.  
  261. buttonContainer.appendChild(okButton)
  262. popup.appendChild(header)
  263. popup.appendChild(content)
  264. popup.appendChild(buttonContainer)
  265. overlay.appendChild(popup)
  266.  
  267. document.body.appendChild(overlay)
  268. return overlay
  269. }
  270.  
  271. function createPatreonAuthPopup() {
  272. const overlay = document.createElement("div")
  273. overlay.style.cssText = `
  274. position: fixed;
  275. top: 0;
  276. left: 0;
  277. width: 100%;
  278. height: 100%;
  279. background-color: rgba(0, 0, 0, 0.35);
  280. backdrop-filter: blur(2.5px);
  281. display: flex;
  282. justify-content: center;
  283. align-items: center;
  284. z-index: 10001;
  285. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  286. `
  287.  
  288. const popup = document.createElement("div")
  289. popup.style.cssText = `
  290. background-color: #ffffff;
  291. color: #0f172a;
  292. border-radius: 16px;
  293. width: 320px;
  294. max-width: 90%;
  295. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  296. overflow: hidden;
  297. `
  298.  
  299. const header = document.createElement("div")
  300. header.style.cssText = `
  301. padding: 16px;
  302. border-bottom: 1px solid #e2e8f0;
  303. font-weight: bold;
  304. font-size: 16px;
  305. text-align: center;
  306. `
  307. header.textContent = "Patreon Authentication Required"
  308.  
  309. const content = document.createElement("div")
  310. content.style.cssText = `
  311. padding: 16px;
  312. text-align: center;
  313. `
  314. const message = document.createElement("p")
  315. message.style.cssText = `
  316. margin-bottom: 16px;
  317. line-height: 1.5;
  318. `
  319. message.textContent = "Please enter your Patreon authentication code. This feature requires a paid membership to access."
  320. content.appendChild(message)
  321.  
  322. const patreonButton = document.createElement("a")
  323. patreonButton.href = "https://www.patreon.com/exyezed"
  324. patreonButton.target = "_blank"
  325. patreonButton.style.cssText = `
  326. display: flex;
  327. align-items: center;
  328. justify-content: center;
  329. background-color: #f1f5f9;
  330. color: #0f172a;
  331. text-decoration: none;
  332. padding: 10px 16px;
  333. border-radius: 8px;
  334. margin-top: 8px;
  335. transition: background-color 0.2s;
  336. `
  337. patreonButton.innerHTML = createPatreonIcon().outerHTML + "Join Patreon Membership"
  338. patreonButton.addEventListener("mouseenter", () => {
  339. patreonButton.style.backgroundColor = "#e2e8f0"
  340. })
  341. patreonButton.addEventListener("mouseleave", () => {
  342. patreonButton.style.backgroundColor = "#f1f5f9"
  343. })
  344. content.appendChild(patreonButton)
  345.  
  346. const buttonContainer = document.createElement("div")
  347. buttonContainer.style.cssText = `
  348. padding: 16px;
  349. display: flex;
  350. justify-content: center;
  351. border-top: 1px solid #e2e8f0;
  352. `
  353.  
  354. const okButton = document.createElement("button")
  355. okButton.style.cssText = `
  356. background-color: #0ea5e9;
  357. color: white;
  358. border: none;
  359. border-radius: 9999px;
  360. padding: 8px 24px;
  361. font-weight: bold;
  362. cursor: pointer;
  363. transition: background-color 0.2s;
  364. `
  365. okButton.textContent = "OK"
  366. okButton.addEventListener("mouseenter", () => {
  367. okButton.style.backgroundColor = "#0284c7"
  368. })
  369. okButton.addEventListener("mouseleave", () => {
  370. okButton.style.backgroundColor = "#0ea5e9"
  371. })
  372. okButton.onclick = () => {
  373. document.body.removeChild(overlay)
  374. settingsTab.click()
  375. }
  376.  
  377. buttonContainer.appendChild(okButton)
  378. popup.appendChild(header)
  379. popup.appendChild(content)
  380. popup.appendChild(buttonContainer)
  381. overlay.appendChild(popup)
  382.  
  383. document.body.appendChild(overlay)
  384. return overlay
  385. }
  386.  
  387. function createConfirmDialog(message, onConfirm, onCancel) {
  388. const overlay = document.createElement("div")
  389. overlay.style.cssText = `
  390. position: fixed;
  391. top: 0;
  392. left: 0;
  393. width: 100%;
  394. height: 100%;
  395. background-color: rgba(0, 0, 0, 0.35);
  396. backdrop-filter: blur(2.5px);
  397. display: flex;
  398. justify-content: center;
  399. align-items: center;
  400. z-index: 10001;
  401. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  402. `
  403.  
  404. const dialog = document.createElement("div")
  405. dialog.style.cssText = `
  406. background-color: #ffffff;
  407. color: #334155;
  408. border-radius: 16px;
  409. width: 300px;
  410. max-width: 90%;
  411. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  412. overflow: hidden;
  413. `
  414.  
  415. const header = document.createElement("div")
  416. header.style.cssText = `
  417. padding: 16px;
  418. border-bottom: 1px solid #e2e8f0;
  419. font-weight: bold;
  420. font-size: 16px;
  421. text-align: center;
  422. `
  423. header.textContent = "Confirmation"
  424.  
  425. const content = document.createElement("div")
  426. content.style.cssText = `
  427. padding: 16px;
  428. text-align: center;
  429. `
  430. content.textContent = message
  431.  
  432. const buttons = document.createElement("div")
  433. buttons.style.cssText = `
  434. display: flex;
  435. padding: 16px;
  436. border-top: 1px solid #e2e8f0;
  437. `
  438.  
  439. const cancelButton = document.createElement("button")
  440. cancelButton.style.cssText = `
  441. flex: 1;
  442. background-color: #94a3b8;
  443. color: white;
  444. border: none;
  445. border-radius: 9999px;
  446. padding: 8px 16px;
  447. margin-right: 8px;
  448. font-weight: bold;
  449. cursor: pointer;
  450. text-align: center;
  451. transition: background-color 0.2s;
  452. `
  453. cancelButton.textContent = "No"
  454. cancelButton.addEventListener("mouseenter", () => {
  455. cancelButton.style.backgroundColor = "#64748b"
  456. })
  457. cancelButton.addEventListener("mouseleave", () => {
  458. cancelButton.style.backgroundColor = "#94a3b8"
  459. })
  460. cancelButton.onclick = () => {
  461. document.body.removeChild(overlay)
  462. if (onCancel) onCancel()
  463. }
  464.  
  465. const confirmButton = document.createElement("button")
  466. confirmButton.style.cssText = `
  467. flex: 1;
  468. background-color: #ef4444;
  469. color: white;
  470. border: none;
  471. border-radius: 9999px;
  472. padding: 8px 16px;
  473. font-weight: bold;
  474. cursor: pointer;
  475. text-align: center;
  476. transition: background-color 0.2s;
  477. `
  478. confirmButton.textContent = "Yes"
  479. confirmButton.addEventListener("mouseenter", () => {
  480. confirmButton.style.backgroundColor = "#dc2626"
  481. })
  482. confirmButton.addEventListener("mouseleave", () => {
  483. confirmButton.style.backgroundColor = "#ef4444"
  484. })
  485. confirmButton.onclick = () => {
  486. document.body.removeChild(overlay)
  487. if (onConfirm) onConfirm()
  488. }
  489.  
  490. buttons.appendChild(cancelButton)
  491. buttons.appendChild(confirmButton)
  492.  
  493. dialog.appendChild(header)
  494. dialog.appendChild(content)
  495. dialog.appendChild(buttons)
  496. overlay.appendChild(dialog)
  497.  
  498. document.body.appendChild(overlay)
  499. }
  500.  
  501. function formatDate(dateString) {
  502. const date = new Date(dateString)
  503. const year = date.getFullYear()
  504. const month = String(date.getMonth() + 1).padStart(2, "0")
  505. const day = String(date.getDate()).padStart(2, "0")
  506. const hours = String(date.getHours()).padStart(2, "0")
  507. const minutes = String(date.getMinutes()).padStart(2, "0")
  508. const seconds = String(date.getSeconds()).padStart(2, "0")
  509.  
  510. return `${year}${month}${day}_${hours}${minutes}${seconds}`
  511. }
  512.  
  513. function getCurrentTimestamp() {
  514. const now = new Date()
  515. const year = now.getFullYear()
  516. const month = String(now.getMonth() + 1).padStart(2, "0")
  517. const day = String(now.getDate()).padStart(2, "0")
  518. const hours = String(now.getHours()).padStart(2, "0")
  519. const minutes = String(now.getMinutes()).padStart(2, "0")
  520. const seconds = String(now.getSeconds()).padStart(2, "0")
  521.  
  522. return `${year}${month}${day}_${hours}${minutes}${seconds}`
  523. }
  524.  
  525. function fetchData(url) {
  526. return new Promise((resolve, reject) => {
  527. GM_xmlhttpRequest({
  528. method: "GET",
  529. url: url,
  530. responseType: "json",
  531. onload: (response) => {
  532. if (response.status >= 200 && response.status < 300) {
  533. resolve(response.response)
  534. } else {
  535. reject(new Error(`Request failed with status ${response.status}`))
  536. }
  537. },
  538. onerror: (error) => {
  539. reject(new Error(`Network error: ${error?.message || "Unknown error"}`))
  540. },
  541. ontimeout: () => {
  542. reject(new Error("Request timed out"))
  543. }
  544. })
  545. })
  546. }
  547.  
  548. function fetchBinary(url) {
  549. return new Promise((resolve, reject) => {
  550. GM_xmlhttpRequest({
  551. method: "GET",
  552. url: url,
  553. responseType: "blob",
  554. onload: (response) => {
  555. if (response.status >= 200 && response.status < 300) {
  556. resolve(response.response)
  557. } else {
  558. reject(new Error(`Request failed with status ${response.status}`))
  559. }
  560. },
  561. onerror: () => {
  562. reject(new Error("Network error"))
  563. },
  564. })
  565. })
  566. }
  567.  
  568. function getMediaTypeLabel(mediaType) {
  569. switch (mediaType) {
  570. case "image":
  571. return "Image"
  572. case "video":
  573. return "Video"
  574. case "gif":
  575. return "GIF"
  576. default:
  577. return "Media"
  578. }
  579. }
  580.  
  581. function createToggleSwitch(options, selectedValue, onChange) {
  582. const toggleWrapper = document.createElement("div")
  583. toggleWrapper.style.cssText = `
  584. position: relative;
  585. height: 40px;
  586. background-color: #f1f5f9;
  587. border-radius: 8px;
  588. padding: 0;
  589. cursor: pointer;
  590. width: 100%;
  591. margin-bottom: 16px;
  592. overflow: hidden;
  593. `
  594. const toggleSlider = document.createElement("div")
  595. toggleSlider.style.cssText = `
  596. position: absolute;
  597. height: 100%;
  598. background-color: #0ea5e9;
  599. border-radius: 8px;
  600. transition: transform 0.3s ease, width 0.3s ease;
  601. z-index: 1;
  602. `
  603. const optionsContainer = document.createElement("div")
  604. optionsContainer.style.cssText = `
  605. position: relative;
  606. display: flex;
  607. height: 100%;
  608. z-index: 2;
  609. width: 100%;
  610. `
  611. const selectedIndex = options.findIndex((option) => option.value === selectedValue)
  612. const optionWidth = 100 / options.length
  613. toggleSlider.style.width = `${optionWidth}%`
  614. toggleSlider.style.transform = `translateX(${selectedIndex * 100}%)`
  615. options.forEach((option, index) => {
  616. const optionElement = document.createElement("div")
  617. optionElement.style.cssText = `
  618. flex: 1;
  619. display: flex;
  620. align-items: center;
  621. justify-content: center;
  622. font-size: 14px;
  623. transition: color 0.3s ease;
  624. color: ${option.value === selectedValue ? "white" : "#64748b"};
  625. cursor: pointer;
  626. user-select: none;
  627. text-align: center;
  628. height: 100%;
  629. padding: 0 4px;
  630. `
  631. if (option.icon) {
  632. const iconContainer = document.createElement("span")
  633. iconContainer.style.cssText = `
  634. display: flex;
  635. align-items: center;
  636. justify-content: center;
  637. margin-right: 6px;
  638. `
  639. const iconClone = option.icon.cloneNode(true)
  640. const paths = iconClone.querySelectorAll("path")
  641. paths.forEach((path) => {
  642. path.setAttribute("fill", option.value === selectedValue ? "white" : "#64748b")
  643. })
  644. iconContainer.appendChild(iconClone)
  645. optionElement.appendChild(iconContainer)
  646. }
  647. const text = document.createElement("span")
  648. text.textContent = option.label
  649. text.style.cssText = `
  650. display: inline-block;
  651. text-align: center;
  652. `
  653. optionElement.appendChild(text)
  654. optionElement.addEventListener("click", (e) => {
  655. e.stopPropagation()
  656. onChange(option.value)
  657. toggleSlider.style.transform = `translateX(${index * 100}%)`
  658. optionsContainer.querySelectorAll("div").forEach((opt, i) => {
  659. opt.style.color = i === index ? "white" : "#64748b"
  660. const optIcon = opt.querySelector("svg")
  661. if (optIcon) {
  662. const optPaths = optIcon.querySelectorAll("path")
  663. optPaths.forEach((path) => {
  664. path.setAttribute("fill", i === index ? "white" : "#64748b")
  665. })
  666. }
  667. })
  668. })
  669. optionsContainer.appendChild(optionElement)
  670. })
  671. toggleWrapper.appendChild(toggleSlider)
  672. toggleWrapper.appendChild(optionsContainer)
  673. return toggleWrapper
  674. }
  675.  
  676. function createMediaTypeIcons() {
  677. const allIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  678. allIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
  679. allIcon.setAttribute("viewBox", "0 0 640 512")
  680. allIcon.setAttribute("width", "16")
  681. allIcon.setAttribute("height", "16")
  682. allIcon.style.verticalAlign = "middle"
  683. const allPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
  684. allPath.setAttribute("fill", "#64748b")
  685. allPath.setAttribute("d", "M256 48c-8.8 0-16 7.2-16 16l0 224c0 8.7 6.9 15.8 15.6 16l69.1-94.2c4.5-6.2 11.7-9.8 19.4-9.8s14.8 3.6 19.4 9.8L380 232.4l56-85.6c4.4-6.8 12-10.9 20.1-10.9s15.7 4.1 20.1 10.9L578.7 303.8c7.6-1.3 13.3-7.9 13.3-15.8l0-224c0-8.8-7.2-16-16-16L256 48zM192 64c0-35.3 28.7-64 64-64L576 0c35.3 0 64 28.7 64 64l0 224c0 35.3-28.7 64-64 64l-320 0c-35.3 0-64-28.7-64-64l0-224zm-56 64l24 0 0 48 0 88 0 112 0 8 0 80 192 0 0-80 48 0 0 80 48 0c8.8 0 16-7.2 16-16l0-64 48 0 0 64c0 35.3-28.7 64-64 64l-48 0-24 0-24 0-192 0-24 0-24 0-48 0c-35.3 0-64-28.7-64-64L0 192c0-35.3 28.7-64 64-64l48 0 24 0zm-24 48l-48 0c-8.8 0-16 7.2-16 16l0 48 64 0 0-64zm0 288l0-64-64 0 0 48c0 8.8 7.2 16 16 16l48 0zM48 352l64 0 0-64-64 0 0 64zM304 80a32 32 0 1 1 0 64 32 32 0 1 1 0-64z")
  686. allIcon.appendChild(allPath)
  687. const imageIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  688. imageIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
  689. imageIcon.setAttribute("viewBox", "0 0 512 512")
  690. imageIcon.setAttribute("width", "16")
  691. imageIcon.setAttribute("height", "16")
  692. imageIcon.style.verticalAlign = "middle"
  693. const imagePath = document.createElementNS("http://www.w3.org/2000/svg", "path")
  694. imagePath.setAttribute("fill", "#64748b")
  695. imagePath.setAttribute("d", "M448 80c8.8 0 16 7.2 16 16l0 319.8-5-6.5-136-176c-4.5-5.9-11.6-9.3-19-9.3s-14.4 3.4-19 9.3L202 340.7l-30.5-42.7C167 291.7 159.8 288 152 288s-15 3.7-19.5 10.1l-80 112L48 416.3l0-.3L48 96c0-8.8 7.2-16 16-16l384 0zM64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm80 192a48 48 0 1 0 0-96 48 48 0 1 0 0 96z")
  696. imageIcon.appendChild(imagePath)
  697. const videoIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  698. videoIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
  699. videoIcon.setAttribute("viewBox", "0 0 512 512")
  700. videoIcon.setAttribute("width", "16")
  701. videoIcon.setAttribute("height", "16")
  702. videoIcon.style.verticalAlign = "middle"
  703. const videoPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
  704. videoPath.setAttribute("fill", "#64748b")
  705. videoPath.setAttribute("d", "M352 432l-192 0 0-112 0-40 192 0 0 40 0 112zm0-200l-192 0 0-40 0-112 192 0 0 112 0 40zM64 80l48 0 0 88-64 0 0-72c0-8.8 7.2-16 16-16zM48 216l64 0 0 80-64 0 0-80zm64 216l-48 0c-8.8 0-16-7.2-16-16l0-72 64 0 0 88zM400 168l0-88 48 0c8.8 0 16 7.2 16 16l0 72-64 0zm0 48l64 0 0 80-64 0 0-80zm0 128l64 0 0 72c0 8.8-7.2 16-16 16l-48 0 0-88zM448 32L64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64z")
  706. videoIcon.appendChild(videoPath)
  707. const gifIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  708. gifIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
  709. gifIcon.setAttribute("viewBox", "0 0 576 512")
  710. gifIcon.setAttribute("width", "16")
  711. gifIcon.setAttribute("height", "16")
  712. gifIcon.style.verticalAlign = "middle"
  713. const gifPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
  714. gifPath.setAttribute("fill", "#64748b")
  715. gifPath.setAttribute("d", "M512 80c8.8 0 16 7.2 16 16l0 320c0 8.8-7.2 16-16 16L64 432c-8.8 0-16-7.2-16-16L48 96c0-8.8 7.2-16 16-16l448 0zM64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l448 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zM296 160c-13.3 0-24 10.7-24 24l0 144c0 13.3 10.7 24 24 24s24-10.7 24-24l0-144c0-13.3-10.7-24-24-24zm56 24l0 80 0 64c0 13.3 10.7 24 24 24s24-10.7 24-24l0-40 40 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-40 0 0-32 64 0c13.3 0 24-10.7 24-24s-10.7-24-24-24l-88 0c-13.3 0-24 10.7-24 24zM128 256c0-26.5 21.5-48 48-48c8 0 15.4 1.9 22 5.3c11.8 6.1 26.3 1.5 32.3-10.3s1.5-26.3-10.3-32.3c-13.2-6.8-28.2-10.7-44-10.7c-53 0-96 43-96 96s43 96 96 96c19.6 0 37.5-6.1 52.8-15.8c7-4.4 11.2-12.1 11.2-20.3l0-51.9c0-13.3-10.7-24-24-24l-32 0c-13.3 0-24 10.7-24 24s10.7 24 24 24l8 0 0 13.1c-5.3 1.9-10.6 2.9-16 2.9c-26.5 0-48-21.5-48-48z")
  716. gifIcon.appendChild(gifPath)
  717. return {
  718. all: allIcon,
  719. image: imageIcon,
  720. video: videoIcon,
  721. gif: gifIcon
  722. }
  723. }
  724.  
  725. function createSlider(options, selectedValue, onChange) {
  726. const toggleOptions = options.map((option) => {
  727. let label = option.toString()
  728. if (typeof option === "number" && option >= 60 && option % 60 === 0) {
  729. label = `${option / 60}h`
  730. }
  731. return { value: option, label: label }
  732. })
  733.  
  734. return createToggleSwitch(toggleOptions, selectedValue, onChange)
  735. }
  736.  
  737. function createModal(username) {
  738. const existingModal = document.getElementById("media-downloader-modal")
  739. if (existingModal) {
  740. existingModal.remove()
  741. }
  742.  
  743. const settings = getSettings()
  744.  
  745. const modal = document.createElement("div")
  746. modal.id = "media-downloader-modal"
  747. modal.style.cssText = `
  748. position: fixed;
  749. top: 0;
  750. left: 0;
  751. width: 100%;
  752. height: 100%;
  753. background-color: rgba(0, 0, 0, 0.35);
  754. backdrop-filter: blur(2.5px);
  755. display: flex;
  756. justify-content: center;
  757. align-items: center;
  758. z-index: 10000;
  759. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  760. `
  761.  
  762. const modalContent = document.createElement("div")
  763. modalContent.style.cssText = `
  764. background-color: #ffffff;
  765. color: #334155;
  766. border-radius: 16px;
  767. width: 500px;
  768. max-width: 90%;
  769. max-height: 90vh;
  770. overflow-y: auto;
  771. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  772. `
  773.  
  774. const header = document.createElement("div")
  775. header.style.cssText = `
  776. display: flex;
  777. justify-content: space-between;
  778. align-items: center;
  779. padding: 16px;
  780. border-bottom: 1px solid #e2e8f0;
  781. `
  782.  
  783. const title = document.createElement("h2")
  784. title.innerHTML = `Download ${getMediaTypeLabel(settings.mediaType)}: <span style="color: #0ea5e9">${username}</span>`
  785. title.style.cssText = `
  786. margin: 0;
  787. font-size: 18px;
  788. font-weight: bold;
  789. color: #334155;
  790. `
  791.  
  792. const closeButton = document.createElement("button")
  793. closeButton.innerHTML = "&times;"
  794. closeButton.style.cssText = `
  795. background: none;
  796. border: none;
  797. color: #0f172a;
  798. font-size: 24px;
  799. cursor: pointer;
  800. padding: 0;
  801. line-height: 1;
  802. transition: color 0.2s;
  803. `
  804. closeButton.addEventListener("mouseenter", () => {
  805. closeButton.style.color = "#0ea5e9"
  806. })
  807. closeButton.addEventListener("mouseleave", () => {
  808. closeButton.style.color = "#0f172a"
  809. })
  810. closeButton.onclick = () => modal.remove()
  811.  
  812. header.appendChild(title)
  813. header.appendChild(closeButton)
  814.  
  815. const tabs = document.createElement("div")
  816. tabs.style.cssText = `
  817. display: flex;
  818. border-bottom: 1px solid #e2e8f0;
  819. `
  820.  
  821. const mainTab = document.createElement("div")
  822. mainTab.textContent = "Main"
  823. mainTab.className = "active-tab"
  824. mainTab.style.cssText = `
  825. padding: 12px 16px;
  826. cursor: pointer;
  827. flex: 1;
  828. text-align: center;
  829. border-bottom: 2px solid #0ea5e9;
  830. `
  831.  
  832. const settingsTab = document.createElement("div")
  833. settingsTab.textContent = "Settings"
  834. settingsTab.style.cssText = `
  835. padding: 12px 16px;
  836. cursor: pointer;
  837. flex: 1;
  838. text-align: center;
  839. color: #64748b;
  840. `
  841.  
  842. tabs.appendChild(mainTab)
  843. tabs.appendChild(settingsTab)
  844.  
  845. const mainContent = document.createElement("div")
  846. mainContent.style.cssText = `
  847. padding: 16px;
  848. `
  849.  
  850. const settingsContent = document.createElement("div")
  851. settingsContent.style.cssText = `
  852. padding: 16px;
  853. display: none;
  854. `
  855.  
  856. const fetchButton = document.createElement("button")
  857. const mediaTypeLabelText = getMediaTypeLabel(settings.mediaType).toLowerCase()
  858. const fetchIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  859. fetchIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
  860. fetchIcon.setAttribute("viewBox", "0 0 448 512")
  861. fetchIcon.setAttribute("width", "16")
  862. fetchIcon.setAttribute("height", "16")
  863. fetchIcon.style.marginRight = "8px"
  864.  
  865. const fetchPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
  866. fetchPath.setAttribute("fill", "currentColor")
  867. fetchPath.setAttribute(
  868. "d",
  869. "M374.6 214.6l-128 128c-12.5 12.5-32.8 12.5-45.3 0l-128-128c-12.5-12.5-12.5-32.8 0-45.3s32.8-12.5 45.3 0L192 242.7 192 32c0-17.7 14.3-32 32-32s32 14.3 32 32l0 210.7 73.4-73.4c12.5-12.5 32.8-12.5 45.3 0s12.5 32.8 0 45.3zM64 352l0 64c0 17.7 14.3 32 32 32l256 0c17.7 0 32-14.3 32-32l0-64c0-17.7 14.3-32 32-32s32 14.3 32 32l0 64c0 53-43 96-96 96L96 512c-53 0-96-43-96-96l0-64c0-17.7 14.3-32 32-32s32 14.3 32 32z",
  870. )
  871. fetchIcon.appendChild(fetchPath)
  872.  
  873. const fetchButtonText = document.createElement("span")
  874. fetchButtonText.textContent =
  875. settings.mediaType === "all"
  876. ? "Fetch Media"
  877. : `Fetch ${mediaTypeLabelText === "gif" ? "GIF" : mediaTypeLabelText.charAt(0).toUpperCase() + mediaTypeLabelText.slice(1)}`
  878.  
  879. fetchButton.innerHTML = ""
  880. fetchButton.appendChild(fetchIcon)
  881. fetchButton.appendChild(fetchButtonText)
  882.  
  883. fetchButton.style.cssText = `
  884. background-color: #22c55e;
  885. color: white;
  886. border: none;
  887. border-radius: 9999px;
  888. padding: 8px 16px;
  889. font-weight: bold;
  890. cursor: pointer;
  891. margin: 16px auto;
  892. width: 50%;
  893. display: flex;
  894. justify-content: center;
  895. align-items: center;
  896. text-align: center;
  897. transition: background-color 0.2s;
  898. `
  899. fetchButton.addEventListener("mouseenter", () => {
  900. fetchButton.style.backgroundColor = "#16a34a"
  901. })
  902. fetchButton.addEventListener("mouseleave", () => {
  903. fetchButton.style.backgroundColor = "#22c55e"
  904. })
  905.  
  906. const infoContainer = document.createElement("div")
  907. infoContainer.style.cssText = `
  908. background-color: #f1f5f9;
  909. border-radius: 8px;
  910. padding: 12px;
  911. margin-bottom: 16px;
  912. display: none;
  913. `
  914.  
  915. const buttonContainer = document.createElement("div")
  916. buttonContainer.style.cssText = `
  917. display: none;
  918. gap: 8px;
  919. margin-bottom: 16px;
  920. `
  921.  
  922. const downloadCurrentButton = document.createElement("button")
  923. downloadCurrentButton.textContent = "Download Current Batch"
  924. downloadCurrentButton.style.cssText = `
  925. background-color: #0ea5e9;
  926. color: white;
  927. border: none;
  928. border-radius: 9999px;
  929. padding: 8px 16px;
  930. font-weight: bold;
  931. cursor: pointer;
  932. flex: 1;
  933. display: block;
  934. text-align: center;
  935. transition: background-color 0.2s;
  936. `
  937. downloadCurrentButton.addEventListener("mouseenter", () => {
  938. downloadCurrentButton.style.backgroundColor = "#0284c7"
  939. })
  940. downloadCurrentButton.addEventListener("mouseleave", () => {
  941. downloadCurrentButton.style.backgroundColor = "#0ea5e9"
  942. })
  943.  
  944. const downloadAllButton = document.createElement("button")
  945. downloadAllButton.textContent = "Download All Batches"
  946. downloadAllButton.style.cssText = `
  947. background-color: #0ea5e9;
  948. color: white;
  949. border: none;
  950. border-radius: 9999px;
  951. padding: 8px 16px;
  952. font-weight: bold;
  953. cursor: pointer;
  954. flex: 1;
  955. display: block;
  956. text-align: center;
  957. transition: background-color 0.2s;
  958. `
  959. downloadAllButton.addEventListener("mouseenter", () => {
  960. downloadAllButton.style.backgroundColor = "#0284c7"
  961. })
  962. downloadAllButton.addEventListener("mouseleave", () => {
  963. downloadAllButton.style.backgroundColor = "#0ea5e9"
  964. })
  965.  
  966. const downloadButton = document.createElement("button")
  967. downloadButton.textContent = "Download"
  968. downloadButton.style.cssText = `
  969. background-color: #0ea5e9;
  970. color: white;
  971. border: none;
  972. border-radius: 9999px;
  973. padding: 8px 16px;
  974. font-weight: bold;
  975. cursor: pointer;
  976. width: 50%;
  977. margin-left: auto;
  978. margin-right: auto;
  979. display: block;
  980. text-align: center;
  981. transition: background-color 0.2s;
  982. `
  983. downloadButton.addEventListener("mouseenter", () => {
  984. downloadButton.style.backgroundColor = "#0284c7"
  985. })
  986. downloadButton.addEventListener("mouseleave", () => {
  987. downloadButton.style.backgroundColor = "#0ea5e9"
  988. })
  989. downloadButton.onclick = () => downloadMedia(false)
  990.  
  991. if (settings.batchEnabled) {
  992. buttonContainer.style.display = "none"
  993. } else {
  994. buttonContainer.appendChild(downloadButton)
  995. }
  996.  
  997. const batchButtonsContainer = document.createElement("div")
  998. batchButtonsContainer.style.cssText = `
  999. display: none;
  1000. gap: 8px;
  1001. margin-bottom: 16px;
  1002. `
  1003.  
  1004. const nextBatchButton = document.createElement("button")
  1005. nextBatchButton.textContent = "Next Batch"
  1006. nextBatchButton.style.cssText = `
  1007. background-color: #6366f1;
  1008. color: white;
  1009. border: none;
  1010. border-radius: 9999px;
  1011. padding: 8px 16px;
  1012. font-weight: bold;
  1013. cursor: pointer;
  1014. flex: 1;
  1015. display: block;
  1016. text-align: center;
  1017. transition: background-color 0.2s;
  1018. `
  1019. nextBatchButton.addEventListener("mouseenter", () => {
  1020. nextBatchButton.style.backgroundColor = "#4f46e5"
  1021. })
  1022. nextBatchButton.addEventListener("mouseleave", () => {
  1023. nextBatchButton.style.backgroundColor = "#6366f1"
  1024. })
  1025.  
  1026. const autoBatchButton = document.createElement("button")
  1027. autoBatchButton.textContent = "Auto Batch"
  1028. autoBatchButton.style.cssText = `
  1029. background-color: #6366f1;
  1030. color: white;
  1031. border: none;
  1032. border-radius: 9999px;
  1033. padding: 8px 16px;
  1034. font-weight: bold;
  1035. cursor: pointer;
  1036. flex: 1;
  1037. display: block;
  1038. text-align: center;
  1039. transition: background-color 0.2s;
  1040. `
  1041. autoBatchButton.addEventListener("mouseenter", () => {
  1042. autoBatchButton.style.backgroundColor = "#4f46e5"
  1043. })
  1044. autoBatchButton.addEventListener("mouseleave", () => {
  1045. autoBatchButton.style.backgroundColor = "#6366f1"
  1046. })
  1047.  
  1048. batchButtonsContainer.appendChild(nextBatchButton)
  1049. batchButtonsContainer.appendChild(autoBatchButton)
  1050.  
  1051. const stopBatchButton = document.createElement("button")
  1052. stopBatchButton.textContent = "Stop Batch"
  1053. stopBatchButton.style.cssText = `
  1054. background-color: #ef4444;
  1055. color: white;
  1056. border: none;
  1057. border-radius: 9999px;
  1058. padding: 8px 16px;
  1059. font-weight: bold;
  1060. cursor: pointer;
  1061. margin-bottom: 16px;
  1062. width: 100%;
  1063. display: none;
  1064. text-align: center;
  1065. transition: background-color 0.2s;
  1066. `
  1067. stopBatchButton.addEventListener("mouseenter", () => {
  1068. stopBatchButton.style.backgroundColor = "#dc2626"
  1069. })
  1070. stopBatchButton.addEventListener("mouseleave", () => {
  1071. stopBatchButton.style.backgroundColor = "#ef4444"
  1072. })
  1073.  
  1074. const progressContainer = document.createElement("div")
  1075. progressContainer.style.cssText = `
  1076. margin-top: 16px;
  1077. display: none;
  1078. `
  1079.  
  1080. const progressText = document.createElement("div")
  1081. progressText.style.cssText = `
  1082. margin-bottom: 8px;
  1083. font-size: 14px;
  1084. text-align: center;
  1085. `
  1086. progressText.textContent = "Downloading..."
  1087.  
  1088. const progressBar = document.createElement("div")
  1089. progressBar.style.cssText = `
  1090. width: 100%;
  1091. height: 8px;
  1092. background-color: #f1f5f9;
  1093. border-radius: 4px;
  1094. overflow: hidden;
  1095. `
  1096.  
  1097. const progressFill = document.createElement("div")
  1098. progressFill.style.cssText = `
  1099. height: 100%;
  1100. width: 0%;
  1101. background-color: #0ea5e9;
  1102. transition: width 0.3s ease-in-out;
  1103. will-change: width;
  1104. `
  1105.  
  1106. progressBar.appendChild(progressFill)
  1107. progressContainer.appendChild(progressText)
  1108. progressContainer.appendChild(progressBar)
  1109.  
  1110. mainContent.appendChild(fetchButton)
  1111. mainContent.appendChild(infoContainer)
  1112. mainContent.appendChild(buttonContainer)
  1113. mainContent.appendChild(batchButtonsContainer)
  1114. mainContent.appendChild(stopBatchButton)
  1115. mainContent.appendChild(progressContainer)
  1116.  
  1117. const settingsForm = document.createElement("div")
  1118. settingsForm.style.cssText = `
  1119. display: flex;
  1120. flex-direction: column;
  1121. gap: 16px;
  1122. `
  1123.  
  1124. const patreonAuthGroup = document.createElement("div")
  1125. patreonAuthGroup.style.cssText = `
  1126. display: flex;
  1127. flex-direction: column;
  1128. gap: 8px;
  1129. `
  1130.  
  1131. const patreonAuthLabel = document.createElement("label")
  1132. patreonAuthLabel.textContent = "Patreon Auth:"
  1133. patreonAuthLabel.style.cssText = `
  1134. font-size: 14px;
  1135. font-weight: bold;
  1136. color: #334155;
  1137. `
  1138.  
  1139. const patreonAuthInputContainer = document.createElement("div")
  1140. patreonAuthInputContainer.style.cssText = `
  1141. position: relative;
  1142. display: flex;
  1143. align-items: center;
  1144. `
  1145.  
  1146. const patreonAuthInput = document.createElement("input")
  1147. patreonAuthInput.type = "text"
  1148. patreonAuthInput.value = settings.patreonAuth
  1149. patreonAuthInput.style.cssText = `
  1150. background-color: #f1f5f9;
  1151. border: 1px solid transparent;
  1152. border-radius: 4px;
  1153. padding: 8px 12px;
  1154. color: #64748b;
  1155. width: 100%;
  1156. box-sizing: border-box;
  1157. transition: all 0.2s ease;
  1158. `
  1159. patreonAuthInput.addEventListener("focus", () => {
  1160. patreonAuthInput.style.border = "1px solid #0ea5e9"
  1161. patreonAuthInput.style.outline = "none"
  1162. })
  1163. patreonAuthInput.addEventListener("blur", () => {
  1164. patreonAuthInput.style.border = "1px solid transparent"
  1165. })
  1166.  
  1167. patreonAuthInput.addEventListener("input", () => {
  1168. const newSettings = getSettings()
  1169. newSettings.patreonAuth = patreonAuthInput.value
  1170. saveSettings(newSettings)
  1171. patreonAuthClearButton.style.display = patreonAuthInput.value ? "block" : "none"
  1172. })
  1173.  
  1174. const patreonAuthClearButton = document.createElement("button")
  1175. patreonAuthClearButton.innerHTML = "&times;"
  1176. patreonAuthClearButton.style.cssText = `
  1177. position: absolute;
  1178. right: 8px;
  1179. background: none;
  1180. border: none;
  1181. color: #64748b;
  1182. font-size: 18px;
  1183. cursor: pointer;
  1184. padding: 0;
  1185. display: ${settings.patreonAuth ? "block" : "none"};
  1186. `
  1187. patreonAuthClearButton.addEventListener("click", () => {
  1188. patreonAuthInput.value = ""
  1189. const newSettings = getSettings()
  1190. newSettings.patreonAuth = ""
  1191. saveSettings(newSettings)
  1192. patreonAuthClearButton.style.display = "none"
  1193. })
  1194.  
  1195. patreonAuthInputContainer.appendChild(patreonAuthInput)
  1196. patreonAuthInputContainer.appendChild(patreonAuthClearButton)
  1197. patreonAuthGroup.appendChild(patreonAuthLabel)
  1198. patreonAuthGroup.appendChild(patreonAuthInputContainer)
  1199.  
  1200. const tokenGroup = document.createElement("div")
  1201. tokenGroup.style.cssText = `
  1202. display: flex;
  1203. flex-direction: column;
  1204. gap: 8px;
  1205. `
  1206.  
  1207. const tokenLabel = document.createElement("label")
  1208. tokenLabel.textContent = "Auth Token:"
  1209. tokenLabel.style.cssText = `
  1210. font-size: 14px;
  1211. font-weight: bold;
  1212. color: #334155;
  1213. `
  1214.  
  1215. const tokenInputContainer = document.createElement("div")
  1216. tokenInputContainer.style.cssText = `
  1217. position: relative;
  1218. display: flex;
  1219. align-items: center;
  1220. `
  1221.  
  1222. const tokenInput = document.createElement("input")
  1223. tokenInput.type = "text"
  1224. tokenInput.value = settings.authToken
  1225. tokenInput.style.cssText = `
  1226. background-color: #f1f5f9;
  1227. border: 1px solid transparent;
  1228. border-radius: 4px;
  1229. padding: 8px 12px;
  1230. color: #64748b;
  1231. width: 100%;
  1232. box-sizing: border-box;
  1233. transition: all 0.2s ease;
  1234. `
  1235. tokenInput.addEventListener("focus", () => {
  1236. tokenInput.style.border = "1px solid #0ea5e9"
  1237. tokenInput.style.outline = "none"
  1238. })
  1239. tokenInput.addEventListener("blur", () => {
  1240. tokenInput.style.border = "1px solid transparent"
  1241. })
  1242.  
  1243. tokenInput.addEventListener("input", () => {
  1244. const newSettings = getSettings()
  1245. newSettings.authToken = tokenInput.value
  1246. saveSettings(newSettings)
  1247. tokenClearButton.style.display = tokenInput.value ? "block" : "none"
  1248. })
  1249.  
  1250. const tokenClearButton = document.createElement("button")
  1251. tokenClearButton.innerHTML = "&times;"
  1252. tokenClearButton.style.cssText = `
  1253. position: absolute;
  1254. right: 8px;
  1255. background: none;
  1256. border: none;
  1257. color: #64748b;
  1258. font-size: 18px;
  1259. cursor: pointer;
  1260. padding: 0;
  1261. display: ${settings.authToken ? "block" : "none"};
  1262. `
  1263. tokenClearButton.addEventListener("click", () => {
  1264. tokenInput.value = ""
  1265. const newSettings = getSettings()
  1266. newSettings.authToken = ""
  1267. saveSettings(newSettings)
  1268. tokenClearButton.style.display = "none"
  1269. })
  1270.  
  1271. tokenInputContainer.appendChild(tokenInput)
  1272. tokenInputContainer.appendChild(tokenClearButton)
  1273. tokenGroup.appendChild(tokenLabel)
  1274. tokenGroup.appendChild(tokenInputContainer)
  1275.  
  1276. const apiServerGroup = document.createElement("div")
  1277. apiServerGroup.style.cssText = `
  1278. display: flex;
  1279. flex-direction: column;
  1280. gap: 8px;
  1281. `
  1282.  
  1283. const apiServerLabel = document.createElement("label")
  1284. apiServerLabel.textContent = "Service:"
  1285. apiServerLabel.style.cssText = `
  1286. font-size: 14px;
  1287. font-weight: bold;
  1288. color: #334155;
  1289. `
  1290.  
  1291. const apiServerOptions = [
  1292. { value: "default", label: "Default" },
  1293. { value: "backup", label: "Backup" }
  1294. ]
  1295.  
  1296. const apiServerToggle = createToggleSwitch(apiServerOptions, settings.apiServer, (value) => {
  1297. const newSettings = getSettings()
  1298. newSettings.apiServer = value
  1299. saveSettings(newSettings)
  1300. settings.apiServer = value
  1301. })
  1302.  
  1303. apiServerGroup.appendChild(apiServerLabel)
  1304. apiServerGroup.appendChild(apiServerToggle)
  1305.  
  1306. const batchGroup = document.createElement("div")
  1307. batchGroup.style.cssText = `
  1308. display: flex;
  1309. align-items: center;
  1310. gap: 8px;
  1311. `
  1312. const batchLabel = document.createElement("label")
  1313. batchLabel.style.cssText = `
  1314. font-size: 14px;
  1315. font-weight: bold;
  1316. color: #334155;
  1317. flex: 1;
  1318. display: flex;
  1319. align-items: center;
  1320. `
  1321. const batchLabelText = document.createElement("span")
  1322. batchLabelText.textContent = "Batch:"
  1323. batchLabelText.style.marginRight = "8px"
  1324. batchLabel.appendChild(batchLabelText)
  1325. const batchStatusText = document.createElement("span")
  1326. batchStatusText.textContent = settings.batchEnabled ? "Enabled" : "Disabled"
  1327. batchStatusText.style.cssText = `
  1328. font-size: 14px;
  1329. font-weight: normal;
  1330. color: ${settings.batchEnabled ? "#22c55e" : "#64748b"};
  1331. `
  1332. batchLabel.appendChild(batchStatusText)
  1333. const batchToggle = document.createElement("div")
  1334. batchToggle.style.cssText = `
  1335. position: relative;
  1336. width: 50px;
  1337. height: 24px;
  1338. background-color: ${settings.batchEnabled ? "#22c55e" : "#cbd5e1"};
  1339. border-radius: 12px;
  1340. cursor: pointer;
  1341. transition: background-color 0.3s;
  1342. `
  1343. const batchToggleHandle = document.createElement("div")
  1344. batchToggleHandle.style.cssText = `
  1345. position: absolute;
  1346. top: 2px;
  1347. left: ${settings.batchEnabled ? "28px" : "2px"};
  1348. width: 20px;
  1349. height: 20px;
  1350. background-color: white;
  1351. border-radius: 50%;
  1352. transition: left 0.3s;
  1353. `
  1354. batchToggle.appendChild(batchToggleHandle)
  1355. batchToggle.addEventListener("click", () => {
  1356. const newSettings = getSettings()
  1357. newSettings.batchEnabled = !newSettings.batchEnabled
  1358. saveSettings(newSettings)
  1359. batchToggle.style.backgroundColor = newSettings.batchEnabled ? "#22c55e" : "#cbd5e1"
  1360. batchToggleHandle.style.left = newSettings.batchEnabled ? "28px" : "2px"
  1361. batchStatusText.textContent = newSettings.batchEnabled ? "Enabled" : "Disabled"
  1362. batchStatusText.style.color = newSettings.batchEnabled ? "#22c55e" : "#64748b"
  1363. batchSizeGroup.style.display = newSettings.batchEnabled ? "flex" : "none"
  1364. if (!newSettings.batchEnabled) {
  1365. buttonContainer.innerHTML = ""
  1366. buttonContainer.appendChild(downloadButton)
  1367. buttonContainer.style.display = infoContainer.style.display === "block" ? "block" : "none"
  1368. } else {
  1369. buttonContainer.innerHTML = ""
  1370. buttonContainer.style.display = "none"
  1371. }
  1372. })
  1373. batchGroup.appendChild(batchLabel)
  1374. batchGroup.appendChild(batchToggle)
  1375.  
  1376. const batchSizeGroup = document.createElement("div")
  1377. batchSizeGroup.style.cssText = `
  1378. display: ${settings.batchEnabled ? "flex" : "none"};
  1379. flex-direction: column;
  1380. gap: 8px;
  1381. `
  1382.  
  1383. const batchSizeLabel = document.createElement("label")
  1384. batchSizeLabel.textContent = "Batch Size:"
  1385. batchSizeLabel.style.cssText = `
  1386. font-size: 14px;
  1387. font-weight: bold;
  1388. color: #334155;
  1389. `
  1390.  
  1391. const batchSizeToggle = createSlider(batchSizes, settings.batchSize, (value) => {
  1392. const newSettings = getSettings()
  1393. newSettings.batchSize = value
  1394. saveSettings(newSettings)
  1395. settings.batchSize = value
  1396. })
  1397.  
  1398. batchSizeGroup.appendChild(batchSizeLabel)
  1399. batchSizeGroup.appendChild(batchSizeToggle)
  1400.  
  1401. const timelineTypeGroup = document.createElement("div")
  1402. timelineTypeGroup.style.cssText = `
  1403. display: flex;
  1404. flex-direction: column;
  1405. gap: 8px;
  1406. `
  1407.  
  1408. const timelineTypeLabel = document.createElement("label")
  1409. timelineTypeLabel.textContent = "Timeline Type:"
  1410. timelineTypeLabel.style.cssText = `
  1411. font-size: 14px;
  1412. font-weight: bold;
  1413. color: #334155;
  1414. `
  1415.  
  1416. const timelineTypeOptions = [
  1417. { value: "media", label: "Media" },
  1418. { value: "timeline", label: "Post" },
  1419. { value: "tweets", label: "Tweets" },
  1420. { value: "with_replies", label: "Replies" },
  1421. ]
  1422.  
  1423. const timelineTypeToggle = createToggleSwitch(timelineTypeOptions, settings.timelineType, (value) => {
  1424. const newSettings = getSettings()
  1425. newSettings.timelineType = value
  1426. saveSettings(newSettings)
  1427. settings.timelineType = value
  1428. })
  1429.  
  1430. timelineTypeGroup.appendChild(timelineTypeLabel)
  1431. timelineTypeGroup.appendChild(timelineTypeToggle)
  1432.  
  1433. const mediaTypeGroup = document.createElement("div")
  1434. mediaTypeGroup.style.cssText = `
  1435. display: flex;
  1436. flex-direction: column;
  1437. gap: 8px;
  1438. `
  1439.  
  1440. const mediaTypeLabel = document.createElement("label")
  1441. mediaTypeLabel.textContent = "Media Type:"
  1442. mediaTypeLabel.style.cssText = `
  1443. font-size: 14px;
  1444. font-weight: bold;
  1445. color: #334155;
  1446. `
  1447.  
  1448. const mediaTypeIcons = createMediaTypeIcons()
  1449. const mediaTypeOptions = [
  1450. { value: "all", label: "All", icon: mediaTypeIcons.all },
  1451. { value: "image", label: "Image", icon: mediaTypeIcons.image },
  1452. { value: "video", label: "Video", icon: mediaTypeIcons.video },
  1453. { value: "gif", label: "GIF", icon: mediaTypeIcons.gif },
  1454. ]
  1455.  
  1456. const mediaTypeToggle = createToggleSwitch(mediaTypeOptions, settings.mediaType, (value) => {
  1457. const newSettings = getSettings()
  1458. newSettings.mediaType = value
  1459. saveSettings(newSettings)
  1460. settings.mediaType = value
  1461.  
  1462. const newMediaTypeLabel = getMediaTypeLabel(value).toLowerCase()
  1463. const newFetchButtonText =
  1464. value === "all"
  1465. ? "Fetch Media"
  1466. : `Fetch ${newMediaTypeLabel === "gif" ? "GIF" : newMediaTypeLabel.charAt(0).toUpperCase() + newMediaTypeLabel.slice(1)}`
  1467.  
  1468. fetchButton.innerHTML = ""
  1469. const newFetchIcon = fetchIcon.cloneNode(true)
  1470. fetchButton.appendChild(newFetchIcon)
  1471. fetchButton.appendChild(document.createTextNode(newFetchButtonText))
  1472.  
  1473. title.innerHTML = `Download ${getMediaTypeLabel(value)}: <span style="color: #0ea5e9">${username}</span>`
  1474. })
  1475.  
  1476. mediaTypeGroup.appendChild(mediaTypeLabel)
  1477. mediaTypeGroup.appendChild(mediaTypeToggle)
  1478.  
  1479. const concurrentGroup = document.createElement("div")
  1480. concurrentGroup.style.cssText = `
  1481. display: flex;
  1482. flex-direction: column;
  1483. gap: 8px;
  1484. `
  1485.  
  1486. const concurrentLabel = document.createElement("label")
  1487. concurrentLabel.textContent = "Batch Download Items:"
  1488. concurrentLabel.style.cssText = `
  1489. font-size: 14px;
  1490. font-weight: bold;
  1491. color: #334155;
  1492. `
  1493.  
  1494. const concurrentToggle = createSlider(concurrentSizes, settings.concurrentDownloads, (value) => {
  1495. const newSettings = getSettings()
  1496. newSettings.concurrentDownloads = value
  1497. saveSettings(newSettings)
  1498. settings.concurrentDownloads = value
  1499. })
  1500.  
  1501. concurrentGroup.appendChild(concurrentLabel)
  1502. concurrentGroup.appendChild(concurrentToggle)
  1503.  
  1504. const cacheDurationGroup = document.createElement("div")
  1505. cacheDurationGroup.style.cssText = `
  1506. display: flex;
  1507. flex-direction: column;
  1508. gap: 8px;
  1509. `
  1510.  
  1511. const cacheDurationLabel = document.createElement("label")
  1512. cacheDurationLabel.textContent = "Cache Duration:"
  1513. cacheDurationLabel.style.cssText = `
  1514. font-size: 14px;
  1515. font-weight: bold;
  1516. color: #334155;
  1517. `
  1518.  
  1519. const cacheDurationOptions = cacheDurations.map((duration) => {
  1520. let label = duration.toString() + "m"
  1521. if (duration >= 60 && duration % 60 === 0) {
  1522. label = `${duration / 60}h`
  1523. }
  1524. return { value: duration, label: label }
  1525. })
  1526.  
  1527. const cacheDurationToggle = createToggleSwitch(cacheDurationOptions, settings.cacheDuration, (value) => {
  1528. const newSettings = getSettings()
  1529. newSettings.cacheDuration = value
  1530. saveSettings(newSettings)
  1531. settings.cacheDuration = value
  1532. })
  1533.  
  1534. cacheDurationGroup.appendChild(cacheDurationLabel)
  1535. cacheDurationGroup.appendChild(cacheDurationToggle)
  1536.  
  1537. const clearCacheButton = document.createElement("button")
  1538. const trashIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
  1539. trashIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
  1540. trashIcon.setAttribute("viewBox", "0 0 448 512")
  1541. trashIcon.setAttribute("width", "16")
  1542. trashIcon.setAttribute("height", "16")
  1543. trashIcon.style.marginRight = "8px"
  1544.  
  1545. const trashPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
  1546. trashPath.setAttribute("fill", "currentColor")
  1547. trashPath.setAttribute(
  1548. "d",
  1549. "M135.2 17.7C140.6 6.8 151.7 0 163.8 0L284.2 0c12.1 0 23.2 6.8 28.6 17.7L320 32l96 0c17.7 0 32 14.3 32 32s-14.3 32-32 32L32 96C14.3 96 0 81.7 0 64S14.3 32 32 32l96 0 7.2-14.3zM32 128l384 0 0 320c0 35.3-28.7 64-64 64L96 512c-35.3 0-64-28.7-64-64l0-320zm96 64c-8.8 0-16 7.2-16 16l0 224c0 8.8 7.2 16 16 16s16-7.2 16-16l0-224c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16l0 224c0 8.8 7.2 16 16 16s16-7.2 16-16l0-224c0-8.8-7.2-16-16-16zm96 0c-8.8 0-16 7.2-16 16l0 224c0 8.8 7.2 16 16 16s16-7.2 16-16l0-224c0-8.8-7.2-16-16-16z",
  1550. )
  1551. trashIcon.appendChild(trashPath)
  1552.  
  1553. clearCacheButton.appendChild(trashIcon)
  1554. clearCacheButton.appendChild(document.createTextNode("Clear Cache"))
  1555. clearCacheButton.style.cssText = `
  1556. background-color: #ef4444;
  1557. color: white;
  1558. border: none;
  1559. border-radius: 9999px;
  1560. padding: 8px 16px;
  1561. font-weight: bold;
  1562. cursor: pointer;
  1563. margin-top: 16px;
  1564. width: 50%;
  1565. margin-left: auto;
  1566. margin-right: auto;
  1567. display: flex;
  1568. align-items: center;
  1569. justify-content: center;
  1570. text-align: center;
  1571. transition: background-color 0.2s;
  1572. `
  1573. clearCacheButton.addEventListener("mouseenter", () => {
  1574. clearCacheButton.style.backgroundColor = "#dc2626"
  1575. })
  1576. clearCacheButton.addEventListener("mouseleave", () => {
  1577. clearCacheButton.style.backgroundColor = "#ef4444"
  1578. })
  1579.  
  1580. clearCacheButton.addEventListener("click", () => {
  1581. createConfirmDialog("Are you sure about clearing the cache?", () => {
  1582. cacheManager.clear()
  1583.  
  1584. const notification = document.createElement("div")
  1585. notification.style.cssText = `
  1586. position: fixed;
  1587. bottom: 20px;
  1588. left: 50%;
  1589. transform: translateX(-50%);
  1590. background-color: #0ea5e9;
  1591. color: white;
  1592. padding: 12px 24px;
  1593. border-radius: 9999px;
  1594. font-weight: bold;
  1595. z-index: 10002;
  1596. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  1597. text-align: center;
  1598. `
  1599. notification.textContent = "Cache cleared successfully"
  1600. document.body.appendChild(notification)
  1601.  
  1602. setTimeout(() => {
  1603. document.body.removeChild(notification)
  1604. }, 3000)
  1605. })
  1606. })
  1607.  
  1608. const patreonLink = document.createElement("a")
  1609. patreonLink.href = "https://www.patreon.com/exyezed"
  1610. patreonLink.target = "_blank"
  1611. patreonLink.style.cssText = `
  1612. display: flex;
  1613. align-items: center;
  1614. justify-content: center;
  1615. color: #64748b;
  1616. text-decoration: none;
  1617. margin-top: 16px;
  1618. padding: 8px;
  1619. border-radius: 8px;
  1620. transition: background-color 0.2s, color 0.2s;
  1621. `
  1622. patreonLink.innerHTML = createPatreonIcon().outerHTML + "Patreon Authentication"
  1623.  
  1624. patreonLink.addEventListener("mouseenter", () => {
  1625. patreonLink.style.backgroundColor = "#f1f5f9"
  1626. patreonLink.style.color = "#0ea5e9"
  1627. })
  1628.  
  1629. patreonLink.addEventListener("mouseleave", () => {
  1630. patreonLink.style.backgroundColor = "transparent"
  1631. patreonLink.style.color = "#64748b"
  1632. })
  1633.  
  1634. settingsForm.appendChild(patreonAuthGroup)
  1635. settingsForm.appendChild(tokenGroup)
  1636. settingsForm.appendChild(apiServerGroup)
  1637. settingsForm.appendChild(batchGroup)
  1638. settingsForm.appendChild(batchSizeGroup)
  1639. settingsForm.appendChild(timelineTypeGroup)
  1640. settingsForm.appendChild(mediaTypeGroup)
  1641. settingsForm.appendChild(concurrentGroup)
  1642. settingsForm.appendChild(cacheDurationGroup)
  1643. settingsForm.appendChild(clearCacheButton)
  1644. settingsForm.appendChild(patreonLink)
  1645.  
  1646. settingsContent.appendChild(settingsForm)
  1647.  
  1648. mainTab.addEventListener("click", () => {
  1649. mainTab.style.borderBottom = "2px solid #0ea5e9"
  1650. mainTab.style.color = "#0f172a"
  1651. settingsTab.style.borderBottom = "none"
  1652. settingsTab.style.color = "#64748b"
  1653. mainContent.style.display = "block"
  1654. settingsContent.style.display = "none"
  1655. })
  1656.  
  1657. settingsTab.addEventListener("click", () => {
  1658. settingsTab.style.borderBottom = "2px solid #0ea5e9"
  1659. settingsTab.style.color = "#0f172a"
  1660. mainTab.style.borderBottom = "none"
  1661. mainTab.style.color = "#64748b"
  1662. settingsContent.style.display = "block"
  1663. mainContent.style.display = "none"
  1664. })
  1665.  
  1666. modalContent.appendChild(header)
  1667. modalContent.appendChild(tabs)
  1668. modalContent.appendChild(mainContent)
  1669. modalContent.appendChild(settingsContent)
  1670. modal.appendChild(modalContent)
  1671.  
  1672. const mediaData = {
  1673. username: username,
  1674. currentPage: 0,
  1675. mediaItems: [],
  1676. allMediaItems: [],
  1677. hasMore: false,
  1678. downloading: false,
  1679. totalDownloaded: 0,
  1680. totalToDownload: 0,
  1681. totalItems: 0,
  1682. autoBatchRunning: false,
  1683. }
  1684.  
  1685. fetchButton.addEventListener("click", async () => {
  1686. const settings = getSettings()
  1687.  
  1688. if (!settings.authToken) {
  1689. createAuthTokenPopup()
  1690. return
  1691. }
  1692.  
  1693. if (!settings.patreonAuth) {
  1694. createPatreonAuthPopup()
  1695. return
  1696. }
  1697.  
  1698. infoContainer.style.display = "none"
  1699. buttonContainer.style.display = "none"
  1700. nextBatchButton.style.display = "none"
  1701. autoBatchButton.style.display = "none"
  1702. stopBatchButton.style.display = "none"
  1703. progressContainer.style.display = "none"
  1704. fetchButton.disabled = true
  1705. fetchButton.innerHTML = ""
  1706. fetchButton.appendChild(document.createTextNode("Fetching..."))
  1707.  
  1708. try {
  1709. const cacheKey = `${settings.timelineType}_${settings.mediaType}_${username}_${mediaData.currentPage}_${settings.batchSize}_batch_${settings.batchEnabled}`
  1710. let data = cacheManager.get(cacheKey)
  1711.  
  1712. if (!data) {
  1713. let url
  1714. if (settings.batchEnabled) {
  1715. url = `${getServiceBaseUrl()}/metadata/${settings.timelineType}/${settings.batchSize}/${mediaData.currentPage}/${settings.mediaType}/${username}/${settings.authToken}/${settings.patreonAuth || ""}`
  1716. } else {
  1717. url = `${getServiceBaseUrl()}/metadata/${settings.timelineType}/${settings.mediaType}/${username}/${settings.authToken}/${settings.patreonAuth || ""}`
  1718. }
  1719.  
  1720. data = await fetchData(url)
  1721. if (data && data.timeline && data.timeline.length > 0) {
  1722. cacheManager.set(cacheKey, data, true)
  1723. }
  1724. }
  1725.  
  1726. if (data.timeline && data.timeline.length > 0) {
  1727. mediaData.mediaItems = data.timeline
  1728. mediaData.hasMore = data.metadata.has_more
  1729. mediaData.totalItems = data.total_urls
  1730.  
  1731. if (mediaData.currentPage === 0) {
  1732. mediaData.allMediaItems = [...data.timeline]
  1733. } else {
  1734. mediaData.allMediaItems = [...mediaData.allMediaItems, ...data.timeline]
  1735. }
  1736.  
  1737. const mediaTypeLabel = getMediaTypeLabel(settings.mediaType)
  1738.  
  1739. if (settings.batchEnabled) {
  1740. infoContainer.innerHTML = `
  1741. <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
  1742. <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
  1743. <div style="margin-top: 8px;"><strong>Batch:</strong> ${mediaData.currentPage + 1}</div>
  1744. <div style="margin-top: 8px;"><strong>Total Items:</strong> ${formatNumber(mediaData.allMediaItems.length)}</div>
  1745. `
  1746. } else {
  1747. const currentPart = Math.floor(mediaData.allMediaItems.length / 500) + 1
  1748. if (currentPart === 1) {
  1749. infoContainer.innerHTML = `
  1750. <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
  1751. <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
  1752. `
  1753. } else {
  1754. infoContainer.innerHTML = `
  1755. <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
  1756. <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
  1757. <div style="margin-top: 8px;"><strong>Part:</strong> ${currentPart}</div>
  1758. `
  1759. }
  1760. }
  1761.  
  1762. infoContainer.style.display = "block"
  1763.  
  1764. if (settings.batchEnabled) {
  1765. buttonContainer.innerHTML = ""
  1766. buttonContainer.appendChild(downloadCurrentButton)
  1767. buttonContainer.appendChild(downloadAllButton)
  1768. buttonContainer.style.display = "flex"
  1769. } else {
  1770. buttonContainer.innerHTML = ""
  1771. buttonContainer.appendChild(downloadButton)
  1772. buttonContainer.style.display = "block"
  1773. }
  1774.  
  1775. if (settings.batchEnabled && mediaData.hasMore) {
  1776. batchButtonsContainer.style.display = "flex"
  1777. nextBatchButton.style.display = "block"
  1778. autoBatchButton.style.display = "block"
  1779. }
  1780.  
  1781. downloadCurrentButton.onclick = () => downloadMedia(false)
  1782. downloadAllButton.onclick = () => downloadMedia(true)
  1783.  
  1784. fetchButton.disabled = false
  1785. const currentMediaTypeLabel = getMediaTypeLabel(settings.mediaType).toLowerCase()
  1786. const updatedFetchButtonText =
  1787. settings.mediaType === "all"
  1788. ? "Fetch Media"
  1789. : `Fetch ${currentMediaTypeLabel === "gif" ? "GIF" : currentMediaTypeLabel.charAt(0).toUpperCase() + currentMediaTypeLabel.slice(1)}`
  1790.  
  1791. fetchButton.innerHTML = ""
  1792. const updatedFetchIcon = fetchIcon.cloneNode(true)
  1793. fetchButton.appendChild(updatedFetchIcon)
  1794. fetchButton.appendChild(document.createTextNode(updatedFetchButtonText))
  1795. } else {
  1796. infoContainer.innerHTML = '<div style="color: #ef4444;">No media found, invalid token, or invalid Patreon authentication.</div>'
  1797. infoContainer.style.display = "block"
  1798. fetchButton.disabled = false
  1799. const currentMediaTypeLabel = getMediaTypeLabel(settings.mediaType).toLowerCase()
  1800. const updatedFetchButtonText =
  1801. settings.mediaType === "all"
  1802. ? "Fetch Media"
  1803. : `Fetch ${currentMediaTypeLabel === "gif" ? "GIF" : currentMediaTypeLabel.charAt(0).toUpperCase() + currentMediaTypeLabel.slice(1)}`
  1804.  
  1805. fetchButton.innerHTML = ""
  1806. const updatedFetchIcon = fetchIcon.cloneNode(true)
  1807. fetchButton.appendChild(updatedFetchIcon)
  1808. fetchButton.appendChild(document.createTextNode(updatedFetchButtonText))
  1809. }
  1810. } catch (error) {
  1811. infoContainer.innerHTML = `<div style="color: #ef4444;">Error: ${error.message}</div>`
  1812. infoContainer.style.display = "block"
  1813. fetchButton.disabled = false
  1814. const currentMediaTypeLabel = getMediaTypeLabel(settings.mediaType).toLowerCase()
  1815. const updatedFetchButtonText =
  1816. settings.mediaType === "all"
  1817. ? "Fetch Media"
  1818. : `Fetch ${currentMediaTypeLabel === "gif" ? "GIF" : currentMediaTypeLabel.charAt(0).toUpperCase() + currentMediaTypeLabel.slice(1)}`
  1819.  
  1820. fetchButton.innerHTML = ""
  1821. const updatedFetchIcon = fetchIcon.cloneNode(true)
  1822. fetchButton.appendChild(updatedFetchIcon)
  1823. fetchButton.appendChild(document.createTextNode(updatedFetchButtonText))
  1824. }
  1825. })
  1826.  
  1827. nextBatchButton.addEventListener("click", () => {
  1828. mediaData.currentPage++
  1829. fetchButton.click()
  1830. })
  1831.  
  1832. autoBatchButton.addEventListener("click", () => {
  1833. if (mediaData.autoBatchRunning) {
  1834. return
  1835. }
  1836.  
  1837. mediaData.autoBatchRunning = true
  1838. autoBatchButton.style.display = "none"
  1839. stopBatchButton.style.display = "block"
  1840. nextBatchButton.style.display = "none"
  1841.  
  1842. startAutoBatch()
  1843. })
  1844.  
  1845. stopBatchButton.addEventListener("click", () => {
  1846. createConfirmDialog("Stop auto batch download?", () => {
  1847. mediaData.autoBatchRunning = false
  1848. stopBatchButton.style.display = "none"
  1849. autoBatchButton.style.display = "block"
  1850. if (mediaData.hasMore) {
  1851. nextBatchButton.style.display = "block"
  1852. }
  1853. })
  1854. })
  1855.  
  1856. async function startAutoBatch() {
  1857. while (mediaData.hasMore && mediaData.autoBatchRunning) {
  1858. mediaData.currentPage++
  1859.  
  1860. downloadCurrentButton.disabled = true
  1861. downloadAllButton.disabled = true
  1862.  
  1863. await new Promise((resolve) => {
  1864. const settings = getSettings()
  1865. const cacheKey = `${settings.timelineType}_${settings.mediaType}_${username}_${mediaData.currentPage}_${settings.batchSize}_batch_${settings.batchEnabled}`
  1866. const data = cacheManager.get(cacheKey)
  1867.  
  1868. if (data) {
  1869. processNextBatch(data)
  1870. resolve()
  1871. } else {
  1872. let url
  1873. if (settings.batchEnabled) {
  1874. url = `${getServiceBaseUrl()}/metadata/${settings.timelineType}/${settings.batchSize}/${mediaData.currentPage}/${settings.mediaType}/${username}/${settings.authToken}/${settings.patreonAuth || ""}`
  1875. } else {
  1876. url = `${getServiceBaseUrl()}/metadata/${settings.timelineType}/${settings.mediaType}/${username}/${settings.authToken}/${settings.patreonAuth || ""}`
  1877. }
  1878.  
  1879. fetchData(url)
  1880. .then((data) => {
  1881. if (data && data.timeline && data.timeline.length > 0) {
  1882. cacheManager.set(cacheKey, data, true)
  1883. }
  1884. processNextBatch(data)
  1885. resolve()
  1886. })
  1887. .catch(() => {
  1888. mediaData.autoBatchRunning = false
  1889. stopBatchButton.style.display = "none"
  1890. autoBatchButton.style.display = "block"
  1891.  
  1892. downloadCurrentButton.disabled = false
  1893. downloadAllButton.disabled = false
  1894.  
  1895. if (mediaData.hasMore) {
  1896. nextBatchButton.style.display = "block"
  1897. }
  1898.  
  1899. resolve()
  1900. })
  1901. }
  1902. })
  1903.  
  1904. await new Promise((resolve) => setTimeout(resolve, 1000))
  1905. }
  1906.  
  1907. if (mediaData.autoBatchRunning) {
  1908. mediaData.autoBatchRunning = false
  1909. stopBatchButton.style.display = "none"
  1910. autoBatchButton.style.display = "none"
  1911. }
  1912.  
  1913. downloadCurrentButton.disabled = false
  1914. downloadAllButton.disabled = false
  1915. }
  1916.  
  1917. function processNextBatch(data) {
  1918. if (data.timeline && data.timeline.length > 0) {
  1919. mediaData.mediaItems = data.timeline
  1920. mediaData.hasMore = data.metadata.has_more
  1921.  
  1922. mediaData.allMediaItems = [...mediaData.allMediaItems, ...data.timeline]
  1923.  
  1924. const settings = getSettings()
  1925. const mediaTypeLabel = getMediaTypeLabel(settings.mediaType)
  1926.  
  1927. infoContainer.innerHTML = `
  1928. <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
  1929. <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
  1930. <div style="margin-top: 8px;"><strong>Batch:</strong> ${mediaData.currentPage + 1}</div>
  1931. <div style="margin-top: 8px;"><strong>Total Items:</strong> ${formatNumber(mediaData.allMediaItems.length)}</div>
  1932. `
  1933.  
  1934. if (!mediaData.hasMore) {
  1935. nextBatchButton.style.display = "none"
  1936. autoBatchButton.style.display = "none"
  1937. stopBatchButton.style.display = "none"
  1938. }
  1939. } else {
  1940. mediaData.hasMore = false
  1941. nextBatchButton.style.display = "none"
  1942. autoBatchButton.style.display = "none"
  1943. stopBatchButton.style.display = "none"
  1944. }
  1945. }
  1946.  
  1947. function chunkMediaItems(items) {
  1948. const chunks = []
  1949. for (let i = 0; i < items.length; i += 500) {
  1950. chunks.push(items.slice(i, i + 500))
  1951. }
  1952. return chunks
  1953. }
  1954.  
  1955. async function downloadMedia(downloadAll) {
  1956. if (mediaData.downloading) return
  1957.  
  1958. mediaData.downloading = true
  1959.  
  1960. const settings = getSettings()
  1961. const timestamp = getCurrentTimestamp()
  1962.  
  1963. let itemsToDownload
  1964. if (downloadAll) {
  1965. itemsToDownload = mediaData.allMediaItems
  1966. } else {
  1967. itemsToDownload = mediaData.mediaItems
  1968. }
  1969.  
  1970. mediaData.totalToDownload = itemsToDownload.length
  1971. mediaData.totalDownloaded = 0
  1972.  
  1973. progressText.textContent = `Downloading 0/${formatNumber(mediaData.totalToDownload)}`
  1974. progressFill.style.width = "0%"
  1975. progressContainer.style.display = "block"
  1976.  
  1977. fetchButton.disabled = true
  1978. if (settings.batchEnabled) {
  1979. downloadCurrentButton.disabled = true
  1980. downloadAllButton.disabled = true
  1981. } else {
  1982. downloadButton.disabled = true
  1983. }
  1984. nextBatchButton.disabled = true
  1985. autoBatchButton.disabled = true
  1986. stopBatchButton.disabled = true
  1987.  
  1988. const chunks = chunkMediaItems(itemsToDownload)
  1989.  
  1990. for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
  1991. const chunk = chunks[chunkIndex]
  1992.  
  1993. if (chunk.length === 1 && chunks.length === 1) {
  1994. try {
  1995. const item = chunk[0]
  1996. const formattedDate = formatDate(item.date)
  1997. const baseFilename = `${username}_${formattedDate}_${item.tweet_id}`
  1998. const fileExtension = item.type === "photo" ? "jpg" : "mp4"
  1999. const filename = `${baseFilename}.${fileExtension}`
  2000.  
  2001. progressText.textContent = `Downloading 0/1`
  2002.  
  2003. const blob = await fetchBinary(item.url)
  2004.  
  2005. const downloadLink = document.createElement("a")
  2006. downloadLink.href = URL.createObjectURL(blob)
  2007. downloadLink.download = filename
  2008. document.body.appendChild(downloadLink)
  2009. downloadLink.click()
  2010. document.body.removeChild(downloadLink)
  2011.  
  2012. mediaData.totalDownloaded = 1
  2013. progressText.textContent = `Downloading 1/1`
  2014. progressFill.style.width = "100%"
  2015.  
  2016. continue
  2017. } catch (error) {}
  2018. }
  2019.  
  2020. const zip = new JSZip()
  2021.  
  2022. const hasImages = chunk.some((item) => item.type === "photo")
  2023. const hasVideos = chunk.some((item) => item.type === "video")
  2024. const hasGifs = chunk.some((item) => item.type === "gif")
  2025.  
  2026. let imageFolder, videoFolder, gifFolder
  2027. if (settings.mediaType === "all") {
  2028. if (hasImages) imageFolder = zip.folder("image")
  2029. if (hasVideos) videoFolder = zip.folder("video")
  2030. if (hasGifs) gifFolder = zip.folder("gif")
  2031. }
  2032.  
  2033. const filenameMap = {}
  2034.  
  2035. let completedCount = 0
  2036.  
  2037. for (let i = 0; i < chunk.length; i++) {
  2038. const item = chunk[i]
  2039. try {
  2040. const formattedDate = formatDate(item.date)
  2041. let baseFilename = `${username}_${formattedDate}_${item.tweet_id}`
  2042.  
  2043. if (filenameMap[baseFilename] !== undefined) {
  2044. filenameMap[baseFilename]++
  2045. baseFilename = `${baseFilename}_${String(filenameMap[baseFilename]).padStart(2, "0")}`
  2046. } else {
  2047. filenameMap[baseFilename] = 0
  2048. }
  2049.  
  2050. const fileExtension = item.type === "photo" ? "jpg" : "mp4"
  2051. const filename = `${baseFilename}.${fileExtension}`
  2052.  
  2053. completedCount = mediaData.totalDownloaded + i
  2054. progressText.textContent = `Downloading ${formatNumber(completedCount)}/${formatNumber(mediaData.totalToDownload)}`
  2055. progressFill.style.width = `${(completedCount / mediaData.totalToDownload) * 100}%`
  2056.  
  2057. await new Promise((resolve) => setTimeout(resolve, 0))
  2058.  
  2059. const blob = await fetchBinary(item.url)
  2060.  
  2061. if (settings.mediaType === "all") {
  2062. if (item.type === "photo") {
  2063. imageFolder.file(filename, blob)
  2064. } else if (item.type === "video") {
  2065. videoFolder.file(filename, blob)
  2066. } else if (item.type === "gif") {
  2067. gifFolder.file(filename, blob)
  2068. }
  2069. } else {
  2070. zip.file(filename, blob)
  2071. }
  2072.  
  2073. completedCount = mediaData.totalDownloaded + i + 1
  2074. progressText.textContent = `Downloading ${formatNumber(completedCount)}/${formatNumber(mediaData.totalToDownload)}`
  2075. progressFill.style.width = `${(completedCount / mediaData.totalToDownload) * 100}%`
  2076.  
  2077. await new Promise((resolve) => setTimeout(resolve, 0))
  2078. } catch (error) {
  2079. console.error("Error downloading item:", error)
  2080. }
  2081. }
  2082.  
  2083. mediaData.totalDownloaded += chunk.length
  2084.  
  2085. progressText.textContent = `Creating ZIP file ${chunkIndex + 1}/${chunks.length}...`
  2086.  
  2087. try {
  2088. const zipBlob = await zip.generateAsync({ type: "blob" })
  2089.  
  2090. let zipFilename
  2091. if (chunks.length === 1 && chunk.length < 500) {
  2092. zipFilename = `${username}_${timestamp}.zip`
  2093. } else if (settings.batchEnabled && !downloadAll) {
  2094. zipFilename = `${username}_${timestamp}_part_${String(mediaData.currentPage + 1).padStart(2, "0")}.zip`
  2095. } else {
  2096. zipFilename = `${username}_${timestamp}_part_${String(chunkIndex + 1).padStart(2, "0")}.zip`
  2097. }
  2098.  
  2099. const downloadLink = document.createElement("a")
  2100. downloadLink.href = URL.createObjectURL(zipBlob)
  2101. downloadLink.download = zipFilename
  2102. document.body.appendChild(downloadLink)
  2103. downloadLink.click()
  2104. document.body.removeChild(downloadLink)
  2105. } catch (error) {
  2106. progressText.textContent = `Error creating ZIP ${chunkIndex + 1}: ${error.message}`
  2107. }
  2108. }
  2109.  
  2110. progressText.textContent = "Download complete!"
  2111. progressFill.style.width = "100%"
  2112.  
  2113. setTimeout(() => {
  2114. fetchButton.disabled = false
  2115. if (settings.batchEnabled) {
  2116. downloadCurrentButton.disabled = false
  2117. downloadAllButton.disabled = false
  2118. } else {
  2119. downloadButton.disabled = false
  2120. }
  2121. nextBatchButton.disabled = false
  2122. autoBatchButton.disabled = false
  2123. stopBatchButton.disabled = false
  2124.  
  2125. mediaData.downloading = false
  2126. }, 2000)
  2127. }
  2128.  
  2129. document.body.appendChild(modal)
  2130. }
  2131.  
  2132. function extractUsername() {
  2133. const pathParts = window.location.pathname.split("/").filter((part) => part)
  2134. if (pathParts.length > 0) {
  2135. return pathParts[0]
  2136. }
  2137. return null
  2138. }
  2139.  
  2140. function insertDownloadIcon() {
  2141. const usernameDivs = document.querySelectorAll('[data-testid="UserName"]')
  2142.  
  2143. usernameDivs.forEach((usernameDiv) => {
  2144. if (!usernameDiv.querySelector(".download-icon")) {
  2145. const username = extractUsername()
  2146. if (!username) return
  2147.  
  2148. const verifiedButton = usernameDiv
  2149. .querySelector('[aria-label*="verified"], [aria-label*="Verified"]')
  2150. ?.closest("button")
  2151.  
  2152. const targetElement = verifiedButton
  2153. ? verifiedButton.parentElement
  2154. : usernameDiv.querySelector(".css-1jxf684")?.closest("span")
  2155.  
  2156. if (targetElement) {
  2157. const downloadIcon = createDownloadIcon()
  2158.  
  2159. const iconDiv = document.createElement("div")
  2160. iconDiv.className = "download-icon css-175oi2r r-1awozwy r-xoduu5"
  2161. iconDiv.style.cssText = `
  2162. display: inline-flex;
  2163. align-items: center;
  2164. margin-left: 6px;
  2165. margin-right: 6px;
  2166. gap: 6px;
  2167. padding: 0 3px;
  2168. transition: transform 0.2s, color 0.2s;
  2169. `
  2170. iconDiv.appendChild(downloadIcon)
  2171.  
  2172. iconDiv.addEventListener("mouseenter", () => {
  2173. iconDiv.style.transform = "scale(1.1)"
  2174. iconDiv.style.color = "#0ea5e9"
  2175. })
  2176.  
  2177. iconDiv.addEventListener("mouseleave", () => {
  2178. iconDiv.style.transform = "scale(1)"
  2179. iconDiv.style.color = ""
  2180. })
  2181.  
  2182. iconDiv.addEventListener("click", (e) => {
  2183. e.stopPropagation()
  2184. createModal(username)
  2185. })
  2186.  
  2187. const wrapperDiv = document.createElement("div")
  2188. wrapperDiv.style.cssText = `
  2189. display: inline-flex;
  2190. align-items: center;
  2191. gap: 4px;
  2192. `
  2193. wrapperDiv.appendChild(iconDiv)
  2194. targetElement.parentNode.insertBefore(wrapperDiv, targetElement.nextSibling)
  2195. }
  2196. }
  2197. })
  2198. }
  2199.  
  2200. insertDownloadIcon()
  2201.  
  2202. function checkForUserNameElement() {
  2203. const usernameDivs = document.querySelectorAll('[data-testid="UserName"]')
  2204. if (usernameDivs.length > 0) {
  2205. insertDownloadIcon()
  2206. }
  2207. }
  2208.  
  2209. setInterval(checkForUserNameElement, 100)
  2210.  
  2211. let lastUrl = location.href
  2212. let lastUsername = extractUsername()
  2213.  
  2214. function checkForChanges() {
  2215. const currentUrl = location.href
  2216. const currentUsername = extractUsername()
  2217.  
  2218. if (currentUrl !== lastUrl || currentUsername !== lastUsername) {
  2219. lastUrl = currentUrl
  2220. lastUsername = currentUsername
  2221.  
  2222. document.querySelectorAll(".download-icon").forEach((icon) => {
  2223. const wrapper = icon.closest("div[style*='display: inline-flex']")
  2224. if (wrapper) {
  2225. wrapper.remove()
  2226. }
  2227. })
  2228.  
  2229. setTimeout(insertDownloadIcon, 50)
  2230. }
  2231. }
  2232.  
  2233. const observer = new MutationObserver(() => {
  2234. checkForChanges()
  2235. checkForUserNameElement()
  2236. })
  2237.  
  2238. observer.observe(document.body, {
  2239. childList: true,
  2240. subtree: true,
  2241. attributes: true,
  2242. characterData: true,
  2243. })
  2244.  
  2245. setInterval(checkForChanges, 300)
  2246.  
  2247. const originalPushState = history.pushState
  2248. const originalReplaceState = history.replaceState
  2249.  
  2250. history.pushState = function () {
  2251. originalPushState.apply(this, arguments)
  2252. checkForChanges()
  2253. insertDownloadIcon()
  2254. }
  2255.  
  2256. history.replaceState = function () {
  2257. originalReplaceState.apply(this, arguments)
  2258. checkForChanges()
  2259. insertDownloadIcon()
  2260. }
  2261.  
  2262. window.addEventListener("popstate", () => {
  2263. checkForChanges()
  2264. insertDownloadIcon()
  2265. })
  2266. })()