Tuna browser script

Get song information from web players, based on NowSniper by Kıraç Armağan Önal

  1. // ==UserScript==
  2. // @name Tuna browser script
  3. // @namespace univrsal
  4. // @version 1.0.6
  5. // @description Get song information from web players, based on NowSniper by Kıraç Armağan Önal
  6. // @author univrsal
  7. // @match *://open.spotify.com/*
  8. // @match *://soundcloud.com/*
  9. // @match *://music.yandex.com/*
  10. // @match *://music.yandex.ru/*
  11. // @match *://www.deezer.com/*
  12. // @match *://www.pandora.com/*
  13. // @grant unsafeWindow
  14. // @license GPLv2
  15. // ==/UserScript==
  16.  
  17. (function() {
  18. 'use strict';
  19. // Configuration
  20. var port = 1608;
  21. var refresh_rate_ms = 500;
  22. var cooldown_ms = 5000;
  23.  
  24. // Tuna isn't running we sleep, because every failed request will log into the console
  25. // so we don't want to spam it
  26. var failure_count = 0;
  27. var cooldown = 0;
  28.  
  29. function post(data){
  30. var url = 'http://localhost:' + port + '/';
  31. var xhr = new XMLHttpRequest();
  32. xhr.open('POST', url);
  33.  
  34. xhr.setRequestHeader('Accept', 'application/json');
  35. xhr.setRequestHeader('Content-Type', 'application/json');
  36. xhr.setRequestHeader('Access-Control-Allow-Headers', '*');
  37. xhr.setRequestHeader('Access-Control-Allow-Origin', '*');
  38.  
  39. xhr.onreadystatechange = function () {
  40. if (xhr.readyState === 4) {
  41. if (xhr.status !== 200) {
  42. failure_count++;
  43. }
  44. }
  45. };
  46. xhr.send(JSON.stringify({data,hostname:window.location.hostname,date:Date.now()}));
  47. }
  48.  
  49. // Safely query something, and perform operations on it
  50. function query(target, fun, alt = null) {
  51. var element = document.querySelector(target);
  52. if (element !== null) {
  53. return fun(element);
  54. }
  55. return alt;
  56. }
  57.  
  58. function timestamp_to_ms(ts) {
  59. var splits = ts.split(':');
  60. if (splits.length == 2) {
  61. return splits[0] * 60 * 1000 + splits[1] * 1000;
  62. } else if (splits.length == 3) {
  63. return splits[0] * 60 * 60 * 1000 + splits[1] * 60 * 1000 + splits[0] * 1000;
  64. }
  65. return 0;
  66. }
  67.  
  68. function StartFunction() {
  69. setInterval(()=>{
  70. if (failure_count > 3) {
  71. console.log('Failed to connect multiple times, waiting a few seconds');
  72. cooldown = cooldown_ms;
  73. failure_count = 0;
  74. }
  75.  
  76. if (cooldown > 0) {
  77. cooldown -= refresh_rate_ms;
  78. return;
  79. }
  80.  
  81. let hostname = window.location.hostname;
  82.  
  83. // TODO: maybe add more?
  84. if (hostname == 'soundcloud.com') {
  85. let status = query('.playControl', e => e.classList.contains('playing') ? "playing" : "stopped", 'unknown');
  86. let cover_url = query('.playbackSoundBadge span.sc-artwork', e => e.style.backgroundImage.slice(5, -2).replace('t50x50','t500x500'));
  87. let title = query('.playbackSoundBadge__titleLink', e => e.title);
  88. let artists = [ query('.playbackSoundBadge__lightLink', e => e.title) ];
  89. let progress = query('.playbackTimeline__timePassed span:nth-child(2)', e => timestamp_to_ms(e.textContent));
  90. let duration = query('.playbackTimeline__duration span:nth-child(2)', e => timestamp_to_ms(e.textContent));
  91. let album_url = query('.playbackSoundBadge__titleLink', e => e.href);
  92.  
  93. if (title !== null) {
  94. post({ cover_url, title, artists, status, progress, duration, album_url });
  95. }
  96. } else if (hostname == 'open.spotify.com') {
  97. let status = query('.player-controls [data-testid="control-button-pause"]', e => !!e ? 'playing' : 'stopped', 'unknown');
  98. let cover_url = query('[data-testid="CoverSlotExpanded__container"] .cover-art-image', e => e.style.backgroundImage.slice(5, -2));
  99. let title = query('[data-testid="nowplaying-track-link"]', e => e.textContent);
  100. let artists = query('span[draggable] a[href*="artist"]', e => Array.from(e));
  101. let progress = query('.playback-bar .playback-bar__progress-time', e => timestamp_to_ms(e[0].textContent));
  102. let duration = query('.playback-bar .playback-bar__progress-time', e => timestamp_to_ms(e[1].textContent));
  103. let album_url = query('[data-testid="nowplaying-track-link"]', e => e.href);
  104.  
  105. if (title !== null) {
  106. post({ cover_url, title, artists, status, progress, duration, album_url });
  107. }
  108. } else if (hostname == 'music.yandex.ru') {
  109. // Yandex music support by MjKey
  110. let status = query('.player-controls__btn_play', e => e.classList.contains('player-controls__btn_pause') ? "playing" : "stopped", 'unknown');
  111. let cover_url = query('.entity-cover__image', e => e.style.backgroundImage.slice(5, -2).replace('50x50','200x200'));
  112. let title = query('.track__title', e => e.title);
  113. let artists = [ query('.track__artists', e => e.textContent) ];
  114. let progress = query('.progress__left', e => timestamp_to_ms(e.textContent));
  115. let duration = query('.progress__right', e => timestamp_to_ms(e.textContent));
  116. let album_url = query('.track-cover a', e => e.title);
  117.  
  118. if (title !== null) {
  119. post({ cover_url, title, artists, status, progress, duration, album_url });
  120. }
  121. } else if (hostname == 'www.deezer.com') {
  122. let status = query('.player-controls', e => {
  123. let pause = e.getElementsByClassName('svg-icon-pause');
  124. let play = e.getElementsByClassName('svg-icon-play');
  125. if (pause.length > 0) {
  126. return "playing";
  127. }
  128. if (play.length > 0) {
  129. return "stopped";
  130. }
  131. return "unknown";
  132. });
  133.  
  134. let cover_url = query('button.queuelist.is-available', e => {
  135. let img = e.getElementsByTagName('img');
  136. if (img.length > 0) {
  137. let src = img[0].src; // https://e-cdns-images.dzcdn.net/images/cover/c4217689cc86e3e6a289162239424dc3/28x28-000000-80-0-0.jpg
  138. return src.replace('28x28', '512x512');
  139. }
  140. return null;
  141. });
  142.  
  143. let title = query('.marquee-content', e => {
  144. let links = e.getElementsByClassName('track-link');
  145. if (links.length > 0) {
  146. return links[0].textContent;
  147. }
  148. return null;
  149. });
  150. let artists = query('.marquee-content', e => {
  151. let links = e.getElementsByClassName('track-link');
  152. let artists = [];
  153. if (links.length > 1) {
  154. for (var i = 1; i < links.length; i++) {
  155. artists.push(links[i].textContent);
  156. }
  157. return artists;
  158. }
  159. return null;
  160. });
  161.  
  162. let duration = query('.slider-counter-max', e => timestamp_to_ms(e.textContent));
  163. let progress = query('.slider-counter-current', e => timestamp_to_ms(e.textContent));
  164. if (title !== null) {
  165. post({ cover_url, title, artists, status, progress, duration });
  166. }
  167. } else if (hostname == 'www.pandora.com') {
  168. let button_node = document.querySelector('button[data-qa].PlayButton');
  169. let button_shown = button_node ? button_node.getAttribute('data-qa') : '';
  170. let status = 'unknown';
  171. if(!!button_node && !!button_shown) {
  172. status = button_shown == 'pause_button' ? 'playing' : 'stopped';
  173. }
  174. let cover_url = query('.nowPlayingTopInfo__artContainer__art .ImageLoader img', e => e.src);
  175. let title1 = query('.NowPlaying__content .Marquee', e => e.textContent);
  176. let title_scrolling = query('.NowPlaying__content .Marquee__wrapper__content__child:nth-child(1)', e => e.textContent);
  177. let title = null;
  178. if (!!title_scrolling){
  179. title = title_scrolling;
  180. }
  181. else if (!!title1){
  182. title = title1;
  183. }
  184. let artists = [ query('.NowPlaying__content .NowPlayingTopInfo__current__artistName', e => e.textContent) ];
  185. let progress = query('.Duration span[data-qa="elapsed_time"]', e => timestamp_to_ms(e.textContent));
  186. let duration = query('.Duration span[data-qa="remaining_time"]', e => timestamp_to_ms(e.textContent));
  187. let album_name = query('.NowPlaying__content .nowPlayingTopInfo__current__albumName', e => e.textContent);
  188. let album_url = '';
  189. if (title === 'Your station will be right back.') {
  190. cover_url = 'https://res-2.cloudinary.com/crunchbase-production/image/upload/c_lpad,h_170,w_170,f_auto,b_white,q_auto:eco/nenzclww7vhpwgzb7ozj';
  191. title = ' ';
  192. artists = [' '];
  193. album_url = ' ';
  194. }
  195. if (title !== null) {
  196. post({ cover_url, title, artists, status, progress, duration, album_url });
  197. }
  198. }
  199. }, 500);
  200.  
  201. }
  202.  
  203. StartFunction();
  204. })();