1
0

initial commit

This commit is contained in:
anon 2024-10-09 20:17:26 +00:00
commit 0b6058965f
2 changed files with 405 additions and 0 deletions

29
readme.md Normal file
View File

@ -0,0 +1,29 @@
# youtube music scrobbler for antisocial.moe
so i made this script that scrobbles your youtube music plays to my blog, antisocial.moe. not sure if anyone else will use it, but whatever.
## installation
1. install tampermonkey, violentmonkey, or any userscript manager.
2. create a new script and paste in the code from the script file.
3. save it.
## setup
you need to set your `AUTH_KEY` for it to work.
1. after installing the script, click on the userscript manager icon (it's probably in your browser toolbar).
2. find the script (probably called "antisocial.moe - youtube music scrobbler").
3. click on "Set antisocial.moe Auth Key" in the menu.
4. enter your `AUTH_KEY`. if you don't have one, well, i guess you need to get one somehow.
## usage
just play music on youtube music, and it'll scrobble the songs to antisocial.moe after they've been playing for 10 seconds. if you skip songs before 10 seconds, it won't scrobble them.
## notes
- only scrobbles when the song is playing.
- waits 10 seconds before scrobbling to avoid spamming with skipped tracks.
- if it's not working, check the console for errors. maybe i screwed something up.
- probably needs more testing, but it works for me.

376
script.js Normal file
View File

@ -0,0 +1,376 @@
// ==UserScript==
// @name antisocial.moe - youtube music scrobbler
// @namespace http://tampermonkey.net/
// @version 2.0
// @author
// @match https://music.youtube.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @connect antisocial.moe
// ==/UserScript==
(function() {
'use strict';
/**
* ============================
* Helper Functions
* ============================
*/
/**
* Recursively searches for elements matching the selector within shadow DOMs.
* @param {string} selector - The CSS selector to match.
* @param {Document|ShadowRoot} root - The root to start searching from.
* @returns {Element|null} - The first matching element or null if not found.
*/
function querySelectorDeep(selector, root = document) {
const result = root.querySelector(selector);
if (result) return result;
// Traverse shadow roots
const allElements = root.querySelectorAll('*');
for (let el of allElements) {
if (el.shadowRoot) {
const shadowResult = querySelectorDeep(selector, el.shadowRoot);
if (shadowResult) return shadowResult;
}
}
return null;
}
/**
* Extracts the current song information from the YouTube Music player bar.
* Implements multiple extraction methods to handle different HTML structures.
* @returns {Object|null} An object containing title, author, and album, or null if not found.
*/
function getSongInfo() {
// Primary Extraction Method
let titleElement = querySelectorDeep('.ytmusic-player-bar .title');
let bylineElement = querySelectorDeep('.ytmusic-player-bar .byline');
if (titleElement && bylineElement) {
const title = titleElement.textContent.trim();
// The byline typically has the format: "Author • Album • Year"
const bylineText = bylineElement.textContent.trim();
const parts = bylineText.split('•').map(part => part.trim());
const author = parts[0] || 'Unknown Artist';
const album = parts[1] || 'Unknown Album';
return { title, author, album };
}
// Fallback Extraction Method: Parse from 'title' attribute of byline
if (bylineElement && bylineElement.getAttribute('title')) {
const bylineTitle = bylineElement.getAttribute('title').trim();
const parts = bylineTitle.split('•').map(part => part.trim());
const author = parts[0] || 'Unknown Artist';
const album = parts[1] || 'Unknown Album';
// Attempt to find the song title from an alternative element or attribute
let title = 'Unknown Title';
if (titleElement) {
title = titleElement.textContent.trim();
} else {
// Try to extract from other possible attributes or elements if available
const parent = bylineElement.parentElement;
if (parent && parent.getAttribute('title')) {
title = parent.getAttribute('title').trim();
}
}
return { title, author, album };
}
// Additional Fallbacks can be added here as needed
return null;
}
/**
* Determines whether the current song is playing.
* @returns {boolean} True if playing, false if paused or unable to determine.
*/
function isPlaying() {
// The play/pause button has id="play-pause-button"
const playPauseButton = querySelectorDeep('#play-pause-button');
if (playPauseButton) {
const titleAttr = playPauseButton.getAttribute('title');
const ariaLabel = playPauseButton.getAttribute('aria-label');
// If the button's title or aria-label is "Pause", it means the song is currently playing
if (titleAttr && titleAttr.toLowerCase() === 'pause') {
return true;
}
if (ariaLabel && ariaLabel.toLowerCase() === 'pause') {
return true;
}
}
return false;
}
/**
* Sends a scrobble request to the specified endpoint.
* @param {string} title - The title of the song.
* @param {string} author - The author/artist of the song.
* @param {string} album - The album name of the song.
*/
function scrobble(title, author, album) {
const AUTH_KEY = GM_getValue('AUTH_KEY', '');
if (!AUTH_KEY) {
console.error('AUTH_KEY is not set. Please set it using the Tampermonkey menu.');
return;
}
const url = 'https://antisocial.moe/wp-json/image-scrobble/v1/scrobble';
const payload = {
song_name: title,
author: author,
album_name: album
};
GM_xmlhttpRequest({
method: 'POST',
url: url,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${AUTH_KEY}`
},
data: JSON.stringify(payload),
onload: function(response) {
if (response.status >= 200 && response.status < 300) {
console.log(`Scrobble successful: "${title}" by ${author} from "${album}"`);
} else {
console.error(`Scrobble failed with status ${response.status}:`, response.responseText);
console.debug('Payload:', payload);
}
// Update lastScrobbledSong regardless of scrobble success to prevent duplicate attempts
lastScrobbledSong = { title, author, album };
// Reset scrobbleScheduled
scrobbleScheduled = false;
},
onerror: function(error) {
console.error('Scrobble request error:', error);
// Update lastScrobbledSong to prevent further attempts
lastScrobbledSong = { title, author, album };
// Reset scrobbleScheduled
scrobbleScheduled = false;
}
});
}
/**
* ============================
* Scrobbling Logic
* ============================
*/
function areSongsEqual(song1, song2) {
if (!song1 || !song2) return false;
return song1.title.toLowerCase() === song2.title.toLowerCase() &&
song1.author.toLowerCase() === song2.author.toLowerCase() &&
song1.album.toLowerCase() === song2.album.toLowerCase();
}
// Variable to keep track of the last scrobbled song
let lastScrobbledSong = null;
// Flag to indicate if a scrobble is already scheduled
let scrobbleScheduled = false;
// Timer for scrobbling after 10 seconds of playback
let playbackTimer = null;
/**
* Handles the scrobbling logic when a song change or playback state change is detected.
* Scrobbles the song only if it has been playing for more than 10 seconds and is currently playing.
*/
function handleScrobble() {
const currentKey = GM_getValue('AUTH_KEY', '');
if (!currentKey) {
return;
}
const songInfo = getSongInfo();
if (songInfo) {
// Log current and detected song information
console.debug('Current scrobbled song:', lastScrobbledSong);
console.debug('Detected song:', songInfo);
// Check if the song has changed to avoid duplicate scrobbles
if (!areSongsEqual(lastScrobbledSong, songInfo)) {
console.log(`New song detected: "${songInfo.title}" by ${songInfo.author} from "${songInfo.album}"`);
// Update lastScrobbledSong to prevent repeated attempts
// Note: We update it here to prevent multiple timers for the same song
lastScrobbledSong = songInfo;
// Clear any existing timer
if (playbackTimer) {
clearTimeout(playbackTimer);
playbackTimer = null;
}
// Check if the song is currently playing
if (isPlaying()) {
if (!scrobbleScheduled) {
scrobbleScheduled = true;
playbackTimer = setTimeout(() => {
if (isPlaying()) {
scrobble(songInfo.title, songInfo.author, songInfo.album);
playbackTimer = null;
} else {
console.log('Playback stopped before scrobble threshold.');
scrobbleScheduled = false;
}
}, 10000); // 10 seconds
console.log('Started 10-second timer for scrobble.');
}
}
} else {
console.debug('Same song, no action needed.');
}
} else {
console.warn('Song information not found.');
}
}
/**
* ============================
* Debouncing Mechanism
* ============================
*/
/**
* Debounce mechanism to prevent rapid consecutive scrobbles.
* Waits for 1 second after the last mutation before handling scrobble.
*/
let debounceTimeout = null;
function debouncedHandleScrobble() {
if (debounceTimeout) {
clearTimeout(debounceTimeout);
}
debounceTimeout = setTimeout(() => {
handleScrobble();
}, 1000); // 1 second debounce
}
/**
* ============================
* MutationObserver Setup
* ============================
*/
/**
* Initializes the MutationObserver to monitor changes in the player bar and play/pause button.
*/
function initObserver() {
const observer = new MutationObserver((mutations) => {
for (let mutation of mutations) {
// Detect changes in child elements (e.g., song title updates)
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
debouncedHandleScrobble();
break; // No need to process further mutations for this batch
}
// Detect attribute changes (e.g., play/pause button title changes)
if (mutation.type === 'attributes' && (mutation.attributeName === 'title' || mutation.attributeName === 'aria-label')) {
debouncedHandleScrobble();
break;
}
}
});
// Configuration for the observer
const config = { childList: true, subtree: true, attributes: true, attributeFilter: ['title', 'aria-label'] };
// Start observing the entire document
observer.observe(document.body, config);
console.log('YouTube Music Scrobbler with Conditional Scrobbling is now active.');
}
/**
* Waits for the player bar to be available in the DOM before initializing the observer.
*/
function waitForPlayerBar() {
const interval = setInterval(() => {
const playerBar = querySelectorDeep('.ytmusic-player-bar');
if (playerBar) {
clearInterval(interval);
initObserver();
// Initial scrobble handling
handleScrobble();
}
}, 1000); // Check every second
// Optionally, add a timeout to stop checking after a certain period
// setTimeout(() => {
// clearInterval(interval);
// console.error('Player bar not found. Scrobbler not initialized.');
// }, 30000); // 30 seconds
}
/**
* ============================
* UI for Managing AUTH_KEY
* ============================
*/
/**
* Prompts the user to enter their AUTH_KEY and stores it using GM_setValue.
*/
function setAuthKey() {
const currentKey = GM_getValue('AUTH_KEY', '');
const newKey = prompt('Enter your AUTH_KEY for antisocial.moe:', currentKey);
if (newKey !== null) { // User didn't cancel
if (newKey.trim() === '') {
alert('AUTH_KEY cannot be empty.');
return;
}
GM_setValue('AUTH_KEY', newKey.trim());
alert('AUTH_KEY has been updated.');
console.log('AUTH_KEY has been set/updated.');
}
}
/**
* Registers a menu command to allow users to set/edit the AUTH_KEY.
*/
function registerMenuCommands() {
GM_registerMenuCommand('Set antisocial.moe Auth Key', setAuthKey);
}
/**
* ============================
* Initialization
* ============================
*/
// Register the menu command for setting the AUTH_KEY
registerMenuCommands();
// Optionally, prompt the user to set the AUTH_KEY if it's not already set
function promptForAuthKeyIfNeeded() {
const authKey = GM_getValue('AUTH_KEY', '');
if (!authKey) {
alert('Please set your antisocial.moe AUTH_KEY using the Tampermonkey menu (right-click the Tampermonkey icon > Your Script > "Set antisocial.moe Auth Key").');
}
}
// Start the script by waiting for the player bar to load
waitForPlayerBar();
// Prompt the user to set the AUTH_KEY if it's not set
promptForAuthKeyIfNeeded();
})();