您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
The script lets you download photos from meduza.io in max quality. It adds 2 buttons to each downloadable photo. One of the buttons opens the photo in a new tab in max quality, the second one downloads the photo.
/* meduza.io photo downloader Copyright (C) 2022 T1mL3arn This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 3. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */ // ==UserScript== // @name meduza.io photo downloader // @description The script lets you download photos from meduza.io in max quality. It adds 2 buttons to each downloadable photo. One of the buttons opens the photo in a new tab in max quality, the second one downloads the photo. // @description:ru Скрипт позволяет скачивать фотографии с meduza.io в максимальном качестве. Он добавляет две кнопки для каждой фотографии, которую можно скачать. Одно кнпока открывает фотографию в новой вкладке в максимальном качестве, другая скачивает фотографию. // @version 1.0.3 // @match https://meduza.io/* // @grant none // @author T1mL3arn // @icon https://pbs.twimg.com/profile_images/1315630633952202757/JDNwKd7P_200x200.png // @license GPL-3.0-only // @namespace https://greasyfork.org/users/51268 // ==/UserScript== const DOWN_BUTTONS_CONTAINER_WRAPPER_PROP = 'mdz-photo-buttons-wrap' const DOWN_BUTTONS_CONTAINER_CLASS = 'mdz-photo-buttons' const DOWN_BUTTONS_CONTAINER_PROP = DOWN_BUTTONS_CONTAINER_CLASS const BUTTON_CLASS = 'mdzd-button' const BUTTON_CLASS__DOWN = 'mdzd-button--dl' const BUTTON_CLASS__TAB = 'mdzd-button--tab' const PROCESSED_FIGURE_ATTR_NAME = 'mdzd-processed' const CSS = ` .${DOWN_BUTTONS_CONTAINER_CLASS} { box-sizing: border-box; position: absolute; right: 12px; /* original button 12px padding + its height 40px + margin 12px*/ bottom: 64px; display: flex; flex-flow: column; opacity: 0; transition: opacity 0.25s ease; pointer-events: none; } /* disable buttons for small avatar pictures (buttons still available in fullscreen) */ .EmbedBlock-module_xs__PNHGz .${DOWN_BUTTONS_CONTAINER_CLASS} { display: none; } .${DOWN_BUTTONS_CONTAINER_CLASS} > *:not(:last-child) { margin-bottom: 12px; } .Image-module_root__H5wAh:hover .${DOWN_BUTTONS_CONTAINER_CLASS} { opacity: 1; } .Lightbox-module-control > .${DOWN_BUTTONS_CONTAINER_CLASS} { margin-top: 12px; position: static; opacity: 1; } .${BUTTON_CLASS} { width: 42px; height: 42px; display: block; background-color: rgba(0,0,0,.65); background-repeat: no-repeat; background-position: 50%; background-size: 65%; border-radius: 50%; opacity: 0.75; cursor: pointer; pointer-events: auto; } .${BUTTON_CLASS}:hover { opacity: 1; } .${BUTTON_CLASS__DOWN} { background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgNDg1IDQ4NSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNDg1IDQ4NTsiIHhtbDpzcGFjZT0icHJlc2VydmUiIGZpbGw9Im5vbmUiPg0KPGcgc3Ryb2tlPScjRUVFRUVFJyBzdHJva2Utd2lkdGg9JzI1JyBmaWxsPScjRUVFRUVFJz4NCgk8Zz4NCgkJPHBhdGggZD0iTTIzMywzNzguN2MyLjYsMi42LDYuMSw0LDkuNSw0czYuOS0xLjMsOS41LTRsMTA3LjUtMTA3LjVjNS4zLTUuMyw1LjMtMTMuOCwwLTE5LjFjLTUuMy01LjMtMTMuOC01LjMtMTkuMSwwTDI1NiwzMzYuNQ0KCQkJdi0zMjNDMjU2LDYsMjUwLDAsMjQyLjUsMFMyMjksNiwyMjksMTMuNXYzMjNsLTg0LjQtODQuNGMtNS4zLTUuMy0xMy44LTUuMy0xOS4xLDBzLTUuMywxMy44LDAsMTkuMUwyMzMsMzc4Ljd6Ii8+DQoJCTxwYXRoIGQ9Ik00MjYuNSw0NThoLTM2OEM1MSw0NTgsNDUsNDY0LDQ1LDQ3MS41UzUxLDQ4NSw1OC41LDQ4NWgzNjhjNy41LDAsMTMuNS02LDEzLjUtMTMuNVM0MzQsNDU4LDQyNi41LDQ1OHoiLz4NCgk8L2c+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8L3N2Zz4NCg==); background-position-y: 37%; } .${BUTTON_CLASS__TAB} { background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGlkPSJpY29uIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxnIGZpbGw9JyNFRUVFRUUnPgogICAgPHBhdGggZD0iTTI2LDI2SDZWNkgxNlY0SDZBMi4wMDIsMi4wMDIsMCwwLDAsNCw2VjI2YTIuMDAyLDIuMDAyLDAsMCwwLDIsMkgyNmEyLjAwMiwyLjAwMiwwLDAsMCwyLTJWMTZIMjZaIi8+CiAgICA8cGF0aCBkPSJNMjYsMjZINlY2SDE2VjRINkEyLjAwMiwyLjAwMiwwLDAsMCw0LDZWMjZhMi4wMDIsMi4wMDIsMCwwLDAsMiwySDI2YTIuMDAyLDIuMDAyLDAsMCwwLDItMlYxNkgyNloiLz4KICA8L2c+CiAgPHBvbHlnb24gc3Ryb2tlPScjRUVFRUVFJyBzdHJva2Utd2lkdGg9JzInIGZpbGw9JyNFRUVFRUUnIHBvaW50cz0iMjYgNiAyNiAyIDI0IDIgMjQgNiAyMCA2IDIwIDggMjQgOCAyNCAxMiAyNiAxMiAyNiA4IDMwIDggMzAgNiAyNiA2Ii8+CiAgPHJlY3QgaWQ9Il9UcmFuc3BhcmVudF9SZWN0YW5nbGVfIiBkYXRhLW5hbWU9IiZsdDtUcmFuc3BhcmVudCBSZWN0YW5nbGUmZ3Q7IiBmaWxsPSJub25lIiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiLz4KPC9zdmc+Cg==); } ` const state = { savingFormat: 'png', // webp is also supported fsButtons: null, } function initStyle() { let elt = document.head.appendChild(document.createElement('style')) elt.id = 'meduza-photo-downloader-css' elt.textContent = CSS } function createDownloadButtons() { let downButton = document.createElement('a') downButton.classList.add(BUTTON_CLASS, BUTTON_CLASS__DOWN) downButton.title = 'Download image in max resolution' let tabButton = document.createElement('a') tabButton.classList.add(BUTTON_CLASS, BUTTON_CLASS__TAB) tabButton.target = "_blank" tabButton.title = 'Open image in new tab' let div = document.createElement('div') div.classList.add(DOWN_BUTTONS_CONTAINER_CLASS) div.appendChild(downButton) div.appendChild(tabButton) return div; } function setupButtons(e) { const figureElt = e.currentTarget // find the best URL let urls = figureElt.querySelectorAll(`source[type="image/${state.savingFormat}"]`).item(0).srcset // srcset here contains "2x" as the first source, so I just take it const bestImageUrl = urls.split(',')[0].split(' ')[0] // build the image name const { fileName } = getImageInfo(figureElt) // setup buttons let buttons = figureElt.querySelectorAll(`.${BUTTON_CLASS}`) if (buttons.length === 0) { // buttons are not here yet, need to add them const target = figureElt[DOWN_BUTTONS_CONTAINER_WRAPPER_PROP] const buttonsContainer = figureElt[DOWN_BUTTONS_CONTAINER_PROP] target.appendChild(buttonsContainer) buttons = buttonsContainer.children } //// download button buttons.item(0).href = bestImageUrl // Download attribute does not work due to image Content-Disposition header // specifies filename, see more https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#sect1 // Workaround (not the good one): https://stackoverflow.com/questions/3102226/how-to-set-name-of-file-downloaded-from-browser#answer-73939464 buttons.item(0).download = fileName //// new tab button buttons.item(1).href = bestImageUrl // setup fullscreen buttons let fsButtons = state.fsButtons || (state.fsButtons = createDownloadButtons()) fsButtons.children.item(0).href = bestImageUrl fsButtons.children.item(0).download = fileName fsButtons.children.item(1).href = bestImageUrl figureElt.querySelector('picture img').addEventListener('click', showFullscreenButtons) } function getImageInfo(figureElt) { let fullDescription = figureElt.querySelector('.MediaCaption-module_caption__ewfcc')?.textContent || '' let fileName = (/(.+?)(?:\.|$)/i).exec(fullDescription)?.[1] || getDate() return ({ fileName: fileName + `.${state.savingFormat}`, fullDescription, }) } function getDate() { return document.body.querySelector('.MetaItem-module_datetime__--O8c time.Timestamp-module_root__jPJ6w')?.textContent ||new Date().toLocaleDateString('ru-RU', {year: 'numeric', month: 'long', day: 'numeric'}) } function showFullscreenButtons(e) { setTimeout(() => { document.querySelector('.Lightbox-module-container .Lightbox-module-control') .appendChild(state.fsButtons) }, 200); } function work() { Array(...document.querySelectorAll(`figure.EmbedBlock-module_root__wNZlD:not([${PROCESSED_FIGURE_ATTR_NAME}]), .HalfBlock-module_image__2lYel:not([${PROCESSED_FIGURE_ATTR_NAME}])`)) .forEach(figureElt => { const img = figureElt.querySelector('.Image-module_root__H5wAh') if (img) { /* I cannot rely on meduza to hold new buttons permanently as children. (example of such page where new buttons are removed from markup https://meduza.io/feature/2018/08/24/150-let-nazad-rossiya-deportirovala-cherkesov-v-siriyu-teper-oni-begut-obratno-no-i-zdes-im-ne-rady) So the buttons are stored as a property of its parent <figure> element */ figureElt[DOWN_BUTTONS_CONTAINER_PROP] = createDownloadButtons() figureElt[DOWN_BUTTONS_CONTAINER_WRAPPER_PROP] = img figureElt.setAttribute(PROCESSED_FIGURE_ATTR_NAME, true) figureElt.addEventListener('mouseover', setupButtons) } }) } (function init() { initStyle(); // TODO make it work for collage pictures // on this https://meduza.io/feature/2022/12/20/chto-tut-bylo page? /* NOTE: images for which buttons didn't work Buttons are added in fullscreen, but not on the preview https://meduza.io/feature/2023/03/14/ukraina-obvinila-dvuh-rossiyskih-snayperov-v-iznasilovanii-chetyrehletney-devochki-i-ee-materi-v-kievskoy-oblasti */ work(); new MutationObserver(work).observe(document.getElementById('root'), { childList: true, subtree: true }) })() /* pages to test: https://meduza.io/feature/2022/12/20/chto-tut-bylo https://meduza.io/feature/2023/03/14/ukraina-obvinila-dvuh-rossiyskih-snayperov-v-iznasilovanii-chetyrehletney-devochki-i-ee-materi-v-kievskoy-oblasti https://meduza.io/feature/2018/08/24/150-let-nazad-rossiya-deportirovala-cherkesov-v-siriyu-teper-oni-begut-obratno-no-i-zdes-im-ne-rady avatar/photo images (download buttons are disabled by default, but still visible in fullscreen): https://meduza.io/feature/2023/09/14/moralnaya-otvetstvennost-budet-na-mne-do-kontsa-zhizni */