commit 6e9e14a817454a85f6a0f1d0e19deea6dfac2b56 Author: yash Date: Sun Jul 19 19:49:22 2026 +0300 init diff --git a/background.js b/background.js new file mode 100644 index 0000000..0e9e6f7 --- /dev/null +++ b/background.js @@ -0,0 +1,11 @@ +// Strip CSP headers from Yandex Music responses so our video can load +browser.webRequest.onHeadersReceived.addListener( + (details) => { + const headers = details.responseHeaders.filter( + (h) => !["content-security-policy", "x-content-security-policy"].includes(h.name.toLowerCase()) + ); + return { responseHeaders: headers }; + }, + { urls: ["*://music.yandex.ru/*", "*://music.yandex.com/*"] }, + ["blocking", "responseHeaders"] +); diff --git a/content.js b/content.js new file mode 100644 index 0000000..8b90851 --- /dev/null +++ b/content.js @@ -0,0 +1,111 @@ +const VIDEOS = [ + "https://cdn.computernetthings.ru/nikit0s.mp4", + // "https://cdn.computernetthings.ru/video2.mp4", +]; + +const PHOTOS = [ + "https://cdn.computernetthings.ru/ph1.jpg", + // "https://cdn.computernetthings.ru/photo2.jpg", +]; + +const CANVAS_SELECTOR = ".VibeWidgetAnimation_root__7fpeP > canvas:nth-child(1)"; +const ROOT_SELECTOR = ".VibeWidgetAnimation_root__7fpeP"; +const PLAY_INDICATOR_SELECTOR = ".VibeResetButton_root__ju8pE"; + +let container = null; +let currentEl = null; + +function random(arr) { + return arr[Math.floor(Math.random() * arr.length)]; +} + +function elStyle(width, height) { + return `display:block;width:${width}px;height:${height}px;object-fit:cover;pointer-events:none;`; + // return `display:block;width:100%;height:100%;object-fit:cover;pointer-events:none;`; +} + +function showPhoto(width, height) { + const img = document.createElement("img"); + img.src = random(PHOTOS); + img.style.cssText = elStyle(width, height); + currentEl?.remove(); + currentEl = img; + container.appendChild(img); +} + +function showVideo(width, height) { + const video = document.createElement("video"); + video.src = random(VIDEOS); + video.setAttribute("autoplay", ""); + video.setAttribute("loop", ""); + video.setAttribute("muted", ""); + video.setAttribute("playsinline", ""); + video.muted = true; + video.style.cssText = elStyle(width, height); + currentEl?.remove(); + currentEl = video; + container.appendChild(video); + video.play().catch(() => {}); +} + +function syncFromIndicator(indicator, width, height) { + if (indicator.getAttribute("aria-hidden") === "true") { + if (PHOTOS.length > 0) { + showPhoto(width, height); + } else { + currentEl?.pause?.(); + } + } else { + showVideo(width, height); + } +} + +function watchForPlayIndicator(width, height) { + const bind = (indicator) => { + syncFromIndicator(indicator, width, height); + new MutationObserver(() => syncFromIndicator(indicator, width, height)).observe(indicator, { + attributes: true, + attributeFilter: ["aria-hidden"], + }); + }; + + const indicator = document.querySelector(PLAY_INDICATOR_SELECTOR); + if (indicator) { + bind(indicator); + return; + } + + const waitObserver = new MutationObserver(() => { + const indicator = document.querySelector(PLAY_INDICATOR_SELECTOR); + if (indicator) { + waitObserver.disconnect(); + bind(indicator); + } + }); + waitObserver.observe(document.body, { childList: true, subtree: true }); +} + +function replaceAnimation(canvas) { + if (canvas.dataset.customReplaced) return; + canvas.dataset.customReplaced = "true"; + canvas.style.display = "none"; + + const { width, height } = canvas.getBoundingClientRect(); + const w = width || canvas.width; + const h = height || canvas.height; + + container = canvas.closest(ROOT_SELECTOR) ?? canvas.parentElement; + + showVideo(w, h); + watchForPlayIndicator(w, h); +} + +const observer = new MutationObserver(() => { + const canvas = document.querySelector(CANVAS_SELECTOR); + if (canvas) replaceAnimation(canvas); +}); + +observer.observe(document.body, { childList: true, subtree: true }); + +const initial = document.querySelector(CANVAS_SELECTOR); +if (initial) replaceAnimation(initial); diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..05d5cf1 --- /dev/null +++ b/manifest.json @@ -0,0 +1,27 @@ +{ + "manifest_version": 2, + "name": "Yandex Music Custom Animation", + "version": "1.0.0", + "description": "Replaces Yandex Music animation with a custom video", + "permissions": [ + "activeTab", + "webRequest", + "webRequestBlocking", + "*://music.yandex.ru/*", + "*://music.yandex.com/*" + ], + "background": { + "scripts": ["background.js"] + }, + "content_scripts": [ + { + "matches": ["*://music.yandex.ru/*", "*://music.yandex.com/*"], + "js": ["content.js"], + "run_at": "document_idle" + } + ], + "icons": { + "48": "icon.png", + "96": "icon.png" + } +}