- // ==UserScript==
- // @name Twitter/X Media Batch Downloader
- // @description Batch download all images and videos from a Twitter/X account, including withheld accounts, in original quality.
- // @icon https://raw.githubusercontent.com/afkarxyz/Twitter-X-Media-Batch-Downloader/refs/heads/main/Archived/icon.svg
- // @version 3.6
- // @author afkarxyz
- // @namespace https://github.com/afkarxyz/userscripts/
- // @supportURL https://github.com/afkarxyz/userscripts/issues
- // @license MIT
- // @match https://twitter.com/*
- // @match https://x.com/*
- // @grant GM_xmlhttpRequest
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_download
- // @connect api.gallerydl.web.id
- // @connect backup.gallerydl.web.id
- // @connect pbs.twimg.com
- // @connect video.twimg.com
- // @require https://cdn.jsdelivr.net/npm/jszip@3.7.1/dist/jszip.min.js
- // ==/UserScript==
-
- ;(() => {
- const defaultSettings = {
- patreonAuth: "",
- authToken: "",
- batchEnabled: false,
- batchSize: 100,
- timelineType: "media",
- mediaType: "all",
- concurrentDownloads: 25,
- cacheDuration: 360,
- apiServer: "default"
- }
-
- const batchSizes = [25, 50, 100, 200]
- const concurrentSizes = [5, 10, 20, 25, 50]
- const cacheDurations = [60, 120, 180, 240, 300, 360, 720, 1440]
-
- function getSettings() {
- return {
- patreonAuth: GM_getValue("patreonAuth", defaultSettings.patreonAuth),
- authToken: GM_getValue("authToken", defaultSettings.authToken),
- batchEnabled: GM_getValue("batchEnabled", defaultSettings.batchEnabled),
- batchSize: GM_getValue("batchSize", defaultSettings.batchSize),
- timelineType: GM_getValue("timelineType", defaultSettings.timelineType),
- mediaType: GM_getValue("mediaType", defaultSettings.mediaType),
- concurrentDownloads: GM_getValue("concurrentDownloads", defaultSettings.concurrentDownloads),
- cacheDuration: GM_getValue("cacheDuration", defaultSettings.cacheDuration),
- apiServer: GM_getValue("apiServer", defaultSettings.apiServer),
- }
- }
-
- function saveSettings(settings) {
- GM_setValue("patreonAuth", settings.patreonAuth)
- GM_setValue("authToken", settings.authToken)
- GM_setValue("batchEnabled", settings.batchEnabled)
- GM_setValue("batchSize", settings.batchSize)
- GM_setValue("timelineType", settings.timelineType)
- GM_setValue("mediaType", settings.mediaType)
- GM_setValue("concurrentDownloads", settings.concurrentDownloads)
- GM_setValue("cacheDuration", settings.cacheDuration)
- GM_setValue("apiServer", settings.apiServer)
- }
-
- function getServiceBaseUrl() {
- const settings = getSettings()
- return settings.apiServer === "default"
- ? "https://api.gallerydl.web.id"
- : "https://backup.gallerydl.web.id"
- }
-
- function formatNumber(num) {
- return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
- }
-
- const cacheManager = {
- set: (key, data, success = true) => {
- if (!success) return;
-
- const settings = getSettings()
- const cacheItem = {
- data: data,
- timestamp: Date.now(),
- expiry: Date.now() + settings.cacheDuration * 60 * 1000,
- }
- localStorage.setItem(`twitter_dl_${key}`, JSON.stringify(cacheItem))
- },
-
- get: (key) => {
- const cacheItem = localStorage.getItem(`twitter_dl_${key}`)
- if (!cacheItem) return null
-
- try {
- const parsed = JSON.parse(cacheItem)
- if (Date.now() > parsed.expiry) {
- localStorage.removeItem(`twitter_dl_${key}`)
- return null
- }
- return parsed.data
- } catch (e) {
- localStorage.removeItem(`twitter_dl_${key}`)
- return null
- }
- },
-
- clear: () => {
- const keysToRemove = []
- for (let i = 0; i < localStorage.length; i++) {
- const key = localStorage.key(i)
- if (key.startsWith("twitter_dl_")) {
- keysToRemove.push(key)
- }
- }
-
- keysToRemove.forEach((key) => localStorage.removeItem(key))
- },
- }
-
- function createDownloadIcon() {
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
- svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
- svg.setAttribute("viewBox", "0 0 512 512")
- svg.setAttribute("width", "18")
- svg.setAttribute("height", "18")
- svg.style.verticalAlign = "middle"
- svg.style.cursor = "pointer"
-
- const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs")
- const style = document.createElementNS("http://www.w3.org/2000/svg", "style")
- style.textContent = ".fa-secondary{opacity:.4}"
- defs.appendChild(style)
- svg.appendChild(defs)
-
- const secondaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
- secondaryPath.setAttribute("class", "fa-secondary")
- secondaryPath.setAttribute("fill", "currentColor")
- secondaryPath.setAttribute(
- "d",
- "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",
- )
- svg.appendChild(secondaryPath)
-
- const primaryPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
- primaryPath.setAttribute("class", "fa-primary")
- primaryPath.setAttribute("fill", "currentColor")
- primaryPath.setAttribute(
- "d",
- "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",
- )
- svg.appendChild(primaryPath)
-
- return svg
- }
-
- function createPatreonIcon() {
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
- svg.setAttribute("xmlns", "http://www.w3.org/2000/svg")
- svg.setAttribute("viewBox", "0 0 512 512")
- svg.setAttribute("width", "18")
- svg.setAttribute("height", "18")
- svg.style.verticalAlign = "middle"
- svg.style.marginRight = "8px"
-
- const path = document.createElementNS("http://www.w3.org/2000/svg", "path")
- path.setAttribute("fill", "currentColor")
- path.setAttribute(
- "d",
- "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",
- )
- svg.appendChild(path)
-
- return svg
- }
-
- function createAuthTokenPopup() {
- const overlay = document.createElement("div")
- overlay.style.cssText = `
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.35);
- backdrop-filter: blur(2.5px);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 10001;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- `
-
- const popup = document.createElement("div")
- popup.style.cssText = `
- background-color: #ffffff;
- color: #0f172a;
- border-radius: 16px;
- width: 300px;
- max-width: 90%;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- overflow: hidden;
- `
-
- const header = document.createElement("div")
- header.style.cssText = `
- padding: 16px;
- border-bottom: 1px solid #e2e8f0;
- font-weight: bold;
- font-size: 16px;
- text-align: center;
- `
- header.textContent = "Authentication Required"
-
- const content = document.createElement("div")
- content.style.cssText = `
- padding: 16px;
- text-align: center;
- `
-
- const authLink = document.createElement("a")
- authLink.href = "https://www.patreon.com/posts/127206894"
- authLink.target = "_blank"
- authLink.textContent = "How to Obtain Auth Token"
- authLink.style.cssText = `
- color: #0ea5e9;
- text-decoration: none;
- cursor: pointer;
- `
- content.appendChild(authLink)
-
- const buttonContainer = document.createElement("div")
- buttonContainer.style.cssText = `
- padding: 16px;
- display: flex;
- justify-content: center;
- border-top: 1px solid #e2e8f0;
- `
-
- const okButton = document.createElement("button")
- okButton.style.cssText = `
- background-color: #0ea5e9;
- color: white;
- border: none;
- border-radius: 9999px;
- padding: 8px 24px;
- font-weight: bold;
- cursor: pointer;
- transition: background-color 0.2s;
- `
- okButton.textContent = "OK"
- okButton.addEventListener("mouseenter", () => {
- okButton.style.backgroundColor = "#0284c7"
- })
- okButton.addEventListener("mouseleave", () => {
- okButton.style.backgroundColor = "#0ea5e9"
- })
- okButton.onclick = () => {
- document.body.removeChild(overlay)
- settingsTab.click()
- }
-
- buttonContainer.appendChild(okButton)
- popup.appendChild(header)
- popup.appendChild(content)
- popup.appendChild(buttonContainer)
- overlay.appendChild(popup)
-
- document.body.appendChild(overlay)
- return overlay
- }
-
- function createPatreonAuthPopup() {
- const overlay = document.createElement("div")
- overlay.style.cssText = `
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.35);
- backdrop-filter: blur(2.5px);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 10001;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- `
-
- const popup = document.createElement("div")
- popup.style.cssText = `
- background-color: #ffffff;
- color: #0f172a;
- border-radius: 16px;
- width: 320px;
- max-width: 90%;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- overflow: hidden;
- `
-
- const header = document.createElement("div")
- header.style.cssText = `
- padding: 16px;
- border-bottom: 1px solid #e2e8f0;
- font-weight: bold;
- font-size: 16px;
- text-align: center;
- `
- header.textContent = "Patreon Authentication Required"
-
- const content = document.createElement("div")
- content.style.cssText = `
- padding: 16px;
- text-align: center;
- `
-
- const message = document.createElement("p")
- message.style.cssText = `
- margin-bottom: 16px;
- line-height: 1.5;
- `
- message.textContent = "Please enter your Patreon authentication code. This feature requires a paid membership to access."
- content.appendChild(message)
-
- const patreonButton = document.createElement("a")
- patreonButton.href = "https://www.patreon.com/exyezed"
- patreonButton.target = "_blank"
- patreonButton.style.cssText = `
- display: flex;
- align-items: center;
- justify-content: center;
- background-color: #f1f5f9;
- color: #0f172a;
- text-decoration: none;
- padding: 10px 16px;
- border-radius: 8px;
- margin-top: 8px;
- transition: background-color 0.2s;
- `
- patreonButton.innerHTML = createPatreonIcon().outerHTML + "Join Patreon Membership"
- patreonButton.addEventListener("mouseenter", () => {
- patreonButton.style.backgroundColor = "#e2e8f0"
- })
- patreonButton.addEventListener("mouseleave", () => {
- patreonButton.style.backgroundColor = "#f1f5f9"
- })
- content.appendChild(patreonButton)
-
- const buttonContainer = document.createElement("div")
- buttonContainer.style.cssText = `
- padding: 16px;
- display: flex;
- justify-content: center;
- border-top: 1px solid #e2e8f0;
- `
-
- const okButton = document.createElement("button")
- okButton.style.cssText = `
- background-color: #0ea5e9;
- color: white;
- border: none;
- border-radius: 9999px;
- padding: 8px 24px;
- font-weight: bold;
- cursor: pointer;
- transition: background-color 0.2s;
- `
- okButton.textContent = "OK"
- okButton.addEventListener("mouseenter", () => {
- okButton.style.backgroundColor = "#0284c7"
- })
- okButton.addEventListener("mouseleave", () => {
- okButton.style.backgroundColor = "#0ea5e9"
- })
- okButton.onclick = () => {
- document.body.removeChild(overlay)
- settingsTab.click()
- }
-
- buttonContainer.appendChild(okButton)
- popup.appendChild(header)
- popup.appendChild(content)
- popup.appendChild(buttonContainer)
- overlay.appendChild(popup)
-
- document.body.appendChild(overlay)
- return overlay
- }
-
- function createConfirmDialog(message, onConfirm, onCancel) {
- const overlay = document.createElement("div")
- overlay.style.cssText = `
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.35);
- backdrop-filter: blur(2.5px);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 10001;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- `
-
- const dialog = document.createElement("div")
- dialog.style.cssText = `
- background-color: #ffffff;
- color: #334155;
- border-radius: 16px;
- width: 300px;
- max-width: 90%;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- overflow: hidden;
- `
-
- const header = document.createElement("div")
- header.style.cssText = `
- padding: 16px;
- border-bottom: 1px solid #e2e8f0;
- font-weight: bold;
- font-size: 16px;
- text-align: center;
- `
- header.textContent = "Confirmation"
-
- const content = document.createElement("div")
- content.style.cssText = `
- padding: 16px;
- text-align: center;
- `
- content.textContent = message
-
- const buttons = document.createElement("div")
- buttons.style.cssText = `
- display: flex;
- padding: 16px;
- border-top: 1px solid #e2e8f0;
- `
-
- const cancelButton = document.createElement("button")
- cancelButton.style.cssText = `
- flex: 1;
- background-color: #94a3b8;
- color: white;
- border: none;
- border-radius: 9999px;
- padding: 8px 16px;
- margin-right: 8px;
- font-weight: bold;
- cursor: pointer;
- text-align: center;
- transition: background-color 0.2s;
- `
- cancelButton.textContent = "No"
- cancelButton.addEventListener("mouseenter", () => {
- cancelButton.style.backgroundColor = "#64748b"
- })
- cancelButton.addEventListener("mouseleave", () => {
- cancelButton.style.backgroundColor = "#94a3b8"
- })
- cancelButton.onclick = () => {
- document.body.removeChild(overlay)
- if (onCancel) onCancel()
- }
-
- const confirmButton = document.createElement("button")
- confirmButton.style.cssText = `
- flex: 1;
- background-color: #ef4444;
- color: white;
- border: none;
- border-radius: 9999px;
- padding: 8px 16px;
- font-weight: bold;
- cursor: pointer;
- text-align: center;
- transition: background-color 0.2s;
- `
- confirmButton.textContent = "Yes"
- confirmButton.addEventListener("mouseenter", () => {
- confirmButton.style.backgroundColor = "#dc2626"
- })
- confirmButton.addEventListener("mouseleave", () => {
- confirmButton.style.backgroundColor = "#ef4444"
- })
- confirmButton.onclick = () => {
- document.body.removeChild(overlay)
- if (onConfirm) onConfirm()
- }
-
- buttons.appendChild(cancelButton)
- buttons.appendChild(confirmButton)
-
- dialog.appendChild(header)
- dialog.appendChild(content)
- dialog.appendChild(buttons)
- overlay.appendChild(dialog)
-
- document.body.appendChild(overlay)
- }
-
- function formatDate(dateString) {
- const date = new Date(dateString)
- const year = date.getFullYear()
- const month = String(date.getMonth() + 1).padStart(2, "0")
- const day = String(date.getDate()).padStart(2, "0")
- const hours = String(date.getHours()).padStart(2, "0")
- const minutes = String(date.getMinutes()).padStart(2, "0")
- const seconds = String(date.getSeconds()).padStart(2, "0")
-
- return `${year}${month}${day}_${hours}${minutes}${seconds}`
- }
-
- function getCurrentTimestamp() {
- const now = new Date()
- const year = now.getFullYear()
- const month = String(now.getMonth() + 1).padStart(2, "0")
- const day = String(now.getDate()).padStart(2, "0")
- const hours = String(now.getHours()).padStart(2, "0")
- const minutes = String(now.getMinutes()).padStart(2, "0")
- const seconds = String(now.getSeconds()).padStart(2, "0")
-
- return `${year}${month}${day}_${hours}${minutes}${seconds}`
- }
-
- function fetchData(url) {
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: url,
- responseType: "json",
- onload: (response) => {
- if (response.status >= 200 && response.status < 300) {
- resolve(response.response)
- } else {
- reject(new Error(`Request failed with status ${response.status}`))
- }
- },
- onerror: (error) => {
- reject(new Error(`Network error: ${error?.message || "Unknown error"}`))
- },
- ontimeout: () => {
- reject(new Error("Request timed out"))
- }
- })
- })
- }
-
- function fetchBinary(url) {
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url: url,
- responseType: "blob",
- onload: (response) => {
- if (response.status >= 200 && response.status < 300) {
- resolve(response.response)
- } else {
- reject(new Error(`Request failed with status ${response.status}`))
- }
- },
- onerror: () => {
- reject(new Error("Network error"))
- },
- })
- })
- }
-
- function getMediaTypeLabel(mediaType) {
- switch (mediaType) {
- case "image":
- return "Image"
- case "video":
- return "Video"
- case "gif":
- return "GIF"
- default:
- return "Media"
- }
- }
-
- function createToggleSwitch(options, selectedValue, onChange) {
- const toggleWrapper = document.createElement("div")
- toggleWrapper.style.cssText = `
- position: relative;
- height: 40px;
- background-color: #f1f5f9;
- border-radius: 8px;
- padding: 0;
- cursor: pointer;
- width: 100%;
- margin-bottom: 16px;
- overflow: hidden;
- `
-
- const toggleSlider = document.createElement("div")
- toggleSlider.style.cssText = `
- position: absolute;
- height: 100%;
- background-color: #0ea5e9;
- border-radius: 8px;
- transition: transform 0.3s ease, width 0.3s ease;
- z-index: 1;
- `
-
- const optionsContainer = document.createElement("div")
- optionsContainer.style.cssText = `
- position: relative;
- display: flex;
- height: 100%;
- z-index: 2;
- width: 100%;
- `
-
- const selectedIndex = options.findIndex((option) => option.value === selectedValue)
- const optionWidth = 100 / options.length
-
- toggleSlider.style.width = `${optionWidth}%`
- toggleSlider.style.transform = `translateX(${selectedIndex * 100}%)`
-
- options.forEach((option, index) => {
- const optionElement = document.createElement("div")
- optionElement.style.cssText = `
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 14px;
- transition: color 0.3s ease;
- color: ${option.value === selectedValue ? "white" : "#64748b"};
- cursor: pointer;
- user-select: none;
- text-align: center;
- height: 100%;
- padding: 0 4px;
- `
-
- if (option.icon) {
- const iconContainer = document.createElement("span")
- iconContainer.style.cssText = `
- display: flex;
- align-items: center;
- justify-content: center;
- margin-right: 6px;
- `
-
- const iconClone = option.icon.cloneNode(true)
-
- const paths = iconClone.querySelectorAll("path")
- paths.forEach((path) => {
- path.setAttribute("fill", option.value === selectedValue ? "white" : "#64748b")
- })
-
- iconContainer.appendChild(iconClone)
- optionElement.appendChild(iconContainer)
- }
-
- const text = document.createElement("span")
- text.textContent = option.label
- text.style.cssText = `
- display: inline-block;
- text-align: center;
- `
- optionElement.appendChild(text)
-
- optionElement.addEventListener("click", (e) => {
- e.stopPropagation()
- onChange(option.value)
-
- toggleSlider.style.transform = `translateX(${index * 100}%)`
-
- optionsContainer.querySelectorAll("div").forEach((opt, i) => {
- opt.style.color = i === index ? "white" : "#64748b"
-
- const optIcon = opt.querySelector("svg")
- if (optIcon) {
- const optPaths = optIcon.querySelectorAll("path")
- optPaths.forEach((path) => {
- path.setAttribute("fill", i === index ? "white" : "#64748b")
- })
- }
- })
- })
-
- optionsContainer.appendChild(optionElement)
- })
-
- toggleWrapper.appendChild(toggleSlider)
- toggleWrapper.appendChild(optionsContainer)
-
- return toggleWrapper
- }
-
- function createMediaTypeIcons() {
- const allIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
- allIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
- allIcon.setAttribute("viewBox", "0 0 640 512")
- allIcon.setAttribute("width", "16")
- allIcon.setAttribute("height", "16")
- allIcon.style.verticalAlign = "middle"
-
- const allPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
- allPath.setAttribute("fill", "#64748b")
- 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")
- allIcon.appendChild(allPath)
-
- const imageIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
- imageIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
- imageIcon.setAttribute("viewBox", "0 0 512 512")
- imageIcon.setAttribute("width", "16")
- imageIcon.setAttribute("height", "16")
- imageIcon.style.verticalAlign = "middle"
-
- const imagePath = document.createElementNS("http://www.w3.org/2000/svg", "path")
- imagePath.setAttribute("fill", "#64748b")
- 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")
- imageIcon.appendChild(imagePath)
-
- const videoIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
- videoIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
- videoIcon.setAttribute("viewBox", "0 0 512 512")
- videoIcon.setAttribute("width", "16")
- videoIcon.setAttribute("height", "16")
- videoIcon.style.verticalAlign = "middle"
-
- const videoPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
- videoPath.setAttribute("fill", "#64748b")
- 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")
- videoIcon.appendChild(videoPath)
-
- const gifIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
- gifIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
- gifIcon.setAttribute("viewBox", "0 0 576 512")
- gifIcon.setAttribute("width", "16")
- gifIcon.setAttribute("height", "16")
- gifIcon.style.verticalAlign = "middle"
-
- const gifPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
- gifPath.setAttribute("fill", "#64748b")
- 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")
- gifIcon.appendChild(gifPath)
-
- return {
- all: allIcon,
- image: imageIcon,
- video: videoIcon,
- gif: gifIcon
- }
- }
-
- function createSlider(options, selectedValue, onChange) {
- const toggleOptions = options.map((option) => {
- let label = option.toString()
- if (typeof option === "number" && option >= 60 && option % 60 === 0) {
- label = `${option / 60}h`
- }
- return { value: option, label: label }
- })
-
- return createToggleSwitch(toggleOptions, selectedValue, onChange)
- }
-
- function createModal(username) {
- const existingModal = document.getElementById("media-downloader-modal")
- if (existingModal) {
- existingModal.remove()
- }
-
- const settings = getSettings()
-
- const modal = document.createElement("div")
- modal.id = "media-downloader-modal"
- modal.style.cssText = `
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background-color: rgba(0, 0, 0, 0.35);
- backdrop-filter: blur(2.5px);
- display: flex;
- justify-content: center;
- align-items: center;
- z-index: 10000;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
- `
-
- const modalContent = document.createElement("div")
- modalContent.style.cssText = `
- background-color: #ffffff;
- color: #334155;
- border-radius: 16px;
- width: 500px;
- max-width: 90%;
- max-height: 90vh;
- overflow-y: auto;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- `
-
- const header = document.createElement("div")
- header.style.cssText = `
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16px;
- border-bottom: 1px solid #e2e8f0;
- `
-
- const title = document.createElement("h2")
- title.innerHTML = `Download ${getMediaTypeLabel(settings.mediaType)}: <span style="color: #0ea5e9">${username}</span>`
- title.style.cssText = `
- margin: 0;
- font-size: 18px;
- font-weight: bold;
- color: #334155;
- `
-
- const closeButton = document.createElement("button")
- closeButton.innerHTML = "×"
- closeButton.style.cssText = `
- background: none;
- border: none;
- color: #0f172a;
- font-size: 24px;
- cursor: pointer;
- padding: 0;
- line-height: 1;
- transition: color 0.2s;
- `
- closeButton.addEventListener("mouseenter", () => {
- closeButton.style.color = "#0ea5e9"
- })
- closeButton.addEventListener("mouseleave", () => {
- closeButton.style.color = "#0f172a"
- })
- closeButton.onclick = () => modal.remove()
-
- header.appendChild(title)
- header.appendChild(closeButton)
-
- const tabs = document.createElement("div")
- tabs.style.cssText = `
- display: flex;
- border-bottom: 1px solid #e2e8f0;
- `
-
- const mainTab = document.createElement("div")
- mainTab.textContent = "Main"
- mainTab.className = "active-tab"
- mainTab.style.cssText = `
- padding: 12px 16px;
- cursor: pointer;
- flex: 1;
- text-align: center;
- border-bottom: 2px solid #0ea5e9;
- `
-
- const settingsTab = document.createElement("div")
- settingsTab.textContent = "Settings"
- settingsTab.style.cssText = `
- padding: 12px 16px;
- cursor: pointer;
- flex: 1;
- text-align: center;
- color: #64748b;
- `
-
- tabs.appendChild(mainTab)
- tabs.appendChild(settingsTab)
-
- const mainContent = document.createElement("div")
- mainContent.style.cssText = `
- padding: 16px;
- `
-
- const settingsContent = document.createElement("div")
- settingsContent.style.cssText = `
- padding: 16px;
- display: none;
- `
-
- const fetchButton = document.createElement("button")
- const mediaTypeLabelText = getMediaTypeLabel(settings.mediaType).toLowerCase()
- const fetchIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
- fetchIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
- fetchIcon.setAttribute("viewBox", "0 0 448 512")
- fetchIcon.setAttribute("width", "16")
- fetchIcon.setAttribute("height", "16")
- fetchIcon.style.marginRight = "8px"
-
- const fetchPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
- fetchPath.setAttribute("fill", "currentColor")
- fetchPath.setAttribute(
- "d",
- "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",
- )
- fetchIcon.appendChild(fetchPath)
-
- const fetchButtonText = document.createElement("span")
- fetchButtonText.textContent =
- settings.mediaType === "all"
- ? "Fetch Media"
- : `Fetch ${mediaTypeLabelText === "gif" ? "GIF" : mediaTypeLabelText.charAt(0).toUpperCase() + mediaTypeLabelText.slice(1)}`
-
- fetchButton.innerHTML = ""
- fetchButton.appendChild(fetchIcon)
- fetchButton.appendChild(fetchButtonText)
-
- fetchButton.style.cssText = `
- background-color: #22c55e;
- color: white;
- border: none;
- border-radius: 9999px;
- padding: 8px 16px;
- font-weight: bold;
- cursor: pointer;
- margin: 16px auto;
- width: 50%;
- display: flex;
- justify-content: center;
- align-items: center;
- text-align: center;
- transition: background-color 0.2s;
- `
- fetchButton.addEventListener("mouseenter", () => {
- fetchButton.style.backgroundColor = "#16a34a"
- })
- fetchButton.addEventListener("mouseleave", () => {
- fetchButton.style.backgroundColor = "#22c55e"
- })
-
- const infoContainer = document.createElement("div")
- infoContainer.style.cssText = `
- background-color: #f1f5f9;
- border-radius: 8px;
- padding: 12px;
- margin-bottom: 16px;
- display: none;
- `
-
- const buttonContainer = document.createElement("div")
- buttonContainer.style.cssText = `
- display: none;
- gap: 8px;
- margin-bottom: 16px;
- `
-
- const downloadCurrentButton = document.createElement("button")
- downloadCurrentButton.textContent = "Download Current Batch"
- downloadCurrentButton.style.cssText = `
- background-color: #0ea5e9;
- color: white;
- border: none;
- border-radius: 9999px;
- padding: 8px 16px;
- font-weight: bold;
- cursor: pointer;
- flex: 1;
- display: block;
- text-align: center;
- transition: background-color 0.2s;
- `
- downloadCurrentButton.addEventListener("mouseenter", () => {
- downloadCurrentButton.style.backgroundColor = "#0284c7"
- })
- downloadCurrentButton.addEventListener("mouseleave", () => {
- downloadCurrentButton.style.backgroundColor = "#0ea5e9"
- })
-
- const downloadAllButton = document.createElement("button")
- downloadAllButton.textContent = "Download All Batches"
- downloadAllButton.style.cssText = `
- background-color: #0ea5e9;
- color: white;
- border: none;
- border-radius: 9999px;
- padding: 8px 16px;
- font-weight: bold;
- cursor: pointer;
- flex: 1;
- display: block;
- text-align: center;
- transition: background-color 0.2s;
- `
- downloadAllButton.addEventListener("mouseenter", () => {
- downloadAllButton.style.backgroundColor = "#0284c7"
- })
- downloadAllButton.addEventListener("mouseleave", () => {
- downloadAllButton.style.backgroundColor = "#0ea5e9"
- })
-
- const downloadButton = document.createElement("button")
- downloadButton.textContent = "Download"
- downloadButton.style.cssText = `
- background-color: #0ea5e9;
- color: white;
- border: none;
- border-radius: 9999px;
- padding: 8px 16px;
- font-weight: bold;
- cursor: pointer;
- width: 50%;
- margin-left: auto;
- margin-right: auto;
- display: block;
- text-align: center;
- transition: background-color 0.2s;
- `
- downloadButton.addEventListener("mouseenter", () => {
- downloadButton.style.backgroundColor = "#0284c7"
- })
- downloadButton.addEventListener("mouseleave", () => {
- downloadButton.style.backgroundColor = "#0ea5e9"
- })
- downloadButton.onclick = () => downloadMedia(false)
-
- if (settings.batchEnabled) {
- buttonContainer.style.display = "none"
- } else {
- buttonContainer.appendChild(downloadButton)
- }
-
- const batchButtonsContainer = document.createElement("div")
- batchButtonsContainer.style.cssText = `
- display: none;
- gap: 8px;
- margin-bottom: 16px;
- `
-
- const nextBatchButton = document.createElement("button")
- nextBatchButton.textContent = "Next Batch"
- nextBatchButton.style.cssText = `
- background-color: #6366f1;
- color: white;
- border: none;
- border-radius: 9999px;
- padding: 8px 16px;
- font-weight: bold;
- cursor: pointer;
- flex: 1;
- display: block;
- text-align: center;
- transition: background-color 0.2s;
- `
- nextBatchButton.addEventListener("mouseenter", () => {
- nextBatchButton.style.backgroundColor = "#4f46e5"
- })
- nextBatchButton.addEventListener("mouseleave", () => {
- nextBatchButton.style.backgroundColor = "#6366f1"
- })
-
- const autoBatchButton = document.createElement("button")
- autoBatchButton.textContent = "Auto Batch"
- autoBatchButton.style.cssText = `
- background-color: #6366f1;
- color: white;
- border: none;
- border-radius: 9999px;
- padding: 8px 16px;
- font-weight: bold;
- cursor: pointer;
- flex: 1;
- display: block;
- text-align: center;
- transition: background-color 0.2s;
- `
- autoBatchButton.addEventListener("mouseenter", () => {
- autoBatchButton.style.backgroundColor = "#4f46e5"
- })
- autoBatchButton.addEventListener("mouseleave", () => {
- autoBatchButton.style.backgroundColor = "#6366f1"
- })
-
- batchButtonsContainer.appendChild(nextBatchButton)
- batchButtonsContainer.appendChild(autoBatchButton)
-
- const stopBatchButton = document.createElement("button")
- stopBatchButton.textContent = "Stop Batch"
- stopBatchButton.style.cssText = `
- background-color: #ef4444;
- color: white;
- border: none;
- border-radius: 9999px;
- padding: 8px 16px;
- font-weight: bold;
- cursor: pointer;
- margin-bottom: 16px;
- width: 100%;
- display: none;
- text-align: center;
- transition: background-color 0.2s;
- `
- stopBatchButton.addEventListener("mouseenter", () => {
- stopBatchButton.style.backgroundColor = "#dc2626"
- })
- stopBatchButton.addEventListener("mouseleave", () => {
- stopBatchButton.style.backgroundColor = "#ef4444"
- })
-
- const progressContainer = document.createElement("div")
- progressContainer.style.cssText = `
- margin-top: 16px;
- display: none;
- `
-
- const progressText = document.createElement("div")
- progressText.style.cssText = `
- margin-bottom: 8px;
- font-size: 14px;
- text-align: center;
- `
- progressText.textContent = "Downloading..."
-
- const progressBar = document.createElement("div")
- progressBar.style.cssText = `
- width: 100%;
- height: 8px;
- background-color: #f1f5f9;
- border-radius: 4px;
- overflow: hidden;
- `
-
- const progressFill = document.createElement("div")
- progressFill.style.cssText = `
- height: 100%;
- width: 0%;
- background-color: #0ea5e9;
- transition: width 0.3s ease-in-out;
- will-change: width;
- `
-
- progressBar.appendChild(progressFill)
- progressContainer.appendChild(progressText)
- progressContainer.appendChild(progressBar)
-
- mainContent.appendChild(fetchButton)
- mainContent.appendChild(infoContainer)
- mainContent.appendChild(buttonContainer)
- mainContent.appendChild(batchButtonsContainer)
- mainContent.appendChild(stopBatchButton)
- mainContent.appendChild(progressContainer)
-
- const settingsForm = document.createElement("div")
- settingsForm.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 16px;
- `
-
- const patreonAuthGroup = document.createElement("div")
- patreonAuthGroup.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 8px;
- `
-
- const patreonAuthLabel = document.createElement("label")
- patreonAuthLabel.textContent = "Patreon Auth:"
- patreonAuthLabel.style.cssText = `
- font-size: 14px;
- font-weight: bold;
- color: #334155;
- `
-
- const patreonAuthInputContainer = document.createElement("div")
- patreonAuthInputContainer.style.cssText = `
- position: relative;
- display: flex;
- align-items: center;
- `
-
- const patreonAuthInput = document.createElement("input")
- patreonAuthInput.type = "text"
- patreonAuthInput.value = settings.patreonAuth
- patreonAuthInput.style.cssText = `
- background-color: #f1f5f9;
- border: 1px solid transparent;
- border-radius: 4px;
- padding: 8px 12px;
- color: #64748b;
- width: 100%;
- box-sizing: border-box;
- transition: all 0.2s ease;
- `
- patreonAuthInput.addEventListener("focus", () => {
- patreonAuthInput.style.border = "1px solid #0ea5e9"
- patreonAuthInput.style.outline = "none"
- })
- patreonAuthInput.addEventListener("blur", () => {
- patreonAuthInput.style.border = "1px solid transparent"
- })
-
- patreonAuthInput.addEventListener("input", () => {
- const newSettings = getSettings()
- newSettings.patreonAuth = patreonAuthInput.value
- saveSettings(newSettings)
- patreonAuthClearButton.style.display = patreonAuthInput.value ? "block" : "none"
- })
-
- const patreonAuthClearButton = document.createElement("button")
- patreonAuthClearButton.innerHTML = "×"
- patreonAuthClearButton.style.cssText = `
- position: absolute;
- right: 8px;
- background: none;
- border: none;
- color: #64748b;
- font-size: 18px;
- cursor: pointer;
- padding: 0;
- display: ${settings.patreonAuth ? "block" : "none"};
- `
- patreonAuthClearButton.addEventListener("click", () => {
- patreonAuthInput.value = ""
- const newSettings = getSettings()
- newSettings.patreonAuth = ""
- saveSettings(newSettings)
- patreonAuthClearButton.style.display = "none"
- })
-
- patreonAuthInputContainer.appendChild(patreonAuthInput)
- patreonAuthInputContainer.appendChild(patreonAuthClearButton)
- patreonAuthGroup.appendChild(patreonAuthLabel)
- patreonAuthGroup.appendChild(patreonAuthInputContainer)
-
- const tokenGroup = document.createElement("div")
- tokenGroup.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 8px;
- `
-
- const tokenLabel = document.createElement("label")
- tokenLabel.textContent = "Auth Token:"
- tokenLabel.style.cssText = `
- font-size: 14px;
- font-weight: bold;
- color: #334155;
- `
-
- const tokenInputContainer = document.createElement("div")
- tokenInputContainer.style.cssText = `
- position: relative;
- display: flex;
- align-items: center;
- `
-
- const tokenInput = document.createElement("input")
- tokenInput.type = "text"
- tokenInput.value = settings.authToken
- tokenInput.style.cssText = `
- background-color: #f1f5f9;
- border: 1px solid transparent;
- border-radius: 4px;
- padding: 8px 12px;
- color: #64748b;
- width: 100%;
- box-sizing: border-box;
- transition: all 0.2s ease;
- `
- tokenInput.addEventListener("focus", () => {
- tokenInput.style.border = "1px solid #0ea5e9"
- tokenInput.style.outline = "none"
- })
- tokenInput.addEventListener("blur", () => {
- tokenInput.style.border = "1px solid transparent"
- })
-
- tokenInput.addEventListener("input", () => {
- const newSettings = getSettings()
- newSettings.authToken = tokenInput.value
- saveSettings(newSettings)
- tokenClearButton.style.display = tokenInput.value ? "block" : "none"
- })
-
- const tokenClearButton = document.createElement("button")
- tokenClearButton.innerHTML = "×"
- tokenClearButton.style.cssText = `
- position: absolute;
- right: 8px;
- background: none;
- border: none;
- color: #64748b;
- font-size: 18px;
- cursor: pointer;
- padding: 0;
- display: ${settings.authToken ? "block" : "none"};
- `
- tokenClearButton.addEventListener("click", () => {
- tokenInput.value = ""
- const newSettings = getSettings()
- newSettings.authToken = ""
- saveSettings(newSettings)
- tokenClearButton.style.display = "none"
- })
-
- tokenInputContainer.appendChild(tokenInput)
- tokenInputContainer.appendChild(tokenClearButton)
- tokenGroup.appendChild(tokenLabel)
- tokenGroup.appendChild(tokenInputContainer)
-
- const apiServerGroup = document.createElement("div")
- apiServerGroup.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 8px;
- `
-
- const apiServerLabel = document.createElement("label")
- apiServerLabel.textContent = "Service:"
- apiServerLabel.style.cssText = `
- font-size: 14px;
- font-weight: bold;
- color: #334155;
- `
-
- const apiServerOptions = [
- { value: "default", label: "Default" },
- { value: "backup", label: "Backup" }
- ]
-
- const apiServerToggle = createToggleSwitch(apiServerOptions, settings.apiServer, (value) => {
- const newSettings = getSettings()
- newSettings.apiServer = value
- saveSettings(newSettings)
- settings.apiServer = value
- })
-
- apiServerGroup.appendChild(apiServerLabel)
- apiServerGroup.appendChild(apiServerToggle)
-
- const batchGroup = document.createElement("div")
- batchGroup.style.cssText = `
- display: flex;
- align-items: center;
- gap: 8px;
- `
-
- const batchLabel = document.createElement("label")
- batchLabel.style.cssText = `
- font-size: 14px;
- font-weight: bold;
- color: #334155;
- flex: 1;
- display: flex;
- align-items: center;
- `
-
- const batchLabelText = document.createElement("span")
- batchLabelText.textContent = "Batch:"
- batchLabelText.style.marginRight = "8px"
- batchLabel.appendChild(batchLabelText)
-
- const batchStatusText = document.createElement("span")
- batchStatusText.textContent = settings.batchEnabled ? "Enabled" : "Disabled"
- batchStatusText.style.cssText = `
- font-size: 14px;
- font-weight: normal;
- color: ${settings.batchEnabled ? "#22c55e" : "#64748b"};
- `
- batchLabel.appendChild(batchStatusText)
-
- const batchToggle = document.createElement("div")
- batchToggle.style.cssText = `
- position: relative;
- width: 50px;
- height: 24px;
- background-color: ${settings.batchEnabled ? "#22c55e" : "#cbd5e1"};
- border-radius: 12px;
- cursor: pointer;
- transition: background-color 0.3s;
- `
-
- const batchToggleHandle = document.createElement("div")
- batchToggleHandle.style.cssText = `
- position: absolute;
- top: 2px;
- left: ${settings.batchEnabled ? "28px" : "2px"};
- width: 20px;
- height: 20px;
- background-color: white;
- border-radius: 50%;
- transition: left 0.3s;
- `
-
- batchToggle.appendChild(batchToggleHandle)
- batchToggle.addEventListener("click", () => {
- const newSettings = getSettings()
- newSettings.batchEnabled = !newSettings.batchEnabled
- saveSettings(newSettings)
-
- batchToggle.style.backgroundColor = newSettings.batchEnabled ? "#22c55e" : "#cbd5e1"
- batchToggleHandle.style.left = newSettings.batchEnabled ? "28px" : "2px"
-
- batchStatusText.textContent = newSettings.batchEnabled ? "Enabled" : "Disabled"
- batchStatusText.style.color = newSettings.batchEnabled ? "#22c55e" : "#64748b"
-
- batchSizeGroup.style.display = newSettings.batchEnabled ? "flex" : "none"
-
- if (!newSettings.batchEnabled) {
- buttonContainer.innerHTML = ""
- buttonContainer.appendChild(downloadButton)
- buttonContainer.style.display = infoContainer.style.display === "block" ? "block" : "none"
- } else {
- buttonContainer.innerHTML = ""
- buttonContainer.style.display = "none"
- }
- })
-
- batchGroup.appendChild(batchLabel)
- batchGroup.appendChild(batchToggle)
-
- const batchSizeGroup = document.createElement("div")
- batchSizeGroup.style.cssText = `
- display: ${settings.batchEnabled ? "flex" : "none"};
- flex-direction: column;
- gap: 8px;
- `
-
- const batchSizeLabel = document.createElement("label")
- batchSizeLabel.textContent = "Batch Size:"
- batchSizeLabel.style.cssText = `
- font-size: 14px;
- font-weight: bold;
- color: #334155;
- `
-
- const batchSizeToggle = createSlider(batchSizes, settings.batchSize, (value) => {
- const newSettings = getSettings()
- newSettings.batchSize = value
- saveSettings(newSettings)
- settings.batchSize = value
- })
-
- batchSizeGroup.appendChild(batchSizeLabel)
- batchSizeGroup.appendChild(batchSizeToggle)
-
- const timelineTypeGroup = document.createElement("div")
- timelineTypeGroup.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 8px;
- `
-
- const timelineTypeLabel = document.createElement("label")
- timelineTypeLabel.textContent = "Timeline Type:"
- timelineTypeLabel.style.cssText = `
- font-size: 14px;
- font-weight: bold;
- color: #334155;
- `
-
- const timelineTypeOptions = [
- { value: "media", label: "Media" },
- { value: "timeline", label: "Post" },
- { value: "tweets", label: "Tweets" },
- { value: "with_replies", label: "Replies" },
- ]
-
- const timelineTypeToggle = createToggleSwitch(timelineTypeOptions, settings.timelineType, (value) => {
- const newSettings = getSettings()
- newSettings.timelineType = value
- saveSettings(newSettings)
- settings.timelineType = value
- })
-
- timelineTypeGroup.appendChild(timelineTypeLabel)
- timelineTypeGroup.appendChild(timelineTypeToggle)
-
- const mediaTypeGroup = document.createElement("div")
- mediaTypeGroup.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 8px;
- `
-
- const mediaTypeLabel = document.createElement("label")
- mediaTypeLabel.textContent = "Media Type:"
- mediaTypeLabel.style.cssText = `
- font-size: 14px;
- font-weight: bold;
- color: #334155;
- `
-
- const mediaTypeIcons = createMediaTypeIcons()
- const mediaTypeOptions = [
- { value: "all", label: "All", icon: mediaTypeIcons.all },
- { value: "image", label: "Image", icon: mediaTypeIcons.image },
- { value: "video", label: "Video", icon: mediaTypeIcons.video },
- { value: "gif", label: "GIF", icon: mediaTypeIcons.gif },
- ]
-
- const mediaTypeToggle = createToggleSwitch(mediaTypeOptions, settings.mediaType, (value) => {
- const newSettings = getSettings()
- newSettings.mediaType = value
- saveSettings(newSettings)
- settings.mediaType = value
-
- const newMediaTypeLabel = getMediaTypeLabel(value).toLowerCase()
- const newFetchButtonText =
- value === "all"
- ? "Fetch Media"
- : `Fetch ${newMediaTypeLabel === "gif" ? "GIF" : newMediaTypeLabel.charAt(0).toUpperCase() + newMediaTypeLabel.slice(1)}`
-
- fetchButton.innerHTML = ""
- const newFetchIcon = fetchIcon.cloneNode(true)
- fetchButton.appendChild(newFetchIcon)
- fetchButton.appendChild(document.createTextNode(newFetchButtonText))
-
- title.innerHTML = `Download ${getMediaTypeLabel(value)}: <span style="color: #0ea5e9">${username}</span>`
- })
-
- mediaTypeGroup.appendChild(mediaTypeLabel)
- mediaTypeGroup.appendChild(mediaTypeToggle)
-
- const concurrentGroup = document.createElement("div")
- concurrentGroup.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 8px;
- `
-
- const concurrentLabel = document.createElement("label")
- concurrentLabel.textContent = "Batch Download Items:"
- concurrentLabel.style.cssText = `
- font-size: 14px;
- font-weight: bold;
- color: #334155;
- `
-
- const concurrentToggle = createSlider(concurrentSizes, settings.concurrentDownloads, (value) => {
- const newSettings = getSettings()
- newSettings.concurrentDownloads = value
- saveSettings(newSettings)
- settings.concurrentDownloads = value
- })
-
- concurrentGroup.appendChild(concurrentLabel)
- concurrentGroup.appendChild(concurrentToggle)
-
- const cacheDurationGroup = document.createElement("div")
- cacheDurationGroup.style.cssText = `
- display: flex;
- flex-direction: column;
- gap: 8px;
- `
-
- const cacheDurationLabel = document.createElement("label")
- cacheDurationLabel.textContent = "Cache Duration:"
- cacheDurationLabel.style.cssText = `
- font-size: 14px;
- font-weight: bold;
- color: #334155;
- `
-
- const cacheDurationOptions = cacheDurations.map((duration) => {
- let label = duration.toString() + "m"
- if (duration >= 60 && duration % 60 === 0) {
- label = `${duration / 60}h`
- }
- return { value: duration, label: label }
- })
-
- const cacheDurationToggle = createToggleSwitch(cacheDurationOptions, settings.cacheDuration, (value) => {
- const newSettings = getSettings()
- newSettings.cacheDuration = value
- saveSettings(newSettings)
- settings.cacheDuration = value
- })
-
- cacheDurationGroup.appendChild(cacheDurationLabel)
- cacheDurationGroup.appendChild(cacheDurationToggle)
-
- const clearCacheButton = document.createElement("button")
- const trashIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg")
- trashIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg")
- trashIcon.setAttribute("viewBox", "0 0 448 512")
- trashIcon.setAttribute("width", "16")
- trashIcon.setAttribute("height", "16")
- trashIcon.style.marginRight = "8px"
-
- const trashPath = document.createElementNS("http://www.w3.org/2000/svg", "path")
- trashPath.setAttribute("fill", "currentColor")
- trashPath.setAttribute(
- "d",
- "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",
- )
- trashIcon.appendChild(trashPath)
-
- clearCacheButton.appendChild(trashIcon)
- clearCacheButton.appendChild(document.createTextNode("Clear Cache"))
- clearCacheButton.style.cssText = `
- background-color: #ef4444;
- color: white;
- border: none;
- border-radius: 9999px;
- padding: 8px 16px;
- font-weight: bold;
- cursor: pointer;
- margin-top: 16px;
- width: 50%;
- margin-left: auto;
- margin-right: auto;
- display: flex;
- align-items: center;
- justify-content: center;
- text-align: center;
- transition: background-color 0.2s;
- `
- clearCacheButton.addEventListener("mouseenter", () => {
- clearCacheButton.style.backgroundColor = "#dc2626"
- })
- clearCacheButton.addEventListener("mouseleave", () => {
- clearCacheButton.style.backgroundColor = "#ef4444"
- })
-
- clearCacheButton.addEventListener("click", () => {
- createConfirmDialog("Are you sure about clearing the cache?", () => {
- cacheManager.clear()
-
- const notification = document.createElement("div")
- notification.style.cssText = `
- position: fixed;
- bottom: 20px;
- left: 50%;
- transform: translateX(-50%);
- background-color: #0ea5e9;
- color: white;
- padding: 12px 24px;
- border-radius: 9999px;
- font-weight: bold;
- z-index: 10002;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- text-align: center;
- `
- notification.textContent = "Cache cleared successfully"
- document.body.appendChild(notification)
-
- setTimeout(() => {
- document.body.removeChild(notification)
- }, 3000)
- })
- })
-
- const patreonLink = document.createElement("a")
- patreonLink.href = "https://www.patreon.com/exyezed"
- patreonLink.target = "_blank"
- patreonLink.style.cssText = `
- display: flex;
- align-items: center;
- justify-content: center;
- color: #64748b;
- text-decoration: none;
- margin-top: 16px;
- padding: 8px;
- border-radius: 8px;
- transition: background-color 0.2s, color 0.2s;
- `
- patreonLink.innerHTML = createPatreonIcon().outerHTML + "Patreon Authentication"
-
- patreonLink.addEventListener("mouseenter", () => {
- patreonLink.style.backgroundColor = "#f1f5f9"
- patreonLink.style.color = "#0ea5e9"
- })
-
- patreonLink.addEventListener("mouseleave", () => {
- patreonLink.style.backgroundColor = "transparent"
- patreonLink.style.color = "#64748b"
- })
-
- settingsForm.appendChild(patreonAuthGroup)
- settingsForm.appendChild(tokenGroup)
- settingsForm.appendChild(apiServerGroup)
- settingsForm.appendChild(batchGroup)
- settingsForm.appendChild(batchSizeGroup)
- settingsForm.appendChild(timelineTypeGroup)
- settingsForm.appendChild(mediaTypeGroup)
- settingsForm.appendChild(concurrentGroup)
- settingsForm.appendChild(cacheDurationGroup)
- settingsForm.appendChild(clearCacheButton)
- settingsForm.appendChild(patreonLink)
-
- settingsContent.appendChild(settingsForm)
-
- mainTab.addEventListener("click", () => {
- mainTab.style.borderBottom = "2px solid #0ea5e9"
- mainTab.style.color = "#0f172a"
- settingsTab.style.borderBottom = "none"
- settingsTab.style.color = "#64748b"
- mainContent.style.display = "block"
- settingsContent.style.display = "none"
- })
-
- settingsTab.addEventListener("click", () => {
- settingsTab.style.borderBottom = "2px solid #0ea5e9"
- settingsTab.style.color = "#0f172a"
- mainTab.style.borderBottom = "none"
- mainTab.style.color = "#64748b"
- settingsContent.style.display = "block"
- mainContent.style.display = "none"
- })
-
- modalContent.appendChild(header)
- modalContent.appendChild(tabs)
- modalContent.appendChild(mainContent)
- modalContent.appendChild(settingsContent)
- modal.appendChild(modalContent)
-
- const mediaData = {
- username: username,
- currentPage: 0,
- mediaItems: [],
- allMediaItems: [],
- hasMore: false,
- downloading: false,
- totalDownloaded: 0,
- totalToDownload: 0,
- totalItems: 0,
- autoBatchRunning: false,
- }
-
- fetchButton.addEventListener("click", async () => {
- const settings = getSettings()
-
- if (!settings.authToken) {
- createAuthTokenPopup()
- return
- }
-
- if (!settings.patreonAuth) {
- createPatreonAuthPopup()
- return
- }
-
- infoContainer.style.display = "none"
- buttonContainer.style.display = "none"
- nextBatchButton.style.display = "none"
- autoBatchButton.style.display = "none"
- stopBatchButton.style.display = "none"
- progressContainer.style.display = "none"
- fetchButton.disabled = true
- fetchButton.innerHTML = ""
- fetchButton.appendChild(document.createTextNode("Fetching..."))
-
- try {
- const cacheKey = `${settings.timelineType}_${settings.mediaType}_${username}_${mediaData.currentPage}_${settings.batchSize}_batch_${settings.batchEnabled}`
- let data = cacheManager.get(cacheKey)
-
- if (!data) {
- let url
- if (settings.batchEnabled) {
- url = `${getServiceBaseUrl()}/metadata/${settings.timelineType}/${settings.batchSize}/${mediaData.currentPage}/${settings.mediaType}/${username}/${settings.authToken}/${settings.patreonAuth || ""}`
- } else {
- url = `${getServiceBaseUrl()}/metadata/${settings.timelineType}/${settings.mediaType}/${username}/${settings.authToken}/${settings.patreonAuth || ""}`
- }
-
- data = await fetchData(url)
- if (data && data.timeline && data.timeline.length > 0) {
- cacheManager.set(cacheKey, data, true)
- }
- }
-
- if (data.timeline && data.timeline.length > 0) {
- mediaData.mediaItems = data.timeline
- mediaData.hasMore = data.metadata.has_more
- mediaData.totalItems = data.total_urls
-
- if (mediaData.currentPage === 0) {
- mediaData.allMediaItems = [...data.timeline]
- } else {
- mediaData.allMediaItems = [...mediaData.allMediaItems, ...data.timeline]
- }
-
- const mediaTypeLabel = getMediaTypeLabel(settings.mediaType)
-
- if (settings.batchEnabled) {
- infoContainer.innerHTML = `
- <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
- <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
- <div style="margin-top: 8px;"><strong>Batch:</strong> ${mediaData.currentPage + 1}</div>
- <div style="margin-top: 8px;"><strong>Total Items:</strong> ${formatNumber(mediaData.allMediaItems.length)}</div>
- `
- } else {
- const currentPart = Math.floor(mediaData.allMediaItems.length / 500) + 1
-
- if (currentPart === 1) {
- infoContainer.innerHTML = `
- <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
- <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
- `
- } else {
- infoContainer.innerHTML = `
- <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
- <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
- <div style="margin-top: 8px;"><strong>Part:</strong> ${currentPart}</div>
- `
- }
- }
-
- infoContainer.style.display = "block"
-
- if (settings.batchEnabled) {
- buttonContainer.innerHTML = ""
- buttonContainer.appendChild(downloadCurrentButton)
- buttonContainer.appendChild(downloadAllButton)
- buttonContainer.style.display = "flex"
- } else {
- buttonContainer.innerHTML = ""
- buttonContainer.appendChild(downloadButton)
- buttonContainer.style.display = "block"
- }
-
- if (settings.batchEnabled && mediaData.hasMore) {
- batchButtonsContainer.style.display = "flex"
- nextBatchButton.style.display = "block"
- autoBatchButton.style.display = "block"
- }
-
- downloadCurrentButton.onclick = () => downloadMedia(false)
- downloadAllButton.onclick = () => downloadMedia(true)
-
- fetchButton.disabled = false
- const currentMediaTypeLabel = getMediaTypeLabel(settings.mediaType).toLowerCase()
- const updatedFetchButtonText =
- settings.mediaType === "all"
- ? "Fetch Media"
- : `Fetch ${currentMediaTypeLabel === "gif" ? "GIF" : currentMediaTypeLabel.charAt(0).toUpperCase() + currentMediaTypeLabel.slice(1)}`
-
- fetchButton.innerHTML = ""
- const updatedFetchIcon = fetchIcon.cloneNode(true)
- fetchButton.appendChild(updatedFetchIcon)
- fetchButton.appendChild(document.createTextNode(updatedFetchButtonText))
- } else {
- infoContainer.innerHTML = '<div style="color: #ef4444;">No media found, invalid token, or invalid Patreon authentication.</div>'
- infoContainer.style.display = "block"
- fetchButton.disabled = false
- const currentMediaTypeLabel = getMediaTypeLabel(settings.mediaType).toLowerCase()
- const updatedFetchButtonText =
- settings.mediaType === "all"
- ? "Fetch Media"
- : `Fetch ${currentMediaTypeLabel === "gif" ? "GIF" : currentMediaTypeLabel.charAt(0).toUpperCase() + currentMediaTypeLabel.slice(1)}`
-
- fetchButton.innerHTML = ""
- const updatedFetchIcon = fetchIcon.cloneNode(true)
- fetchButton.appendChild(updatedFetchIcon)
- fetchButton.appendChild(document.createTextNode(updatedFetchButtonText))
- }
- } catch (error) {
- infoContainer.innerHTML = `<div style="color: #ef4444;">Error: ${error.message}</div>`
- infoContainer.style.display = "block"
- fetchButton.disabled = false
- const currentMediaTypeLabel = getMediaTypeLabel(settings.mediaType).toLowerCase()
- const updatedFetchButtonText =
- settings.mediaType === "all"
- ? "Fetch Media"
- : `Fetch ${currentMediaTypeLabel === "gif" ? "GIF" : currentMediaTypeLabel.charAt(0).toUpperCase() + currentMediaTypeLabel.slice(1)}`
-
- fetchButton.innerHTML = ""
- const updatedFetchIcon = fetchIcon.cloneNode(true)
- fetchButton.appendChild(updatedFetchIcon)
- fetchButton.appendChild(document.createTextNode(updatedFetchButtonText))
- }
- })
-
- nextBatchButton.addEventListener("click", () => {
- mediaData.currentPage++
- fetchButton.click()
- })
-
- autoBatchButton.addEventListener("click", () => {
- if (mediaData.autoBatchRunning) {
- return
- }
-
- mediaData.autoBatchRunning = true
- autoBatchButton.style.display = "none"
- stopBatchButton.style.display = "block"
- nextBatchButton.style.display = "none"
-
- startAutoBatch()
- })
-
- stopBatchButton.addEventListener("click", () => {
- createConfirmDialog("Stop auto batch download?", () => {
- mediaData.autoBatchRunning = false
- stopBatchButton.style.display = "none"
- autoBatchButton.style.display = "block"
- if (mediaData.hasMore) {
- nextBatchButton.style.display = "block"
- }
- })
- })
-
- async function startAutoBatch() {
- while (mediaData.hasMore && mediaData.autoBatchRunning) {
- mediaData.currentPage++
-
- downloadCurrentButton.disabled = true
- downloadAllButton.disabled = true
-
- await new Promise((resolve) => {
- const settings = getSettings()
- const cacheKey = `${settings.timelineType}_${settings.mediaType}_${username}_${mediaData.currentPage}_${settings.batchSize}_batch_${settings.batchEnabled}`
- const data = cacheManager.get(cacheKey)
-
- if (data) {
- processNextBatch(data)
- resolve()
- } else {
- let url
- if (settings.batchEnabled) {
- url = `${getServiceBaseUrl()}/metadata/${settings.timelineType}/${settings.batchSize}/${mediaData.currentPage}/${settings.mediaType}/${username}/${settings.authToken}/${settings.patreonAuth || ""}`
- } else {
- url = `${getServiceBaseUrl()}/metadata/${settings.timelineType}/${settings.mediaType}/${username}/${settings.authToken}/${settings.patreonAuth || ""}`
- }
-
- fetchData(url)
- .then((data) => {
- if (data && data.timeline && data.timeline.length > 0) {
- cacheManager.set(cacheKey, data, true)
- }
- processNextBatch(data)
- resolve()
- })
- .catch(() => {
- mediaData.autoBatchRunning = false
- stopBatchButton.style.display = "none"
- autoBatchButton.style.display = "block"
-
- downloadCurrentButton.disabled = false
- downloadAllButton.disabled = false
-
- if (mediaData.hasMore) {
- nextBatchButton.style.display = "block"
- }
-
- resolve()
- })
- }
- })
-
- await new Promise((resolve) => setTimeout(resolve, 1000))
- }
-
- if (mediaData.autoBatchRunning) {
- mediaData.autoBatchRunning = false
- stopBatchButton.style.display = "none"
- autoBatchButton.style.display = "none"
- }
-
- downloadCurrentButton.disabled = false
- downloadAllButton.disabled = false
- }
-
- function processNextBatch(data) {
- if (data.timeline && data.timeline.length > 0) {
- mediaData.mediaItems = data.timeline
- mediaData.hasMore = data.metadata.has_more
-
- mediaData.allMediaItems = [...mediaData.allMediaItems, ...data.timeline]
-
- const settings = getSettings()
- const mediaTypeLabel = getMediaTypeLabel(settings.mediaType)
-
- infoContainer.innerHTML = `
- <div style="margin-bottom: 8px;"><strong>Account:</strong> ${data.account_info.name}</div>
- <div style="margin-bottom: 8px;"><strong>${mediaTypeLabel} Found:</strong> ${formatNumber(data.total_urls)}</div>
- <div style="margin-top: 8px;"><strong>Batch:</strong> ${mediaData.currentPage + 1}</div>
- <div style="margin-top: 8px;"><strong>Total Items:</strong> ${formatNumber(mediaData.allMediaItems.length)}</div>
- `
-
- if (!mediaData.hasMore) {
- nextBatchButton.style.display = "none"
- autoBatchButton.style.display = "none"
- stopBatchButton.style.display = "none"
- }
- } else {
- mediaData.hasMore = false
- nextBatchButton.style.display = "none"
- autoBatchButton.style.display = "none"
- stopBatchButton.style.display = "none"
- }
- }
-
- function chunkMediaItems(items) {
- const chunks = []
- for (let i = 0; i < items.length; i += 500) {
- chunks.push(items.slice(i, i + 500))
- }
- return chunks
- }
-
- async function downloadMedia(downloadAll) {
- if (mediaData.downloading) return
-
- mediaData.downloading = true
-
- const settings = getSettings()
- const timestamp = getCurrentTimestamp()
-
- let itemsToDownload
- if (downloadAll) {
- itemsToDownload = mediaData.allMediaItems
- } else {
- itemsToDownload = mediaData.mediaItems
- }
-
- mediaData.totalToDownload = itemsToDownload.length
- mediaData.totalDownloaded = 0
-
- progressText.textContent = `Downloading 0/${formatNumber(mediaData.totalToDownload)}`
- progressFill.style.width = "0%"
- progressContainer.style.display = "block"
-
- fetchButton.disabled = true
- if (settings.batchEnabled) {
- downloadCurrentButton.disabled = true
- downloadAllButton.disabled = true
- } else {
- downloadButton.disabled = true
- }
- nextBatchButton.disabled = true
- autoBatchButton.disabled = true
- stopBatchButton.disabled = true
-
- const chunks = chunkMediaItems(itemsToDownload)
-
- for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
- const chunk = chunks[chunkIndex]
-
- if (chunk.length === 1 && chunks.length === 1) {
- try {
- const item = chunk[0]
- const formattedDate = formatDate(item.date)
- const baseFilename = `${username}_${formattedDate}_${item.tweet_id}`
- const fileExtension = item.type === "photo" ? "jpg" : "mp4"
- const filename = `${baseFilename}.${fileExtension}`
-
- progressText.textContent = `Downloading 0/1`
-
- const blob = await fetchBinary(item.url)
-
- const downloadLink = document.createElement("a")
- downloadLink.href = URL.createObjectURL(blob)
- downloadLink.download = filename
- document.body.appendChild(downloadLink)
- downloadLink.click()
- document.body.removeChild(downloadLink)
-
- mediaData.totalDownloaded = 1
- progressText.textContent = `Downloading 1/1`
- progressFill.style.width = "100%"
-
- continue
- } catch (error) {}
- }
-
- const zip = new JSZip()
-
- const hasImages = chunk.some((item) => item.type === "photo")
- const hasVideos = chunk.some((item) => item.type === "video")
- const hasGifs = chunk.some((item) => item.type === "gif")
-
- let imageFolder, videoFolder, gifFolder
- if (settings.mediaType === "all") {
- if (hasImages) imageFolder = zip.folder("image")
- if (hasVideos) videoFolder = zip.folder("video")
- if (hasGifs) gifFolder = zip.folder("gif")
- }
-
- const filenameMap = {}
-
- let completedCount = 0
-
- for (let i = 0; i < chunk.length; i++) {
- const item = chunk[i]
- try {
- const formattedDate = formatDate(item.date)
- let baseFilename = `${username}_${formattedDate}_${item.tweet_id}`
-
- if (filenameMap[baseFilename] !== undefined) {
- filenameMap[baseFilename]++
- baseFilename = `${baseFilename}_${String(filenameMap[baseFilename]).padStart(2, "0")}`
- } else {
- filenameMap[baseFilename] = 0
- }
-
- const fileExtension = item.type === "photo" ? "jpg" : "mp4"
- const filename = `${baseFilename}.${fileExtension}`
-
- completedCount = mediaData.totalDownloaded + i
- progressText.textContent = `Downloading ${formatNumber(completedCount)}/${formatNumber(mediaData.totalToDownload)}`
- progressFill.style.width = `${(completedCount / mediaData.totalToDownload) * 100}%`
-
- await new Promise((resolve) => setTimeout(resolve, 0))
-
- const blob = await fetchBinary(item.url)
-
- if (settings.mediaType === "all") {
- if (item.type === "photo") {
- imageFolder.file(filename, blob)
- } else if (item.type === "video") {
- videoFolder.file(filename, blob)
- } else if (item.type === "gif") {
- gifFolder.file(filename, blob)
- }
- } else {
- zip.file(filename, blob)
- }
-
- completedCount = mediaData.totalDownloaded + i + 1
- progressText.textContent = `Downloading ${formatNumber(completedCount)}/${formatNumber(mediaData.totalToDownload)}`
- progressFill.style.width = `${(completedCount / mediaData.totalToDownload) * 100}%`
-
- await new Promise((resolve) => setTimeout(resolve, 0))
- } catch (error) {
- console.error("Error downloading item:", error)
- }
- }
-
- mediaData.totalDownloaded += chunk.length
-
- progressText.textContent = `Creating ZIP file ${chunkIndex + 1}/${chunks.length}...`
-
- try {
- const zipBlob = await zip.generateAsync({ type: "blob" })
-
- let zipFilename
- if (chunks.length === 1 && chunk.length < 500) {
- zipFilename = `${username}_${timestamp}.zip`
- } else if (settings.batchEnabled && !downloadAll) {
- zipFilename = `${username}_${timestamp}_part_${String(mediaData.currentPage + 1).padStart(2, "0")}.zip`
- } else {
- zipFilename = `${username}_${timestamp}_part_${String(chunkIndex + 1).padStart(2, "0")}.zip`
- }
-
- const downloadLink = document.createElement("a")
- downloadLink.href = URL.createObjectURL(zipBlob)
- downloadLink.download = zipFilename
- document.body.appendChild(downloadLink)
- downloadLink.click()
- document.body.removeChild(downloadLink)
- } catch (error) {
- progressText.textContent = `Error creating ZIP ${chunkIndex + 1}: ${error.message}`
- }
- }
-
- progressText.textContent = "Download complete!"
- progressFill.style.width = "100%"
-
- setTimeout(() => {
- fetchButton.disabled = false
- if (settings.batchEnabled) {
- downloadCurrentButton.disabled = false
- downloadAllButton.disabled = false
- } else {
- downloadButton.disabled = false
- }
- nextBatchButton.disabled = false
- autoBatchButton.disabled = false
- stopBatchButton.disabled = false
-
- mediaData.downloading = false
- }, 2000)
- }
-
- document.body.appendChild(modal)
- }
-
- function extractUsername() {
- const pathParts = window.location.pathname.split("/").filter((part) => part)
- if (pathParts.length > 0) {
- return pathParts[0]
- }
- return null
- }
-
- function insertDownloadIcon() {
- const usernameDivs = document.querySelectorAll('[data-testid="UserName"]')
-
- usernameDivs.forEach((usernameDiv) => {
- if (!usernameDiv.querySelector(".download-icon")) {
- const username = extractUsername()
- if (!username) return
-
- const verifiedButton = usernameDiv
- .querySelector('[aria-label*="verified"], [aria-label*="Verified"]')
- ?.closest("button")
-
- const targetElement = verifiedButton
- ? verifiedButton.parentElement
- : usernameDiv.querySelector(".css-1jxf684")?.closest("span")
-
- if (targetElement) {
- const downloadIcon = createDownloadIcon()
-
- const iconDiv = document.createElement("div")
- iconDiv.className = "download-icon css-175oi2r r-1awozwy r-xoduu5"
- iconDiv.style.cssText = `
- display: inline-flex;
- align-items: center;
- margin-left: 6px;
- margin-right: 6px;
- gap: 6px;
- padding: 0 3px;
- transition: transform 0.2s, color 0.2s;
- `
- iconDiv.appendChild(downloadIcon)
-
- iconDiv.addEventListener("mouseenter", () => {
- iconDiv.style.transform = "scale(1.1)"
- iconDiv.style.color = "#0ea5e9"
- })
-
- iconDiv.addEventListener("mouseleave", () => {
- iconDiv.style.transform = "scale(1)"
- iconDiv.style.color = ""
- })
-
- iconDiv.addEventListener("click", (e) => {
- e.stopPropagation()
- createModal(username)
- })
-
- const wrapperDiv = document.createElement("div")
- wrapperDiv.style.cssText = `
- display: inline-flex;
- align-items: center;
- gap: 4px;
- `
- wrapperDiv.appendChild(iconDiv)
- targetElement.parentNode.insertBefore(wrapperDiv, targetElement.nextSibling)
- }
- }
- })
- }
-
- insertDownloadIcon()
-
- function checkForUserNameElement() {
- const usernameDivs = document.querySelectorAll('[data-testid="UserName"]')
- if (usernameDivs.length > 0) {
- insertDownloadIcon()
- }
- }
-
- setInterval(checkForUserNameElement, 100)
-
- let lastUrl = location.href
- let lastUsername = extractUsername()
-
- function checkForChanges() {
- const currentUrl = location.href
- const currentUsername = extractUsername()
-
- if (currentUrl !== lastUrl || currentUsername !== lastUsername) {
- lastUrl = currentUrl
- lastUsername = currentUsername
-
- document.querySelectorAll(".download-icon").forEach((icon) => {
- const wrapper = icon.closest("div[style*='display: inline-flex']")
- if (wrapper) {
- wrapper.remove()
- }
- })
-
- setTimeout(insertDownloadIcon, 50)
- }
- }
-
- const observer = new MutationObserver(() => {
- checkForChanges()
- checkForUserNameElement()
- })
-
- observer.observe(document.body, {
- childList: true,
- subtree: true,
- attributes: true,
- characterData: true,
- })
-
- setInterval(checkForChanges, 300)
-
- const originalPushState = history.pushState
- const originalReplaceState = history.replaceState
-
- history.pushState = function () {
- originalPushState.apply(this, arguments)
- checkForChanges()
- insertDownloadIcon()
- }
-
- history.replaceState = function () {
- originalReplaceState.apply(this, arguments)
- checkForChanges()
- insertDownloadIcon()
- }
-
- window.addEventListener("popstate", () => {
- checkForChanges()
- insertDownloadIcon()
- })
- })()