All checks were successful
build-and-push / build (push) Successful in 16s
- POST /api/ytmusic/start + /poll: kör device-flödet mot Google och skriver ytmusicapi-kompatibel oauth.json till Youtubarrs datamapp (mounten byts till rw i stacken) - Settings-sektion: Client ID/secret, 'Spara & koppla' visar kod + länk och pollar tills godkänt; 'Testa kopplingen' verifierar med en token-förnyelse mot Google - 4 nya tester, totalt 21 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01824ZrvG2mDYYrmwqypLNup
68 lines
2.5 KiB
JavaScript
68 lines
2.5 KiB
JavaScript
import { writeFileSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
const SCOPE = 'https://www.googleapis.com/auth/youtube'
|
|
const FORM = { 'Content-Type': 'application/x-www-form-urlencoded' }
|
|
|
|
// Googles device-flöde för YT Music (ytmusicapi-kompatibel oauth.json).
|
|
// Klienten (settings-sidan) visar kod + länk och pollar tills godkänt.
|
|
export function ytmusicFlow({ config, store, fetchImpl = fetch }) {
|
|
const dataDir = config.youtubarr?.dataDir ?? '/youtubarr-data'
|
|
|
|
function creds() {
|
|
const s = store.load()
|
|
return { clientId: s.ytmusic?.clientId ?? '', clientSecret: s.ytmusic?.clientSecret ?? '' }
|
|
}
|
|
|
|
return {
|
|
oauthPath: path.join(dataDir, 'oauth.json'),
|
|
|
|
async start() {
|
|
const { clientId } = creds()
|
|
if (!clientId) return { error: 'spara Client ID först' }
|
|
const res = await fetchImpl('https://oauth2.googleapis.com/device/code', {
|
|
method: 'POST',
|
|
headers: FORM,
|
|
body: new URLSearchParams({ client_id: clientId, scope: SCOPE }).toString(),
|
|
})
|
|
const js = await res.json()
|
|
if (!res.ok) return { error: js.error_description ?? js.error ?? 'okänt fel från Google' }
|
|
return {
|
|
deviceCode: js.device_code,
|
|
userCode: js.user_code,
|
|
url: js.verification_url ?? 'https://www.google.com/device',
|
|
interval: js.interval ?? 5,
|
|
expiresIn: js.expires_in,
|
|
}
|
|
},
|
|
|
|
async poll(deviceCode) {
|
|
const { clientId, clientSecret } = creds()
|
|
if (!clientId || !clientSecret) return { status: 'error', message: 'client id/secret saknas' }
|
|
const res = await fetchImpl('https://oauth2.googleapis.com/token', {
|
|
method: 'POST',
|
|
headers: FORM,
|
|
body: new URLSearchParams({
|
|
client_id: clientId,
|
|
client_secret: clientSecret,
|
|
device_code: deviceCode,
|
|
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
}).toString(),
|
|
})
|
|
const js = await res.json()
|
|
if (js.error === 'authorization_pending' || js.error === 'slow_down') return { status: 'pending' }
|
|
if (js.error) return { status: 'error', message: js.error_description ?? js.error }
|
|
const token = {
|
|
scope: js.scope ?? SCOPE,
|
|
token_type: js.token_type ?? 'Bearer',
|
|
access_token: js.access_token,
|
|
refresh_token: js.refresh_token,
|
|
expires_in: js.expires_in,
|
|
expires_at: Math.floor(Date.now() / 1000) + (js.expires_in ?? 0),
|
|
}
|
|
writeFileSync(this.oauthPath, JSON.stringify(token, null, 2))
|
|
return { status: 'klar' }
|
|
},
|
|
}
|
|
}
|