Download media with high performance + customizable filename pattern
当前为
// ==UserScript==
// @name Threads Media Downloader (Optimized v2.1 + Custom Filename)
// @namespace https://threads.com
// @version 2.1
// @license MIT
// @description Download media with high performance + customizable filename pattern
// @match https://www.threads.net/*
// @match https://www.threads.com/*
// @grant GM_download
// ==/UserScript==
(() => {
"use strict";
// ⭐⭐⭐ 你可以在這裡自訂檔名格式 ⭐⭐⭐
// 可使用變數:{username} {postId} {datetime} {index} {ext}
const filenamePattern = "{username}_{postId}_{datetime}_{index}{ext}";
const SEL_POST = ".x78zum5.xdt5ytf";
const SEL_BTN_WRAP = "div.x6s0dn4.xamitd3.x40hh3e.x78zum5.x1q0g3np.x1xdureb.x1fc57z9.x1hm9lzh.xvijh9v";
const SEL_SCOPE = ".x1xmf6yo";
const download = (url, name) =>
new Promise(res =>
typeof GM_download === "function"
? GM_download({ url, name, onload: res, onerror: () => (fallback(url, name), res()) })
: fallback(url, name).then(res)
);
const fallback = (url, name) => {
const a = Object.assign(document.createElement("a"), { href: url, download: name });
document.body.appendChild(a).click();
a.remove();
return Promise.resolve();
};
const getMeta = post => {
const link = post.querySelector("a[href*='/post/'][role='link']");
const time = post.querySelector("time[datetime]");
if (!link || !time) return;
const m = link.href.match(/\/@([^/]+)\/post\/([^/]+)/);
if (!m) return;
const [, username, postId] = m;
const d = new Date(time.getAttribute("datetime"));
const P = n => `${n}`.padStart(2, "0");
const datetime =
`${d.getUTCFullYear()}${P(d.getUTCMonth() + 1)}${P(d.getUTCDate())}_` +
`${P(d.getUTCHours())}${P(d.getUTCMinutes())}${P(d.getUTCSeconds())}`;
return { username, postId, datetime };
};
const formatFilename = (meta, index, ext) =>
filenamePattern
.replace("{username}", meta.username)
.replace("{postId}", meta.postId)
.replace("{datetime}", meta.datetime)
.replace("{index}", index)
.replace("{ext}", ext);
const addButton = post => {
const wrap = post.querySelector(SEL_BTN_WRAP);
if (!wrap || wrap.__hasDownloadBtn) return;
wrap.__hasDownloadBtn = true;
const btn = document.createElement("button");
btn.textContent = "下載";
btn.style.cursor = "pointer";
btn.onclick = async e => {
e.stopPropagation();
e.preventDefault();
const meta = getMeta(post);
if (!meta) return;
const scope = post.querySelector(SEL_SCOPE);
if (!scope) return;
let i = 1;
for (const media of scope.querySelectorAll("img, video")) {
const url = media.src || media.poster;
if (!url) continue;
const ext = url.includes(".mp4") ? ".mp4" : ".jpg";
const name = formatFilename(meta, i++, ext);
await download(url, name);
}
};
wrap.appendChild(btn);
};
const scan = () => document.querySelectorAll(SEL_POST).forEach(addButton);
scan();
let pending = false;
new MutationObserver(() => {
if (!pending) {
pending = true;
requestAnimationFrame(() => {
scan();
pending = false;
});
}
}).observe(document.body, { childList: true, subtree: true });
})();