YT Music-koppling i settings: Googles device-flöde inbyggt
All checks were successful
build-and-push / build (push) Successful in 16s
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
This commit is contained in:
@@ -3,10 +3,12 @@ import { onMounted, ref } from 'vue'
|
||||
|
||||
const gemini = ref({ apiKey: '', model: '' })
|
||||
const jellyfin = ref({ url: '', apiKey: '', userId: '' })
|
||||
const saved = ref({ gemini: {}, jellyfin: {} })
|
||||
const ytmusic = ref({ clientId: '', clientSecret: '' })
|
||||
const saved = ref({ gemini: {}, jellyfin: {}, ytmusic: {} })
|
||||
const results = ref({}) // service -> {ok, message}
|
||||
const busy = ref({})
|
||||
const jfUsers = ref([])
|
||||
const device = ref(null) // {url, userCode} under pågående koppling
|
||||
|
||||
onMounted(async () => {
|
||||
const res = await fetch('/api/settings')
|
||||
@@ -74,6 +76,60 @@ async function test(service) {
|
||||
async function pickUser() {
|
||||
await saveAndTest('jellyfin')
|
||||
}
|
||||
|
||||
async function connectYtMusic() {
|
||||
busy.value = { ...busy.value, ytmusic: true }
|
||||
results.value = { ...results.value, ytmusic: null }
|
||||
device.value = null
|
||||
try {
|
||||
await fetch('/api/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
ytmusic: {
|
||||
clientId: ytmusic.value.clientId || null,
|
||||
clientSecret: ytmusic.value.clientSecret || null,
|
||||
},
|
||||
}),
|
||||
})
|
||||
const startRes = await fetch('/api/ytmusic/start', { method: 'POST' })
|
||||
const start = await startRes.json()
|
||||
if (!startRes.ok) {
|
||||
results.value = { ...results.value, ytmusic: { ok: false, message: start.error } }
|
||||
return
|
||||
}
|
||||
device.value = { url: start.url, userCode: start.userCode }
|
||||
const deadline = Date.now() + (start.expiresIn ?? 600) * 1000
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((r) => setTimeout(r, (start.interval ?? 5) * 1000))
|
||||
const pollRes = await fetch('/api/ytmusic/poll', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ deviceCode: start.deviceCode }),
|
||||
})
|
||||
const poll = await pollRes.json()
|
||||
if (poll.status === 'klar') {
|
||||
device.value = null
|
||||
ytmusic.value = { clientId: '', clientSecret: '' }
|
||||
results.value = { ...results.value, ytmusic: { ok: true, message: 'kopplad ✓ — Liked Music kan nu läsas' } }
|
||||
saved.value = await (await fetch('/api/settings')).json()
|
||||
return
|
||||
}
|
||||
if (poll.status === 'error') {
|
||||
device.value = null
|
||||
results.value = { ...results.value, ytmusic: { ok: false, message: poll.message } }
|
||||
return
|
||||
}
|
||||
}
|
||||
device.value = null
|
||||
results.value = { ...results.value, ytmusic: { ok: false, message: 'koden hann gå ut — prova igen' } }
|
||||
} catch {
|
||||
device.value = null
|
||||
results.value = { ...results.value, ytmusic: { ok: false, message: 'något gick fel' } }
|
||||
} finally {
|
||||
busy.value = { ...busy.value, ytmusic: false }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -124,6 +180,37 @@ async function pickUser() {
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<h3>
|
||||
YouTube Music
|
||||
<span v-if="saved.ytmusic.connected" class="badge">kopplad ✓</span>
|
||||
<span v-else-if="saved.ytmusic.clientIdSet" class="badge warn">ej kopplad</span>
|
||||
</h3>
|
||||
<p class="dim">
|
||||
Ger åtkomst till dina gillade låtar och privata spellistor. Kräver en
|
||||
OAuth-klient av typen "TVs and Limited Input devices" i Google Cloud
|
||||
Console (projektet youtubarr → Credentials).
|
||||
</p>
|
||||
<label>Client ID
|
||||
<input v-model="ytmusic.clientId" type="text" :placeholder="saved.ytmusic.clientIdSet ? '(sparad — fyll i för att byta)' : '….apps.googleusercontent.com'" />
|
||||
</label>
|
||||
<label>Client secret
|
||||
<input v-model="ytmusic.clientSecret" type="password" :placeholder="saved.ytmusic.clientSecretSet ? '•••••• (sparad — fyll i för att byta)' : 'GOCSPX-…'" />
|
||||
</label>
|
||||
<div v-if="device" class="device">
|
||||
Öppna <a :href="device.url" target="_blank" rel="noopener">{{ device.url }}</a>
|
||||
och ange koden: <strong class="code">{{ device.userCode }}</strong>
|
||||
<span class="dim">(väntar på godkännande…)</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button :disabled="busy.ytmusic" @click="connectYtMusic">
|
||||
{{ busy.ytmusic ? 'Väntar på Google…' : 'Spara & koppla YT Music' }}
|
||||
</button>
|
||||
<button class="ghostbtn" :disabled="busy.ytmusic" @click="test('ytmusic')">Testa kopplingen</button>
|
||||
<span v-if="results.ytmusic" :class="results.ytmusic.ok ? 'ok' : 'fail'">{{ results.ytmusic.message }}</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<h3>Status — fasta kopplingar</h3>
|
||||
<p class="dim">Lidarr och Youtubarr konfigureras i stackens .env på Pi5:an; här kan du testa att de svarar.</p>
|
||||
@@ -214,4 +301,27 @@ select {
|
||||
.fail {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.badge.warn {
|
||||
background: rgba(251, 191, 36, 0.15);
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.device {
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
border: 1px solid rgba(99, 102, 241, 0.4);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.7rem 0.9rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
font-size: 1.2rem;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
|
||||
.ghostbtn {
|
||||
background: transparent;
|
||||
color: var(--text-dim);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user