Sångare-förslag + schema/antal/källor per typ + YT Music-skrivfix
Some checks failed
build-and-push / build (push) Failing after 12s

- Ny batch-typ 'singers': baseras på de 10 senaste unika sångarna ur
  gillat (YT Music) + spelat (Jellyfin); Gemini ger per sångare 5
  topplåtar + 3 relaterade artister, filtrerade mot Lidarr-biblioteket
  och gillalistan; klick på kortet öppnar modal med YT-länkar och
  Hämta-knappar
- Förslags-scheman per typ: intervall med enhet (timmar/dagar/månader),
  antal förslag (1-25) och källo-toggles; gamla intervalHours/
  trackIntervalDays migreras automatiskt; schemaläggaren kör alla tre
- Inställningar: Förslag-kortet omgjort till hopfällbara sektioner per
  typ (details/summary) så det inte blir överväldigande
- ytmusic: tydligt fel om oauth.json inte kan skrivas (root-ägd mapp
  var orsaken till 'inte kopplad än' — ägarskap fixat på Pi5)
- 28 tester gröna

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 02:19:58 +02:00
parent 6ff8e4d7ca
commit ac93b25827
12 changed files with 714 additions and 228 deletions

View File

@@ -4,7 +4,7 @@ import { mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { buildApp } from '../src/app.js'
import { settingsStore, effectiveConfig } from '../src/settings.js'
import { settingsStore, effectiveConfig, suggestConfig } from '../src/settings.js'
import { makeTesters } from '../src/testers.js'
function tmpDataDir() {
@@ -44,6 +44,22 @@ test('effectiveConfig: sparat har företräde, env är fallback', () => {
assert.equal(eff.gemini.model, 'gemini-2.5-flash')
})
test('suggestConfig: defaults, migrering av gamla nycklar och clamping', () => {
const def = suggestConfig({})
assert.equal(def.mix.every, 24)
assert.equal(def.tracks.unit, 'days')
assert.equal(def.singers.unit, 'months')
const legacy = suggestConfig({ suggest: { intervalHours: 12, trackIntervalDays: 3 } })
assert.equal(legacy.mix.every, 12)
assert.equal(legacy.mix.unit, 'hours')
assert.equal(legacy.tracks.every, 3)
const clamped = suggestConfig({ suggest: { mix: { count: 99, unit: 'veckor' } } })
assert.equal(clamped.mix.count, 25)
assert.equal(clamped.mix.unit, 'hours')
})
test('PUT + GET /api/settings maskerar hemligheter', async () => {
const store = settingsStore(tmpDataDir())
const app = buildApp(baseConfig, { lidarr: {}, musicbrainz: {}, store })

View File

@@ -134,6 +134,39 @@ test('run(tracks): gillade + mest spelade in i prompten, egen batch-typ', async
assert.equal(engine.lastRun('mix'), null)
})
test('run(singers): unika sångare i prompt, filtrering mot bibliotek + gillat', async () => {
const { engine, fetchImpl } = setup({
geminiResponse: JSON.stringify([
{
name: 'Floor Jansen',
motivation: 'Kraftfull sopran.',
topTracks: [
{ name: 'Gimme! Gimme! Gimme!', artist: 'ABBA' },
{ name: 'Storm', artist: 'Floor Jansen' },
],
relatedArtists: ['Powerwolf', 'Epica'],
},
]),
})
const batch = await engine.run('manuell', 'singers')
assert.match(fetchImpl.prompt, /SÅNGARE/)
assert.match(fetchImpl.prompt, /ABBA/, 'sångare ur gillat ska in i prompten')
assert.match(fetchImpl.prompt, /1000mods/, 'sångare ur spelat ska in i prompten')
assert.equal(batch.type, 'singers')
const s = batch.items[0]
assert.equal(s.kind, 'singer')
assert.equal(s.topTracks.length, 1, 'gillad låt (Gimme! Gimme! Gimme!) ska filtreras bort')
assert.equal(s.topTracks[0].name, 'Storm')
assert.deepEqual(
s.relatedArtists.map((a) => a.name),
['Epica'],
'Powerwolf finns i biblioteket och ska filtreras bort',
)
assert.equal(s.relatedArtists[0].mbid, 'mbid-x')
})
test('run utan Gemini-nyckel ger begripligt fel', async () => {
const { engine, dataDir } = setup()
const store = settingsStore(dataDir)