Twonky Enhancer

Fix Twonky public Web UI

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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';
            };
            /**/
        }

    }
    /**/
})();