WikiArt Downloader

Add button to download full resolution images from WikiArt

  1. // ==UserScript==
  2. // @name WikiArt Downloader
  3. // @namespace https://greasyfork.org/en/scripts/492666-wikiart-downloader
  4. // @version 1.0
  5. // @description Add button to download full resolution images from WikiArt
  6. // @author CertifiedDiplodocus
  7. // @match https://www.wikiart.org/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=wikiart.org
  9. // @grant GM_addStyle
  10. // @license GPL-3.0-or-later
  11. // ==/UserScript==
  12.  
  13. /* PURPOSE: Adds a "download" button to WikiArt gallery, single painting, and fullscreen views.
  14. - Default filename from painting info.
  15. - To change the default formatting, edit the variable saveAsName in the function savePaintingAs() below.
  16. - Verbose logging for debugging purposes. Errors will always be logged.
  17.  
  18. LIST OF JSON ATTRIBUTES (e.g. painting.title):
  19. title, year, width, height, artistName,
  20. imageUrl (key renamed from "image"),
  21. paintingUrl (url on WikiArt, e.g. "/en/zdzislaw-beksinski/untitled-1972-0"),
  22. artistUrl (on WikiArt, e.g. "/en/zdzislaw-beksinski")
  23.  
  24. ----------------------------------------------------------------------------------------------------------------------
  25. */
  26.  
  27. (function() {
  28. 'use strict';
  29.  
  30. const enableVerboseLogging = false // set to 'true' for debugging purposes
  31.  
  32. // Attempt to find paintings
  33. verboseLog ('Page loading. Starting to search for painting information...')
  34. let painting
  35. let paintingInfoDiv = document.querySelector ('div[ng-init^="paintingJson"]') // why doesn't this detect the gallery items? (which I appreciate, but...)
  36. let galleryOuterContainer = document.querySelector ('.masonry-content')
  37.  
  38. if (!paintingInfoDiv && !galleryOuterContainer) {verboseLog ('No gallery or painting info found.'); return null} // EXIT
  39.  
  40. // Create download button in fullscreen mode
  41. const newButtonFullscreen = createButton ('a','download-button-fullscreen', downloadFromFullscreen);
  42. document.getElementsByClassName('sueprsized-navigation-panel-right')[0].appendChild(newButtonFullscreen); // typo in source code
  43.  
  44. // Create download button on the main artwork page
  45. if (paintingInfoDiv) {
  46. verboseLog('Found div with painting information.')
  47.  
  48. const newButtonStandard = createButton ('div', 'download-button-standard', downloadFromDetailedView)
  49. document.getElementsByClassName('fav-controls-wrapper')[0].appendChild (newButtonStandard)
  50. }
  51.  
  52. if (galleryOuterContainer) {
  53. verboseLog ('Gallery found! Waiting for user to select a painting...');
  54.  
  55. // On page load (before the user does anything) get the container and watch for paintings being added or removed
  56. let typeOfGallery = document.querySelector('.masonry-content').firstElementChild.getAttribute('ng-switch-when')
  57. let galleryContainer = getGalleryContainer ()
  58. if (galleryContainer) {
  59. createGalleryObserver()
  60. }
  61.  
  62. // If the user switches between details / masonry / text views, identify and create an observer for the new gallery
  63. const galleryTypeObserver = new MutationObserver (galleryTypeChange)
  64. galleryTypeObserver.observe (galleryOuterContainer, {childList: true})
  65.  
  66. function galleryTypeChange (mutations, galleryTypeObserver) {
  67. for (const mutation of mutations) {
  68. for (const addedNode of mutation.addedNodes) {
  69. if (addedNode.nodeType === 1) { // select elements, ignore comments
  70. typeOfGallery = addedNode.getAttribute('ng-switch-when')
  71. galleryContainer = getGalleryContainer ()
  72. if (galleryContainer) {
  73. createGalleryObserver()
  74. }
  75. }
  76. }
  77. }
  78. }
  79.  
  80. function getGalleryContainer () {
  81. let galleryMasonryContainer = document.querySelector ('.wiki-masonry-container')
  82. let galleryDetailedContainer = document.querySelector('.wiki-detailed-container')
  83. switch (typeOfGallery) {
  84. case 'detailed':
  85. case 'masonry':
  86. verboseLog (`A valid (${typeOfGallery}) gallery view was found.`);
  87. return galleryMasonryContainer || galleryDetailedContainer;
  88. case 'text':
  89. verboseLog ('Cannot download from gallery text view. Waiting for valid gallery type...');
  90. return false;
  91. default:
  92. console.error ('Could not evaluate the type of gallery.');
  93. return null
  94. }
  95. }
  96.  
  97. // If gallery items change...
  98. function createGalleryObserver() {
  99. const galleryObserver = new MutationObserver (galleryItemChanges)
  100. galleryObserver.observe (galleryContainer, {childList: true})
  101. }
  102.  
  103. // ...create download buttons for each newly-added painting. Buttons are appended to a <div> with the painting's unique ID or JSON.
  104. function galleryItemChanges(mutations, galleryObserver) {
  105. let a = 0
  106. let r = 0
  107. let newGalleryItems = [] // empty array
  108. let classOfButtonParent
  109. typeOfGallery = document.querySelector('[ng-switch-when]').getAttribute('ng-switch-when')
  110. switch (typeOfGallery) {
  111. case 'masonry': classOfButtonParent = '.title-block'; break;
  112. case 'detailed': classOfButtonParent = '.wiki-layout-painting-info-bottom'; break;
  113. }
  114.  
  115. // Select the added paintings (element type). Track removed paintings for debugging purposes.
  116. for (const mutation of mutations) {
  117. for (const addedNode of mutation.addedNodes) {
  118. if (addedNode.nodeType === 1) {
  119. let buttonParent = addedNode.querySelector(classOfButtonParent)
  120. newGalleryItems.push(buttonParent)
  121. a++
  122. }
  123. }
  124. for (const removedNode of mutation.removedNodes) {
  125. if (removedNode.nodeType === 1) {r++}
  126. }
  127. }
  128. verboseLog (`Loading: removed ${r} paintings, added ${a} paintings.`)
  129. if (a > 0) { createButtonsInGallery(newGalleryItems) }
  130. }
  131.  
  132. // Create download buttons in gallery view
  133. function createButtonsInGallery (galleryItemList) {
  134. verboseLog (`Created buttons in ${typeOfGallery} view.`)
  135. switch (typeOfGallery) {
  136. case 'masonry':
  137. galleryItemList.forEach(item => {
  138. const newButtonGallery = createButton ('div', 'download-button-gallery like-overlay', downloadFromGalleryMasonry)
  139. item.appendChild (newButtonGallery)
  140. })
  141. break;
  142. case 'detailed':
  143. galleryItemList.forEach(item => {
  144. const newButtonGallery = createButton ('div', 'download-button-standard', downloadFromDetailedView)
  145. item.querySelector('.fav-controls-wrapper').appendChild(newButtonGallery) // needs to be in the fav wrapper or it will not be clickable
  146. })
  147. }
  148. }
  149. }
  150.  
  151. // EVENT LISTENER HANDLERS -------------------------------------------------------
  152. // 'this' = the 'buttonGallery' element (https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#the_value_of_this_within_the_handler)
  153.  
  154. function downloadFromDetailedView() { // single and gallery modes
  155. parsePaintingJson (this.parentElement.parentElement)
  156. savePaintingAs()
  157. }
  158.  
  159. function downloadFromGalleryMasonry() {
  160. let parentId = this.parentElement.id
  161. painting = {
  162. title: document.querySelector(`#${parentId} .artwork-name`).innerText,
  163. year: document.querySelector(`#${parentId} .artwork-year`).innerText,
  164. artistName: document.querySelector(`#${parentId} .artist-name`).innerHTML.trim().split('&nbsp;')[0],
  165. imageUrl: this.parentElement.parentElement.querySelector('img').src.split('!')[0]
  166. }
  167. savePaintingAs()
  168. }
  169.  
  170. function downloadFromFullscreen() {
  171. painting = {
  172. title: document.querySelector('.supersized-slide-name').title,
  173. year: document.querySelector('span.year').innerText.slice(1).slice(0,-1), // remove the first and last characters: (1956) -> 1956
  174. artistName: document.querySelector('.supersized-slide-header').title,
  175. imageUrl: document.querySelector('.primary-image').src
  176. }
  177. savePaintingAs()
  178. }
  179.  
  180. function parsePaintingJson (sourceDiv) {
  181. let initContent = sourceDiv.getAttribute('ng-init')
  182. let jsonString = initContent.match('paintingJson = ({.*?})')[1] // clean up
  183. jsonString = jsonString.replace('"image" :','"imageUrl" :') // better key name
  184.  
  185. if (!jsonString || jsonString.length<=1) {console.error ("Could not extract JSON from the element."); return null} // EXIT
  186. verboseLog ('Extracted JSON string:', jsonString);
  187.  
  188. try {
  189. painting = JSON.parse(jsonString);
  190. verboseLog (painting.title + ' - ' + painting.year + ' (' + painting.artistName + ')');
  191. verboseLog ('Painting information parsed and extracted!');
  192. } catch (e) {
  193. console.error ('Error parsing JSON:', e);
  194. alert ('There was an error parsing the painting information. Check the console for more details.');
  195. };
  196. }
  197.  
  198. // When a download button is clicked, save URL with the default filename = ARTIST - TITLE (YEAR).EXT
  199. function savePaintingAs () {
  200. const imgExtension = painting.imageUrl.split('.').pop(); // pop() returns the last element from an array; in this case, the extension
  201. let saveAsName = `${painting.artistName} - ${painting.title} (${painting.year}).${imgExtension}`;
  202. saveAsName = decodeHTML(saveAsName); // handle special characters (í, ç etc)
  203. downloadImage(painting.imageUrl, saveAsName);
  204. }
  205.  
  206. // FUNCTIONS AND STYLES ---------------------------------------------------------------
  207.  
  208. // Create a button with an event listener
  209. function createButton (elementType, buttonClass, callbackFunctionName) {
  210. let newButton = document.createElement (elementType)
  211. newButton.className = buttonClass
  212. newButton.addEventListener ('click', callbackFunctionName, false)
  213. return newButton
  214. }
  215.  
  216. // Check for HTML codes in text and convert to characters
  217. function decodeHTML (stringInput) {
  218. let txt = document.createElement("textarea");
  219. txt.innerHTML = stringInput;
  220. return txt.value;
  221. }
  222.  
  223. // Save image using fetch (a workaround because Chrome and Firefox both block the "download" attribute for images on different domains)
  224. async function downloadImage(imageSrc, imageName) {
  225. const image = await fetch(imageSrc)
  226. const imageBlob = await image.blob()
  227. const imageURL = URL.createObjectURL(imageBlob)
  228.  
  229. const link = document.createElement('a')
  230. link.href = imageURL
  231. link.download = imageName
  232. document.body.appendChild(link)
  233. link.click()
  234. document.body.removeChild(link)
  235. }
  236.  
  237. function verboseLog (textForConsole) {
  238. if (!enableVerboseLogging) {return null}
  239. console.log (textForConsole)
  240. }
  241.  
  242. //--- Style newly-added button in CSS. Modify the page CSS to make room for the button (overwrite with !important flag).
  243. GM_addStyle ( `
  244. .download-button-fullscreen {
  245. display:block;
  246. height:40px;
  247. width:40px;
  248. background:url(https://upload.wikimedia.org/wikipedia/commons/8/8a/Download-icon.svg) center center no-repeat;
  249. margin:0 10px;
  250. cursor:pointer
  251. }
  252. .download-button-standard {
  253. display:block;
  254. height:40px;
  255. width:40px;
  256. position:absolute;
  257. right:0px;
  258. background:url(https://upload.wikimedia.org/wikipedia/commons/7/72/Download-icon-green.svg) center center no-repeat;
  259. background-size:24px;
  260. cursor:pointer
  261. }
  262. .download-button-gallery {
  263. left:calc(50% - 20px - 8px - 40px) !important;
  264. background:url(https://upload.wikimedia.org/wikipedia/commons/7/72/Download-icon-green.svg) center center no-repeat !important;
  265. background-size:24px !important;
  266. /*; background-color:#e9e9eb !important */
  267. }
  268. .wiki-masonry-container>li:hover .title-block .like-overlay.like-overlay-left {
  269. left:calc(50% - 20px) !important
  270. }
  271. .wiki-masonry-container>li:hover .title-block .like-overlay.like-overlay-right {
  272. left:calc(50% + 20px + 8px) !important
  273. }
  274.  
  275. /* Fix weird formatting in gallery detailed view */
  276. .fav-controls-wrapper {
  277. width:120px !important
  278. }
  279. .copyright-wrapper {
  280. width:calc(100% - 120px) !important
  281. }
  282. .fav-controls-heart {
  283. top:0px !important
  284. }
  285. .fav-controls-folder {
  286. top:0px !important;
  287. right:40px !important
  288. }
  289. ` );
  290.  
  291. })();