Skip to content

Instantly share code, notes, and snippets.

@Baw-Appie
Last active June 15, 2025 08:29
Show Gist options
  • Save Baw-Appie/be7b66a1f43db694f85834b03c5e9aca to your computer and use it in GitHub Desktop.
Save Baw-Appie/be7b66a1f43db694f85834b03c5e9aca to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Tuna browser script
// @namespace univrsal
// @version 1.0.28
// @description Get song information from web players, based on NowSniper by Kıraç Armağan Önal
// @author univrsal
// @match *://music.youtube.com/*
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @license GPLv2
// ==/UserScript==
(() => {
'use strict';
/**************** 1. setPositionState 후킹 ****************/
const origSetPos = navigator.mediaSession.setPositionState?.bind(navigator.mediaSession);
let basePos = 0; // 마지막 기준 위치(초)
let baseStamp = 0; // 기준 시각(ms, performance.now)
let duration = 0; // 총 길이(모를 때 0)
let playbackRate = 1; // 배속
let isPlaying = false; // 재생 여부
function updateBase(now = performance.now()) {
// 호출 시점을 새로운 기준으로 갱신
baseStamp = now;
}
navigator.mediaSession.setPositionState = (obj = {}) => {
const now = performance.now();
// 재생 중이라면 도착하기까지 경과한 시간만큼 위치를 보정한 뒤 새 기준으로 삼음
if (isPlaying) {
basePos += ((now - baseStamp) / 1000) * playbackRate;
updateBase(now);
}
if (typeof obj.position === 'number') basePos = obj.position;
if (typeof obj.duration === 'number') duration = obj.duration;
if (typeof obj.playbackRate === 'number') playbackRate = obj.playbackRate;
try { origSetPos?.(obj); } catch (_) {}
};
/**************** 2. play / pause 이벤트 감지 ****************/
function handlePlayPause(playing) {
const now = performance.now();
if (isPlaying && !playing) {
// └ 재생 → 일시정지로 전환될 때, 직전까지 지나간 시간을 반영
basePos += ((now - baseStamp) / 1000) * playbackRate;
updateBase(now);
} else if (!isPlaying && playing) {
// └ 일시정지 → 재생으로 전환될 때, 기준 시각 초기화
updateBase(now);
}
isPlaying = playing;
}
// 모든 audio/video 요소 추적 (currentTime 불사용)
const observeMedia = (el) => {
if (el.__msObserved) return;
el.__msObserved = true;
el.addEventListener('play', () => handlePlayPause(true), true);
el.addEventListener('pause', () => handlePlayPause(false), true);
el.addEventListener('ended', () => handlePlayPause(false), true);
// 초기 상태 반영
handlePlayPause(!el.paused && !el.ended);
// 배속 변경은 position 계산에 필요하므로 따로 추적
el.addEventListener('ratechange', () => {
playbackRate = el.playbackRate || 1;
}, true);
};
const scan = () => document.querySelectorAll('audio,video').forEach(observeMedia);
new MutationObserver(scan).observe(document.documentElement, { childList: true, subtree: true });
scan(); // 초기 탐색
/**************** 3. 현재 위치 계산 ****************/
function calcPosition() {
if (!baseStamp) return null;
if (!isPlaying) return basePos;
const elapsed = (performance.now() - baseStamp) / 1000;
let pos = basePos + elapsed * playbackRate;
if (duration && pos > duration) pos = duration;
return pos;
}
/**************** 4. 외부 API 및 주기적 이벤트 ****************/
unsafeWindow.getMediaPosition = () => ({
position : calcPosition(),
duration
});
})();
(function () {
'use strict';
console.log("Loading tuna browser script");
// Tampermonkey and violent monkey seem to have differing implementations
function makeRequest(data) {
return GM.xmlHttpRequest(data);
}
// Configuration
var port = 1608;
var refresh_rate_ms = 1000;
var cooldown_ms = 10000;
// Tuna isn't running we sleep, because every failed request will log into the console
// so we don't want to spam it
var failure_count = 0;
var cooldown = 0;
var last_state = {};
function post(data) {
if (data.status) {
/* if this tab isn't playing and the status hasn't changed we don't send an update
* otherwise tabs that are paused would constantly send the paused/stopped state
* which interferes another tab that is playing something
*/
if (data.status !== "playing" && last_state.status === data.status) {
return; // Prevent the paused state from being continously sent, since this tab is not playing, should prevent tabs from clashing with eachother
}
}
last_state = data;
var url = 'http://localhost:' + port + '/';
var xhr = makeRequest( {
'method' : 'POST',
'url' : url,
data: JSON.stringify({ data, hostname: window.location.hostname, date: Date.now() }),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Access-Control-Allow-Headers': '*',
'Access-Control-Allow-Origin': '*'
},
timeout: 1500
});
setTimeout(() => {
xhr.abort()
}, 1000)
}
function StartFunction() {
if (failure_count > 3) {
console.log('Failed to connect multiple times, waiting a few seconds');
cooldown = cooldown_ms;
failure_count = 0;
}
if (cooldown > 0) {
cooldown -= refresh_rate_ms;
return;
}
const mediaSessionStatesToTunaStates = {
"none": "unknown",
"playing": "playing",
"paused": "stopped"
}
let status = mediaSessionStatesToTunaStates[navigator.mediaSession.playbackState] || navigator.mediaSession.playbackState;
if (navigator.mediaSession.metadata) {
let title = navigator.mediaSession.metadata.title;
let artists = [navigator.mediaSession.metadata.artist];
let mediaElem = document.querySelectorAll('audio,video')[0];
let progress = unsafeWindow.getMediaPosition().position * 1000;
let duration = unsafeWindow.getMediaPosition().duration * 1000;
let artworks = navigator.mediaSession.metadata.artwork;
let album = navigator.mediaSession.metadata.album;
let album_url = artworks[artworks.length - 1].src;
let cover = album_url; // For now.
if (title !== null) {
post({ cover, title, artists, status, progress, duration, album, album_url });
}
}
}
setInterval(() => {
StartFunction();
}, refresh_rate_ms);
let metadata = null
let title = null
Object.defineProperty(navigator.mediaSession, 'metadata', {
async set(newValue) {
console.log('new value for metadata: ', newValue);
await (new Promise(r => setTimeout(r, 200)))
metadata = newValue;
title = newValue.title;
Object.defineProperty(metadata, 'title', {
set(newValue) {
console.log('new title: ', newValue)
title = newValue;
StartFunction();
},
get() {
return title
}
})
StartFunction();
},
get() {
return metadata
}
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment