Last active
April 25, 2019 01:56
-
-
Save yanorei32/ef72fa76511c1ce5d5d6d725c2fa8b88 to your computer and use it in GitHub Desktop.
ツイッターの画像を簡単に保存するUserスクリプトです。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name Ez Twimg Downloader | |
// @description ツイッターの画像を簡単に保存するUserScriptです。 | |
// @author yanorei32 | |
// @namespace http://tyan0.dip.jp/~rei/ | |
// @include https://twitter.com/* | |
// @include https://pbs.twimg.com/media/* | |
// @version 3.7 | |
// @grant none | |
// @license MIT License | |
// @updateURL https://gist.githubusercontent.com/Yanorei32/ef72fa76511c1ce5d5d6d725c2fa8b88/raw/EzTwimgDownloader.user.js | |
// ==/UserScript== | |
// このプログラムは、以下の4つのサイト/ソースを参考に作りました。 | |
// | |
// 1: Extract images for Twitter | |
// -> https://greasyfork.org/ja/scripts/15271-extract-images-for-twitter | |
// 2: 【覚書】クロスドメインな画像を任意のファイル名でダウンロードするためのユーザースクリプトの記述方法 | |
// -> http://furyu.hatenablog.com/entry/20160118/1453113706 | |
// 3: twOpenOriginalImage | |
// -> https://github.com/furyutei/twOpenOriginalImage | |
// | |
// | |
// # 2からの主な変更点 | |
// * 最近のブラウザではパフォーマンスに大差ないため、varをletに | |
// * Twitter新仕様に本格対応 | |
// * TweetIDをファイル名に含めるようになった | |
// * ちょっといじるだけで保存ファイル名をカスタマイズできるようにした | |
// * その他最適化 | |
// | |
// # 1からの主な変更点 | |
// * 「…」ボタンより左側にボタンが追加されるようにした。 | |
// * 画像がない場合は、ボタンを非表示とした。 | |
// * DOM更新周りを3を参考に軽量化した | |
// * マウスホバー時の色変更をCSS任せにした。(軽量化) | |
// * 2を参考にDLできるようにした。 | |
// | |
;(function() { | |
'use strict'; | |
// download filename format | |
// https://twitter.com/{userName}/status/{tweetId} | |
// https://pbs.twimg.com/media/{randomImageNameWithoutExtension}.{extension} or | |
// https://pbs.twimg.com/media/{randomImageName} | |
let FILENAME_FORMAT = 'Twitter-{tweetId}-{userName}-{randomImageNameWithoutExtension}.{extension}'; | |
let BLACK_LIST = [ | |
'TwTimez', | |
'kankoreyasumi', | |
]; | |
// CONST VALUES | |
let SCRIPT_NAME = 'EZ_Twitter_Image_Downloader'; | |
let IFRAME_NAME = SCRIPT_NAME + '_download_frame'; | |
if(window !== window.parent){ | |
// iframe functions | |
// check iframe name | |
if(window.name.split(',')[0] != IFRAME_NAME) return; | |
// create link elem and download | |
(function() { | |
let linkElem = document.createElement('a'); | |
let userName = window.name.split(',')[1]; | |
let tweetId = window.name.split(',')[2]; | |
let randomImageName = (window.location.href.split('/').pop().split(':')[0]); | |
let randomImageNameWithoutExtension = randomImageName.slice(0, randomImageName.length - 4); | |
let extension = randomImageName.slice(-3); | |
let filename = FILENAME_FORMAT; | |
linkElem.href = window.location.href; | |
filename = filename.replace(/{userName}/g, userName); | |
filename = filename.replace(/{tweetId}/g, tweetId); | |
filename = filename.replace(/{randomImageNameWithoutExtension}/g, randomImageNameWithoutExtension); | |
filename = filename.replace(/{randomImageName}/g, randomImageName); | |
filename = filename.replace(/{extension}/g, extension); | |
linkElem.download = filename; | |
document.documentElement.appendChild(linkElem); | |
linkElem.click(); | |
})(); | |
}else{ | |
// parent window functions | |
// init processed list | |
let processedLists = new WeakMap(); | |
// iframe | |
let iframeDiv; | |
// write css | |
(function(){ | |
let cssElem = document.createElement('style'); | |
cssElem.innerHTML = | |
'.ProfileTweet-action--ExtractImages:hover:hover * {' + | |
'color : rgb(47,194,239);' + | |
'}' + | |
'.tweet.Tweet--invertedColors .ProfileTweet-action--ExtractImages:hover .ProfileTweet-actionButton {' + | |
'color : rgb(47,194,239);' + | |
'}' + | |
'.tweet.Tweet--invertedColors .ProfileTweet-action--ExtractImages:hover .ProfileTweet-actionCount {' + | |
'color : rgb(47,194,239);' + | |
'}' + | |
'.tweet.Tweet--invertedColors .ProfileTweet-action--ExtractImages .ProfileTweet-actionButton {' + | |
'color : #fff;' + | |
'}' + | |
'.tweet.Tweet--invertedColors .ProfileTweet-action--ExtractImages .ProfileTweet-actionCount {' + | |
'color : #fff;' + | |
'}' + | |
'.stream-container .AdaptiveStreamGridImage .grid-tweet-action.action-extractImage-container {' + | |
'margin: 6px 4px;' + | |
'}' + | |
'.stream-container .AdaptiveStreamGridImage .grid-tweet-actions {' + | |
'width: 110px !important;' + | |
'}' | |
; | |
document.documentElement.appendChild(cssElem); | |
})(); | |
// write div for iframe | |
(function(){ | |
iframeDiv = document.createElement('div'); | |
document.documentElement.appendChild(iframeDiv); | |
})(); | |
let iframeClear = function(){ | |
iframeDiv.textContent = null; | |
}; | |
let iframeAdd = function(imageBaseURL, userName, tweetId){ | |
let iframe = document.createElement('iframe'); | |
iframe.style.width = iframe.style.height = '0'; | |
iframe.style.visibility = 'hidden'; | |
iframe.src = imageBaseURL + ':orig'; | |
iframe.name = IFRAME_NAME + ',' + userName + ',' + tweetId; | |
iframeDiv.appendChild(iframe); | |
}; | |
let originalTweetUserCheckByBlackList = function(userName){ | |
if(BLACK_LIST.includes(userName)) | |
if(confirm(userName + 'はBLACK_LISTに登録されています。\nダウンロードを中止しますか?')) | |
return false; | |
return true; | |
}; | |
// create button element | |
let createButton = function(list){ | |
// get photo container | |
let imgs = list.parentNode.parentNode.getElementsByClassName('AdaptiveMedia-photoContainer'); | |
// return if image not found | |
if(imgs.length == 0) return undefined; | |
let getTweetElementByListElement = function(elm){ | |
while(1){ | |
if(elm.classList.contains('tweet')) break; | |
elm = elm.parentNode; | |
} | |
return elm; | |
} | |
let btn = document.createElement('div'); | |
btn.setAttribute('class', 'ProfileTweet-action ProfileTweet-action--ExtractImages'); | |
btn.innerHTML = | |
'<button class="ProfileTweet-actionButton js-actionButton" type="button">' + | |
'<div class="IconContainer js-tooltip" title="Extract Images">' + | |
'<span class="Icon Icon--medium Icon--media"></span> ' + | |
'</div>' + | |
'<span class="ProfileTweet-actionCount">' + | |
'<span class="ProfileTweet-actionCountForPresentation">' + imgs.length + '</span>' + | |
'</span>' + | |
'</button>' | |
; | |
btn.addEventListener('click',function(event){ | |
// if not press shift key | |
if(!(event || window.event).shiftKey){ | |
let tweetDivElement = getTweetElementByListElement(list); | |
let tweetId = tweetDivElement.getAttribute('data-tweet-id'); | |
let userName = tweetDivElement.getAttribute('data-screen-name'); | |
if(!originalTweetUserCheckByBlackList(userName)) return; | |
iframeClear(); | |
for(let i = 0;i < imgs.length;i++) | |
iframeAdd(imgs[i].getAttribute('data-image-url'), userName, tweetId); | |
}else{ | |
let lastIndex = (imgs.length - 1); | |
for(let i = lastIndex;0 <= i;i--){ | |
let url = imgs[i].getAttribute('data-image-url') + ':orig'; | |
window.open(url); | |
} | |
} | |
}); | |
return btn; | |
}; | |
let createButtonForStream = function(list){ | |
let btn = document.createElement('li'); | |
btn.setAttribute('class', 'action-extractImage-container grid-tweet-action'); | |
btn.innerHTML = | |
'<a class="js-action-extractImage" role="button">' + | |
'<span class="Icon Icon--media Icon--small u-textUserColorHover">' + | |
'</span>' + | |
'<span class="u-hiddenVisually">画像保存</span>' + | |
'</a>' | |
; | |
btn.addEventListener('click',function(event){ | |
let tweetElement = list.parentNode.parentNode; | |
// if not press shift key | |
if(!(event || window.event).shiftKey){ | |
let imgBaseUrl = tweetElement.getAttribute('data-url'); | |
let userName = tweetElement.getAttribute('data-screen-name'); | |
let tweetId = tweetElement.getAttribute('data-tweet-id'); | |
if(!originalTweetUserCheckByBlackList(userName)) return; | |
iframeClear(); | |
iframeAdd(imgBaseUrl, userName, tweetId); | |
}else{ | |
window.open(tweetElement.getAttribute('data-url') + ':orig'); | |
} | |
}); | |
return btn; | |
}; | |
// add buttons 2 elem | |
let addButtons = function(findNode){ | |
let lists; | |
// get action list | |
lists = findNode.getElementsByClassName('ProfileTweet-actionList'); | |
for(let i = 0;i < lists.length;i++){ | |
let list = lists[i]; | |
// check processed list | |
if(processedLists.has(list)) continue; | |
// set to processed list | |
processedLists.set(list, 1); | |
// create button | |
let createdButton = createButton(list); | |
// add button if createdButton is not undefined | |
if(createdButton != undefined){ | |
// remove old button (perhaps a browser bug) | |
let oldButton = list.getElementsByClassName('ProfileTweet-action--ExtractImages')[0]; | |
if(oldButton) oldButton.parentNode.removeChild(oldButton); | |
list.insertBefore( | |
createdButton, | |
list.getElementsByClassName('ProfileTweet-action--more')[0] | |
); | |
} | |
} | |
// get action list | |
lists = findNode.getElementsByClassName('grid-tweet-actions'); | |
for(let i = 0;i < lists.length;i++){ | |
let list = lists[i]; | |
if(processedLists.has(list)) continue; | |
processedLists.set(list, 1); | |
let createdButton = createButtonForStream(list); | |
if(createdButton != undefined){ | |
let oldButton = list.getElementsByClassName('action-extractImage-container')[0]; | |
if(oldButton) oldButton.parentNode.removeChild(oldButton); | |
list.appendChild(createdButton); | |
} | |
} | |
}; | |
let toArray = function(array_like_object) { | |
return Array.prototype.slice.call(array_like_object); | |
}; | |
// create mutation observer | |
new MutationObserver(function(records){ | |
// loop each record | |
records.forEach(function(record){ | |
// loop each added node | |
toArray(record.addedNodes).forEach(function(addedNode){ | |
// return if node is not a element node | |
if(addedNode.nodeType != Node.ELEMENT_NODE) return; | |
addButtons(addedNode); | |
}); | |
}); | |
}).observe( | |
document.body, | |
{ | |
childList: true, | |
subtree: true, | |
} | |
); | |
// add button | |
addButtons(document); | |
} | |
})(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment