Skip to content

Instantly share code, notes, and snippets.

@hemiipatu
Forked from uhwot/qbztoken.user.js
Last active October 4, 2026 07:39
Show Gist options
  • Select an option

  • Save hemiipatu/34a0e3cd35c7fd25260fd84b6991f53b to your computer and use it in GitHub Desktop.

Select an option

Save hemiipatu/34a0e3cd35c7fd25260fd84b6991f53b to your computer and use it in GitHub Desktop.
allows logging in via auth tokens on play.qobuz.com
// ==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

hemiipatu commented May 18, 2026 •

Copy link
Copy Markdown
Author

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;

  • UI enhancements following the Qobuz aestetic.
  • A "remember me" toggle.
  • The ability to login with normal credentials if the user_token and app_id are empty. (I might add a toggle to achieve this when the fields are filled in.)

Screenshot of the login for reference.
Screenshot 2026-05-18 095304

24/08/2026 - Update 2.0.

  • A new toggle (Enable custom auth) has been added that enables users to bypass the custom login method and instead proceed to the default Qobuz login without needing to disable the script.
Screenshot 2026-08-24 222135

01/10/2026 - Update 2.2.

@D3rel

D3rel commented May 31, 2026

Copy link
Copy Markdown

@hemiipatu I tried it, but it didn't work for me!

@hemiipatu

hemiipatu commented Jul 5, 2026 •

Copy link
Copy Markdown
Author

@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.

@krinzal

krinzal commented Jul 22, 2026

Copy link
Copy Markdown

thank you so much! This is the only script that worked for me

@hemiipatu

hemiipatu commented Aug 24, 2026 •

Copy link
Copy Markdown
Author

thank you so much! This is the only script that worked for me

You're welcome @krinzal, I'm really pleased it has!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment