Twonky Enhancer

Fix Twonky public Web UI

  1. // ==UserScript==
  2. // @name Twonky Enhancer
  3. // @version v20230809.1524
  4. // @description Fix Twonky public Web UI
  5. // @author ltlwinston
  6. // @match http*://*/*
  7. // @grant GM_addElement
  8. // @grant GM_setClipboard
  9. // @require https://cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.js
  10. // @namespace https://greasyfork.org/users/754595
  11. // ==/UserScript==
  12. GM_addElement('link',{
  13. rel: "stylesheet",
  14. href: "//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.css"
  15. });
  16. GM_addElement('link',{
  17. rel: "stylesheet",
  18. href: "//cdnjs.cloudflare.com/ajax/libs/awesomplete/1.1.5/awesomplete.min.css"
  19. });
  20.  
  21. const interesting_words = [
  22. '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'
  23. ];
  24.  
  25. (async function () {
  26. 'use strict';
  27.  
  28. const USE_CACHE = true;
  29.  
  30. if (document.title.match(/(twonky|pv connect|mediaserver)/i)) {
  31. if(document.body.innerText.indexOf('Access is restricted to MediaServer configuration!')>=0) {
  32. window.location.href = '/webbrowse';
  33. return;
  34. }
  35.  
  36. async function loadServerStatus() {
  37. const status = {};
  38. await fetch('/rpc/info_status').then(r => r.text()).then(s => s.split(/[\t\n ]/).forEach(i => {
  39. const [k,v] = i.split('|');
  40. status[k] = isNaN(v) ? v : parseInt(v);
  41. }));
  42. return status;
  43. }
  44.  
  45. async function loadPhotoAlbums(SERVER_UUID) {
  46. 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';
  47. const albumResult = await fetch(albumUrl).then(x=>x.json());
  48. if (!albumResult || !albumResult.item) {
  49. throw 'ERR: Cannot load photo albums';
  50. }
  51. return albumResult.item.map(x=>({title: x.title, bookmark: x.bookmark}));
  52. }
  53. async function loadVideoAlbums(SERVER_UUID) {
  54. 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';
  55. const albumResult = await fetch(albumUrl).then(x=>x.json());
  56. if (!albumResult || !albumResult.item) {
  57. throw 'ERR: Cannot load video albums';
  58. }
  59. return albumResult.item.map(x=>({title: x.title, bookmark: x.bookmark}));
  60. }
  61. async function getPath(bookmark) {
  62. return fetch('/nmc/rpc/get_item_path?server='+encodeURIComponent(bookmark)).then(x => x.text());
  63. }
  64.  
  65. if (typeof unsafeWindow['statusData'] == 'undefined') {
  66. unsafeWindow['statusData'] = {'language': 'en'};
  67. }
  68. if (!('language' in unsafeWindow['statusData'])) {
  69. unsafeWindow['statusData']['language'] = 'en';
  70. initPage();
  71. }
  72.  
  73. const statusElem = document.createElement('div');
  74. statusElem.id = 'te_status';
  75. statusElem.style.position = 'fixed';
  76. statusElem.style.color = 'black';
  77. statusElem.style.top = '1em';
  78. statusElem.style.left = '1em';
  79. statusElem.innerHTML = '<a href="javascript:return false;"><i class="fa fa-refresh"></i></a><br>'
  80. document.body.appendChild(statusElem);
  81.  
  82. const status = await loadServerStatus();
  83. let SERVER_UUID = '';
  84. let photoAlbums = {};
  85. let videoAlbums = {};
  86.  
  87. if (status) {
  88. if (('videos' in status) && ('pictures' in status)) {
  89. let nPics = status.pictures;
  90. let nVids = status.videos;
  91. statusElem.innerHTML += `<i class="fa fa-photo"></i> ${nPics} <i class="fa fa-video-camera"></i> ${nVids}`;
  92.  
  93. SERVER_UUID = status.server_udn;
  94. if (SERVER_UUID) {
  95. const pAlbumStatus = document.createElement('div');
  96. const vAlbumStatus = document.createElement('div');
  97. statusElem.appendChild(pAlbumStatus);
  98. statusElem.appendChild(vAlbumStatus);
  99.  
  100. pAlbumStatus.innerHTML = '<i class="fa fa-file-image-o"></i> Loading...';
  101. loadPhotoAlbums(SERVER_UUID).then(a => {
  102. a.forEach(x => {photoAlbums[x.title] = x});
  103. pAlbumStatus.innerHTML = (a.length+' <i class="fa fa-file-image-o"></i><br><input id="pasearch" placeholder="Search a photo album">');
  104. const pasearch = document.querySelector('#pasearch');
  105. pasearch.addEventListener('blur', function(e){this.value = ''});
  106. pasearch.addEventListener('awesomplete-select', function(e){
  107. openPhotoAlbum(SERVER_UUID, e.text.value);
  108. //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');
  109. e.preventDefault();
  110. });
  111. new Awesomplete(pasearch, {list: a, data: i => ({label:i.title, value:i.bookmark})});
  112. }).catch(e => {
  113. pAlbumStatus.innerText = (e);
  114. });
  115. vAlbumStatus.innerHTML = '<i class="fa fa-file-video-o"></i> Loading...';
  116. loadVideoAlbums(SERVER_UUID).then(a => {
  117. a.forEach(x => {videoAlbums[x.title] = x});
  118. vAlbumStatus.innerHTML = (a.length+' <i class="fa fa-file-video-o"></i><br><input id="vasearch" placeholder="Search a video album">');
  119. const vasearch = document.querySelector('#vasearch');
  120. vasearch.addEventListener('blur', function(e){this.value = ''});
  121. vasearch.addEventListener('awesomplete-select', function(e){
  122. 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');
  123. e.preventDefault();
  124. });
  125. new Awesomplete(vasearch, {list: a, data: i => ({label:i.title, value:i.bookmark})});
  126. }).catch(e => {
  127. vAlbumStatus.innerText = (e);
  128. });
  129. } else {
  130. const pAlbumStatus = document.createElement('div');
  131. pAlbumStatus.innerText = 'Album search not available.';
  132. statusElem.appendChild(pAlbumStatus);
  133. }
  134. }
  135. }
  136.  
  137. function fixUrl(url) {
  138. if (!url || typeof url !== 'string') {
  139. return "";
  140. }
  141. 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;
  142. return url.replace(re,window.location.host);
  143. }
  144.  
  145. unsafeWindow.fixLoadedPage = function fixLoadedPage() {
  146. document.querySelectorAll('img').forEach(function(img){
  147. if (img.src) {
  148. img.src = fixUrl(img.src);
  149. }
  150. });
  151. document.querySelectorAll('a').forEach(function(a){
  152. if (a.href) {
  153. a.href = fixUrl(a.href);
  154. }
  155. });
  156. }
  157.  
  158. function hijackXHR() {
  159. var rawOpen = XMLHttpRequest.prototype.open;
  160. XMLHttpRequest.prototype.open = function(method, url, async, user, password) {
  161. if (!this._hooked) {
  162. this._hooked = true;
  163. this._url = url;
  164. setupHook(this);
  165. }
  166. rawOpen.apply(this, [method, url, async, user, password]);
  167. }
  168. function setupHook(xhr) {
  169. function get() {
  170. delete xhr.responseText;
  171. var ret = xhr.responseText;
  172. try {
  173. if (USE_CACHE && xhr._url && xhr._url.match(/start=/)) {
  174. var index = parseInt(xhr._url.match(/start=(\d+)/)[1]);
  175. var json = JSON.parse(ret);
  176. if (json && json.item && json.item.length) {
  177. json.item.forEach((i,k) => {
  178. if (i && i.meta && i.meta.id) {
  179. var id1 = 'fTh' + i.meta.id;
  180. var id2 = 'fThBB' + (index + k);
  181. cachePut(id1, i);
  182. cachePut(id2, i);
  183. }
  184. });
  185. }
  186. }
  187. } catch (ex) {}
  188. setup();
  189. return fixUrl(ret);
  190. }
  191.  
  192. function set(str) {
  193. // Should be unused
  194. console.log('set responseText: %s', str);
  195. }
  196.  
  197. function setup() {
  198. Object.defineProperty(xhr, 'responseText', { get, set, configurable: true });
  199. }
  200. setup();
  201. }
  202. }
  203.  
  204. const CACHE = unsafeWindow.CACHE = {};
  205. function cachePut(k,v) {
  206. CACHE[k] = v;
  207. }
  208. function cacheGet(k, defaultValue='') {
  209. return k in CACHE ? CACHE[k] : defaultValue;
  210. }
  211.  
  212. function getFilename(url) {
  213. if (!url) return '';
  214. var match = url.match(/[^/]+$/);
  215. if (!match.length) return false;
  216. return match[0].replace(/\?.*$/,'');
  217. }
  218.  
  219. function addShortcuts() {
  220. document.body.addEventListener('keyup', function (e) {
  221. var currentPage = document.querySelector('#browsePages span');
  222. if (!currentPage) return;
  223. switch(e.keyCode) {
  224. // Left
  225. case 37:
  226. currentPage.previousElementSibling && currentPage.previousElementSibling.click();
  227. console.log('prev');
  228. break;
  229. // Right
  230. case 39:
  231. currentPage.nextElementSibling && currentPage.nextElementSibling.click();
  232. console.log('next');
  233. break;
  234. }
  235. });
  236. }
  237.  
  238. function watchOnNewNodes(baseElementSelector, newNodeSelector, callback) {
  239. const observer = new MutationObserver(function(mutationsList, observer) {
  240. for(const mutation of mutationsList) {
  241. if (mutation.type === 'childList') {
  242. mutation.addedNodes.forEach(function(n){
  243. if (!n || !n.querySelectorAll) return;
  244. n.querySelectorAll(newNodeSelector).forEach(node => {
  245. if(node) callback(node);
  246. })
  247. });
  248. }
  249. }
  250. });
  251. let targetNode = baseElementSelector;
  252. if (typeof baseElementSelector === 'string') {
  253. targetNode = document.querySelector(baseElementSelector);
  254. }
  255. if (!targetNode) {
  256. return;
  257. }
  258. const config = { attributes: false, childList: true, subtree: true };
  259. observer.observe(targetNode, config);
  260. }
  261. function watchOnEvent(baseElementSelector, eventName, selector, callback) {
  262. watchOnNewNodes(baseElementSelector, selector, function(node){
  263. node.addEventListener(eventName, callback);
  264. });
  265. }
  266. function createPhotoAlbumUrl(SERVER_UUID, bookmark) {
  267. return window.location.pathname + "#"+window.location.origin+"/nmc/rss/server/RB" + SERVER_UUID + ",0/IB" + bookmark + '?start=0&count=30';
  268. }
  269. function openPhotoAlbum(SERVER_UUID, bookmark) {
  270. window.open(createPhotoAlbumUrl(SERVER_UUID, bookmark), '_blank');
  271. }
  272.  
  273. fixLoadedPage();
  274. hijackXHR();
  275. addShortcuts();
  276.  
  277. watchOnNewNodes('#wrapper', '.byFolderContainer', function(n){
  278. const link = n.querySelector('.myLibraryBeamContainerNmcLocalDevice');
  279. const link2 = n.querySelector('.beam-button');
  280. const title = n.querySelector('.titleContainer');
  281. if (link && title) {
  282. const href = '/#' + (title.onclick+'').match(/http[^']+/)[0];
  283. link.href = href;
  284. link.title = 'Open album';
  285. link.style.height = 'auto';
  286. link.style.marginTop = '7px';
  287. link.style.background = 'none';
  288. link.style.backgroundImage = 'none';
  289. link.innerHTML = '<button><i class="fa fa-external-link"></i></button>';
  290. link.target = '_blank';
  291. link.onclick = function(e) {
  292. e.stopPropagation();
  293. };
  294. }
  295. else if (link2){
  296. const a = document.createElement('a');
  297. a.innerHTML = '<button><i class="fa fa-external-link"></i></button>';
  298. a.target = '_blank';
  299. a.href = '/webbrowse#' + (n.onclick+'').match(/http[^']+/)[0];
  300. link2.parentElement.appendChild(a);
  301. link2.parentElement.removeChild(link2);
  302. }
  303. });
  304. if (USE_CACHE) {
  305. /**/
  306. const footer = document.createElement('div');
  307. footer.id = 'info_footer';
  308. footer.style.color = 'black';
  309. footer.style.padding = '1em';
  310. footer.style.display = 'none';
  311. footer.style.position = 'fixed';
  312. footer.style.background = 'grey';
  313. document.body.appendChild(footer);
  314. watchOnEvent('#wrapper', 'mouseleave', '.photoThumbnail', async function (e) {
  315. footer.innerHTML = '';
  316. footer.style.display = 'none';
  317. let pathBtn = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'pathbtn');
  318. });
  319. watchOnEvent('#wrapper', 'mouseleave', '.myLibraryMediaIconVideo img', async function (e) {
  320. footer.innerHTML = '';
  321. footer.style.display = 'none';
  322. let pathBtn = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'pathbtn');
  323. });
  324. watchOnEvent('#wrapper', 'mouseenter', '.myLibraryMediaIconVideo img', async function (e) {
  325. var info = cacheGet(this.id);
  326. if (info) {
  327. let btnContainer = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'btncontainer');
  328. if (!btnContainer) {
  329. btnContainer = document.createElement('div');
  330. btnContainer.id = this.id + 'btncontainer';
  331. btnContainer.style.position = 'absolute';
  332. btnContainer.style.bottom = '0px';
  333. let container = this.parentElement.parentElement;
  334. container.appendChild(btnContainer);
  335. }
  336. let aVid = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'avid');
  337. if (this.src && !aVid) {
  338. const url = fixUrl(info.meta.res[0].value);
  339. this.parentElement.href = url;
  340. this.parentElement.onclick = function(){};
  341. aVid = document.createElement('a');
  342. aVid.id = this.id + 'avid';
  343. aVid.href = url;
  344. aVid.target = '_blank';
  345. aVid.title = 'Open video in new tab';
  346. aVid.innerHTML = '<button style="font-size:0.8em;"><i class="fa fa-film"></i></button>';
  347. btnContainer.appendChild(aVid);
  348. }
  349. let toAlbumBtn = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'toalbum');
  350. if (!toAlbumBtn && (info.meta['upnp:album'] in photoAlbums)) {
  351. const a = document.createElement('a');
  352. toAlbumBtn = document.createElement('button');
  353. toAlbumBtn.id = this.id + 'toalbum';
  354. toAlbumBtn.title = 'Open photo album';
  355. toAlbumBtn.innerHTML = '<i class="fa fa-external-link"></i>';
  356. toAlbumBtn.style.fontSize = '0.8em';
  357. btnContainer.appendChild(a);
  358. a.target = '_blank';
  359. a.href = createPhotoAlbumUrl(status.server_udn, photoAlbums[info.meta['upnp:album']].bookmark);
  360. a.appendChild(toAlbumBtn);
  361. }
  362.  
  363. if (!info.path) {
  364. info.path = await getPath(info.bookmark);
  365. }
  366. let pathBtn = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'pathbtn');
  367. if (!pathBtn) {
  368. pathBtn = document.createElement('button');
  369. pathBtn.id = this.id + 'pathbtn';
  370. pathBtn.title = 'Click to copy file path';
  371. pathBtn.innerHTML = '<i class="fa fa-clipboard"></i>';
  372. pathBtn.style.fontSize = '0.8em';
  373. btnContainer.appendChild(pathBtn);
  374. pathBtn.addEventListener('click', function(){
  375. GM_setClipboard(info.path);
  376. });
  377. }
  378. footer.innerHTML = ('ALBUM: ' + info.meta['upnp:album'] + '<br>PATH: ' + info.path);
  379. footer.style.display = 'block';
  380. }
  381. });
  382. watchOnEvent('#wrapper', 'mouseenter', '.photoThumbnail', async function (e) {
  383. var info = cacheGet(this.id);
  384. if (info) {
  385. let btnContainer = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'btncontainer');
  386. if (!btnContainer) {
  387. btnContainer = document.createElement('div');
  388. btnContainer.id = this.id + 'btncontainer';
  389. btnContainer.style.position = 'absolute';
  390. btnContainer.style.bottom = '0px';
  391. let container = this.parentElement.parentElement;
  392. container.appendChild(btnContainer);
  393. }
  394. let aImg = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'aimg');
  395. if (this.src && !aImg) {
  396. aImg = document.createElement('a');
  397. aImg.id = this.id + 'aimg';
  398. aImg.href = this.src.replace(/\?.*/,'');
  399. aImg.target = '_blank';
  400. aImg.title = 'Open image in new tab';
  401. aImg.innerHTML = '<button style="font-size:0.8em;"><i class="fa fa-photo"></i></button>';
  402. btnContainer.appendChild(aImg);
  403. }
  404. let toAlbumBtn = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'toalbum');
  405. if (!toAlbumBtn && (info.meta['upnp:album'] in photoAlbums)) {
  406. const a = document.createElement('a');
  407. toAlbumBtn = document.createElement('button');
  408. toAlbumBtn.id = this.id + 'toalbum';
  409. toAlbumBtn.title = 'Open photo album';
  410. toAlbumBtn.innerHTML = '<i class="fa fa-external-link"></i>';
  411. toAlbumBtn.style.fontSize = '0.8em';
  412. btnContainer.appendChild(a);
  413. a.target = '_blank';
  414. a.href = createPhotoAlbumUrl(status.server_udn, photoAlbums[info.meta['upnp:album']].bookmark);
  415. a.appendChild(toAlbumBtn);
  416. }
  417.  
  418. if (!info.path) {
  419. info.path = await getPath(info.bookmark);
  420. }
  421. let pathBtn = document.querySelector('#' + this.id.replace(/\$/g,'\\$') + 'pathbtn');
  422. if (!pathBtn) {
  423. pathBtn = document.createElement('button');
  424. pathBtn.id = this.id + 'pathbtn';
  425. pathBtn.title = 'Click to copy file path';
  426. pathBtn.innerHTML = '<i class="fa fa-clipboard"></i>';
  427. pathBtn.style.fontSize = '0.8em';
  428. btnContainer.appendChild(pathBtn);
  429. pathBtn.addEventListener('click', function(){
  430. GM_setClipboard(info.path);
  431. });
  432. }
  433. footer.innerHTML = ('ALBUM: ' + info.meta['upnp:album'] + '<br>PATH: ' + info.path);
  434. footer.style.display = 'block';
  435. }
  436. });
  437. window.onmousemove = function (e) {
  438. footer.style.top = (e.clientY + 20) + 'px';
  439. footer.style.left = (e.clientX + 20) + 'px';
  440. };
  441. /**/
  442. }
  443.  
  444. }
  445. /**/
  446. })();