Created
August 2, 2024 13:45
-
-
Save ricky9w/f00b7511274669703e623e5180c33bd5 to your computer and use it in GitHub Desktop.
下载小红书无水印图文/视频作品
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name XHS-Downloader | |
// @namespace https://github.com/ricky9w | |
// @version 1.4.1 | |
// @description 下载小红书无水印图文/视频作品 | |
// @author JoeanAmier (improved and compiled by ricky9w & Claude AI) | |
// @match http*://www.xiaohongshu.com/* | |
// @grant GM_getValue | |
// @grant GM_setValue | |
// @grant unsafeWindow | |
// @grant GM_setClipboard | |
// @credit https://github.com/JoeanAmier/XHS-Downloader/blob/master/static/XHS-Downloader.js | |
// ==/UserScript== | |
(function() { | |
const generateVideoUrl = note => { | |
try { | |
return [`https://sns-video-bd.xhscdn.com/${note.video.consumer.originVideoKey}`]; | |
} catch (error) { | |
console.error("Error generating video URL:", error); | |
return []; | |
} | |
}; | |
const generateImageUrl = note => { | |
const regex = /http:\/\/sns-webpic-qc\.xhscdn.com\/\d+\/[0-9a-z]+\/(\S+)!/; | |
return note.imageList.map(item => { | |
const match = item.urlDefault.match(regex); | |
return match ? `https://ci.xiaohongshu.com/${match[1]}?imageView2/2/w/format/png` : null; | |
}).filter(Boolean); | |
}; | |
const downloadFile = async (url, filename, progressCallback, abortSignal) => { | |
try { | |
const response = await fetch(url, { signal: abortSignal }); | |
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); | |
const contentLength = response.headers.get('Content-Length'); | |
const total = parseInt(contentLength, 10); | |
let loaded = 0; | |
const reader = response.body.getReader(); | |
const chunks = []; | |
while (true) { | |
const { done, value } = await reader.read(); | |
if (done) break; | |
chunks.push(value); | |
loaded += value.length; | |
progressCallback(loaded / total); | |
if (abortSignal.aborted) { | |
throw new Error('Download cancelled'); | |
} | |
} | |
const blob = new Blob(chunks); | |
const blobUrl = URL.createObjectURL(blob); | |
const a = document.createElement('a'); | |
a.href = blobUrl; | |
a.download = filename; | |
a.click(); | |
URL.revokeObjectURL(blobUrl); | |
return true; | |
} catch (error) { | |
if (error.name === 'AbortError') { | |
console.log(`Download cancelled: ${filename}`); | |
} else { | |
console.error(`Download failed (${filename}):`, error); | |
} | |
return false; | |
} | |
}; | |
const extractNoteInfo = () => { | |
const match = window.location.href.match(/\/explore\/([^?]+)/); | |
return match ? unsafeWindow.__INITIAL_STATE__.note.noteDetailMap[match[1]] : null; | |
}; | |
const extractLinks = (extractor) => { | |
const ids = extractor(); | |
return [...ids].map(id => `https://www.xiaohongshu.com/explore/${id}`).join(" "); | |
}; | |
const createButton = (text, onClick) => { | |
const button = document.createElement('button'); | |
button.textContent = text; | |
button.addEventListener('click', onClick); | |
return button; | |
}; | |
const createProgressUI = () => { | |
const progressContainer = document.createElement('div'); | |
progressContainer.id = 'xhsProgressContainer'; | |
progressContainer.style.cssText = ` | |
background-color: white; | |
padding: 10px; | |
border-radius: 10px; | |
box-shadow: 0 0 10px rgba(0,0,0,0.1); | |
margin-bottom: 10px; | |
font-size: 12px; | |
max-height: 300px; | |
overflow-y: auto; | |
width: 250px; | |
display: none; | |
`; | |
return progressContainer; | |
}; | |
const createTaskProgressUI = (taskId, title, onCancel) => { | |
const taskContainer = document.createElement('div'); | |
taskContainer.id = `task-${taskId}`; | |
taskContainer.style.cssText = ` | |
margin-bottom: 10px; | |
border-bottom: 1px solid #eee; | |
padding-bottom: 5px; | |
`; | |
taskContainer.innerHTML = ` | |
<div style="display: flex; justify-content: space-between; align-items: center;"> | |
<h3 style="margin: 0 0 5px 0; font-size: 14px; font-weight: normal; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex: 1;" title="${title}">${title}</h3> | |
<button class="cancel-button" style="font-size: 12px; padding: 2px 5px; background-color: #ff4757; color: white; border: none; border-radius: 3px; cursor: pointer;">取消</button> | |
</div> | |
<div class="files-container"></div> | |
`; | |
taskContainer.querySelector('.cancel-button').addEventListener('click', onCancel); | |
return taskContainer; | |
}; | |
const updateTaskProgressUI = (taskContainer, files, progress) => { | |
const filesContainer = taskContainer.querySelector('.files-container'); | |
filesContainer.innerHTML = files.map((file, index) => ` | |
<div style="margin-bottom: 5px;"> | |
<div style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;" title="${file}">${file}</div> | |
<div style="width:100%;height:5px;background-color:#ddd;border-radius:2px;"> | |
<div style="width:${progress[index] * 100}%;height:100%;background-color:#4CAF50;border-radius:2px;"></div> | |
</div> | |
</div> | |
`).join(''); | |
}; | |
let taskIdCounter = 0; | |
let activeTaskCount = 0; | |
const addButtons = () => { | |
const container = document.createElement('div'); | |
container.id = 'xhsFunctionContainer'; | |
container.style.cssText = ` | |
position: fixed; | |
bottom: 20px; | |
left: 20px; | |
z-index: 2147483647; | |
display: flex; | |
flex-direction: column; | |
align-items: flex-start; | |
gap: 10px; | |
`; | |
const progressContainer = createProgressUI(); | |
container.appendChild(progressContainer); | |
const buttons = [ | |
createButton("下载无水印文件", async () => { | |
const note = extractNoteInfo()?.note; | |
if (!note) return alert("无法获取作品信息"); | |
const urls = note.type === "normal" ? generateImageUrl(note) : generateVideoUrl(note); | |
const baseFileName = note.title || 'download'; | |
const fileExtension = note.type === "normal" ? 'png' : 'mp4'; | |
const files = urls.map((_, i) => | |
urls.length === 1 ? `${baseFileName}.${fileExtension}` : `${baseFileName}_${i + 1}.${fileExtension}` | |
); | |
const progress = new Array(urls.length).fill(0); | |
const taskId = taskIdCounter++; | |
const abortController = new AbortController(); | |
const taskContainer = createTaskProgressUI(taskId, baseFileName, () => { | |
abortController.abort(); | |
taskContainer.remove(); | |
activeTaskCount--; | |
if (activeTaskCount === 0) { | |
progressContainer.style.display = 'none'; | |
} | |
}); | |
progressContainer.appendChild(taskContainer); | |
activeTaskCount++; | |
progressContainer.style.display = 'block'; | |
updateTaskProgressUI(taskContainer, files, progress); | |
for (let [i, url] of urls.entries()) { | |
if (abortController.signal.aborted) break; | |
await downloadFile(url, files[i], (p) => { | |
progress[i] = p; | |
updateTaskProgressUI(taskContainer, files, progress); | |
}, abortController.signal); | |
} | |
if (!abortController.signal.aborted) { | |
// 立即隐藏取消按钮 | |
const cancelButton = taskContainer.querySelector('.cancel-button'); | |
if (cancelButton) { | |
cancelButton.style.display = 'none'; | |
} | |
setTimeout(() => { | |
taskContainer.remove(); | |
activeTaskCount--; | |
if (activeTaskCount === 0) { | |
progressContainer.style.display = 'none'; | |
} | |
}, 2000); | |
} | |
}), | |
createButton("提取作品链接", () => { | |
const links = extractLinks(() => new Set(unsafeWindow.__INITIAL_STATE__.user.notes._rawValue[0].map(({id}) => id))); | |
GM_setClipboard(links, "text"); | |
alert('作品链接已复制到剪贴板!'); | |
}) | |
]; | |
buttons.forEach(button => { | |
button.style.cssText = ` | |
padding: 10px 20px; | |
background-color: #ff2442; | |
color: white; | |
border: none; | |
border-radius: 20px; | |
cursor: pointer; | |
font-size: 14px; | |
font-weight: bold; | |
transition: background-color 0.3s; | |
display: block; | |
`; | |
button.addEventListener('mouseover', () => { | |
button.style.backgroundColor = '#ff4757'; | |
}); | |
button.addEventListener('mouseout', () => { | |
button.style.backgroundColor = '#ff2442'; | |
}); | |
container.appendChild(button); | |
}); | |
document.body.appendChild(container); | |
}; | |
window.addEventListener('load', addButtons); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment