// ==UserScript==
// @name 抖音表情包下载(网页版)douyin
// @namespace https://local.9527/
// @version 0.1.1
// @description Parse already-loaded Douyin comment stickers safely, preview them, and download individually or as a zip bundle.
// @author 代号_9527
// @match https://www.douyin.com/*
// @grant GM_addStyle
// @grant GM_download
// @grant GM.download
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @connect *
// ==/UserScript==
(function () {
"use strict";
const PANEL_ID = "dy-sticker-parser-panel";
const MODAL_ID = "dy-sticker-parser-modal";
const LAUNCHER_CAT_DATA_URL = "data:image/png;base64,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";
const STATE = {
items: [],
lastScanAt: null,
pageUrl: "",
pageTitle: "",
ui: {
collapsed: false,
position: null,
drag: null,
},
};
const CRC32_TABLE = (() => {
const table = new Uint32Array(256);
for (let i = 0; i < 256; i += 1) {
let c = i;
for (let j = 0; j < 8; j += 1) {
c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
}
table[i] = c >>> 0;
}
return table;
})();
function getGmXmlHttpRequest() {
if (typeof GM_xmlhttpRequest === "function") return GM_xmlhttpRequest;
if (typeof GM !== "undefined" && GM && typeof GM.xmlHttpRequest === "function") {
return GM.xmlHttpRequest.bind(GM);
}
return null;
}
function getGmDownload() {
if (typeof GM_download === "function") return GM_download;
if (typeof GM !== "undefined" && GM && typeof GM.download === "function") {
return GM.download.bind(GM);
}
return null;
}
function isStickerResourceUrl(url) {
if (!url || typeof url !== "string") return false;
return (
url.includes("douyinpic.com") &&
(url.includes("sticker_comment") || url.includes("biz_tag=aweme_comment"))
);
}
function isInlineEmojiImage(img) {
if (!img || img.tagName !== "IMG") return false;
const src = img.getAttribute("src") || "";
const alt = img.getAttribute("alt") || "";
return !!alt && !isStickerResourceUrl(src);
}
function sanitizeFilename(input) {
const value = String(input || "")
.replace(/[\\/:*?"<>|]/g, "_")
.replace(/\s+/g, " ")
.trim();
return value || "sticker";
}
function extensionFromContentType(contentType) {
const value = String(contentType || "").toLowerCase();
if (value.includes("image/gif")) return ".gif";
if (value.includes("image/webp")) return ".webp";
if (value.includes("image/png")) return ".png";
if (value.includes("image/jpeg")) return ".jpg";
if (value.includes("image/jpg")) return ".jpg";
if (value.includes("image/bmp")) return ".bmp";
if (value.includes("image/heic")) return ".heic";
if (value.includes("image/heif")) return ".heif";
return ".bin";
}
function normalizeUrl(url) {
return String(url || "").replace(/&/g, "&").trim();
}
function inferExtensionFromUrl(url) {
const clean = normalizeUrl(url);
try {
const parsed = new URL(clean, location.href);
const path = parsed.pathname.toLowerCase();
const match = path.match(/\.(gif|png|jpe?g|webp|bmp|heic|heif)$/);
return match ? `.${match[1] === "jpeg" ? "jpg" : match[1]}` : "";
} catch (e) {
console.warn("[douyin-sticker-parser] 无法从 URL 推断扩展名:", e);
return "";
}
}
function buildReadmeText({ pageTitle, pageUrl, generatedAt, items, failures }) {
const lines = [
"抖音评论贴纸导出摘要",
"",
`导出时间:${generatedAt}`,
`页面标题:${pageTitle || ""}`,
`页面链接:${pageUrl || ""}`,
`资源总数:${items.length}`,
`失败项:${failures.length}`,
"",
"资源明细:",
];
items.forEach((item, index) => {
lines.push(
`${index + 1}. 文件名:${item.filename}`,
` 作者:${item.author || "未知作者"}`,
` 时间:${item.timeText || "未知时间"}`,
` 类型:${item.resourceKind || "未知类型"}`,
` 原始 MIME:${item.originalMime || "未知"}`,
` 最终 MIME:${item.finalMime || "未知"}`,
` 来源:${item.resourceUrl}`,
""
);
});
if (failures.length > 0) {
lines.push("失败明细:");
failures.forEach((failure, index) => {
lines.push(
`${index + 1}. 链接:${failure.resourceUrl}`,
` 原因:${failure.reason}`,
""
);
});
}
return lines.join("\n");
}
function parseContentTypeFromHeaders(headersText) {
const line = String(headersText || "")
.split(/\r?\n/)
.find((entry) => /^content-type:/i.test(entry));
return line ? line.split(":").slice(1).join(":").trim() : "";
}
function clampPanelPosition(position, viewport, panelSize, margin = 12) {
const maxLeft = Math.max(margin, viewport.width - panelSize.width - margin);
const maxTop = Math.max(margin, viewport.height - panelSize.height - margin);
return {
left: Math.min(Math.max(position.left, margin), maxLeft),
top: Math.min(Math.max(position.top, margin), maxTop),
};
}
function getCollapsedBadgeText(count) {
if (!Number.isFinite(count) || count <= 0) return "";
return count > 99 ? "99+" : String(count);
}
function createStyles() {
const css = `
#${PANEL_ID} {
position: fixed;
right: 20px;
bottom: 20px;
z-index: 2147483640;
width: 368px;
max-width: calc(100vw - 20px);
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 14px;
pointer-events: none;
color: #553246;
font: 13px/1.5 "Trebuchet MS", "Microsoft YaHei", sans-serif;
--dysp-bg-1: #fff9fc;
--dysp-bg-2: #ffe5f1;
--dysp-bg-3: #ffd2e7;
--dysp-ink: #553246;
--dysp-subtle: #8a6476;
--dysp-accent: #ff72ac;
--dysp-accent-strong: #ff4f95;
--dysp-accent-soft: #ffc0da;
--dysp-line: rgba(255, 119, 174, 0.2);
--dysp-shadow: 0 24px 60px rgba(255, 116, 172, 0.22);
}
#${PANEL_ID} * {
box-sizing: border-box;
}
#${PANEL_ID} .dysp-launcher {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
min-width: 58px;
height: 48px;
padding: 0 12px;
pointer-events: auto;
border: 1px solid rgba(255, 122, 180, 0.34);
border-radius: 18px;
background:
radial-gradient(circle at top left, rgba(255, 255, 255, 0.96), transparent 38%),
linear-gradient(135deg, rgba(255, 227, 239, 0.98), rgba(255, 244, 249, 0.98) 56%, rgba(255, 210, 231, 0.98));
color: #8b4069;
box-shadow:
0 18px 34px rgba(255, 109, 170, 0.22),
inset 0 1px 0 rgba(255, 255, 255, 0.92);
transform-origin: right bottom;
transition:
transform 340ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 240ms ease,
filter 240ms ease,
box-shadow 240ms ease;
}
#${PANEL_ID} .dysp-launcher::before {
content: "✿";
font-size: 18px;
line-height: 1;
display: none;
}
#${PANEL_ID} .dysp-launcher-badge:empty {
display: none;
}
#${PANEL_ID} .dysp-launcher-cat {
width: 24px;
height: 24px;
flex: none;
object-fit: contain;
pointer-events: none;
}
#${PANEL_ID} .dysp-launcher-face {
font-size: 12px;
font-weight: 800;
letter-spacing: 0.03em;
}
#${PANEL_ID} .dysp-launcher-badge {
min-width: 18px;
height: 18px;
padding: 0 5px;
border-radius: 999px;
display: inline-flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #ff76b1, #ff5a97);
color: #fff8fc;
box-shadow: 0 8px 18px rgba(255, 91, 150, 0.28);
font-size: 11px;
font-weight: 800;
}
#${PANEL_ID} .dysp-shell {
display: flex;
flex-direction: column;
min-height: 0;
width: 100%;
max-height: 78vh;
pointer-events: auto;
position: relative;
overflow: hidden;
border: 1px solid var(--dysp-line);
border-radius: 28px;
background:
radial-gradient(circle at top right, rgba(255, 255, 255, 0.92), transparent 28%),
linear-gradient(180deg, rgba(255, 251, 253, 0.98), rgba(255, 234, 243, 0.96) 55%, rgba(255, 223, 236, 0.96));
box-shadow: var(--dysp-shadow);
backdrop-filter: blur(16px);
transform-origin: right bottom;
transition:
transform 360ms cubic-bezier(0.22, 1, 0.36, 1),
opacity 260ms ease,
visibility 260ms ease,
max-height 260ms ease,
filter 260ms ease;
}
#${PANEL_ID} .dysp-shell::before {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(circle at 16% 18%, rgba(255, 255, 255, 0.9), transparent 16%),
radial-gradient(circle at 84% 12%, rgba(255, 191, 221, 0.35), transparent 18%),
linear-gradient(135deg, rgba(255, 255, 255, 0.2), transparent 48%);
pointer-events: none;
}
#${PANEL_ID}.is-collapsed {
width: auto;
max-width: none;
}
#${PANEL_ID}.is-collapsed .dysp-launcher {
opacity: 1;
visibility: visible;
transform: translateY(0) scale(1);
filter: saturate(1);
}
#${PANEL_ID}.is-collapsed .dysp-shell {
opacity: 0;
visibility: hidden;
pointer-events: none;
max-height: 0;
transform: translateY(22px) scale(0.86);
filter: blur(2px);
}
#${PANEL_ID}.is-expanded .dysp-launcher {
opacity: 0;
visibility: hidden;
pointer-events: none;
transform: translateY(14px) scale(0.84);
filter: saturate(0.85);
animation: none;
}
#${PANEL_ID}.is-expanded .dysp-shell {
opacity: 1;
visibility: visible;
max-height: 78vh;
transform: translateY(0) scale(1);
filter: blur(0);
}
#${PANEL_ID} .dysp-header {
position: relative;
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 18px 18px 14px;
background:
radial-gradient(circle at top right, rgba(255, 255, 255, 0.9), transparent 26%),
linear-gradient(135deg, rgba(255, 228, 240, 0.96), rgba(255, 247, 251, 0.98) 58%, rgba(255, 219, 235, 0.94));
border-bottom: 1px solid var(--dysp-line);
cursor: grab;
user-select: none;
touch-action: none;
}
#${PANEL_ID}.is-dragging .dysp-header {
cursor: grabbing;
}
#${PANEL_ID} .dysp-header::after {
content: "";
width: 16px;
height: 16px;
border-radius: 999px;
background: radial-gradient(circle, rgba(255, 255, 255, 0.88), rgba(255, 196, 221, 0.28) 70%, transparent 72%);
color: transparent;
font-size: 0;
}
#${PANEL_ID} .dysp-header-copy {
min-width: 0;
}
#${PANEL_ID} .dysp-title {
font-size: 16px;
font-weight: 800;
letter-spacing: 0.02em;
}
#${PANEL_ID} .dysp-subtitle {
margin-top: 7px;
color: var(--dysp-subtle);
font-size: 12px;
}
#${PANEL_ID} .dysp-actions {
display: flex;
gap: 10px;
padding: 12px 16px;
border-bottom: 1px solid var(--dysp-line);
background: rgba(255, 252, 254, 0.7);
}
#${PANEL_ID} button {
appearance: none;
border: 1px solid rgba(255, 128, 181, 0.22);
border-radius: 16px;
padding: 10px 12px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(255, 241, 247, 0.94));
color: var(--dysp-ink);
cursor: pointer;
font-weight: 700;
box-shadow:
0 8px 18px rgba(255, 134, 187, 0.12),
inset 0 1px 0 rgba(255, 255, 255, 0.9);
transition:
transform 180ms ease,
box-shadow 180ms ease,
background 180ms ease,
border-color 180ms ease;
}
#${PANEL_ID} button:hover {
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 231, 241, 0.96));
box-shadow:
0 12px 22px rgba(255, 118, 177, 0.18),
inset 0 1px 0 rgba(255, 255, 255, 0.94);
transform: translateY(-1px);
}
#${PANEL_ID} .dysp-utility-button {
color: #151318;
border-color: rgba(84, 58, 72, 0.18);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 243, 248, 0.94));
}
#${PANEL_ID} .dysp-utility-button:hover {
color: #0f0e12;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 238, 245, 0.96));
}
#${PANEL_ID} button:active {
transform: translateY(0);
}
#${PANEL_ID} button:disabled {
cursor: not-allowed;
opacity: 0.65;
transform: none;
box-shadow: none;
}
#${PANEL_ID} .dysp-icon-button {
flex: none;
min-width: 44px;
padding: 10px 0;
border-radius: 14px;
font-size: 12px;
}
#${PANEL_ID} .dysp-primary {
background: linear-gradient(135deg, var(--dysp-accent), var(--dysp-accent-strong));
color: #fff9fc;
border-color: transparent;
box-shadow:
0 14px 26px rgba(255, 95, 156, 0.3),
inset 0 1px 0 rgba(255, 255, 255, 0.22);
}
#${PANEL_ID} .dysp-primary:hover {
background: linear-gradient(135deg, #ff6ca8, #ff4b90);
color: #fff9fc;
}
#${PANEL_ID} .dysp-status {
position: relative;
padding: 12px 16px;
border-bottom: 1px solid var(--dysp-line);
color: #70475d;
background: linear-gradient(180deg, rgba(255, 248, 251, 0.82), rgba(255, 236, 244, 0.74));
}
#${PANEL_ID} .dysp-list {
overflow: auto;
padding: 14px;
display: grid;
gap: 12px;
}
#${PANEL_ID} .dysp-empty {
padding: 18px;
text-align: center;
color: var(--dysp-subtle);
border: 1px dashed rgba(255, 128, 181, 0.28);
border-radius: 18px;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.85), rgba(255, 242, 248, 0.88));
}
#${PANEL_ID} .dysp-card {
display: grid;
grid-template-columns: 80px 1fr;
gap: 12px;
padding: 12px;
border-radius: 20px;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(255, 241, 247, 0.9)),
linear-gradient(135deg, rgba(255, 236, 244, 0.7), rgba(255, 255, 255, 0.1));
border: 1px solid rgba(255, 140, 188, 0.18);
box-shadow: 0 12px 26px rgba(255, 144, 190, 0.1);
}
#${PANEL_ID} .dysp-thumb {
width: 80px;
height: 80px;
border-radius: 18px;
overflow: hidden;
background: linear-gradient(135deg, #ffe0ef, #fff8fc);
display: flex;
align-items: center;
justify-content: center;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.88);
}
#${PANEL_ID} .dysp-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
#${PANEL_ID} .dysp-thumb-button {
padding: 0;
border: none;
cursor: zoom-in;
}
#${PANEL_ID} .dysp-thumb-button:hover,
#${PANEL_ID} .dysp-thumb-button:active {
background: linear-gradient(135deg, #ffe0ef, #fff8fc);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.88);
transform: none;
}
#${PANEL_ID} .dysp-card-title {
font-weight: 700;
margin-bottom: 6px;
word-break: break-all;
}
#${PANEL_ID} .dysp-card-title-button {
display: block;
width: 100%;
padding: 0;
margin-bottom: 6px;
border: none;
background: transparent;
box-shadow: none;
border-radius: 0;
color: #402635;
font-size: 15px;
font-weight: 800;
text-align: left;
word-break: break-all;
cursor: pointer;
}
#${PANEL_ID} .dysp-card-title-button:hover,
#${PANEL_ID} .dysp-card-title-button:active {
background: transparent;
box-shadow: none;
color: #23131d;
transform: none;
}
#${PANEL_ID} .dysp-meta {
color: #7c6070;
margin-bottom: 8px;
word-break: break-word;
}
#${PANEL_ID} .dysp-tag {
display: inline-flex;
align-items: center;
padding: 4px 9px;
border-radius: 999px;
background: linear-gradient(135deg, rgba(255, 209, 229, 0.98), rgba(255, 236, 245, 0.98));
color: #af4f7b;
font-size: 11px;
margin-bottom: 8px;
border: 1px solid rgba(255, 141, 190, 0.24);
}
#${PANEL_ID} .dysp-download-button {
display: inline-flex;
width: 100%;
justify-content: center;
}
#${MODAL_ID} {
position: fixed;
inset: 0;
z-index: 2147483646;
display: none;
align-items: center;
justify-content: center;
background: rgba(64, 30, 48, 0.48);
padding: 24px;
}
#${MODAL_ID}.is-open {
display: flex;
}
#${MODAL_ID} .dysp-modal-card {
width: min(720px, 100%);
max-height: 86vh;
overflow: auto;
background:
radial-gradient(circle at top right, rgba(255, 255, 255, 0.92), transparent 24%),
linear-gradient(180deg, rgba(255, 250, 253, 0.98), rgba(255, 233, 242, 0.96));
border-radius: 28px;
padding: 20px;
border: 1px solid rgba(255, 155, 198, 0.28);
box-shadow: 0 28px 60px rgba(98, 43, 69, 0.28);
}
#${MODAL_ID} .dysp-modal-preview {
width: 100%;
min-height: 260px;
border-radius: 20px;
background: linear-gradient(135deg, #ffe7f2, #fffafc);
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
#${MODAL_ID} .dysp-modal-preview img {
max-width: 100%;
max-height: 60vh;
object-fit: contain;
}
#${MODAL_ID} .dysp-modal-actions {
display: flex;
gap: 10px;
margin-top: 14px;
}
@media (prefers-reduced-motion: reduce) {
#${PANEL_ID} .dysp-launcher,
#${PANEL_ID} .dysp-shell,
#${PANEL_ID} button {
animation: none !important;
transition: none !important;
}
}
@media (max-width: 680px) {
#${PANEL_ID} {
width: calc(100vw - 20px);
max-width: calc(100vw - 20px);
bottom: 10px;
right: 10px;
left: auto;
}
#${PANEL_ID} .dysp-shell {
max-height: 70vh;
}
#${PANEL_ID}.is-expanded .dysp-shell {
max-height: 70vh;
}
#${PANEL_ID} .dysp-actions {
flex-direction: column;
}
}`;
if (typeof GM_addStyle === "function") {
GM_addStyle(css);
return;
}
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
}
function findCommentRoot(node) {
if (!node || typeof node.closest !== "function") return null;
return (
node.closest('[data-e2e="comment-item"]') ||
node.closest(".UuCzPLbi") ||
node.closest(".EpsntdUI")
);
}
function textOf(element) {
return element ? String(element.textContent || "").trim() : "";
}
function simpleHash(input) {
const text = String(input || "");
let hash = 0;
for (let i = 0; i < text.length; i += 1) {
hash = ((hash << 5) - hash + text.charCodeAt(i)) | 0;
}
return Math.abs(hash).toString(36);
}
function formatLabelFromMime(contentType) {
const value = String(contentType || "").toLowerCase();
if (value.includes("image/gif")) return "GIF";
if (value.includes("image/png")) return "PNG";
if (value.includes("image/jpeg")) return "JPG";
if (value.includes("image/webp")) return "WEBP";
if (value.includes("image/heic")) return "HEIC";
if (value.includes("image/heif")) return "HEIF";
return "";
}
function formatLabelFromUrl(url) {
const value = String(url || "").toLowerCase();
if (value.includes(".gif")) return "GIF";
if (value.includes(".png")) return "PNG";
if (value.includes(".jpg") || value.includes(".jpeg")) return "JPG";
if (value.includes(".webp")) return "WEBP";
if (value.includes("sc=sticker_heif")) return "HEIF";
return "未知";
}
function resourceKindFromUrl(url) {
const clean = normalizeUrl(url).toLowerCase();
if (clean.includes(".gif")) return "gif";
if (clean.includes("sticker_heif")) return "sticker";
if (clean.includes("sticker_comment")) return "sticker";
return "image";
}
function collectLoadedResources() {
const seen = new Set();
const items = [];
const candidates = Array.from(document.querySelectorAll("img[src]"));
for (const img of candidates) {
const rawSrc = img.getAttribute("src") || "";
const resourceUrl = normalizeUrl(rawSrc);
if (!isStickerResourceUrl(resourceUrl)) continue;
if (isInlineEmojiImage(img)) continue;
const commentRoot = findCommentRoot(img);
if (!commentRoot) continue;
const key = `${location.href}::${resourceUrl}`;
if (seen.has(key)) continue;
seen.add(key);
const authorLink =
commentRoot.querySelector(".comment-item-info-wrap a") ||
commentRoot.querySelector('a[href*="/user/"]');
const timeNode = commentRoot.querySelector(".fJhvAqos") || commentRoot.querySelector("time");
const contentWrap = commentRoot.querySelector(".C7LroK_h") || commentRoot;
const textFlow = contentWrap.querySelector(".WFJiGxr7");
const textContent = textOf(textFlow || contentWrap).replace(/\s+/g, " ").trim();
items.push({
id: key,
resourceUrl,
previewUrl: resourceUrl,
author: textOf(authorLink) || "未知作者",
timeText: textOf(timeNode) || "未知时间",
commentText: textContent || "无可读文本",
resourceKind: resourceKindFromUrl(resourceUrl),
formatHint: formatLabelFromUrl(resourceUrl),
originalMime: "",
finalMime: "",
});
}
return items;
}
// Legacy panel markup removed. Use ensurePanel() instead.
function ensurePanel() {
let panel = document.getElementById(PANEL_ID);
if (panel) return panel;
panel = document.createElement("aside");
panel.id = PANEL_ID;
panel.innerHTML = `