YouTube BPM Display

Displays the BPM of a song on YouTube using Spotify API

  1. // ==UserScript==
  2. // @name YouTube BPM Display
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.4
  5. // @description Displays the BPM of a song on YouTube using Spotify API
  6. // @author Sergi0
  7. // @match https://www.youtube.com/*
  8. // @grant GM_xmlhttpRequest
  9. // @connect api.spotify.com
  10. // @icon https://www.freeiconspng.com/uploads/youtube-icon-app-logo-png-9.png
  11. // @license MIT
  12. // @homepageURL https://gf.qytechs.cn/es/scripts/511311
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // 1. Spotify API credentials
  19. const CLIENT_ID = 'YOUR_SPOTIFY_CLIENT_ID'; // Replace with your Client ID
  20. const CLIENT_SECRET = 'YOUR_SPOTIFY_CLIENT_SECRET'; // Replace with your Client Secret
  21. let accessToken = '';
  22. let lastTitle = '';
  23. let lastUrl = '';
  24.  
  25. // 2. Function to get Spotify access token
  26. function getAccessToken(callback) {
  27. if (CLIENT_ID.includes("YOUR_SPOTIFY") || CLIENT_SECRET.includes("YOUR_SPOTIFY")) {
  28. setTimeout(() => {
  29. displaySongInfo("Please provide your Spotify API keys to use this script.");
  30. }, 2000);
  31. return;
  32. }
  33.  
  34. const authString = btoa(`${CLIENT_ID}:${CLIENT_SECRET}`);
  35. GM_xmlhttpRequest({
  36. method: 'POST',
  37. url: 'https://accounts.spotify.com/api/token',
  38. headers: {
  39. 'Authorization': `Basic ${authString}`,
  40. 'Content-Type': 'application/x-www-form-urlencoded'
  41. },
  42. data: 'grant_type=client_credentials',
  43. onload: function(response) {
  44. if (response.status === 200) {
  45. const data = JSON.parse(response.responseText);
  46. accessToken = data.access_token;
  47. callback();
  48. } else {
  49. setTimeout(() => {
  50. displaySongInfo("Invalid Spotify API keys. Please provide valid keys.");
  51. }, 2000);
  52. }
  53. }
  54. });
  55. }
  56.  
  57. // Function to detect if full screen mode is active
  58. function isFullScreen() {
  59. return !!(document.fullscreenElement || document.mozFullScreenElement || document.webkitFullscreenElement || document.msFullscreenElement);
  60. }
  61.  
  62. // Function to move song-info based on full-screen state
  63. function handleFullScreenChange() {
  64. let songInfoElement = document.getElementById('song_info');
  65. if (songInfoElement) {
  66. if (isFullScreen()) {
  67. const fullScreenContainer = document.querySelector('#player-full-bleed-container');
  68. if (fullScreenContainer) {
  69. songInfoElement.remove();
  70. fullScreenContainer.appendChild(songInfoElement);
  71. }
  72. } else {
  73. const playerElement = document.querySelector('#player');
  74. if (playerElement) {
  75. songInfoElement.remove();
  76. playerElement.appendChild(songInfoElement);
  77. }
  78. }
  79. }
  80. }
  81.  
  82. // 3. Function to search track on Spotify
  83. function searchTrack(trackName, callback) {
  84. const query = encodeURIComponent(trackName);
  85. const apiUrl = `https://api.spotify.com/v1/search?q=${query}&type=track&limit=1`;
  86.  
  87. GM_xmlhttpRequest({
  88. method: 'GET',
  89. url: apiUrl,
  90. headers: {
  91. 'Authorization': `Bearer ${accessToken}`
  92. },
  93. onload: function(response) {
  94. if (response.status === 200) {
  95. const data = JSON.parse(response.responseText);
  96. if (data.tracks.items.length > 0) {
  97. const track = data.tracks.items[0];
  98. callback(track);
  99. } else {
  100. displaySongInfo("No track found.");
  101. }
  102. } else {
  103. displaySongInfo("Error in search request.");
  104. }
  105. }
  106. });
  107. }
  108.  
  109. // 4. Function to get track features (including BPM)
  110. function getTrackFeatures(trackId, callback) {
  111. const apiUrl = `https://api.spotify.com/v1/audio-features/${trackId}`;
  112.  
  113. GM_xmlhttpRequest({
  114. method: 'GET',
  115. url: apiUrl,
  116. headers: {
  117. 'Authorization': `Bearer ${accessToken}`
  118. },
  119. onload: function(response) {
  120. if (response.status === 200) {
  121. const data = JSON.parse(response.responseText);
  122. callback(data.tempo);
  123. } else {
  124. displaySongInfo("Error fetching track features.");
  125. }
  126. }
  127. });
  128. }
  129.  
  130. // 5. Function to display BPM and track information
  131. function displaySongInfo(message) {
  132. let existingElement = document.getElementById('song_info');
  133. if (existingElement) {
  134. existingElement.remove();
  135. }
  136.  
  137. let songInfoElement = document.createElement('div');
  138. songInfoElement.id = 'song_info';
  139. songInfoElement.style.position = 'absolute';
  140. songInfoElement.style.top = '10px';
  141. songInfoElement.style.right = '10px';
  142. songInfoElement.style.backgroundColor = '#ff0000';
  143. songInfoElement.style.color = 'white';
  144. songInfoElement.style.padding = '15px';
  145. songInfoElement.style.borderRadius = '5px';
  146. songInfoElement.style.fontWeight = 'bold';
  147. songInfoElement.style.fontSize = '20px';
  148. songInfoElement.style.textAlign = 'center';
  149. songInfoElement.innerText = message;
  150.  
  151. const playerElement = document.querySelector('#player');
  152. if (playerElement) {
  153. playerElement.appendChild(songInfoElement);
  154. } else {
  155. console.error("Could not find the player element to add BPM.");
  156. }
  157.  
  158. handleFullScreenChange();
  159. }
  160.  
  161. // 6. Function to update BPM and track information
  162. function updateSongInfo(bpm, track) {
  163. let existingElement = document.getElementById('song_info');
  164. if (existingElement) {
  165. existingElement.innerText = '';
  166. } else {
  167. displaySongInfo('');
  168. existingElement = document.getElementById('song_info');
  169. }
  170.  
  171. let bpmElement = document.createElement('div');
  172. bpmElement.id = 'bpm';
  173. bpmElement.style.fontSize = '60px';
  174. bpmElement.innerText = `BPM: ${bpm ? bpm.toFixed(2) : 'Not available'}`;
  175.  
  176. let artistElement = document.createElement('div');
  177. artistElement.id = 'artist';
  178. artistElement.style.fontSize = '20px';
  179. artistElement.innerText = `Artist: ${track.artists[0].name}`;
  180.  
  181. let songElement = document.createElement('div');
  182. songElement.id = 'song';
  183. songElement.style.fontSize = '20px';
  184. songElement.innerText = `Song: ${track.name}`;
  185.  
  186. existingElement.appendChild(bpmElement);
  187. existingElement.appendChild(artistElement);
  188. existingElement.appendChild(songElement);
  189. }
  190.  
  191. // 7. Main function
  192. function main() {
  193. const videoTitleElement = document.querySelector('#title h1');
  194. if (videoTitleElement) {
  195. const titleText = videoTitleElement.innerText.trim();
  196. if (titleText !== lastTitle) {
  197. lastTitle = titleText;
  198. searchTrack(titleText, function(track) {
  199. if (track) {
  200. getTrackFeatures(track.id, function(bpm) {
  201. updateSongInfo(bpm, track);
  202. });
  203. } else {
  204. displaySongInfo("No track found.");
  205. }
  206. });
  207. }
  208. }
  209. }
  210.  
  211. // 8. Function to wait for the video title indefinitely
  212. function waitForTitle() {
  213. const interval = setInterval(() => {
  214. const videoTitleElement = document.querySelector('#title h1');
  215. if (videoTitleElement) {
  216. main();
  217. }
  218. }, 1000);
  219. }
  220.  
  221. // 9. Observe changes in URL and full screen mode
  222. function observeUrlChanges() {
  223. const urlObserver = new MutationObserver(() => {
  224. if (location.href !== lastUrl) {
  225. lastUrl = location.href;
  226. waitForTitle();
  227. }
  228. });
  229.  
  230. urlObserver.observe(document.body, { childList: true, subtree: true });
  231. }
  232.  
  233. // 10. Observe fullscreen changes
  234. function observeFullScreenChanges() {
  235. document.addEventListener('fullscreenchange', handleFullScreenChange);
  236. }
  237.  
  238. // 11. Load script
  239. window.addEventListener('load', () => {
  240. getAccessToken(function() {
  241. observeUrlChanges();
  242. observeFullScreenChanges();
  243. waitForTitle();
  244. });
  245. });
  246.  
  247. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址