From 01d62df2fbe7b0df86314f7a28be47cccfe62b9e Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 15 Feb 2026 01:13:10 +0100 Subject: [PATCH] add tampermonkey script --- README.md | 37 ++++++++++++ tampermonkey/script.js | 133 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 tampermonkey/script.js diff --git a/README.md b/README.md index e69de29..27d72b4 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,37 @@ +# Setup +``` +python -m venv .venv +``` +## Windows +``` +.venv\Scripts\activate +pip install -r requirements.txt +ytmusicapi browser +``` +## Linux +``` +source .venv/bin/activate +pip install -r requirements.txt +ytmusicapi browser +``` + +Add the script in [tampermonkey](tampermonkey) as new userscript in tampermonkey + + + +# Usage +``` +flask run +``` + +``` +node server/server.cjs +``` + +# Credits + +Python code and javascript is written by me +The bridge server was also written by me + +CSS was generated by Gemini +The tampermonkey script was also generated by Gemini since my first idea was to use the YTM-Desktop app as player until i realised that it doesn't support adding songs to the queue. diff --git a/tampermonkey/script.js b/tampermonkey/script.js new file mode 100644 index 0000000..e2aa653 --- /dev/null +++ b/tampermonkey/script.js @@ -0,0 +1,133 @@ +// ==UserScript== +// @name YTM Remote Control +// @namespace http://tampermonkey.net/ +// @version 1.0 +// @description Remote control YTM via local server +// @author Gemini +// @match *://music.youtube.com/* +// @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com +// @grant GM_xmlhttpRequest +// @connect localhost +// @run-at document-end +// ==/UserScript== + +(function() { + 'use strict'; + console.log("Remote Script Running..."); + + function poll() { + GM_xmlhttpRequest({ + method: "GET", + url: "http://localhost:3000/poll", + onload: (res) => { + const data = JSON.parse(res.responseText); + if (data && data.videoId && data.title && data.action && data.artist) { + loadPage(data.title,data.artist,data.videoId,data.action); + } + } + }); + } + + function loadPage(title,artist,videoId,action) { + + // 1. Find and open the search bar if it's closed + const searchOpenButton = document.querySelector('ytmusic-search-box') || + document.querySelector('tp-yt-paper-icon-button[aria-label="Search"]'); + + if (searchOpenButton) { + searchOpenButton.click(); + } + + // 2. Wait a moment for the animation/DOM to catch up + setTimeout(() => { + const searchInput = document.querySelector('input.ytmusic-search-box') || + document.querySelector('#input.ytmusic-search-box') || + document.querySelector('input#input'); + + if (searchInput) { + // 3. Focus and set the value + searchInput.focus(); + searchInput.value = title + " " + artist; + + // 4. Force the app to "see" the new text + searchInput.dispatchEvent(new Event('input', { bubbles: true })); + searchInput.dispatchEvent(new Event('change', { bubbles: true })); + + // 5. Instead of pressing Enter, find the "Search" icon inside the bar and click it + // This is usually the glass icon that appears once you start typing + const searchSubmitButton = document.querySelector('.search-icon.ytmusic-search-box') || + document.querySelector('ytmusic-search-box [icon="search"]'); + + if (searchSubmitButton) { + searchSubmitButton.click(); + console.log("Search button clicked!"); + } else { + // Fallback: If no button, try the "Enter" key again with a more complete event + const opts = { bubbles: true, cancelable: true, key: 'Enter', code: 'Enter', keyCode: 13, which: 13 }; + searchInput.dispatchEvent(new KeyboardEvent('keydown', opts)); + searchInput.dispatchEvent(new KeyboardEvent('keypress', opts)); + searchInput.dispatchEvent(new KeyboardEvent('keyup', opts)); + console.log("Search button not found, tried full key sequence."); + } + } + }, 300); + setTimeout(() => { + addToQueue(videoId, action); + }, 1000); +} + +function addToQueue(videoId, action) { + console.log("adding"); + const actionLabel = action === 'next' ? 'play next' : 'add to queue'; + + function tryAddToQueue() { + // 1. Find the link for your video + const videoLink = document.querySelector(`a[href*="${videoId}"]`); + + if (!videoLink) { + console.log("Waiting for search results..."); + return false; + } + + // 2. Find the Action Menu button near that link + let container = videoLink.parentElement; + let menuButton = null; + for (let i = 0; i < 10; i++) { + if (!container) break; + menuButton = container.querySelector('button[aria-label="Action menu"]'); + if (menuButton) break; + container = container.parentElement; + } + + if (menuButton) { + menuButton.click(); + + // 3. Find the EXCLUSIVE "Add to queue" option + setTimeout(() => { + const menuOptions = document.querySelectorAll('ytmusic-menu-service-item-renderer'); + // We use a more strict filter to avoid "Play next" + const addToQueue = Array.from(menuOptions).find(el => { + const text = el.textContent.trim().toLowerCase(); + return text === actionLabel; + }); + + if (addToQueue) { + addToQueue.click(); + console.log("Success: Added to the end of the queue!"); + } else { + console.error("Found the menu, but couldn't find the exact 'Add to queue' button."); + } + }, 400); + return true; + } + return false; + } + + // Attempt to run, and if search results aren't ready, try again in 1 second + if (!tryAddToQueue()) { + setTimeout(tryAddToQueue, 1500); + } +} + + setInterval(poll, 1500); +})(); \ No newline at end of file