Files
lyssnarr/backend/test/ytmusic.test.js
claude 07264be338
All checks were successful
build-and-push / build (push) Successful in 19s
YT Music: webbläsar-headers som primär koppling (TV-OAuth ger HTTP 400 hos youtubei)
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
2026-08-01 05:35:06 +02:00

110 lines
3.8 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { existsSync, mkdtempSync, readFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { settingsStore } from '../src/settings.js'
import { ytmusicFlow } from '../src/ytmusic.js'
function setup() {
const dataDir = mkdtempSync(path.join(tmpdir(), 'lyssnarr-yt-'))
const store = settingsStore(dataDir)
store.save({ ytmusic: { clientId: 'klient-id', clientSecret: 'klient-hemlis' } })
const config = { youtubarr: { dataDir } }
return { dataDir, store, config }
}
test('start returnerar kod och länk från Google', async () => {
const { store, config } = setup()
const flow = ytmusicFlow({
config,
store,
fetchImpl: async (url, opts) => {
assert.match(url, /device\/code/)
assert.match(opts.body, /client_id=klient-id/)
return {
ok: true,
json: async () => ({
device_code: 'dev-123',
user_code: 'ABCD-EFGH',
verification_url: 'https://www.google.com/device',
interval: 5,
expires_in: 1800,
}),
}
},
})
const out = await flow.start()
assert.equal(out.userCode, 'ABCD-EFGH')
assert.equal(out.deviceCode, 'dev-123')
})
test('start utan client id ger fel', async () => {
const dataDir = mkdtempSync(path.join(tmpdir(), 'lyssnarr-yt-'))
const flow = ytmusicFlow({ config: { youtubarr: { dataDir } }, store: settingsStore(dataDir) })
const out = await flow.start()
assert.match(out.error, /Client ID/i)
})
test('poll: pending -> klar skriver ytmusicapi-kompatibel oauth.json', async () => {
const { dataDir, store, config } = setup()
let calls = 0
const flow = ytmusicFlow({
config,
store,
fetchImpl: async () => {
calls++
if (calls === 1) return { ok: false, json: async () => ({ error: 'authorization_pending' }) }
return {
ok: true,
json: async () => ({
access_token: 'acc',
refresh_token: 'ref',
expires_in: 3600,
scope: 'https://www.googleapis.com/auth/youtube',
token_type: 'Bearer',
}),
}
},
})
assert.deepEqual(await flow.poll('dev-123'), { status: 'pending' })
assert.deepEqual(await flow.poll('dev-123'), { status: 'klar' })
const p = path.join(dataDir, 'oauth.json')
assert.ok(existsSync(p))
const tok = JSON.parse(readFileSync(p, 'utf8'))
assert.equal(tok.access_token, 'acc')
assert.equal(tok.refresh_token, 'ref')
assert.equal(tok.token_type, 'Bearer')
assert.ok(tok.expires_at > Math.floor(Date.now() / 1000))
})
test('poll: avslag ger felstatus', async () => {
const { store, config } = setup()
const flow = ytmusicFlow({
config,
store,
fetchImpl: async () => ({ ok: false, json: async () => ({ error: 'access_denied', error_description: 'nekad' }) }),
})
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/)
})