YT Music: webbläsar-headers som primär koppling (TV-OAuth ger HTTP 400 hos youtubei)
All checks were successful
build-and-push / build (push) Successful in 19s

Alla autentiserade ytmusicapi-anrop med TV-OAuth-token får 400 'invalid
argument' — känd Google-begränsning. Ny väg: klistra in request-headers
från inloggad music.youtube.com i settings → POST /api/ytmusic/headers
bygger ytmusicapi-kompatibel browser.json i Youtubarrs datamapp.
Youtubarr-forken föredrar browser.json framför oauth.json. Testern
rapporterar header-kopplingen. 2 nya tester, totalt 33.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01824ZrvG2mDYYrmwqypLNup
This commit is contained in:
2026-08-01 05:35:06 +02:00
parent dcd4928813
commit 07264be338
5 changed files with 119 additions and 2 deletions

View File

@@ -185,7 +185,8 @@ export function buildApp(config, deps = {}) {
ytmusic: {
clientIdSet: Boolean(s.ytmusic?.clientId),
clientSecretSet: Boolean(s.ytmusic?.clientSecret),
connected: existsSync(ytmusic.oauthPath),
connected: existsSync(ytmusic.browserPath) || existsSync(ytmusic.oauthPath),
browserConnected: existsSync(ytmusic.browserPath),
},
suggest: suggestConfig(s),
}
@@ -298,6 +299,12 @@ export function buildApp(config, deps = {}) {
}
})
app.post('/api/ytmusic/headers', async (req, reply) => {
const out = ytmusic.saveBrowserHeaders(req.body?.raw)
if (out.error) return reply.code(400).send(out)
return out
})
app.post('/api/ytmusic/poll', async (req, reply) => {
const deviceCode = req.body?.deviceCode
if (!deviceCode) return reply.code(400).send({ error: 'deviceCode saknas' })

View File

@@ -58,11 +58,15 @@ export function makeTesters({ config, store, fetchImpl = fetch }) {
},
async ytmusic() {
const dataDir = config.youtubarr?.dataDir ?? '/youtubarr-data'
if (existsSync(path.join(dataDir, 'browser.json'))) {
return { ok: true, message: 'kopplad via webbläsar-headers ✓' }
}
const s = store.load()
if (!s.ytmusic?.clientId || !s.ytmusic?.clientSecret) {
return { ok: false, message: 'Client ID/secret saknas — spara dem först' }
}
const oauthPath = path.join(config.youtubarr?.dataDir ?? '/youtubarr-data', 'oauth.json')
const oauthPath = path.join(dataDir, 'oauth.json')
if (!existsSync(oauthPath)) {
return { ok: false, message: 'inte kopplad än — klicka "Koppla YT Music"' }
}

View File

@@ -16,6 +16,40 @@ export function ytmusicFlow({ config, store, fetchImpl = fetch }) {
return {
oauthPath: path.join(dataDir, 'oauth.json'),
browserPath: path.join(dataDir, 'browser.json'),
// Bygger ytmusicapi-kompatibel browser.json av råa request-headers
// kopierade från en inloggad music.youtube.com-session.
saveBrowserHeaders(raw) {
const parsed = {}
for (const line of String(raw ?? '').split('\n')) {
const idx = line.indexOf(':')
if (idx < 1) continue
const key = line.slice(0, idx).trim().toLowerCase()
const value = line.slice(idx + 1).trim()
if (key && value) parsed[key] = value
}
const cookie = parsed.cookie
if (!cookie) return { error: 'ingen Cookie-header hittades i det du klistrade in' }
if (!cookie.includes('__Secure-3PAPISID')) {
return { error: 'cookien saknar __Secure-3PAPISID — kopiera headers från ett inloggat music.youtube.com-anrop (t.ex. "browse")' }
}
const browser = {
'User-Agent': parsed['user-agent'] ?? 'Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0',
Accept: '*/*',
'Accept-Language': parsed['accept-language'] ?? 'sv-SE,sv;q=0.8,en;q=0.5',
'Content-Type': 'application/json',
'X-Goog-AuthUser': parsed['x-goog-authuser'] ?? '0',
'x-origin': 'https://music.youtube.com',
Cookie: cookie,
}
try {
writeFileSync(this.browserPath, JSON.stringify(browser, null, 2))
} catch (err) {
return { error: `kunde inte spara ${this.browserPath}: ${err.code ?? err.message}` }
}
return { ok: true }
},
async start() {
const { clientId } = creds()

View File

@@ -87,3 +87,23 @@ test('poll: avslag ger felstatus', async () => {
})
assert.deepEqual(await flow.poll('dev-123'), { status: 'error', message: 'nekad' })
})
test('saveBrowserHeaders: bygger browser.json av råa headers', () => {
const { dataDir, store, config } = setup()
const flow = ytmusicFlow({ config, store })
const out = flow.saveBrowserHeaders(
'POST /youtubei/v1/browse HTTP/2\nUser-Agent: TestUA/1.0\nAccept-Language: sv\nX-Goog-AuthUser: 0\nCookie: VISITOR=1; __Secure-3PAPISID=hemlis; PREF=x\n',
)
assert.deepEqual(out, { ok: true })
const b = JSON.parse(readFileSync(path.join(dataDir, 'browser.json'), 'utf8'))
assert.equal(b['User-Agent'], 'TestUA/1.0')
assert.match(b.Cookie, /__Secure-3PAPISID/)
assert.equal(b['x-origin'], 'https://music.youtube.com')
})
test('saveBrowserHeaders: avvisar headers utan giltig cookie', () => {
const { store, config } = setup()
const flow = ytmusicFlow({ config, store })
assert.match(flow.saveBrowserHeaders('User-Agent: x').error, /Cookie/i)
assert.match(flow.saveBrowserHeaders('Cookie: bara=skräp').error, /__Secure-3PAPISID/)
})