-
-
Save hemiipatu/34a0e3cd35c7fd25260fd84b6991f53b to your computer and use it in GitHub Desktop.
| // ==UserScript== | |
| // @name Qobuz auth token & app-id login | |
| // @namespace io.github.uhwot.qbztoken | |
| // @match https://play.qobuz.com/* | |
| // @grant none | |
| // @version 2.2 | |
| // @author uh wot / hemiipatu | |
| // @description Intercepts native login button with custom credentials, complete UI matching and alignment. | |
| // @homepageURL https://gist.github.com/hemiipatu/34a0e3cd35c7fd25260fd84b6991f53b | |
| // @downloadURL https://gist.github.com/hemiipatu/34a0e3cd35c7fd25260fd84b6991f53b/raw/master/qbztoken.user.js | |
| // @updateURL https://gist.github.com/hemiipatu/34a0e3cd35c7fd25260fd84b6991f53b/raw/master/qbztoken.user.js | |
| // @icon https://play.qobuz.com/resources/favicon/favicon-96x96.png | |
| // @run-at document-start | |
| // ==/UserScript== | |
| // --- 1. Global Interceptor (Fetch & Headers) --- | |
| const token = localStorage.getItem("shitass"); | |
| const customAppId = localStorage.getItem("qobuz_custom_appid"); | |
| // Read state directly from localStorage dynamically | |
| function isCustomAuthActive() { | |
| return localStorage.getItem("qobuz_custom_auth_enabled") !== "false"; | |
| } | |
| const rememberMe = localStorage.getItem("qobuz_remember_credentials") !== "false"; | |
| const savedDisplayToken = rememberMe ? (localStorage.getItem("qobuz_display_token") || "") : ""; | |
| const savedAppId = rememberMe ? (customAppId || "") : ""; | |
| if (!rememberMe) { | |
| localStorage.removeItem("qobuz_display_token"); | |
| localStorage.removeItem("qobuz_custom_appid"); | |
| } | |
| const originalFetch = window.fetch; | |
| window.fetch = async function (url, init) { | |
| const urlStr = typeof url === 'string' ? url : url.url || ''; | |
| init = init || {}; | |
| init.headers = init.headers ? new Headers(init.headers) : new Headers(); | |
| if (isCustomAuthActive() && savedAppId && urlStr.includes("qobuz.com/api.json")) { | |
| init.headers.set("x-app-id", savedAppId); | |
| } | |
| if (isCustomAuthActive() && token && urlStr.includes("oauth/callback") && urlStr.includes("code=!shitass!")) { | |
| return new Response(JSON.stringify({ | |
| token: token, | |
| user_id: "1" | |
| }), { | |
| status: 200, | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| } | |
| return originalFetch.apply(this, [url, init]); | |
| }; | |
| // --- 2. DOM Helper & Session Utility --- | |
| function waitForElm(selector) { | |
| return new Promise(resolve => { | |
| if (document.querySelector(selector)) { | |
| return resolve(document.querySelector(selector)); | |
| } | |
| const observer = new MutationObserver(() => { | |
| if (document.querySelector(selector)) { | |
| resolve(document.querySelector(selector)); | |
| observer.disconnect(); | |
| } | |
| }); | |
| observer.observe(document.documentElement, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| }); | |
| } | |
| function removeContainer() { | |
| const existingContainer = document.getElementById("qobuz-token-login-container"); | |
| if (existingContainer) { | |
| existingContainer.remove(); | |
| } | |
| } | |
| function isLoggedIn() { | |
| return !window.location.pathname.includes("/login") || | |
| document.cookie.includes("user_auth_token") || | |
| localStorage.getItem("user") !== null; | |
| } | |
| // --- 3. UI Injection & Smart Interception --- | |
| function injectTokenInput() { | |
| if (isLoggedIn()) { | |
| removeContainer(); | |
| return; | |
| } | |
| waitForElm("body").then(() => { | |
| if (document.getElementById("qobuz-token-login-container") || isLoggedIn()) return; | |
| const nativeFontStack = `"Gilroy", "Helvetica Neue", Helvetica, Arial, sans-serif`; | |
| const container = document.createElement("div"); | |
| container.id = "qobuz-token-login-container"; | |
| container.setAttribute("style", ` | |
| position: fixed; | |
| bottom: 30px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| width: 100%; | |
| max-width: 420px; | |
| padding: 20px; | |
| background-color: rgba(18, 18, 18, 0.98); | |
| border: 1px solid #2a2a2a; | |
| border-radius: 8px; | |
| box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.7); | |
| z-index: 99999; | |
| box-sizing: border-box; | |
| font-family: ${nativeFontStack}; | |
| text-align: center; | |
| transition: opacity 0.2s ease; | |
| `); | |
| const inputStyle = ` | |
| padding: 11px 14px; | |
| width: 100%; | |
| background: #0f0f0f; | |
| color: #fff; | |
| border: 1px solid #333; | |
| border-radius: 4px; | |
| box-sizing: border-box; | |
| font-family: ${nativeFontStack}; | |
| text-align: center; | |
| margin-bottom: 14px; | |
| font-size: 13px; | |
| outline: none; | |
| letter-spacing: 0.3px; | |
| transition: border-color 0.2s, opacity 0.2s; | |
| `; | |
| const labelStyle = ` | |
| color: #8c8c8c; | |
| font-size: 11px; | |
| margin: 0 auto 6px auto; | |
| font-weight: 600; | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| text-align: center; | |
| display: block; | |
| `; | |
| const inputsWrapper = document.createElement("div"); | |
| inputsWrapper.id = "qobuz-inputs-wrapper"; | |
| inputsWrapper.style.transition = "opacity 0.2s"; | |
| if (!isCustomAuthActive()) inputsWrapper.style.opacity = "0.4"; | |
| // 1. Auth Token Input Elements | |
| const tokenLabel = document.createElement("label"); | |
| tokenLabel.innerText = "User Auth Token"; | |
| tokenLabel.setAttribute("style", labelStyle); | |
| inputsWrapper.appendChild(tokenLabel); | |
| const tokenInput = document.createElement("input"); | |
| tokenInput.id = "custom-auth-token-field"; | |
| tokenInput.setAttribute("type", "password"); | |
| tokenInput.setAttribute("placeholder", "Paste auth token..."); | |
| tokenInput.setAttribute("style", inputStyle); | |
| tokenInput.value = savedDisplayToken; | |
| inputsWrapper.appendChild(tokenInput); | |
| // 2. X-App-Id Input Elements | |
| const appIdLabel = document.createElement("label"); | |
| appIdLabel.innerText = "Custom App ID (Optional)"; | |
| appIdLabel.setAttribute("style", labelStyle); | |
| inputsWrapper.appendChild(appIdLabel); | |
| const appIdInput = document.createElement("input"); | |
| appIdInput.id = "custom-app-id-field"; | |
| appIdInput.setAttribute("type", "text"); | |
| appIdInput.setAttribute("placeholder", "e.g., 100000000"); | |
| appIdInput.setAttribute("style", inputStyle); | |
| appIdInput.value = savedAppId; | |
| inputsWrapper.appendChild(appIdInput); | |
| container.appendChild(inputsWrapper); | |
| const createToggleRow = (text, initialValue, onToggle) => { | |
| const row = document.createElement("div"); | |
| row.setAttribute("style", ` | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| user-select: none; | |
| `); | |
| const toggleLabel = document.createElement("span"); | |
| toggleLabel.innerText = text; | |
| toggleLabel.setAttribute("style", ` | |
| color: #aaaaaa; | |
| font-size: 12px; | |
| font-weight: 500; | |
| font-family: ${nativeFontStack}; | |
| letter-spacing: 0.2px; | |
| `); | |
| row.appendChild(toggleLabel); | |
| const switchTrack = document.createElement("div"); | |
| const getTrackStyle = (isActive) => ` | |
| position: relative; | |
| width: 36px; | |
| height: 20px; | |
| background-color: ${isActive ? '#2196F3' : '#2a2a2a'}; | |
| border: 1px solid ${isActive ? '#2196F3' : '#444'}; | |
| border-radius: 20px; | |
| cursor: pointer; | |
| transition: background-color 0.2s, border-color 0.2s; | |
| `; | |
| const getThumbStyle = (isActive) => ` | |
| position: absolute; | |
| top: 2px; | |
| left: ${isActive ? '18px' : '2px'}; | |
| width: 14px; | |
| height: 14px; | |
| background-color: #ffffff; | |
| border-radius: 50%; | |
| transition: left 0.2s; | |
| `; | |
| let activeState = initialValue; | |
| switchTrack.setAttribute("style", getTrackStyle(activeState)); | |
| const switchThumb = document.createElement("div"); | |
| switchThumb.setAttribute("style", getThumbStyle(activeState)); | |
| switchTrack.appendChild(switchThumb); | |
| row.appendChild(switchTrack); | |
| switchTrack.addEventListener("click", () => { | |
| activeState = !activeState; | |
| switchTrack.setAttribute("style", getTrackStyle(activeState)); | |
| switchThumb.setAttribute("style", getThumbStyle(activeState)); | |
| onToggle(activeState); | |
| }); | |
| return row; | |
| }; | |
| // 3. Dual Toggle Row Section | |
| const togglesContainer = document.createElement("div"); | |
| togglesContainer.setAttribute("style", ` | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| margin-top: 4px; | |
| padding-top: 14px; | |
| border-top: 1px solid #222; | |
| `); | |
| const enableRow = createToggleRow("Enable custom auth", isCustomAuthActive(), (active) => { | |
| localStorage.setItem("qobuz_custom_auth_enabled", active ? "true" : "false"); | |
| inputsWrapper.style.opacity = active ? "1" : "0.4"; | |
| }); | |
| let isRemembered = rememberMe; | |
| const rememberRow = createToggleRow("Remember custom credentials", isRemembered, (active) => { | |
| isRemembered = active; | |
| updateStorage(); | |
| }); | |
| togglesContainer.appendChild(enableRow); | |
| togglesContainer.appendChild(rememberRow); | |
| container.appendChild(togglesContainer); | |
| document.body.appendChild(container); | |
| [tokenInput, appIdInput].forEach(el => { | |
| el.addEventListener('focus', () => el.style.borderColor = '#555'); | |
| el.addEventListener('blur', () => el.style.borderColor = '#333'); | |
| }); | |
| const updateStorage = () => { | |
| if (isRemembered) { | |
| localStorage.setItem("qobuz_remember_credentials", "true"); | |
| localStorage.setItem("qobuz_display_token", tokenInput.value.trim()); | |
| localStorage.setItem("qobuz_custom_appid", appIdInput.value.trim()); | |
| } else { | |
| localStorage.setItem("qobuz_remember_credentials", "false"); | |
| localStorage.removeItem("qobuz_display_token"); | |
| localStorage.removeItem("qobuz_custom_appid"); | |
| } | |
| }; | |
| tokenInput.addEventListener("input", updateStorage); | |
| appIdInput.addEventListener("input", updateStorage); | |
| // 4. Native Button Interception Logic | |
| waitForElm("form button[type='submit'], button.LoginPage__submit, [class*='LoginPage'] button").then((nativeButton) => { | |
| const processSmartLogin = (event) => { | |
| // If custom auth toggle is OFF, exit immediately so standard login executes | |
| if (!isCustomAuthActive()) { | |
| return; | |
| } | |
| const tokenVal = tokenInput.value.trim(); | |
| const appIdVal = appIdInput.value.trim(); | |
| if (tokenVal || appIdVal) { | |
| event.preventDefault(); | |
| event.stopPropagation(); | |
| if (appIdVal) { | |
| localStorage.setItem("qobuz_custom_appid", appIdVal); | |
| } else { | |
| localStorage.removeItem("qobuz_custom_appid"); | |
| } | |
| if (tokenVal) { | |
| localStorage.setItem("shitass", tokenVal); | |
| window.location.href = "https://play.qobuz.com/featured?code_autorisation=!shitass!"; | |
| } else if (appIdVal) { | |
| window.location.reload(); | |
| } | |
| } | |
| }; | |
| nativeButton.addEventListener("click", processSmartLogin, true); | |
| const nativeForm = nativeButton.closest("form"); | |
| if (nativeForm) { | |
| nativeForm.addEventListener("submit", processSmartLogin, true); | |
| } | |
| }); | |
| }); | |
| } | |
| // --- 4. Active Session & Cleanup Observers --- | |
| if (window.location.href.includes("code_autorisation=!shitass!")) { | |
| setTimeout(() => { | |
| localStorage.removeItem("shitass"); | |
| if (localStorage.getItem("qobuz_remember_credentials") === "false") { | |
| localStorage.removeItem("qobuz_display_token"); | |
| localStorage.removeItem("qobuz_custom_appid"); | |
| } | |
| }, 5000); | |
| } | |
| setInterval(() => { | |
| if (isLoggedIn()) { | |
| removeContainer(); | |
| } | |
| }, 300); | |
| window.addEventListener('load', function() { | |
| injectTokenInput(); | |
| const patchHistory = (type) => { | |
| const orig = window.history[type]; | |
| return function() { | |
| const rv = orig.apply(this, arguments); | |
| injectTokenInput(); | |
| return rv; | |
| }; | |
| }; | |
| window.history.pushState = patchHistory('pushState'); | |
| window.history.replaceState = patchHistory('replaceState'); | |
| }, false); |
@hemiipatu I tried it, but it didn't work for me!
@hemiipatu I tried it, but it didn't work for me!
@D3rel I'm unsure what the cause might be as it's working without issue for me. However, from memory, I recall changes (a while ago) to auth_tokens and app_ids which could be the root cause - depending on how long ago you originally retrieved your token/id.
Having said that, can I ask that you follow this guide to make sure both your token/id are correct and up-to-date?
Please let me know how it goes and apologies for the massively delayed reply! - Take care and have a great day.
thank you so much! This is the only script that worked for me
thank you so much! This is the only script that worked for me
You're welcome @krinzal, I'm really pleased it has!
This update/fix restores the ability of logging in to Qobuz web using a user_token and app_id. I have also included a few QoL fixes including;
Screenshot of the login for reference.

24/08/2026 - Update 2.0.
01/10/2026 - Update 2.2.