// ==UserScript== // @name 抖音4K视频免登录下载 // @namespace none // @version 1.0.1 // @description 不登录也可以下载(4K/HDR)视频,适用于抖音网页版。圣诞马哥陪你过冬日! // @author 占南弦 (Gemini协助) // @match https://*.douyin.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=douyin.com // @grant none // @license none // @downloadURL https://update.greasyfork.icu/scripts/559741/%E6%8A%96%E9%9F%B34K%E8%A7%86%E9%A2%91%E5%85%8D%E7%99%BB%E5%BD%95%E4%B8%8B%E8%BD%BD.user.js // @updateURL https://update.greasyfork.icu/scripts/559741/%E6%8A%96%E9%9F%B34K%E8%A7%86%E9%A2%91%E5%85%8D%E7%99%BB%E5%BD%95%E4%B8%8B%E8%BD%BD.meta.js // ==/UserScript== (function () { "use strict"; class Config { static global = new Config(); features = { convert_webp_to_png: true, filename_template: "{nickname}_{id}_{desc}" }; _key = "__douyin-dl-user-js__"; constructor() { try { this.load(); } catch (error) { console.error(error); } } toJSON() { return { features: this.features }; } load() { if (localStorage.getItem(this._key)) { const data = JSON.parse(localStorage.getItem(this._key)); this.features = { ...this.features, ...data.features }; } } save() { localStorage.setItem(this._key, JSON.stringify(this.toJSON())); } } class Downloader { constructor() {} async convertWebPToPNG(blob) { const img = new Image(); img.src = URL.createObjectURL(blob); await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; }); const canvas = document.createElement("canvas"); canvas.width = img.width; canvas.height = img.height; const ctx = canvas.getContext("2d"); ctx.drawImage(img, 0, 0); URL.revokeObjectURL(img.src); return new Promise((resolve) => { canvas.toBlob((pngBlob) => { resolve(pngBlob); }, "image/png"); canvas.onerror = (e) => { resolve(blob); }; }); } async prepare_download_file(imgSrc, filename_input = "") { if (imgSrc.startsWith("//")) imgSrc = `${window.location.protocol}${imgSrc}`; console.log(`[dy-dl] 正在请求资源: ${imgSrc}`); try { const response = await fetch(imgSrc, { referrerPolicy: "no-referrer" }); if (!response.ok) return { ok: false, error: `Fetch failed: ${response.status}` }; const contentType = response.headers.get("content-type"); const isImage = contentType && contentType.startsWith("image/"); const isWebP = contentType && contentType.includes("webp"); let fileExt = contentType ? contentType.split("/")[1] : "mp4"; if (fileExt === "jpeg") fileExt = "jpg"; if (!isImage && !imgSrc.includes(".mp4")) fileExt = "mp4"; const determinedFileExt = isImage && isWebP ? "png" : fileExt; let filename = filename_input || "download"; // 清理非法字符 (恢复为替换所有特殊字符,包括 /) filename = filename.replace(/[\\/:*?"<>|]/g, "_"); if (!filename.endsWith("." + determinedFileExt)) { filename += `.${determinedFileExt}`; } const blob = await response.blob(); let pngBlob = null; if (isImage && isWebP && Config.global.features.convert_webp_to_png) { try { pngBlob = await this.convertWebPToPNG(blob); } catch (e) {} } return { blob, filename, isImage, isWebP, pngBlob, fileExt: determinedFileExt, ok: true }; } catch (e) { return { ok: false, error: e.message }; } } async download_blob(blob, filename) { const link = document.createElement("a"); link.style.display = "none"; link.download = filename; link.href = URL.createObjectURL(blob); document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(link.href); } async download_file(source, filename_input = "", fallback_src = []) { let url_sources = Array.from(new Set([source, ...fallback_src].filter(x => typeof x === "string" && x.length > 0))); for (const url of url_sources) { try { const result = await this.prepare_download_file(url, filename_input); if (result.ok) { await this.download_blob(result.pngBlob || result.blob, result.filename); return; } else { console.warn(`[dy-dl] 地址失效: ${url}, 错误: ${result.error}`); } } catch (e) { console.error(`[dy-dl] Download error for ${url}:`, e); } } alert(`[dy-dl] 下载失败,可能是链接已过期或需要 Referer,请上下滑动确保抓取到源。等我更新`); } } class MediaHandler { player = null; current_media = null; downloading = false; downloader; constructor(downloader) { this.downloader = downloader; this.download_current_media = this._lock_download(this._download_current_media_logic.bind(this, false)); this.download_best_media = this._lock_download(this._download_current_media_logic.bind(this, true)); } static toShortId(bigintStr) { try { return BigInt(bigintStr).toString(36); } catch (e) { return bigintStr; } } _build_filename(media) { const { authorInfo: { nickname }, awemeId, desc } = media; const short_id = MediaHandler.toShortId(awemeId); let rawDesc = desc || ""; rawDesc = rawDesc.replace(/[#/\?<>\\:\*\|":\n\r]/g, "").trim().substring(0, 80); const cfg = Config.global.features; // 默认模板 const f_tmpl = cfg.filename_template || "{nickname}_{id}_{desc}"; let fileName = f_tmpl .replace(/{nickname}/g, nickname || "unknown") .replace(/{id}/g, short_id || "0") .replace(/{desc}/g, rawDesc || "no_desc"); return fileName.trim(); } _bind_player_events() { if (!this.player) return; const update = () => { if (this.player?.config?.awemeInfo) { this.current_media = this.player.config.awemeInfo; } }; update(); this.player.on("play", update); this.player.on("definitionChange", () => { console.log("[dy-dl] 检测到画质切换,更新资源数据..."); update(); }); } async _start_detect_player_change() { while (1) { // @ts-ignore const currentPlayer = window.player; if (this.player !== currentPlayer) { this.player = currentPlayer; if (this.player) this._bind_player_events(); } await new Promise((r) => setTimeout(r, 1000)); } } _lock_download(fn) { return async (...args) => { if (this.downloading) return alert("正在下载中..."); this.downloading = true; try { await fn(...args); } finally { await new Promise(r => setTimeout(r, 300)); this.downloading = false; } }; } _get_video_urls(video_obj) { if (!video_obj) return []; const sources = []; if (video_obj.playApi) sources.push(video_obj.playApi); if (Array.isArray(video_obj.playAddr)) sources.push(...video_obj.playAddr.map(x => x.src)); if (video_obj.bitRateList) video_obj.bitRateList.forEach(x => { if (x.playApi) sources.push(x.playApi); }); return Array.from(new Set(sources.filter(Boolean))); } _get_best_video_url(video_obj) { if (!video_obj) return null; console.group("[dy-dl] 开始分析画质数据"); let candidates = []; if (Array.isArray(video_obj.bitRateList) && video_obj.bitRateList.length > 0) { candidates = video_obj.bitRateList.map(item => { const name = item.gearName || item.gear_name || "未知"; const bitrate = item.bitRate || item.bit_rate || 0; const urlList = []; if (item.playApi) urlList.push(item.playApi); if (item.playAddr && item.playAddr.urlList) urlList.push(...item.playAddr.urlList); return { name, bitrate, urls: urlList, raw: item }; }); } if (candidates.length === 0) { console.warn("未找到 bitRateList,尝试直接读取 playAddr"); const fallback = this._get_video_urls(video_obj); console.groupEnd(); return fallback[0] || null; } candidates.sort((a, b) => b.bitrate - a.bitrate); const best = candidates[0]; console.log(`[dy-dl] 自动选定最高码率: ${best.name} (Bitrate: ${best.bitrate})`); console.groupEnd(); return best.urls[0] || null; } async _download_current_media_logic(forceBestQuality = false) { // @ts-ignore const realTimeMedia = window.player?.config?.awemeInfo || this.current_media; if (!realTimeMedia) return alert("请先播放视频以获取数据。"); const { video, images } = realTimeMedia; const filename_base = this._build_filename(realTimeMedia); if (Array.isArray(images) && images.length > 0) { let count = 0; for (let i = 0; i < images.length; i++) { const item = images[i]; const name = `${filename_base}_${i+1}`; const i_urls = item.urlList?.filter(Boolean); if (i_urls && i_urls.length) { await this.downloader.download_file(i_urls[0], name, i_urls); count++; } } if (count === 0) alert("图集下载失败"); return; } let targetUrl = ""; const allUrls = this._get_video_urls(video); if (forceBestQuality) { targetUrl = this._get_best_video_url(video); if (!targetUrl) targetUrl = allUrls[0]; } else { targetUrl = allUrls[0]; } if (targetUrl) { const finalName = forceBestQuality ? `${filename_base}_Best` : filename_base; await this.downloader.download_file(targetUrl, finalName, allUrls); } else { alert("未找到视频链接"); } } show_media_details() { console.log(this.current_media); } } class FloatingUI { constructor(mediaHandler) { this.mh = mediaHandler; this.iconSVG = ` `; this.initCSS(); this.render(); this.makeDraggable(); } initCSS() { const style = document.createElement('style'); style.textContent = ` #dy-dl-float-container { position: fixed; top: 20%; right: 20px; z-index: 99999; user-select: none; display: flex; flex-direction: row-reverse; align-items: center; } #dy-dl-ball { width: 70px; height: 70px; background: transparent; box-shadow: none; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: transform 0.2s; } #dy-dl-ball:hover { transform: scale(1.1); filter: drop-shadow(0 0 5px rgba(0,0,0,0.5)); } #dy-dl-ball:active { transform: scale(0.95); } #dy-dl-ball svg, #dy-dl-ball img { width: 100%; height: 100%; object-fit: contain; } #dy-dl-menu { opacity: 0; visibility: hidden; background: rgba(0,0,0,0.8); border-radius: 8px; padding: 6px 0; margin-right: 12px; color: #fff; font-size: 13px; width: 130px; transform: translateX(10px); transition: all 0.2s ease; pointer-events: none; } #dy-dl-float-container:hover #dy-dl-menu, #dy-dl-menu:hover { opacity: 1; visibility: visible; transform: translateX(0); pointer-events: auto; } .dy-dl-menu-item { padding: 8px 16px; cursor: pointer; white-space: nowrap; transition: background 0.2s; } .dy-dl-menu-item:hover { background: rgba(255,255,255,0.2); color: #fe2c55; } .dy-dl-divider { height: 1px; background: #444; margin: 4px 10px; } #dy-dl-settings-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 300px; background: #1f1f1f; border-radius: 12px; box-shadow: 0 10px 25px rgba(0,0,0,0.5); z-index: 100000; color: #fff; font-family: sans-serif; padding: 20px; display: none; border: 1px solid #333; } #dy-dl-settings-modal h3 { margin: 0 0 15px 0; font-size: 16px; color: #fe2c55; } .dy-dl-form-group { margin-bottom: 12px; } .dy-dl-form-group label { display: block; font-size: 12px; color: #aaa; margin-bottom: 4px; } .dy-dl-input { width: 100%; box-sizing: border-box; background: #333; border: 1px solid #444; color: #fff; padding: 6px 8px; border-radius: 4px; font-size: 13px; } .dy-dl-btn-row { display: flex; justify-content: flex-end; margin-top: 20px; gap: 10px; } .dy-dl-btn { padding: 6px 14px; border-radius: 4px; cursor: pointer; font-size: 13px; border: none; } .dy-dl-btn-primary { background: #fe2c55; color: #fff; } .dy-dl-btn-cancel { background: #444; color: #ccc; } `; document.head.appendChild(style); } render() { // 主浮窗 const container = document.createElement('div'); container.id = 'dy-dl-float-container'; container.innerHTML = `