From 0d8a162a3b5f12e432a46d320625564e4c569635 Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Fri, 13 Oct 2017 16:09:08 +0200 Subject: [PATCH 01/14] youtube-hide-watched: Start work on rewriting userscripts to pure ecmascript --- .../youtube-hide-watched.meta.js | 3 +- .../youtube-hide-watched.user.js | 74 +++---------------- 2 files changed, 13 insertions(+), 64 deletions(-) diff --git a/youtube-hide-watched/youtube-hide-watched.meta.js b/youtube-hide-watched/youtube-hide-watched.meta.js index 7c68d00..8660218 100644 --- a/youtube-hide-watched/youtube-hide-watched.meta.js +++ b/youtube-hide-watched/youtube-hide-watched.meta.js @@ -7,8 +7,7 @@ // @copyright 2014, Joost Bremmer // @license MIT // @version 1.1.3 -// @date 13-06-2015 -// @require http://code.jquery.com/jquery-latest.min.js +// @date 15-10-2017 // @downloadURL https://rawgit.com/ToostInc/userscripts/master/youtube-hide-watched/youtube-hide-watched.user.js // @updateURL https://rawgit.com/ToostInc/userscripts/master/youtube-hide-watched/youtube-hide-watched.meta.js // @grant none diff --git a/youtube-hide-watched/youtube-hide-watched.user.js b/youtube-hide-watched/youtube-hide-watched.user.js index 13a1608..0ab661a 100644 --- a/youtube-hide-watched/youtube-hide-watched.user.js +++ b/youtube-hide-watched/youtube-hide-watched.user.js @@ -7,16 +7,13 @@ // @copyright 2014, Joost Bremmer // @license MIT // @version 1.1.3 -// @date 13-06-2015 -// @require http://code.jquery.com/jquery-latest.min.js +// @date 15-10-2017 // @downloadURL https://rawgit.com/ToostInc/userscripts/master/youtube-hide-watched/youtube-hide-watched.user.js // @updateURL https://rawgit.com/ToostInc/userscripts/master/youtube-hide-watched/youtube-hide-watched.meta.js // @grant none // ==/UserScript== -// The MIT License -// // Copyright (c) 2014 Joost Bremmer // // Permission is hereby granted, free of charge, to any person obtaining a @@ -38,65 +35,18 @@ // TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE // SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -$(document).ready (function () { - //Add mutation observer, checks for changes in DOM - if (MutationObserver) { - var myObserver = new MutationObserver(hideWatched); - } - else { - var myObserver = new WebKitMutationObserver(hideWatched); - } - myObserver.observe(document, { childList : true, subtree : true }); - hideWatched(); - - // Add checkbox - var checker = '
  • \n'+ - '\t\n'+ - '
  • '; - $("#appbar-nav .appbar-nav-menu").prepend(checker); - $("#checker-container").css({ - 'color': "#666", - "vertical-align" : "middle", - "text-align" : "center" - }); - //checkbox event - $("#hide-videos").change(function() { - if ( $(this).is(":not(:checked)") ) { - showWatched(); - } - - else { - hideWatched(); - }; - - }); - - //BONUS: always enable load more button. - $("button.load-more-button").removeProp("disabled"); - - hideWatched(); - - +document.addEventListener("DOMContentLoaded", event => { + console.log("Prestart"); + main(); }); +function main(){ + console.log("Start!"); -function hideWatched () { - - if ( $("#hide-videos").is(":checked") ) { - $("div.watched-badge").each(function() { - $(this).closest("ol.item-section").hide("200"); - - }); - - } + let viewedVideos = document.querySelectorAll('#progress[style="width: 100%;"]'); + if (viewedVideos.length <= 0) { + return -1; + } else { + console.log(viewedVideos); + } }; - -function showWatched() { - $("div.watched-badge").each(function() { - $(this).closest("ol.item-section").show("300"); - - }); -} From a9d0a9662b26ab754e740b481f34837013742325 Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Thu, 19 Oct 2017 13:35:51 +0200 Subject: [PATCH 02/14] youtube-hide-watched: Work on mutationobservers --- .eslintrc.yaml | 87 +++++++++++ .../youtube-hide-watched.meta.js | 9 +- .../youtube-hide-watched.user.js | 138 +++++++++++++----- 3 files changed, 192 insertions(+), 42 deletions(-) create mode 100644 .eslintrc.yaml diff --git a/.eslintrc.yaml b/.eslintrc.yaml new file mode 100644 index 0000000..60add53 --- /dev/null +++ b/.eslintrc.yaml @@ -0,0 +1,87 @@ +# This is a baseline set of rules intended for usage in any ES6-based project. +# +# Be sure to add your project's specific environment(s) as needed via external configuration. +# See: http://eslint.org/docs/user-guide/configuring#specifying-environments + +--- +env: + es6: true + browser: true + +# http://eslint.org/docs/rules +rules: + + # Possible Errors + + valid-jsdoc: [1] + + # Best Practices + curly: [2] + dot-notation: [2] + eqeqeq: [1] + guard-for-in: [2] + no-div-regex: [2] + no-eval: [2] + no-extend-native: [2] + no-floating-decimal: [2] + no-implied-eval: [2] + no-labels: [2] + no-lone-blocks: [2] + no-loop-func: [2] + no-multi-spaces: [2] + no-native-reassign: [2] + no-new-wrappers: [2] + no-new: [2] + no-redeclare: [2] + no-return-assign: [2] + no-self-compare: [2] + radix: [2] + wrap-iife: [2, "inside"] + yoda: [2, "never"] + + # Strict Mode + + # strict: [2, "global"] + + # Variables + + no-shadow: [2] + no-use-before-define: [2] + + # Stylistic Issues + + block-spacing: [2, "always"] + brace-style: [2, "1tbs", allowSingleLine: true] + camelcase: [2, properties: "always"] + comma-style: [2, "last"] + consistent-this: [2, "self"] + eol-last: [2] + indent: [2, 2, {"VariableDeclarator": { "var": 2, "let": 2, "const": 3}}] + linebreak-style: [2, "unix"] + newline-after-var: [2] + no-lonely-if: [2] + no-trailing-spaces: [2] + no-unneeded-ternary: [2] + quotes: [2, "double"] + semi: [2, "always"] + spaced-comment: [2, "always"] + wrap-regex: [2] + + # ES6 + prefer-arrow-callback: [1] + no-console: [0] + + # Overrides to `eslint:recommended` rule set + + +extends: "eslint:recommended" + + +parserOptions: + arrowFunctions: true + blockBindings: true + defaultParams: true + destructuring: true + forOf: true + spread: true + templateStrings: true diff --git a/youtube-hide-watched/youtube-hide-watched.meta.js b/youtube-hide-watched/youtube-hide-watched.meta.js index 8660218..45db599 100644 --- a/youtube-hide-watched/youtube-hide-watched.meta.js +++ b/youtube-hide-watched/youtube-hide-watched.meta.js @@ -7,8 +7,11 @@ // @copyright 2014, Joost Bremmer // @license MIT // @version 1.1.3 -// @date 15-10-2017 -// @downloadURL https://rawgit.com/ToostInc/userscripts/master/youtube-hide-watched/youtube-hide-watched.user.js -// @updateURL https://rawgit.com/ToostInc/userscripts/master/youtube-hide-watched/youtube-hide-watched.meta.js +// @date 19-10-2017 +// @downloadURL +// https://rawgit.com/ToostInc/userscripts/master/youtube-hide-watched/youtube-hide-watched.user.js +// @updateURL +// https://rawgit.com/ToostInc/userscripts/master/youtube-hide-watched/youtube-hide-watched.meta.js // @grant none +// @runat document-end // ==/UserScript== diff --git a/youtube-hide-watched/youtube-hide-watched.user.js b/youtube-hide-watched/youtube-hide-watched.user.js index 0ab661a..b6e25b0 100644 --- a/youtube-hide-watched/youtube-hide-watched.user.js +++ b/youtube-hide-watched/youtube-hide-watched.user.js @@ -7,46 +7,106 @@ // @copyright 2014, Joost Bremmer // @license MIT // @version 1.1.3 -// @date 15-10-2017 -// @downloadURL https://rawgit.com/ToostInc/userscripts/master/youtube-hide-watched/youtube-hide-watched.user.js -// @updateURL https://rawgit.com/ToostInc/userscripts/master/youtube-hide-watched/youtube-hide-watched.meta.js +// @date 19-10-2017 +// @downloadURL +// https://rawgit.com/ToostInc/userscripts/master/youtube-hide-watched/youtube-hide-watched.user.js +// @updateURL +// https://rawgit.com/ToostInc/userscripts/master/youtube-hide-watched/youtube-hide-watched.meta.js // @grant none +// @runat document-end // ==/UserScript== +/** + * Copyright (c) 2014 Joost Bremmer + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ -// Copyright (c) 2014 Joost Bremmer -// -// Permission is hereby granted, free of charge, to any person obtaining a -// copy of this software and associated documentation files -// (the "Software"), to deal in the Software without restriction, -// including without limitation the rights to use, copy, modify, merge, -// publish, distribute, sublicense, and/or sell copies of the Software, and -// to permit persons to whom the Software is furnished to do so, subject to -// the following conditions: -// -// The above copyright notice and this permission notice shall be included -// in all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -document.addEventListener("DOMContentLoaded", event => { - console.log("Prestart"); - main(); -}); - -function main(){ - console.log("Start!"); - - let viewedVideos = document.querySelectorAll('#progress[style="width: 100%;"]'); - if (viewedVideos.length <= 0) { - return -1; - } else { - console.log(viewedVideos); - } -}; +console.log("Start!"); + +const viewedVideos=[]; + +function getWatchedVideos() { + try { + console.log("Start async func") + + return new Promise((resolve, reject) => { + function checkMutation(mutation) { + if (mutation.target.tagName === + "YTD-THUMBNAIL-OVERLAY-PLAYBACK-STATUS-RENDERER") { + viewedVideos.push(mutation.target); + if (viewedVideos.length == 1) { + console.log(viewedVideos); + resolve(viewedVideos); + } + } + } + const mutate = new MutationObserver((mutations) => { + mutations.forEach(mutation => { checkMutation(mutation); }); + }); + Array.prototype.forEach.call( + document.getElementsByTagName("ytd-thumbnail"), + (element) => { + mutate.observe(element, { childList : true, subtree : true }) + } + ); + }); + } catch (e) { + console.log(e); + reject(e); + } +} + +async function hideWatched() { + elements = getWatchedVideos().then(() => { + console.log(viewedVideos.length); + viewedVideos.forEach((element) => { + console.log("hiding this", element); + element.style.display="none"; + }); + }); + console.log(elements.constructor.name.toString()); + console.log(elements.length); + /* Array.prototype.filter.call(viewedVideos, video => { */ + /* console.log("hideWatched", video, video.style, video.style.width); */ + /* return video.getAttribute("width") === "100%"; */ + /* }); */ +} + + +/* console.log("videolist",document.getElementsByTagName("ytd-section-list-renderer")); */ +/* console.log("thumbnail", document.getElementsByTagName("ytd-thumbnail")); */ + +/* + * Array.prototype.forEach.call( + * document.getElementsByTagName("ytd-thumbnail"), elementToObserve => { + * mutate.observe(elementToObserve, { childList : true, subtree : true }); + * } + * ); + */ + + +/* TODO: checkbox */ + +console.log(""); +hideWatched(); +console.log("End"); + +/* vim: set ts=2 sts=2 sw=2 et: */ From e1f247d54391e6b24bfbd9aa03ec43a2067aca60 Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Sat, 7 Jul 2018 19:22:53 +0200 Subject: [PATCH 03/14] whyfala: initial script --- whyfalala/whyfalalala.meta.js | 12 ++++ whyfalala/whyfalalala.user.js | 125 ++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 whyfalala/whyfalalala.meta.js create mode 100644 whyfalala/whyfalalala.user.js diff --git a/whyfalala/whyfalalala.meta.js b/whyfalala/whyfalalala.meta.js new file mode 100644 index 0000000..38f5ed8 --- /dev/null +++ b/whyfalala/whyfalalala.meta.js @@ -0,0 +1,12 @@ +// ==UserScript== +// @name Whyfalalala download gallery +// @namespace https://github.com/ToostInc/userscripts +// @description Adds download links to scanlation posts +// @include https://whyfalalala.wordpress.com/* +// @author Joost Bremmer < contact@madeofmagicandwires.online > +// @copyright 2018, Joost Bremmer +// @license MIT +// @version `0.1 +// @date 2108-07-07 +// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js +// ==/UserScript== diff --git a/whyfalala/whyfalalala.user.js b/whyfalala/whyfalalala.user.js new file mode 100644 index 0000000..b8e144e --- /dev/null +++ b/whyfalala/whyfalalala.user.js @@ -0,0 +1,125 @@ +// ==UserScript== +// @name Whyfalalala download gallery +// @namespace https://github.com/ToostInc/userscripts +// @description Adds download links to scanlation posts +// @include https://whyfalalala.wordpress.com/* +// @author Joost Bremmer < contact@madeofmagicandwires.online > +// @copyright 2018, Joost Bremmer +// @license MIT +// @version `0.1 +// @date 2108-07-07 +// @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js +// ==/UserScript== + +async function getImageBlob(url){ + return fetch(`https://cors-anywhere.herokuapp.com/${url}`).then(function (response) { + return response.blob(); + }); +} + + +async function getChapterInfo(article){ + let chapter = {}; + + if (article.classList.contains("tag-scanlation")) { + chapter.title = article.getElementsByTagName("h1")[0].innerText; + + if (article.getElementsByClassName("tiled-gallery").length > 0) { + chapter.images = []; + let images = Array.from(article.getElementsByTagName("img")); + for (let img of images) { + let chapImg = {}; + + chapImg.url = img.dataset.origFile; + chapImg.fileName = `${img.dataset.imageTitle}.${chapImg.url.match(/(\w{3,4})$/g)[0]}`; + chapter.images.push(chapImg); + } + } else { + chapter.images = null; + } + return chapter; + } else { + return null; + } +} + +//See https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404 +async function asyncForEach(array, callback) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array) + } +} + + + +function createElements(article) { + let container = document.createElement("div"); + let textArea = document.createElement("textarea"); + let downloadButton = document.createElement("a"); + + + container.classList.add("container"); + textArea.value = "Loading..."; + downloadButton.innerText = "Loading..."; + + container.appendChild(textArea); + container.appendChild(downloadButton); + if (article.getElementsByClassName("entry-summary").length > 0) { + article.getElementsByClassName("entry-summary")[0].appendChild(container); + } else { + article.getElementsByClassName("entry-content")[0].appendChild(container); + } + + + return container; + +} + +function createFile(anchor, data, filename) { + let url = URL.createObjectURL(data); + + anchor.href = url; + anchor.download = filename; + anchor.innerText = "Download"; +} + +async function createZip(chapter) { + let zip = new JSZip(); + + zipCh = zip.folder(chapter.title); + await asyncForEach(chapter.images, async function(imgObj) { + await zipCh.file(imgObj.fileName, getImageBlob(imgObj.url)); + }); + + return zip.generateAsync({type: 'blob', comment: 'generated by whyfalalala downloader'}); +} + +let chapters = []; + +async function start() { + console.log("Start!"); + + let articles = document.getElementsByTagName("article"); + + await asyncForEach(articles, async (article) => { + chapter = await getChapterInfo(article); + if (chapter !== null && chapter.images !== null) { + chapters.push(chapter); + + let container = createElements(article); + let textArea = container.children[0]; + + container.id = `chapter_${chapter.title.replace(' ', '_')}`; + + imageUrls = chapter.images.map((img) => img.url); + textArea.value = imageUrls.join("\n"); + + let chapterZip = await createZip(chapter); + await createFile(container.children[1], chapterZip, `${chapter.title}.cbz`); + } + + }); + return 0; +} + +start(); From 77a77597eabff3e50302ddc62ea6006dd9585fc9 Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Sun, 8 Jul 2018 02:22:52 +0200 Subject: [PATCH 04/14] whyfalalala: save to madokami filename conventions + function madokamiFileName() generates a madokami-compatible filename based on the chapter's title. + Seperate code out into generic wordpress code and blog specific code. --- whyfalala/whyfalalala.meta.js | 2 +- whyfalala/whyfalalala.user.js | 130 +++++++++++++++++++++++----------- 2 files changed, 88 insertions(+), 44 deletions(-) diff --git a/whyfalala/whyfalalala.meta.js b/whyfalala/whyfalalala.meta.js index 38f5ed8..e6de74d 100644 --- a/whyfalala/whyfalalala.meta.js +++ b/whyfalala/whyfalalala.meta.js @@ -6,7 +6,7 @@ // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT -// @version `0.1 +// @version `0.2 // @date 2108-07-07 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== diff --git a/whyfalala/whyfalalala.user.js b/whyfalala/whyfalalala.user.js index b8e144e..8e0d2e8 100644 --- a/whyfalala/whyfalalala.user.js +++ b/whyfalala/whyfalalala.user.js @@ -6,53 +6,71 @@ // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT -// @version `0.1 +// @version `0.2 // @date 2108-07-07 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== +const groupName = 'whyfalalala'; + async function getImageBlob(url){ return fetch(`https://cors-anywhere.herokuapp.com/${url}`).then(function (response) { return response.blob(); }); } +function madokamiFileName(chapter,groupName){ + let chapterInfo = chapter.title.split(/(^.*?)\s?((volume|v)\s?\d{1,2})?\s?((chapter|ch|c)?\s?\d{1,}\-?\d{1,})/gi); + chapterInfo = chapterInfo.splice(1, chapterInfo.length - 2); + + let seriesTitle = chapterInfo[0]; + let volumeNo = (chapterInfo[1]) ? chapterInfo[1] : ''; + let chapterNo = (chapterInfo[3]) ? chapterInfo[3] : ''; + + volumeNo = volumeNo.replace(/(volume|vol)\s?/i, ''); + chapterNo = chapterNo.replace(/(chapter|ch|c)\s?/i, ''); + + // More manipulation requred for volume no. as its optional + if (volumeNo.length < 2 && !volumeNo === ''){ volumeNo = `0${volumeNo}`; } // nullpad + if (volumeNo !== '') { volumeNo = `(v${volumeNo})`; } // output: '(v??)' + + // Example output: 'Giant Killing - c001 (v01) [groupName]' + let madokamiStr = `${seriesTitle} - c${chapterNo} ${volumeNo}[${groupName}]`; + + return madokamiStr; +} + -async function getChapterInfo(article){ +function getChapterInfo(post){ let chapter = {}; - if (article.classList.contains("tag-scanlation")) { - chapter.title = article.getElementsByTagName("h1")[0].innerText; - - if (article.getElementsByClassName("tiled-gallery").length > 0) { - chapter.images = []; - let images = Array.from(article.getElementsByTagName("img")); - for (let img of images) { - let chapImg = {}; - - chapImg.url = img.dataset.origFile; - chapImg.fileName = `${img.dataset.imageTitle}.${chapImg.url.match(/(\w{3,4})$/g)[0]}`; - chapter.images.push(chapImg); - } - } else { - chapter.images = null; + chapter.title = post.getElementsByTagName("h1")[0].innerText; + chapter.fileName = madokamiFileName(chapter, groupName); + + if (post.getElementsByClassName("tiled-gallery").length > 0) { + chapter.images = []; + let images = Array.from(post.getElementsByTagName("img")); + for (let img of images) { + let chapImg = {}; + + chapImg.url = img.dataset.origFile; + chapImg.fileName = `${img.dataset.imageTitle}.${chapImg.url.match(/(\w{3,4})$/g)[0]}`; + chapter.images.push(chapImg); } - return chapter; } else { - return null; + chapter.images = null; } + return chapter; } -//See https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404 +// See https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404 async function asyncForEach(array, callback) { for (let index = 0; index < array.length; index++) { await callback(array[index], index, array) } } - - -function createElements(article) { +function createElements(post) { let container = document.createElement("div"); let textArea = document.createElement("textarea"); let downloadButton = document.createElement("a"); @@ -61,18 +79,29 @@ function createElements(article) { container.classList.add("container"); textArea.value = "Loading..."; downloadButton.innerText = "Loading..."; + downloadButton.style = ` + margin: 5px 0; + display: inline-block; + background: #f8f8f8; + padding: 5px 4px; + border-radius: 5px; + text-decoration: none; + border-color: #bbb; + border-width: 1px; + border-style: solid; + `; container.appendChild(textArea); container.appendChild(downloadButton); - if (article.getElementsByClassName("entry-summary").length > 0) { - article.getElementsByClassName("entry-summary")[0].appendChild(container); + + // append to entry-summary for blogroll or entry-content for specific post + if (post.getElementsByClassName("entry-summary").length > 0) { + post.getElementsByClassName("entry-summary")[0].appendChild(container); } else { - article.getElementsByClassName("entry-content")[0].appendChild(container); + post.getElementsByClassName("entry-content")[0].appendChild(container); } - return container; - } function createFile(anchor, data, filename) { @@ -96,29 +125,44 @@ async function createZip(chapter) { let chapters = []; -async function start() { - console.log("Start!"); +/* +* Blog specific stuff starts here. +*/ - let articles = document.getElementsByTagName("article"); +async function getChapters() { + let chapterElements = Array.prototype.filter.call(document.getElementsByTagName("article"), (article) => { + if (article.classList.contains("tag-translation")) { + return true; + } + return false; - await asyncForEach(articles, async (article) => { - chapter = await getChapterInfo(article); - if (chapter !== null && chapter.images !== null) { - chapters.push(chapter); + }); - let container = createElements(article); - let textArea = container.children[0]; + let statusCode = await asyncForEach(chapterElements, async (article) => { + chapter = getChapterInfo(article); + if (chapter !== null && chapter.images !== null) { + console.log(`Found ${chapter.title}`); + chapters.push(chapter); - container.id = `chapter_${chapter.title.replace(' ', '_')}`; + let container = createElements(article); + let textArea = container.children[0]; - imageUrls = chapter.images.map((img) => img.url); - textArea.value = imageUrls.join("\n"); + container.id = `chapter_${chapter.title.replace(' ', '_')}`; - let chapterZip = await createZip(chapter); - await createFile(container.children[1], chapterZip, `${chapter.title}.cbz`); - } + imageUrls = chapter.images.map((img) => img.url); + textArea.value = imageUrls.join("\n"); + let chapterZip = await createZip(chapter); + await createFile(container.children[1], chapterZip, `${chapter.fileName}.cbz`); + return 0; + } else { return -1; } + return statusCode; }); +} + +async function start() { + console.log("Start!"); + await getChapters(); return 0; } From 386b9853b56060cc19c9a336c4b6876079c77a97 Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Sun, 1 Jul 2018 14:42:34 +0200 Subject: [PATCH 05/14] madokami: Add initial version of madokami-list-files Signed-off-by: Joost Bremmer --- .../madokami-list-files.meta.js | 0 .../madokami-list-files.user.js | 94 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 madokami-list-files/madokami-list-files.meta.js create mode 100644 madokami-list-files/madokami-list-files.user.js diff --git a/madokami-list-files/madokami-list-files.meta.js b/madokami-list-files/madokami-list-files.meta.js new file mode 100644 index 0000000..e69de29 diff --git a/madokami-list-files/madokami-list-files.user.js b/madokami-list-files/madokami-list-files.user.js new file mode 100644 index 0000000..01c88e2 --- /dev/null +++ b/madokami-list-files/madokami-list-files.user.js @@ -0,0 +1,94 @@ +"use strict"; + +/* + * update html + */ +function updateLinks(anchors = []) { + let linksText = document.getElementById("linksText"); + let links = anchors.map((anchor) => decodeURIComponent(anchor.href)); + + linksText.innerHTML = links.sort().join("\n"); +} + +/* + * Recursively parse download links from a page. + * + * Returns an Array containing anchor elements of download links + */ +function parsePage(page, anchors = []) { + + /* + * Make an XMLHttpRequest + */ + function makeRequest (method = "GET", href) { + + let xhr = new XMLHttpRequest(); + + xhr.open(method, href, true); + xhr.responseType = "document"; + xhr.onload = function() { + parsePage(this.responseXML, anchors); + updateLinks(anchors); + }; + xhr.onerror = function () { + console.error("failed with" , this.status , xhr.statusText); + }; + xhr.send(); + } + + let re = /^https?:\/\/.*(?!(reader).)*(\.cbr|\.cbz|\.rar|\.zip)$/; + + Array.prototype.forEach.call(page.getElementsByTagName("a"), (anchor) => { + if (anchor.href.startsWith(window.location.href) && re.test(anchor.href)) { + anchors.push(anchor); + } else if (anchor.href.startsWith(page.URL) && anchor.innerText.endsWith("/") && !(anchor.href === page.URL)) { + makeRequest("GET", anchor.href, anchors); + } + }); + return anchors; +} + +function getDownloads() { + let downloadAnchors = parsePage(document, []); + + return downloadAnchors.map((anchorElement) => decodeURIComponent(anchorElement.href)); +} + + +/* + * Create a file containing data + */ +function createFile(data) { + let file = new Blob([data], {"type": "text/plain"}); + + return URL.createObjectURL(file); +} + +/* + * Create new DOM elements and fill them in + */ +function initElements() { + let indexContainer = document.getElementsByClassName("table-outer"); + let linksContainer = document.createElement("div"); + let linksText = document.createElement("textarea"); + + linksText.setAttribute("style", "min-width: 100%; margin: 30px 0; min-height: 400px"); + linksText.id = "linksText"; + indexContainer[0].appendChild(linksContainer).appendChild(linksText); + + let links = getDownloads(); + + linksText.innerHTML = links.sort().join("\n"); + + let fileLink = document.createElement("a"); + + fileLink.classList.add("button"); + fileLink.innerText = "Download to file"; + fileLink.download = "urls"; + fileLink.addEventListener("click", () => { fileLink.href = createFile(linksText.value); }); + + linksContainer.appendChild(fileLink); +} + +initElements(); + From 7812343501958e423d6bdb14357a729f48de550f Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Wed, 11 Jul 2018 17:33:08 +0200 Subject: [PATCH 06/14] whyfalalala: Version 0.3 + Chapter related functions moved to Chapter class + Chapter.madokamiFileName() now takes source into account (sort of) + Added JSDOC documentation Signed-off-by: Joost Bremmer --- whyfalala/whyfalalala.meta.js | 6 +- whyfalala/whyfalalala.user.js | 334 +++++++++++++++++++++++++++------- 2 files changed, 267 insertions(+), 73 deletions(-) diff --git a/whyfalala/whyfalalala.meta.js b/whyfalala/whyfalalala.meta.js index e6de74d..1e90650 100644 --- a/whyfalala/whyfalalala.meta.js +++ b/whyfalala/whyfalalala.meta.js @@ -1,12 +1,12 @@ // ==UserScript== -// @name Whyfalalala download gallery +// @name Whyfalalala download WordPress gallery images // @namespace https://github.com/ToostInc/userscripts -// @description Adds download links to scanlation posts +// @description Adds download links to posts containing a gallery // @include https://whyfalalala.wordpress.com/* // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT -// @version `0.2 +// @version 0.3 // @date 2108-07-07 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== diff --git a/whyfalala/whyfalalala.user.js b/whyfalala/whyfalalala.user.js index 8e0d2e8..2dace72 100644 --- a/whyfalala/whyfalalala.user.js +++ b/whyfalala/whyfalalala.user.js @@ -1,75 +1,228 @@ // ==UserScript== -// @name Whyfalalala download gallery +// @name Whyfalalala download WordPress gallery images // @namespace https://github.com/ToostInc/userscripts -// @description Adds download links to scanlation posts +// @description Adds download links to posts containing a gallery // @include https://whyfalalala.wordpress.com/* // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT -// @version `0.2 +// @version 0.3 // @date 2108-07-07 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== -const groupName = 'whyfalalala'; - -async function getImageBlob(url){ - return fetch(`https://cors-anywhere.herokuapp.com/${url}`).then(function (response) { - return response.blob(); - }); -} - -function madokamiFileName(chapter,groupName){ - let chapterInfo = chapter.title.split(/(^.*?)\s?((volume|v)\s?\d{1,2})?\s?((chapter|ch|c)?\s?\d{1,}\-?\d{1,})/gi); - chapterInfo = chapterInfo.splice(1, chapterInfo.length - 2); +/** + * @fileOverview A userscript to download (manga) chapters from WordPress based galleries + * @name whyfalalala + */ + + +/** + * String representing the scanlation group. + * @const + * @type {String} + */ +const GROUPNAME = "whyfalalala"; + +/** + * Enum for the scanlation source + * @const + * @enum {String} + */ +const SOURCE = { + /** Use if the source of the scanlation is from magazine scans */ + MAGAZINE: "mag", + /** Use if the source of the scanlation is from mixed scans. */ + MIXED: "mix", + /** Use if the source of the scanlation is from tonkabon scans. */ + VOLUME: "v", + /** Use if the source of the scanlation is from online sources. */ + WEB: "web", + /** Use if the source of the scanlation scans are unknown. */ + UNKNOWN: undefined +}; + + + +/** + * @class Chapter + * @classdec Class representing a chapter. + * @global + * @property {String} title - The title of the chapter, as taken from the post + * @property {String} fileName - The filename of the chapter; generated .from the title. + * @property {?Array.} images - Array of image objects of each image found in post. + * @porperty {String} images[].url - The url of where the image resides. + * @porperty {String} images[].fileName - The file name to where the image needs to be saved to. + */ +class Chapter{ + /** + * Creates a Chapter instance. + * @constructor + * @param {HTMLElement} post - the root element of a WordPress post; usually
    + */ + constructor(post) { + /** @private */ + this._post = post; + /** @private */ + this._title = post.getElementsByTagName("h1")[0].innerText; + /** @private */ + this._fileName = Chapter.madokamiFileName(this, GROUPNAME, SOURCE.VOLUME); + /** @private */ + this._images = this.getImages(this._post); + } - let seriesTitle = chapterInfo[0]; - let volumeNo = (chapterInfo[1]) ? chapterInfo[1] : ''; - let chapterNo = (chapterInfo[3]) ? chapterInfo[3] : ''; + get title() { + return this._title; + } - volumeNo = volumeNo.replace(/(volume|vol)\s?/i, ''); - chapterNo = chapterNo.replace(/(chapter|ch|c)\s?/i, ''); + set title(value) { + this._title = value; + } - // More manipulation requred for volume no. as its optional - if (volumeNo.length < 2 && !volumeNo === ''){ volumeNo = `0${volumeNo}`; } // nullpad - if (volumeNo !== '') { volumeNo = `(v${volumeNo})`; } // output: '(v??)' + get fileName() { + return this._fileName; + } - // Example output: 'Giant Killing - c001 (v01) [groupName]' - let madokamiStr = `${seriesTitle} - c${chapterNo} ${volumeNo}[${groupName}]`; + set fileName(fName) { + this._fileName = fName; + } - return madokamiStr; -} + get images() { + return this._images; + } + set images(post) { + this._images = this.getImages(post); + } -function getChapterInfo(post){ - let chapter = {}; + /** + * Estimates a filename compatible with the Madokami Naming Scheme based on the + * chapter title. + * @see {@link https://github.com/Daiz/manga-naming-scheme} for more info. + * + * @method Chapter.madokamiFileName + * @static + * @param {Chapter} chapter - a Chapter object to base the filename on + * @param {String} chapter.title - the title of the chapter + * + * @param {String} groupName - the name of the scanlation group to use in the filename + * @param {SOURCE.Enum} source - the source of the scanlation can be either (mag|mix|v|web) + * + * @returns {String} the estimated madokami filename; Still needs to be adjusted slightly + */ + static madokamiFileName(chapter, groupName=GROUPNAME, source=SOURCE.UNKNOWN){ + let chapterInfo = chapter.title.split(/(^.*?)\s?((volume|v)\s?\d{1,2})?\s?((chapter|ch|c)?\s?\d{1,}-?\d{1,})/gi); + + chapterInfo = chapterInfo.splice(1, chapterInfo.length - 2); + + let seriesTitle = chapterInfo[0]; + let chapterNo = (chapterInfo[3]) ? chapterInfo[3].replace(/(chapter|ch|c)\s?/i, "") : ""; + let sourceStr = null; + + // Deterimine source. + switch(source) { + case SOURCE.MAGAZINE: + sourceStr = "(mag) "; + break; + case SOURCE.MIX: + sourceStr = "(mix) "; + break; + case SOURCE.VOLUME: + sourceStr = chapterInfo[1] ? chapterInfo[1].replace(/(volume|vol|v)\s?/i, "") : ""; + if (sourceStr.length < 2 && sourceStr !== "") { sourceStr = `0${sourceStr}`; } // nullpad + if (sourceStr !== "") { sourceStr = `(v${sourceStr}) `; } // output: "(v??) " + break; + case SOURCE.WEB: + sourceStr = "(web) "; + break; + case SOURCE.UNKNOWN: + sourceStr = ""; + break; + default: + sourceStr = ""; + } - chapter.title = post.getElementsByTagName("h1")[0].innerText; - chapter.fileName = madokamiFileName(chapter, groupName); + // Example output: "Giant Killing - c001 (v01) [GROUPNAME]" + let madokamiStr = `${seriesTitle} - c${chapterNo} ${sourceStr}[${groupName}]`; - if (post.getElementsByClassName("tiled-gallery").length > 0) { - chapter.images = []; - let images = Array.from(post.getElementsByTagName("img")); - for (let img of images) { - let chapImg = {}; + return madokamiStr; + } - chapImg.url = img.dataset.origFile; - chapImg.fileName = `${img.dataset.imageTitle}.${chapImg.url.match(/(\w{3,4})$/g)[0]}`; - chapter.images.push(chapImg); + /** + * Extracts all images from a Wordpress gallery. + * + * @method Chapter#getImages + * @param {HTMLElement} post - root element of a Wordpress post containing a gallery + * + * @returns {?Array.} - returns an array of Objects or null. + */ + getImages(post) { + if ( post.classList.contains("post_format-post-format-gallery") || post.getElementsByClassName("tiled-gallery").length > 0) { + let images = []; + let imgElements = Array.from(post.getElementsByTagName("img")); + + for (let img of imgElements) { + let chapImg = {}; + + chapImg.url = img.dataset.origFile; + chapImg.fileName = `${img.dataset.imageTitle}.${chapImg.url.match(/\.(\w{3,4})(?:($|\?))/)[1]}`; + images.push(chapImg); + } + return images; + } else { + return null; } - } else { - chapter.images = null; } - return chapter; + + /** + * Fetch an image by url and turn it into a Blob object. + * Uses http://cors-anywhere.herokuapp.com as a porxy because of Same-Origin + * issues + * + * @method Chapter.getImageBlob + * @static + * @param {String} url - the url the image resides at. + * @return {Blob} a Blob object of the fetched image data. + * + */ + static getImageBlob(url){ + const proxy = "https://cors-anywhere.herokuapp.com"; + + return fetch(`${proxy}/${url}`).then((response) => { + return response.blob(); + }); + } } -// See https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404 + + + +/** + * Iterates over objects asynchronously. + * + * @function asyncForEach + * @async + * @param {Array} array - element to iterate over. Also works with things like HTMLCollections. + * @param {Function} callback - function to asynchronously call on each iteration. + * @return {void} + * + * @see {@link https://codeburst.io/javascript-async-await-with-foreach-b6ba62bbf404} + * + */ async function asyncForEach(array, callback) { for (let index = 0; index < array.length; index++) { - await callback(array[index], index, array) + await callback(array[index], index, array); } } +/** + * Creates and appends necessary textarea and anchors to an element. + * + * @function createElements + * @param {HTMLElement} post - root element of a Wordpress post, usually
    + * @returns {HTMLDivElement} - container of appended elements. + * + */ function createElements(post) { let container = document.createElement("div"); let textArea = document.createElement("textarea"); @@ -94,8 +247,11 @@ function createElements(post) { container.appendChild(textArea); container.appendChild(downloadButton); - // append to entry-summary for blogroll or entry-content for specific post + // append to .entry-summary for blogroll or .entry-content for specific post if (post.getElementsByClassName("entry-summary").length > 0) { + if (post.getElementsByClassName("more-link").length > 0) { + container.style.paddingBottom = "100px"; + } post.getElementsByClassName("entry-summary")[0].appendChild(container); } else { post.getElementsByClassName("entry-content")[0].appendChild(container); @@ -104,66 +260,104 @@ function createElements(post) { return container; } -function createFile(anchor, data, filename) { +/** + * Links an achor to a Blob"s ObjectURL + * + * @function createFile + * @param {HTMLAnchorElement} anchor - anchor element that will link to the Blob + * @param {Blob} data - data that will be linked to + * @param {String} fileName - filename that the data will be saved to. + * @returns {void} + * + */ +function createFile(anchor, data, fileName) { let url = URL.createObjectURL(data); anchor.href = url; - anchor.download = filename; + anchor.download = fileName; anchor.innerText = "Download"; } +/** + * creates a Zip file of a Chapter object with {@link Chapter.images} as the + * files. + * + * @function createZip + * @async + * @param {Chapter} chapter - the chapter object to zip. + * @param {String} chapter.fileName - the file name of the folder that will be zipped. + * @param {Array.} chapter.images - the images that will be zipped + * @param {String} chapter.images.url - the url where the image resides. + * @param {String} chapter.images.fileName - the file under which the image data will be zipped + * + * @returns {Blob} - data as Blob of the generated Zip file. + * + */ async function createZip(chapter) { let zip = new JSZip(); - zipCh = zip.folder(chapter.title); - await asyncForEach(chapter.images, async function(imgObj) { - await zipCh.file(imgObj.fileName, getImageBlob(imgObj.url)); + let zipCh = zip.folder(chapter.fileName); + + await asyncForEach(chapter.images, async (imgObj) => { + await zipCh.file(imgObj.fileName, Chapter.getImageBlob(imgObj.url)); }); - return zip.generateAsync({type: 'blob', comment: 'generated by whyfalalala downloader'}); + return zip.generateAsync({type: "blob", comment: `generated by ${GROUPNAME} downloader`}); } let chapters = []; -/* +/****************************************************************************** * Blog specific stuff starts here. -*/ - +* +*******************************************************************************/ + +/** + * Creates {Chapter} objects from all matching posts on a page and appends Zips to them + * + * @function getChapters + * @async + * @returns {Number} - returns zero if all went well or -1 if something went wrong. + */ async function getChapters() { let chapterElements = Array.prototype.filter.call(document.getElementsByTagName("article"), (article) => { if (article.classList.contains("tag-translation")) { return true; } return false; - }); - let statusCode = await asyncForEach(chapterElements, async (article) => { - chapter = getChapterInfo(article); - if (chapter !== null && chapter.images !== null) { - console.log(`Found ${chapter.title}`); - chapters.push(chapter); + await asyncForEach(chapterElements, async (article) => { + let chapter = new Chapter(article); + + if (chapter !== undefined && chapter.images !== null) { + console.log(`Found ${chapter.title}`); + chapters.push(chapter); + + let container = createElements(article); + let textArea = container.children[0]; + + container.id = `chapter_${chapter.title.replace(" ", "_")}`; - let container = createElements(article); - let textArea = container.children[0]; + let imageUrls = chapter.images.map((img) => img.url); - container.id = `chapter_${chapter.title.replace(' ', '_')}`; + textArea.value = imageUrls.join("\n"); - imageUrls = chapter.images.map((img) => img.url); - textArea.value = imageUrls.join("\n"); + let chapterZip = await createZip(chapter); - let chapterZip = await createZip(chapter); - await createFile(container.children[1], chapterZip, `${chapter.fileName}.cbz`); - return 0; - } else { return -1; } - return statusCode; + await createFile(container.children[1], chapterZip, `${chapter.fileName}.cbz`); + return 0; + } else { return -1; } }); } +/** + * initiates script. + * @returns {void} + */ async function start() { console.log("Start!"); await getChapters(); - return 0; } start(); From ba1957e84c919f0830a648ea20b5fb8cb0fe9f79 Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Wed, 11 Jul 2018 17:33:54 +0200 Subject: [PATCH 07/14] eslint: add 2017 EcmaScript syntax for async functions Signed-off-by: Joost Bremmer --- .eslintrc.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintrc.yaml b/.eslintrc.yaml index 60add53..5fca8a4 100644 --- a/.eslintrc.yaml +++ b/.eslintrc.yaml @@ -85,3 +85,4 @@ parserOptions: forOf: true spread: true templateStrings: true + ecmaVersion: 2017 From 5a174d5c67e82dc938c36a57076321ef05aa1d81 Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Wed, 11 Jul 2018 17:57:44 +0200 Subject: [PATCH 08/14] whyfalalala: Edit @include to work more precisely Change the @include to a regex rule to work only on the front and post pages, rather than on the entire domain. Signed-off-by: Joost Bremmer --- whyfalala/whyfalalala.meta.js | 4 ++-- whyfalala/whyfalalala.user.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/whyfalala/whyfalalala.meta.js b/whyfalala/whyfalalala.meta.js index 1e90650..ff57f23 100644 --- a/whyfalala/whyfalalala.meta.js +++ b/whyfalala/whyfalalala.meta.js @@ -2,11 +2,11 @@ // @name Whyfalalala download WordPress gallery images // @namespace https://github.com/ToostInc/userscripts // @description Adds download links to posts containing a gallery -// @include https://whyfalalala.wordpress.com/* +// @include /^https:\/\/whyfalalala\.wordpress\.com\/(\d{4}\/\d{2}\/\d{2}\/\S*\/(\#more-\d{4})?)?$/ // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT // @version 0.3 -// @date 2108-07-07 +// @date 2018-07-11 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== diff --git a/whyfalala/whyfalalala.user.js b/whyfalala/whyfalalala.user.js index 2dace72..8d1b75e 100644 --- a/whyfalala/whyfalalala.user.js +++ b/whyfalala/whyfalalala.user.js @@ -2,12 +2,12 @@ // @name Whyfalalala download WordPress gallery images // @namespace https://github.com/ToostInc/userscripts // @description Adds download links to posts containing a gallery -// @include https://whyfalalala.wordpress.com/* +// @include /^https:\/\/whyfalalala\.wordpress\.com\/(\d{4}\/\d{2}\/\d{2}\/\S*\/(\#more-\d{4})?)?$/ // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT // @version 0.3 -// @date 2108-07-07 +// @date 2018-07-11 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== From 5c072fcbede6243ad97c0783f881c4717973132c Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Wed, 11 Jul 2018 18:04:05 +0200 Subject: [PATCH 09/14] whyfalalala: Bump version number Signed-off-by: Joost Bremmer --- {whyfalala => whyfalalala}/whyfalalala.meta.js | 2 +- {whyfalala => whyfalalala}/whyfalalala.user.js | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) rename {whyfalala => whyfalalala}/whyfalalala.meta.js (96%) rename {whyfalala => whyfalalala}/whyfalalala.user.js (97%) diff --git a/whyfalala/whyfalalala.meta.js b/whyfalalala/whyfalalala.meta.js similarity index 96% rename from whyfalala/whyfalalala.meta.js rename to whyfalalala/whyfalalala.meta.js index ff57f23..b44e1fa 100644 --- a/whyfalala/whyfalalala.meta.js +++ b/whyfalalala/whyfalalala.meta.js @@ -6,7 +6,7 @@ // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT -// @version 0.3 +// @version 1.0 // @date 2018-07-11 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== diff --git a/whyfalala/whyfalalala.user.js b/whyfalalala/whyfalalala.user.js similarity index 97% rename from whyfalala/whyfalalala.user.js rename to whyfalalala/whyfalalala.user.js index 8d1b75e..c9c5d5f 100644 --- a/whyfalala/whyfalalala.user.js +++ b/whyfalalala/whyfalalala.user.js @@ -6,13 +6,15 @@ // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT -// @version 0.3 +// @version 1.0 // @date 2018-07-11 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== /** - * @fileOverview A userscript to download (manga) chapters from WordPress based galleries + * @file A userscript to download (manga) chapters from WordPress based galleries. + * + * It adds a download link at the end of every post where you can download a zip file containing all the images. * @name whyfalalala */ From 7b8f7d7952fea85fd68112cd63af0e54bc369a96 Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Wed, 11 Jul 2018 18:17:34 +0200 Subject: [PATCH 10/14] update-metablocks: Change date format to ISO 8601 Signed-off-by: Joost Bremmer --- update-metablocks | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/update-metablocks b/update-metablocks index 0314417..3438ad6 100755 --- a/update-metablocks +++ b/update-metablocks @@ -30,7 +30,7 @@ SOFTWARE. HELPMSG=" ################################################################### ## ## -## ${PROGRAM} ${VERSION}: ## +## ${PROGRAM} ${VERSION}: ## ## ## ## writes userscript metablock data to *.meta.js files. ## ## ## @@ -56,12 +56,12 @@ DRYRUN=true; INPUTFILE=""; OUTPUTFILE=""; OUTPUTFILEBOOL=false; -DATE=`date +%d-%m-%Y`; +DATE=`date --iso-8601=date`; writeoutput () { if $DRYRUN; then if [[ $(egrep "\@date" ${INPUTFILE}) ]]; then - cat ${INPUTFILE} | sed -r "s/[0-9]{2}\-[0-9]{2}\-[0-9]{4}/$DATE/"| \ + cat ${INPUTFILE} | sed -r "s/[0-9]{2,4}\\-[0-9]{2,4}\\-[0-9]{2,4}/$DATE/"| \ egrep -B 100 "==/UserScript==" ; echo ""; else @@ -73,7 +73,7 @@ writeoutput () { echo "Writing metablock from \"${INPUTFILE}\" into \"${OUTPUTFILE}\""; if [[ $(egrep "\@date" ${INPUTFILE}) ]]; then - sed -i -r "s/[0-9]{2}\-[0-9]{2}\-[0-9]{4}/$DATE/" "${INPUTFILE}"; + sed -i -r "s/[0-9]{2,4}\\-[0-9]{2,4}\\-[0-9]{2,4}/$DATE/" "${INPUTFILE}"; else sed -i -r "/\@version/a // @date $DATE \ " "${INPUTFILE}"; fi From 8658ac5f4457362f2e31bfb76eaaa799ca97554a Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Wed, 11 Jul 2018 19:46:05 +0200 Subject: [PATCH 11/14] whyfalalala: Update regex to work with blogroll pages Update the @include rule to include /page/\d/ urls --- whyfalalala/whyfalalala.meta.js | 4 ++-- whyfalalala/whyfalalala.user.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/whyfalalala/whyfalalala.meta.js b/whyfalalala/whyfalalala.meta.js index b44e1fa..25e3278 100644 --- a/whyfalalala/whyfalalala.meta.js +++ b/whyfalalala/whyfalalala.meta.js @@ -2,11 +2,11 @@ // @name Whyfalalala download WordPress gallery images // @namespace https://github.com/ToostInc/userscripts // @description Adds download links to posts containing a gallery -// @include /^https:\/\/whyfalalala\.wordpress\.com\/(\d{4}\/\d{2}\/\d{2}\/\S*\/(\#more-\d{4})?)?$/ +// @include /^https:\/\/whyfalalala\.wordpress\.com\/(\d{4}\/\d{2}\/\d{2}\/\S*\/(\#more-\d{4})?|page\/\d{1,}\/)?$/ // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT -// @version 1.0 +// @version 1.1 // @date 2018-07-11 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== diff --git a/whyfalalala/whyfalalala.user.js b/whyfalalala/whyfalalala.user.js index c9c5d5f..e21bcab 100644 --- a/whyfalalala/whyfalalala.user.js +++ b/whyfalalala/whyfalalala.user.js @@ -2,11 +2,11 @@ // @name Whyfalalala download WordPress gallery images // @namespace https://github.com/ToostInc/userscripts // @description Adds download links to posts containing a gallery -// @include /^https:\/\/whyfalalala\.wordpress\.com\/(\d{4}\/\d{2}\/\d{2}\/\S*\/(\#more-\d{4})?)?$/ +// @include /^https:\/\/whyfalalala\.wordpress\.com\/(\d{4}\/\d{2}\/\d{2}\/\S*\/(\#more-\d{4})?|page\/\d{1,}\/)?$/ // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT -// @version 1.0 +// @version 1.1 // @date 2018-07-11 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== From c7e5dcf06a91b78b8003f454e273b6c83b9bf82e Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Thu, 12 Jul 2018 19:59:11 +0200 Subject: [PATCH 12/14] whyfalalala: Added changeable zip filename + Added input field with eventlistener to change the chapter's filename variable if it matches the Madokami Naming Scheme. + Added eventlistener on the download button to save to the latest file name Signed-off-by: Joost Bremmer --- whyfalalala/whyfalalala.meta.js | 4 +- whyfalalala/whyfalalala.user.js | 124 ++++++++++++++++++++++++-------- 2 files changed, 98 insertions(+), 30 deletions(-) diff --git a/whyfalalala/whyfalalala.meta.js b/whyfalalala/whyfalalala.meta.js index 25e3278..0a6711b 100644 --- a/whyfalalala/whyfalalala.meta.js +++ b/whyfalalala/whyfalalala.meta.js @@ -6,7 +6,7 @@ // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT -// @version 1.1 -// @date 2018-07-11 +// @version 1.2 +// @date 2018-07-13 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== diff --git a/whyfalalala/whyfalalala.user.js b/whyfalalala/whyfalalala.user.js index e21bcab..1f0e9a2 100644 --- a/whyfalalala/whyfalalala.user.js +++ b/whyfalalala/whyfalalala.user.js @@ -6,15 +6,15 @@ // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT -// @version 1.1 -// @date 2018-07-11 +// @version 1.2 +// @date 2018-07-13 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== /** * @file A userscript to download (manga) chapters from WordPress based galleries. - * * It adds a download link at the end of every post where you can download a zip file containing all the images. + * * @name whyfalalala */ @@ -41,18 +41,35 @@ const SOURCE = { /** Use if the source of the scanlation is from online sources. */ WEB: "web", /** Use if the source of the scanlation scans are unknown. */ - UNKNOWN: undefined + UNKNOWN: "unk" }; +/** + * Regex to check if a string matches the Madokami Naming Scheme + * @const + * @see {@link https://github.com/Daiz/manga-naming-scheme} + */ +const RE = new RegExp([ + /^(.*?)\s(\[\w{3}\])?\s?/, // Name of Manga [lang] + /(-)\s/, // - + /(((c|d)?\d{3,})((x|y|-|z)?((c|d)?\d{1,})?((-|x|y|z)?\d{1,})?((-|x|y)\d{1,})?))\s/, // c000-000x0-0 + /(\((mag|mix|(v\d{2,}(-\d{2,})?)|web)\))\s/, // (mag/web/mix/v00-00) + /(\[.*?\])?\s?/, // [Extra Information] + /(\[.*?\])/, // [Group] + /(\{(v|r)\d*?\})?$/, // {r00} + // /\.(\w{3,4})$/ // File extension +].map(r => r.source).join("")); + /** * @class Chapter * @classdec Class representing a chapter. * @global + * @property {HTMLElement} post - The WordPress post element asociated with the chapter * @property {String} title - The title of the chapter, as taken from the post - * @property {String} fileName - The filename of the chapter; generated .from the title. - * @property {?Array.} images - Array of image objects of each image found in post. + * @property {String} fileName - The filename of the chapter; generated from the post title. + * @property {?Array.} images - Array of image objects of each image found in the post. * @porperty {String} images[].url - The url of where the image resides. * @porperty {String} images[].fileName - The file name to where the image needs to be saved to. */ @@ -63,16 +80,24 @@ class Chapter{ * @param {HTMLElement} post - the root element of a WordPress post; usually
    */ constructor(post) { - /** @private */ + /** @private @readonly */ this._post = post; /** @private */ this._title = post.getElementsByTagName("h1")[0].innerText; /** @private */ - this._fileName = Chapter.madokamiFileName(this, GROUPNAME, SOURCE.VOLUME); + this._fileName = Chapter.madokamiFileName(this, GROUPNAME, SOURCE.UNKNOWN); /** @private */ this._images = this.getImages(this._post); } + get post() { + return this._post; + } + + set post(value) { + // read only + } + get title() { return this._title; } @@ -113,12 +138,13 @@ class Chapter{ * @returns {String} the estimated madokami filename; Still needs to be adjusted slightly */ static madokamiFileName(chapter, groupName=GROUPNAME, source=SOURCE.UNKNOWN){ + let chapterInfo = chapter.title.split(/(^.*?)\s?((volume|v)\s?\d{1,2})?\s?((chapter|ch|c)?\s?\d{1,}-?\d{1,})/gi); chapterInfo = chapterInfo.splice(1, chapterInfo.length - 2); let seriesTitle = chapterInfo[0]; - let chapterNo = (chapterInfo[3]) ? chapterInfo[3].replace(/(chapter|ch|c)\s?/i, "") : ""; + let chapterNo = (chapterInfo[3]) ? chapterInfo[3].replace(/(chapter|ch|c)\s?/i, "").padStart(3, "0") : ""; let sourceStr = null; // Deterimine source. @@ -130,20 +156,21 @@ class Chapter{ sourceStr = "(mix) "; break; case SOURCE.VOLUME: - sourceStr = chapterInfo[1] ? chapterInfo[1].replace(/(volume|vol|v)\s?/i, "") : ""; - if (sourceStr.length < 2 && sourceStr !== "") { sourceStr = `0${sourceStr}`; } // nullpad - if (sourceStr !== "") { sourceStr = `(v${sourceStr}) `; } // output: "(v??) " + // Check if volumeNo was found in ChapterInfo + sourceStr = chapterInfo[1] ? chapterInfo[1].replace(/(volume|vol|v)\s?/i, "").padStart(2,"0") : ""; + sourceStr = (sourceStr !== "") ? `(v${sourceStr}) ` : ""; // output: "(v??) " break; case SOURCE.WEB: sourceStr = "(web) "; break; case SOURCE.UNKNOWN: - sourceStr = ""; + sourceStr = "(unk) "; break; default: - sourceStr = ""; + sourceStr = "(unk) "; } + // Example output: "Giant Killing - c001 (v01) [GROUPNAME]" let madokamiStr = `${seriesTitle} - c${chapterNo} ${sourceStr}[${groupName}]`; @@ -151,10 +178,10 @@ class Chapter{ } /** - * Extracts all images from a Wordpress gallery. + * Extracts all images from a WordPress gallery. * * @method Chapter#getImages - * @param {HTMLElement} post - root element of a Wordpress post containing a gallery + * @param {HTMLElement} post - root element of a WordPress post containing a gallery * * @returns {?Array.} - returns an array of Objects or null. */ @@ -166,7 +193,7 @@ class Chapter{ for (let img of imgElements) { let chapImg = {}; - chapImg.url = img.dataset.origFile; + chapImg.url = img.dataset.origFile.split('?')[0]; // remove any width parameters chapImg.fileName = `${img.dataset.imageTitle}.${chapImg.url.match(/\.(\w{3,4})(?:($|\?))/)[1]}`; images.push(chapImg); } @@ -221,18 +248,48 @@ async function asyncForEach(array, callback) { * Creates and appends necessary textarea and anchors to an element. * * @function createElements - * @param {HTMLElement} post - root element of a Wordpress post, usually
    + * @param {Chapter} chapter - root element of a WordPress post, usually
    * @returns {HTMLDivElement} - container of appended elements. * */ -function createElements(post) { +function createElements(chapter) { let container = document.createElement("div"); + let fNameDiv = container.cloneNode(); + let fNameLabel = document.createElement("label"); + let fNameInput = document.createElement("input"); let textArea = document.createElement("textarea"); let downloadButton = document.createElement("a"); container.classList.add("container"); + + fNameDiv.classList.add("input-container"); + fNameDiv.style.marginBottom = "10px"; + fNameLabel.htmlFor = `${chapter.title}_fName`; + fNameLabel.innerText = "File Name"; + fNameInput.id = `${chapter.title}_fName`; + fNameInput.type = "text"; + fNameInput.value = chapter.fileName; + + // TODO: make this nice. + // let reOld = /^(.*?)\s(\[\w{3}\])?\s?(-)\s(((c|d)?\d{3,})((x|y|-|z)?((c|d)?\d{1,})?((-|x|y|z)?\d{1,})?((-|x|y)\d{1,})?))\s(\((mag|mix|v\d{2,}|web)\))\s(\[.*?\])?\s?(\[.*?\])(\{(v|r)\d*?\})?$/; + + fNameInput.pattern = RE.source; + fNameInput.placeholder = fNameInput.getAttribute("title"); + fNameInput.setAttribute("title", "Name of Manga [lang] - c000-000x0-0 (mag/web/mix/v00-00) [Extra Information] [Group]{revision}"); + fNameInput.style = "min-width: 50%; margin-left: 10px; padding: 0.33rem 0.75rem;"; + + fNameInput.addEventListener("input", () => { + if(fNameInput.validity.valid) { + console.log("fileName is valid; updating"); + chapter.fileName = fNameInput.value; + } else { + console.log(fNameInput.validationMessage); + } + }); + textArea.value = "Loading..."; + downloadButton.innerText = "Loading..."; downloadButton.style = ` margin: 5px 0; @@ -246,17 +303,21 @@ function createElements(post) { border-style: solid; `; + fNameDiv.appendChild(fNameLabel); + fNameDiv.appendChild(fNameInput); + + container.appendChild(fNameDiv); container.appendChild(textArea); container.appendChild(downloadButton); // append to .entry-summary for blogroll or .entry-content for specific post - if (post.getElementsByClassName("entry-summary").length > 0) { - if (post.getElementsByClassName("more-link").length > 0) { + if (chapter.post.getElementsByClassName("entry-summary").length > 0) { + if (chapter.post.getElementsByClassName("more-link").length > 0) { container.style.paddingBottom = "100px"; } - post.getElementsByClassName("entry-summary")[0].appendChild(container); + chapter.post.getElementsByClassName("entry-summary")[0].appendChild(container); } else { - post.getElementsByClassName("entry-content")[0].appendChild(container); + chapter.post.getElementsByClassName("entry-content")[0].appendChild(container); } return container; @@ -319,10 +380,11 @@ let chapters = []; * * @function getChapters * @async + * @param {Node} [element=document] - element to search for WordPress posts * @returns {Number} - returns zero if all went well or -1 if something went wrong. */ -async function getChapters() { - let chapterElements = Array.prototype.filter.call(document.getElementsByTagName("article"), (article) => { +async function getChapters(element=document) { + let chapterElements = Array.prototype.filter.call(element.getElementsByTagName("article"), (article) => { if (article.classList.contains("tag-translation")) { return true; } @@ -336,8 +398,8 @@ async function getChapters() { console.log(`Found ${chapter.title}`); chapters.push(chapter); - let container = createElements(article); - let textArea = container.children[0]; + let container = createElements(chapter); + let textArea = container.children[1]; container.id = `chapter_${chapter.title.replace(" ", "_")}`; @@ -347,7 +409,13 @@ async function getChapters() { let chapterZip = await createZip(chapter); - await createFile(container.children[1], chapterZip, `${chapter.fileName}.cbz`); + await createFile(container.children[2], chapterZip, `${chapter.fileName}.cbz`); + + container.children[2].addEventListener("click", () => { + container.children[2].download = `${chapter.fileName}.cbz`; + }); + + return 0; } else { return -1; } }); From 1c603adb83c25436217812cd553b1d1e7163097a Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Fri, 13 Jul 2018 14:49:36 +0200 Subject: [PATCH 13/14] whyfalalala: Move blog specific code to start() This moves all blog specific code, like filtering for posts containing scanlations from getChapters() to start() to make way for mutationobservers and its callback Signed-off-by: Joost Bremmer --- whyfalalala/whyfalalala.meta.js | 2 +- whyfalalala/whyfalalala.user.js | 37 +++++++++++++++++---------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/whyfalalala/whyfalalala.meta.js b/whyfalalala/whyfalalala.meta.js index 0a6711b..b34f969 100644 --- a/whyfalalala/whyfalalala.meta.js +++ b/whyfalalala/whyfalalala.meta.js @@ -6,7 +6,7 @@ // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT -// @version 1.2 +// @version 1.3 // @date 2018-07-13 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== diff --git a/whyfalalala/whyfalalala.user.js b/whyfalalala/whyfalalala.user.js index 1f0e9a2..7446b12 100644 --- a/whyfalalala/whyfalalala.user.js +++ b/whyfalalala/whyfalalala.user.js @@ -6,7 +6,7 @@ // @author Joost Bremmer < contact@madeofmagicandwires.online > // @copyright 2018, Joost Bremmer // @license MIT -// @version 1.2 +// @version 1.3 // @date 2018-07-13 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== @@ -370,29 +370,19 @@ async function createZip(chapter) { let chapters = []; -/****************************************************************************** -* Blog specific stuff starts here. -* -*******************************************************************************/ /** * Creates {Chapter} objects from all matching posts on a page and appends Zips to them * * @function getChapters * @async - * @param {Node} [element=document] - element to search for WordPress posts + * @param {HTMLCollection} chapterElements - element to search for WordPress posts * @returns {Number} - returns zero if all went well or -1 if something went wrong. */ -async function getChapters(element=document) { - let chapterElements = Array.prototype.filter.call(element.getElementsByTagName("article"), (article) => { - if (article.classList.contains("tag-translation")) { - return true; - } - return false; - }); +async function getChapters(chapterElements) { - await asyncForEach(chapterElements, async (article) => { - let chapter = new Chapter(article); + await asyncForEach(chapterElements, async (chapterElement) => { + let chapter = new Chapter(chapterElement); if (chapter !== undefined && chapter.images !== null) { console.log(`Found ${chapter.title}`); @@ -415,19 +405,30 @@ async function getChapters(element=document) { container.children[2].download = `${chapter.fileName}.cbz`; }); - return 0; } else { return -1; } }); } +/****************************************************************************** +* Blog specific stuff starts here. +* +*******************************************************************************/ + /** - * initiates script. + * Blog specific function that initiates the script * @returns {void} */ async function start() { console.log("Start!"); - await getChapters(); + let chapterElements = Array.prototype.filter.call(document.getElementsByTagName("article"), (article) => { + if (article.classList.contains("tag-translation")) { + return true; + } + return false; + }); + + await getChapters(chapterElements); } start(); From 16ded61f910f9d29e170cbee7972ba0e9ce1b93f Mon Sep 17 00:00:00 2001 From: Joost Bremmer Date: Tue, 17 Jul 2018 22:43:34 +0200 Subject: [PATCH 14/14] whyfalalala: Move filtering of posts to seperate function Moves the filtering of article elements in start() to seperate function filterPosts(). Signed-off-by: Joost Bremmer --- whyfalalala/whyfalalala.meta.js | 2 +- whyfalalala/whyfalalala.user.js | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/whyfalalala/whyfalalala.meta.js b/whyfalalala/whyfalalala.meta.js index b34f969..a42822b 100644 --- a/whyfalalala/whyfalalala.meta.js +++ b/whyfalalala/whyfalalala.meta.js @@ -7,6 +7,6 @@ // @copyright 2018, Joost Bremmer // @license MIT // @version 1.3 -// @date 2018-07-13 +// @date 2018-07-17 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== diff --git a/whyfalalala/whyfalalala.user.js b/whyfalalala/whyfalalala.user.js index 7446b12..8c2ce49 100644 --- a/whyfalalala/whyfalalala.user.js +++ b/whyfalalala/whyfalalala.user.js @@ -7,7 +7,7 @@ // @copyright 2018, Joost Bremmer // @license MIT // @version 1.3 -// @date 2018-07-13 +// @date 2018-07-17 // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js // ==/UserScript== @@ -415,18 +415,30 @@ async function getChapters(chapterElements) { * *******************************************************************************/ +async function filterPosts(elements) { + return new Promise((resolve, reject) => { + let chapterElements = Array.prototype.filter.call(elements, (element) => { + if (element.classList.contains("tag-translation")) { + return true; + } + return false; + }); + + if (chapterElements.length > 0) { + resolve(chapterElements); + } else { + reject(null); + } + }); +} + /** * Blog specific function that initiates the script * @returns {void} */ async function start() { console.log("Start!"); - let chapterElements = Array.prototype.filter.call(document.getElementsByTagName("article"), (article) => { - if (article.classList.contains("tag-translation")) { - return true; - } - return false; - }); + let chapterElements = await filterPosts(document.getElementsByTagName("article")); await getChapters(chapterElements); }