Last active
December 28, 2020 06:56
-
-
Save mozurin/0c3bc302b1106f1adb7d31e616c7df9b to your computer and use it in GitHub Desktop.
Tampermonkey script that extracts hi-res MP4 movie file URLs from tweet permalink page.
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 Twitter video extractor | |
// @namespace https://dislife.com/ | |
// @version 0.0.3 | |
// @description Extract hi-res MP4 movie file URLs from tweet permalink page | |
// @author mizuki@mozurin.com | |
// @match https://twitter.com/*/status/* | |
// @grant none | |
// @run-at document-body | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
const injectCode = function () | |
{ | |
const APP_NAME = 'Twitter video extractor'; | |
const APP_ID = 'TwitterVideoExtractor'; | |
const MENU_TEXT = 'Extract video file URLs'; | |
const NOTFOUND_TEXT = 'Video files are not found.'; | |
function log(msg) | |
{ | |
console.log('[' + APP_ID + '] ' + msg); | |
} | |
// video information extractor | |
let jsonTree; | |
function statusFetched(response) | |
{ | |
jsonTree = ( | |
typeof response == 'string'? | |
JSON.parse(response) : response | |
); | |
} | |
function parseAndShowList() | |
{ | |
if (!jsonTree) { | |
log('Tweet JSON tree not fetched. Updating the script update might be required.'); | |
return; | |
} | |
if (document.getElementById(APP_ID)) { | |
log('Video list box is already open.'); | |
return; | |
} | |
const foundVideos = []; | |
for (const tweet of Object.values(jsonTree.globalObjects.tweets)) { | |
if (!tweet.extended_entities || !tweet.extended_entities.media) { | |
continue; | |
} | |
for (const media of tweet.extended_entities.media) { | |
if (media.type != 'video') { | |
continue; | |
} | |
foundVideos.push( | |
{ | |
thumbnail: media.media_url_https, | |
url: media.video_info.variants.sort( | |
(a, b) => (b.bitrate? b.bitrate : 0) - (a.bitrate? a.bitrate : 0) | |
)[0].url, | |
} | |
); | |
} | |
} | |
// build and show popup | |
const parserBox = document.createElement('div'); | |
let innerSrc = ` | |
<div id="${APP_ID}" style="position: fixed; left: 3em; right: 3em; top: 3em; | |
bottom: 3em; background: white; border: solid gray 1px;"> | |
<p style="background: #348; color: white; margin: 0; | |
padding: 0.5em; padding-left: 3em; overflow: hidden; line-height: 3em;"> | |
${APP_NAME} | |
<button type="button" style="float: right; height: 3em;" | |
onclick="document.getElementById('${APP_ID}').remove();"> | |
close | |
</button> | |
</p> | |
`; | |
if (foundVideos.length < 1) { | |
innerSrc += ` | |
<p style="color: red; font-size: 120%; font-weight: bold; text-align: center;"> | |
${NOTFOUND_TEXT} | |
</p> | |
`; | |
} else { | |
innerSrc += ` | |
<div style="overflow: hidden scroll; position: absolute; top: 4em; | |
bottom: 0; width: 100%;"> | |
`; | |
for (const video of foundVideos) { | |
innerSrc += ` | |
<a href="${video.url}"> | |
<img src="${video.thumbnail}" style="height: 20em; width: auto; | |
margin: 1em; float: left;" /> | |
</a> | |
`; | |
} | |
innerSrc += '</div>'; | |
} | |
innerSrc += '</div>'; | |
parserBox.innerHTML = innerSrc; | |
document.body.appendChild(parserBox.children[0]); | |
} | |
// append contextmenu | |
window.addEventListener( | |
'DOMContentLoaded', | |
() => { | |
const menuId = document.body.getAttribute('contextmenu'); | |
let menu; | |
if (menuId) { | |
menu = document.getElementById(menuId); | |
} else { | |
menu = document.createElement('menu'); | |
menu.setAttribute('id', 'gmscript-menu'); | |
menu.setAttribute('type', 'context'); | |
document.body.appendChild(menu); | |
document.body.setAttribute('contextmenu', 'gmscript-menu'); | |
} | |
const item = document.createElement('menuitem'); | |
item.textContent = MENU_TEXT; | |
item.addEventListener('click', parseAndShowList, false); | |
menu.appendChild(item); | |
}, | |
false | |
); | |
// install hook into XMLHttpRequest | |
const originXHR = window.XMLHttpRequest; | |
window.XMLHttpRequest = function () | |
{ | |
const xhr = new originXHR(...arguments); | |
const originOpen = xhr.open; | |
xhr.open = function (method, url) | |
{ | |
if ( | |
url.match(/\/api\/2\/timeline\/conversation\/\d+\.json/) || | |
url.match(/\/api\/2\/rux\.json/) | |
) { | |
this.addEventListener( | |
'readystatechange', | |
function () | |
{ | |
if (this.readyState != XMLHttpRequest.DONE) { | |
return; | |
} | |
statusFetched(this.response); | |
}, | |
false | |
); | |
} | |
return originOpen.apply(this, arguments); | |
}; | |
return xhr; | |
}; | |
Object.setPrototypeOf(window.XMLHttpRequest, originXHR); | |
}; | |
const script = document.createElement('script'); | |
script.innerHTML = '(' + injectCode.toString() + ')();'; | |
document.body.appendChild(script); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
It doesn't work in my chrome and shows “Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self' 'unsafe-inline'”