// ==UserScript== // @name Instagram: 图片,视频下载器 // @name:en Instagram: pictures, video downloader // @namespace http://tampermonkey.net/ // @version 3.0 // @description Instagram下载器,支持图片和视频 // @description:en Downloader for Instagram, support pictures and videos // @author jaywang // @match https://www.instagram.com/* // @require https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant unsafeWindow // @run-at document-idle // @license MIT // @downloadURL none // ==/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 jaywangdownloadBtn = ''; /** * 当前post的下标 * -1: 代表单图 */ let currentIndex = -1; /** * 当前post */ let currentPost; /** * 下载图片按钮事件 * 点击三个小点更多按钮 */ $('body').click(async (el) => { /** 下载图片按钮事件 */ if (el.target.getAttribute('jaywangdownload') === 'jaywang') { 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); } saveAs(src, getName(username, index)); } /** 点击三个小点更多按钮 */ if (el.target.closest('button.wpO6b')) { /** 获取当前post */ currentPost = el.target.closest('article'); /** 容纳点点点的容器 */ const container = currentPost.querySelector('._97aPb'); const dots = currentPost.querySelector('._3eoV-'); currentIndex = isMultiplePost(container) ? 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(jaywangdownloadBtn); 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'); } /** * 获取资源链接 * @param {string} uri * @param {number} index */ async function getResource(uri, index) { 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(); 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; } /** * 在浏览器里面打开 * @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 saveAs(src, name) { const data = await fetch(src); const blob = await data.blob(); const domString = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = domString; a.setAttribute('download', name); a.click(); } /** * 定位当前图片下标 * 多个元素_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 <= 2; } /** * 获取图片,视频资源链接 * @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()}`; } })();