// ==UserScript== // @name Instagram: 图片,视频批量下载器 // @name:en Instagram: pictures, video batch downloader // @namespace http://tampermonkey.net/ // @version 4.0 // @description Instagram下载器,支持图片和视频批量下载 // @description:en Downloader for Instagram, support batch download pictures and videos // @author jaywang // @match https://www.instagram.com/* // @require https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant unsafeWindow // @run-at document-idle // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/435655/Instagram%3A%20%E5%9B%BE%E7%89%87%EF%BC%8C%E8%A7%86%E9%A2%91%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BD%E5%99%A8.user.js // @updateURL https://update.greasyfork.icu/scripts/435655/Instagram%3A%20%E5%9B%BE%E7%89%87%EF%BC%8C%E8%A7%86%E9%A2%91%E6%89%B9%E9%87%8F%E4%B8%8B%E8%BD%BD%E5%99%A8.meta.js // ==/UserScript== (function () { 'use strict'; // Your code here... /** 更多选项列表的选择器 */ const selectionListSelector = 'div.RnEpo.Yx5HN > div > div > div > div'; /** 当前post下的用户名称选择器 */ const usernameASelector = 'header.Ppjfr a.ZIAjV'; /** 复制图片按钮选择器 */ const copyURLSelector = `${selectionListSelector} > button:nth-last-child(3)`; /** 打开帖子按钮选择器 */ const openPostSelector = `${selectionListSelector} > button:nth-last-child(2)`; /** 单个下载按钮 */ const singleDownloadBtn = ''; /** 批量下载图片按钮 */ const batchDownloadBtn = ''; /** * 当前post的下标 * -1: 代表单图 */ let currentIndex = -1; /** * 当前post */ let currentPost; /** * 下载图片按钮事件 * 点击三个小点更多按钮 */ $('body').click(async (el) => { /** 下载按钮事件 */ if (el.target.getAttribute('jaywangdownload') === 'single') { const index = currentIndex; const post = currentPost; const username = post.querySelector(usernameASelector).text; const isPrivate = isPrivateUser(); let initSrc; let src; if (isPrivate) { const container = post.querySelector('._97aPb'); src = getPrivateSrc(container, index); } else { $(copyURLSelector).click(); initSrc = await navigator.clipboard.readText(); src = await getResource(initSrc, index); } save(src, getName(username, index)); } /** 下载合集按钮事件 */ if (el.target.getAttribute('jaywangdownload') === 'batch') { let initSrc; if (isPrivateUser() && withoutOpenPostBtn()) { //console.log(`location`, location) } else { $(`${selectionListSelector} > button:nth-last-child(${getOpenPostLastLocation()})`).click(); } initSrc = location.href; const data = await fetchPostInformation(initSrc); const batchInfo = getBatchInformation(data); batchSaveAs(batchInfo); } /** 点击三个小点更多按钮 */ if (el.target.closest('button.wpO6b')) { /** 获取当前post */ currentPost = el.target.closest('article'); /** 容纳点点点的容器 */ const container = currentPost.querySelector('._97aPb'); const dots = currentPost.querySelector('._3eoV-'); const isMulti = isMultiplePost(container); currentIndex = isMulti ? getPostIndex(dots) : 0; } }); /** */ /** DOM变动的回调函数 */ const callback = function (mutationRecord) { for (const record of mutationRecord) { const nodeList = record.addedNodes; if (nodeList.length === 1 && isMoreOptionButton(nodeList[0])) { $(selectionListSelector).prepend(singleDownloadBtn); $(selectionListSelector).prepend(batchDownloadBtn); return; } } }; /** 检测DOM变动 */ const observer = new MutationObserver(callback); observer.observe(document.body, { childList: true, subtree: false }); /** * 判断是否是更多选项按钮 * @param {Node} node * @return {boolean} */ function isMoreOptionButton(node) { return node.querySelector('.mt3GC'); } /** * 获取Post信息 * @param uri * @returns {Promise} */ async function fetchPostInformation(uri) { let formatedUri = uri; if (uri.includes('?utm_source')) { formatedUri = uri.match(/.*(?=\?utm_source)/); } formatedUri += '?__a=1'; const result = await fetch(formatedUri); const data = await result.json(); return data; } /** * 获取资源链接 * @param {string} uri * @param {number} index */ async function getResource(uri, index) { const data = await fetchPostInformation(uri); const isSingle = data.graphql.shortcode_media.edge_sidecar_to_children === undefined; const node = isSingle ? data.graphql.shortcode_media : data.graphql.shortcode_media.edge_sidecar_to_children.edges[index].node; const isVideo = node.is_video; const src = isVideo ? node.video_url : node.display_resources[node.display_resources.length - 1].src; return src; } /** * 获取第i个信息 * @param data * @param index * @returns {*|string} */ function getBatchInformation(data) { const isSingle = data.graphql.shortcode_media.edge_sidecar_to_children === undefined; let edges; if (isSingle) { edges = [ { node: data.graphql.shortcode_media } ]; } else { edges = data.graphql.shortcode_media.edge_sidecar_to_children.edges; } const username = data.graphql.shortcode_media.owner.username; const infoList = []; for (const i in edges) { const node = edges[i].node; const isVideo = node.is_video; const src = isVideo ? node.video_url : node.display_resources[node.display_resources.length - 1].src; infoList.push({ name: getName(username, i), src, suffix: isVideo ? 'mp4' : 'jpg' }); } return infoList; } /** * 在浏览器里面打开 * @param {string} src */ function openInBrowser(src) { const a = document.createElement('a'); a.target = '_blank'; a.href = src; document.body.append(a); a.click(); a.remove(); } /** * 另存为图片或视频 * @param {string} src 下载源 * @param {string} name 图片名称 */ async function save(src, name) { const data = await fetch(src); const blob = await data.blob(); downloadBlob(blob, name); } /** * 下载blob * @param {blob} blob */ function downloadBlob(blob) { const domString = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = domString; a.setAttribute('download', name); a.click(); } /** * 批量下载 */ async function batchSaveAs(fileList) { let promiseList = []; let filenameList = []; let filename; for (const { src, name, suffix } of fileList) { filename = name; const promise = fetch(src); promiseList.push(promise); filenameList.push(`${name}.${suffix}`); } const resultList = await Promise.all(promiseList); const blobList = resultList.map((result) => result.blob()); const zip = new JSZip(); const folder = zip.folder(filename); for(let i = 0; i < filenameList.length; i++) { folder.file(filenameList[i], blobList[i]); } const content = await folder.generateAsync({type:"blob"}); saveAs(content, `${filename}.zip`); } /** * 定位当前图片下标 * 多个元素_97aPb内有一个rQDP3,内有_3eoV- * 单个元素_97aPb内没有rQDP3 * @param {Element} node */ function getPostIndex(node) { const nodeList = node.childNodes; for (let i = 0; i < nodeList.length; i++) { const classList = nodeList[i].classList; if (classList && classList.contains('XCodT')) { return i; } } return 0; } /** * 判断post图片/视频数量是多个还是单个 * @param {Element} el container * @return {boolean} */ function isMultiplePost(el) { return el.querySelector('._3eoV-') !== null; } /** * 格式化的日期 * @returns {string} */ function getFormatedDate() { const d = new Date(); return '' + d.getHours() + d.getMinutes() + d.getSeconds(); } /** * 判断是否是私人用户 * @returns {boolean} */ function isPrivateUser() { const nodeList = document.querySelector(selectionListSelector).querySelectorAll('.HoLwm'); return nodeList.length <= 3; } /** * 判断是否有打开帖子按钮 * @returns {boolean} */ function withoutOpenPostBtn() { const nodeList = document.querySelector(selectionListSelector).querySelectorAll('.HoLwm'); return nodeList.length === 1; } /** * 获取打开帖子按钮倒数的位置 * @returns {number} */ function getOpenPostLastLocation() { const nodeList = document.querySelector(selectionListSelector).querySelectorAll('.HoLwm'); return nodeList.length; } /** * 获取图片,视频资源链接 * @param {Element} container * @param {number} index * @returns {string} */ function getPrivateSrc(container, index) { let resourceContainer = container; if (isMultiplePost(container)) { /** * 图片在post开始是第一个有效的li * post结尾是第二个li * post中间是中间一个li(中间) */ const hasPrevBtn = container.querySelectorAll('.POSa_').length !== 0; const hasNextBtn = container.querySelectorAll('._6CZji').length !== 0; let nth = 3; if (hasNextBtn && !hasPrevBtn) { nth = 2; } resourceContainer = container.querySelector(`ul.vi798 li:nth-child(${nth})`); } const video = resourceContainer.querySelector('video'); const img = resourceContainer.querySelector('img'); if (video) { return video.src; } if (img) { const sets = img.srcset.split(','); const lastSet = sets[0]; return lastSet.split(' ')[0]; } } /** * 根据名称获取下标 * @param {string} username * @param {number} index * @returns {string} */ function getName(username, index) { return `${username.split('.').join('')}_${index + 1}_${getFormatedDate()}`; } })();