// ==UserScript==
// @name Twonky Enhancer
// @version v20230809.1524
// @description Fix Twonky public Web UI
// @author ltlwinston
// @match http*://*/*
// @grant GM_addElement
// @grant GM_setClipboard
// @require https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.js
// @namespace https://greasyfork.org/users/754595
// ==/UserScript==
GM_addElement('link',{
rel: "stylesheet",
href: "//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css"
});
GM_addElement('link',{
rel: "stylesheet",
href: "//cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.css"
});
const interesting_words = [
'sex', 'intim', 'sess', 'osé', 'porc', 'porn', 'intim', 'naught', 'xxx', 'privat', 'whatsapp', 'signal', 'telegram', 'sent', 'bitch', 'cunt', 'puttan', 'hot', 'blowjob', 'pussy', 'figa', 'tette', 'culo', 'anal', 'pomp', 'bocchin', 'personal'
];
(async function () {
'use strict';
const USE_CACHE = true;
if (document.title.match(/(twonky|pv connect|mediaserver)/i)) {
if(document.body.innerText.indexOf('Access is restricted to MediaServer configuration!')>=0) {
window.location.href = '/webbrowse';
return;
}
async function loadServerStatus() {
const status = {};
await fetch('/rpc/info_status').then(r => r.text()).then(s => s.split(/[\t\n ]/).forEach(i => {
const [k,v] = i.split('|');
status[k] = isNaN(v) ? v : parseInt(v);
}));
return status;
}
async function loadPhotoAlbums(SERVER_UUID) {
const albumUrl = '/nmc/rss/server/RB' + SERVER_UUID + ',0/IB' + SERVER_UUID + ',_MCQyJDI0,,1,0,_Um9vdA==,0,,0,0,_UGhvdG9z,_MCQz,?start=0&count=30000&fmt=json';
const albumResult = await fetch(albumUrl).then(x=>x.json());
if (!albumResult || !albumResult.item) {
throw 'ERR: Cannot load photo albums';
}
return albumResult.item.map(x=>({title: x.title, bookmark: x.bookmark}));
}
async function loadVideoAlbums(SERVER_UUID) {
const albumUrl = '/nmc/rss/server/RB' + SERVER_UUID + ',0/IB' + SERVER_UUID + ',_MCQzJDM1,,1,0,_Um9vdA==,0,,0,0,_VmlkZW9z,_MCQz,?start=0&count=30000&fmt=json';
const albumResult = await fetch(albumUrl).then(x=>x.json());
if (!albumResult || !albumResult.item) {
throw 'ERR: Cannot load video albums';
}
return albumResult.item.map(x=>({title: x.title, bookmark: x.bookmark}));
}
async function getPath(bookmark) {
return fetch('/nmc/rpc/get_item_path?server='+encodeURIComponent(bookmark)).then(x => x.text());
}
if (typeof unsafeWindow['statusData'] == 'undefined') {
unsafeWindow['statusData'] = {'language': 'en'};
}
if (!('language' in unsafeWindow['statusData'])) {
unsafeWindow['statusData']['language'] = 'en';
initPage();
}
const statusElem = document.createElement('div');
statusElem.id = 'te_status';
statusElem.style.position = 'fixed';
statusElem.style.color = 'black';
statusElem.style.top = '1em';
statusElem.style.left = '1em';
statusElem.innerHTML = '<a href="javascript:return false;"><i class="fa fa-refresh"></i></a><br>'
document.body.appendChild(statusElem);
const status = await loadServerStatus();
let SERVER_UUID = '';
let photoAlbums = {};
let videoAlbums = {};
if (status) {
if (('videos' in status) && ('pictures' in status)) {
let nPics = status.pictures;
let nVids = status.videos;
statusElem.innerHTML += `<i class="fa fa-photo"></i> ${nPics} <i class="fa fa-video-camera"></i> ${nVids}`;
SERVER_UUID = status.server_udn;
if (SERVER_UUID) {
const pAlbumStatus = document.createElement('div');
const vAlbumStatus = document.createElement('div');
statusElem.appendChild(pAlbumStatus);
statusElem.appendChild(vAlbumStatus);
pAlbumStatus.innerHTML = '<i class="fa fa-file-image-o"></i> Loading...';
loadPhotoAlbums(SERVER_UUID).then(a => {
a.forEach(x => {photoAlbums[x.title] = x});
pAlbumStatus.innerHTML = (a.length+' <i class="fa fa-file-image-o"></i><br><input id="pasearch" placeholder="Search a photo album">');
const pasearch = document.querySelector('#pasearch');
pasearch.addEventListener('blur', function(e){this.value = ''});
pasearch.addEventListener('awesomplete-select', function(e){
openPhotoAlbum(SERVER_UUID, e.text.value);
//window.open(window.location.pathname + "#"+window.location.origin+"/nmc/rss/server/RB" + status.server_udn + ",0/IB" + e.text.value + '?start=0&count=30', '_blank');
e.preventDefault();
});
new Awesomplete(pasearch, {list: a, data: i => ({label:i.title, value:i.bookmark})});
}).catch(e => {
pAlbumStatus.innerText = (e);
});
vAlbumStatus.innerHTML = '<i class="fa fa-file-video-o"></i> Loading...';
loadVideoAlbums(SERVER_UUID).then(a => {
a.forEach(x => {videoAlbums[x.title] = x});
vAlbumStatus.innerHTML = (a.length+' <i class="fa fa-file-video-o"></i><br><input id="vasearch" placeholder="Search a video album">');
const vasearch = document.querySelector('#vasearch');
vasearch.addEventListener('blur', function(e){this.value = ''});
vasearch.addEventListener('awesomplete-select', function(e){
window.open(window.location.pathname + "#"+window.location.origin+"/nmc/rss/server/RB" + status.server_udn + ",0/IB" + e.text.value + '?start=0&count=30', '_blank');
e.preventDefault();
});
new Awesomplete(vasearch, {list: a, data: i => ({label:i.title, value:i.bookmark})});
}).catch(e => {
vAlbumStatus.innerText = (e);
});
} else {
const pAlbumStatus = document.createElement('div');
pAlbumStatus.innerText = 'Album search not available.';
statusElem.appendChild(pAlbumStatus);
}
}
}
function fixUrl(url) {
if (!url || typeof url !== 'string') {
return "";
}
const re = /((127\.\d+\.\d+\.\d+)|(10\.\d+\.\d+\.\d+)|(172\.1[6-9]\.\d+\.\d+)|(172\.2[0-9]\.\d+\.\d+)|(172\.3[0-1]\.\d+\.\d+)|(192\.168\.\d+\.\d+))(:\d+)?/g;
return url.replace(re,window.location.host);
}
unsafeWindow.fixLoadedPage = function fixLoadedPage() {
document.querySelectorAll('img').forEach(function(img){
if (img.src) {
img.src = fixUrl(img.src);
}
});
document.querySelectorAll('a').forEach(function(a){
if (a.href) {
a.href = fixUrl(a.href);
}
});
}
function hijackXHR() {
var rawOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
if (!this._hooked) {
this._hooked = true;
this._url = url;
setupHook(this);
}
rawOpen.apply(this, [method, url, async, user, password]);
}
function setupHook(xhr) {
function get() {
delete xhr.responseText;
var ret = xhr.responseText;
try {
if (USE_CACHE && xhr._url && xhr._url.match(/start=/)) {
var index = parseInt(xhr._url.match(/start=(\d+)/)[1]);
var json = JSON.parse(ret);
if (json && json.item && json.item.length) {
json.item.forEach((i,k) => {
if (i && i.meta && i.meta.id) {
var id1 = 'fTh' + i.meta.id;
var id2 = 'fThBB' + (index + k);
cachePut(id1, i);
cachePut(id2, i);
}
});
}
}
} catch (ex) {}
setup();
return fixUrl(ret);
}
function set(str) {
// Should be unused
console.log('set responseText: %s', str);
}
function setup() {
Object.defineProperty(xhr, 'responseText', { get, set, configurable: true });
}
setup();
}
}
const CACHE = unsafeWindow.CACHE = {};
function cachePut(k,v) {
CACHE[k] = v;
}
function cacheGet(k, defaultValue='') {
return k in CACHE ? CACHE[k] : defaultValue;
}
function getFilename(url) {
if (!url) return '';
var match = url.match(/[^/]+$/);
if (!match.length) return false;
return match[0].replace(/\?.*$/,'');
}
function addShortcuts() {
document.body.addEventListener('keyup', function (e) {
var currentPage = document.querySelector('#browsePages span');
if (!currentPage) return;
switch(e.keyCode) {
// Left
case 37:
currentPage.previousElementSibling && currentPage.previousElementSibling.click();
console.log('prev');
break;
// Right
case 39:
currentPage.nextElementSibling && currentPage.nextElementSibling.click();
console.log('next');
break;
}
});
}
function watchOnNewNodes(baseElementSelector, newNodeSelector, callback) {
const observer = new MutationObserver(function(mutationsList, observer) {
for(const mutation of mutationsList) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(function(n){
if (!n || !n.querySelectorAll) return;
n.querySelectorAll(newNodeSelector).forEach(node => {
if(node) callback(node);
})
});
}
}
});
let targetNode = baseElementSelector;
if (typeof baseElementSelector === 'string') {
targetNode = document.querySelector(baseElementSelector);
}
if (!targetNode) {
return;
}
const config = { attributes: false, childList: true, subtree: true };
observer.observe(targetNode, config);
}
function watchOnEvent(baseElementSelector, eventName, selector, callback) {
watchOnNewNodes(baseElementSelector, selector, function(node){
node.addEventListener(eventName, callback);
});
}
function createPhotoAlbumUrl(SERVER_UUID, bookmark) {
return window.location.pathname + "#"+window.location.origin+"/nmc/rss/server/RB" + SERVER_UUID + ",0/IB" + bookmark + '?start=0&count=30';
}
function openPhotoAlbum(SERVER_UUID, bookmark) {
window.open(createPhotoAlbumUrl(SERVER_UUID, bookmark), '_blank');
}
fixLoadedPage();
hijackXHR();
addShortcuts();
watchOnNewNodes('#wrapper', '.byFolderContainer', function(n){
const link = n.querySelector('.myLibraryBeamContainerNmcLocalDevice');
const link2 = n.querySelector('.beam-button');
const title = n.querySelector('.titleContainer');
if (link && title) {
const href = '/#' + (title.onclick+'').match(/http[^']+/)[0];
link.href = href;
link.title = 'Open album';
link.style.height = 'auto';
link.style.marginTop = '7px';
link.style.background = 'none';
link.style.backgroundImage = 'none';
link.innerHTML = '<button><i class="fa fa-external-link"></i></button>';
link.target = '_blank';
link.onclick = function(e) {
e.stopPropagation();
};
}
else if (link2){
const a = document.createElement('a');
a.innerHTML = '<button><i class="fa fa-external-link"></i></button>';
a.target = '_blank';
a.href = '/webbrowse#' + (n.onclick+'').match(/http[^']+/)[0];
link2.parentElement.appendChild(a);
link2.parentElement.removeChild(link2);
}
});
if (USE_CACHE) {
/**/
const footer = document.createElement('div');
footer.id = 'info_footer';
footer.style.color = 'black';
footer.style.padding = '1em';
footer.style.display = 'none';
footer.style.position = 'fixed';
footer.style.background = 'grey';
document.body.appendChild(footer);
watchOnEvent('#wrapper', 'mouseleave', '.photoThumbnail', async function (e) {
footer.innerHTML = '';
footer.style.display = 'none';
let pathBtn = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'pathbtn');
});
watchOnEvent('#wrapper', 'mouseleave', '.myLibraryMediaIconVideo img', async function (e) {
footer.innerHTML = '';
footer.style.display = 'none';
let pathBtn = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'pathbtn');
});
watchOnEvent('#wrapper', 'mouseenter', '.myLibraryMediaIconVideo img', async function (e) {
var info = cacheGet(this.id);
if (info) {
let btnContainer = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'btncontainer');
if (!btnContainer) {
btnContainer = document.createElement('div');
btnContainer.id = this.id + 'btncontainer';
btnContainer.style.position = 'absolute';
btnContainer.style.bottom = '0px';
let container = this.parentElement.parentElement;
container.appendChild(btnContainer);
}
let aVid = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'avid');
if (this.src && !aVid) {
const url = fixUrl(info.meta.res[0].value);
this.parentElement.href = url;
this.parentElement.onclick = function(){};
aVid = document.createElement('a');
aVid.id = this.id + 'avid';
aVid.href = url;
aVid.target = '_blank';
aVid.title = 'Open video in new tab';
aVid.innerHTML = '<button style="font-size:0.8em;"><i class="fa fa-film"></i></button>';
btnContainer.appendChild(aVid);
}
let toAlbumBtn = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'toalbum');
if (!toAlbumBtn && (info.meta['upnp:album'] in photoAlbums)) {
const a = document.createElement('a');
toAlbumBtn = document.createElement('button');
toAlbumBtn.id = this.id + 'toalbum';
toAlbumBtn.title = 'Open photo album';
toAlbumBtn.innerHTML = '<i class="fa fa-external-link"></i>';
toAlbumBtn.style.fontSize = '0.8em';
btnContainer.appendChild(a);
a.target = '_blank';
a.href = createPhotoAlbumUrl(status.server_udn, photoAlbums[info.meta['upnp:album']].bookmark);
a.appendChild(toAlbumBtn);
}
if (!info.path) {
info.path = await getPath(info.bookmark);
}
let pathBtn = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'pathbtn');
if (!pathBtn) {
pathBtn = document.createElement('button');
pathBtn.id = this.id + 'pathbtn';
pathBtn.title = 'Click to copy file path';
pathBtn.innerHTML = '<i class="fa fa-clipboard"></i>';
pathBtn.style.fontSize = '0.8em';
btnContainer.appendChild(pathBtn);
pathBtn.addEventListener('click', function(){
GM_setClipboard(info.path);
});
}
footer.innerHTML = ('ALBUM: ' + info.meta['upnp:album'] + '<br>PATH: ' + info.path);
footer.style.display = 'block';
}
});
watchOnEvent('#wrapper', 'mouseenter', '.photoThumbnail', async function (e) {
var info = cacheGet(this.id);
if (info) {
let btnContainer = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'btncontainer');
if (!btnContainer) {
btnContainer = document.createElement('div');
btnContainer.id = this.id + 'btncontainer';
btnContainer.style.position = 'absolute';
btnContainer.style.bottom = '0px';
let container = this.parentElement.parentElement;
container.appendChild(btnContainer);
}
let aImg = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'aimg');
if (this.src && !aImg) {
aImg = document.createElement('a');
aImg.id = this.id + 'aimg';
aImg.href = this.src.replace(/\?.*/,'');
aImg.target = '_blank';
aImg.title = 'Open image in new tab';
aImg.innerHTML = '<button style="font-size:0.8em;"><i class="fa fa-photo"></i></button>';
btnContainer.appendChild(aImg);
}
let toAlbumBtn = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'toalbum');
if (!toAlbumBtn && (info.meta['upnp:album'] in photoAlbums)) {
const a = document.createElement('a');
toAlbumBtn = document.createElement('button');
toAlbumBtn.id = this.id + 'toalbum';
toAlbumBtn.title = 'Open photo album';
toAlbumBtn.innerHTML = '<i class="fa fa-external-link"></i>';
toAlbumBtn.style.fontSize = '0.8em';
btnContainer.appendChild(a);
a.target = '_blank';
a.href = createPhotoAlbumUrl(status.server_udn, photoAlbums[info.meta['upnp:album']].bookmark);
a.appendChild(toAlbumBtn);
}
if (!info.path) {
info.path = await getPath(info.bookmark);
}
let pathBtn = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'pathbtn');
if (!pathBtn) {
pathBtn = document.createElement('button');
pathBtn.id = this.id + 'pathbtn';
pathBtn.title = 'Click to copy file path';
pathBtn.innerHTML = '<i class="fa fa-clipboard"></i>';
pathBtn.style.fontSize = '0.8em';
btnContainer.appendChild(pathBtn);
pathBtn.addEventListener('click', function(){
GM_setClipboard(info.path);
});
}
footer.innerHTML = ('ALBUM: ' + info.meta['upnp:album'] + '<br>PATH: ' + info.path);
footer.style.display = 'block';
}
});
window.onmousemove = function (e) {
footer.style.top = (e.clientY + 20) + 'px';
footer.style.left = (e.clientX + 20) + 'px';
};
/**/
}
}
/**/
})();