Fix viewing-history artwork and add transcode playback metrics
This commit is contained in:
@@ -13,6 +13,7 @@ const output = process.env.REVIEW_DIR
|
||||
await context.addCookies([{ name: 'magent_logged_in', value: '1', url: base }])
|
||||
let mode = 'ready'
|
||||
let role = 'admin'
|
||||
let brokenArtwork = false
|
||||
const periods = []
|
||||
const mutations = []
|
||||
let settings = [
|
||||
@@ -27,14 +28,19 @@ const output = process.env.REVIEW_DIR
|
||||
top_titles: [{ title: 'Severance', type: 'series', minutes: 460, plays: 10 }, { title: 'Arrival', type: 'movie', minutes: 116, plays: 1 }, { title: 'The Bear', type: 'series', minutes: 91, plays: 3 }],
|
||||
clients: [{ name: 'Jellyfin Web', minutes: 740 }, { name: 'Jellyfin for Android TV', minutes: 301 }],
|
||||
methods: [{ name: 'Direct play', minutes: 880 }, { name: 'Transcode', minutes: 161 }],
|
||||
recent: [{ id: '1', title: 'Good News About Hell', series: 'Severance', type: 'episode', episode: 'S1 · E1', minutes: 57, played_at: '2026-09-07T10:00:00Z', client: 'Jellyfin Web', method: 'Direct play' },
|
||||
{ id: '2', title: 'Arrival', series: '', type: 'movie', minutes: 116, played_at: '2026-09-06T10:00:00Z', client: 'Jellyfin for Android TV', method: 'Direct play' }],
|
||||
transcoding: { video_minutes: 100, audio_minutes: 61, hardware_video_minutes: 80, software_video_minutes: 20, unknown_hardware_minutes: 0, unknown_video_minutes: 0, unknown_audio_minutes: 0, hardware: [{ name: 'NVIDIA NVENC', minutes: 80 }], audio_codecs: [{ name: 'AAC', minutes: 61 }], gpu_busy_minutes: null },
|
||||
recent: [{ id: '1', title: 'Good News About Hell', series: 'Severance', type: 'episode', episode: 'S1 · E1', minutes: 57, played_at: '2026-09-07T10:00:00Z', client: 'Jellyfin Web', method: 'Direct play', artwork_url: '/insights/artwork/series?token=fixture' },
|
||||
{ id: '2', title: 'Arrival', series: '', type: 'movie', minutes: 116, played_at: '2026-09-06T10:00:00Z', client: 'Jellyfin for Android TV', method: 'Direct play', artwork_url: '/insights/artwork/movie?token=fixture' }],
|
||||
requests: { total: 3, movies: 2, tv: 1, pending: 1, approved: 2, declined: 0, recent: [{ request_id: 12, title: 'Dune: Part Two', media_type: 'movie', status: 2 }] },
|
||||
})
|
||||
await context.route('**/api/**', async (route) => {
|
||||
const request = route.request()
|
||||
const url = new URL(request.url())
|
||||
if (url.pathname === '/api/auth/me') return route.fulfill({ json: { username: 'Fixture viewer', role } })
|
||||
if (url.pathname.startsWith('/api/insights/artwork/')) {
|
||||
if (brokenArtwork) return route.fulfill({ status: 404, body: 'Artwork unavailable' })
|
||||
return route.fulfill({ contentType: 'image/svg+xml', body: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 180"><rect width="120" height="180" fill="#243946"/><path d="M0 150L65 35L120 150" fill="#c8bdaa"/><circle cx="75" cy="42" r="20" fill="#dbe6de"/></svg>' })
|
||||
}
|
||||
if (url.pathname === '/api/insights') {
|
||||
const days = Number(url.searchParams.get('days'))
|
||||
periods.push(days)
|
||||
@@ -46,6 +52,12 @@ const output = process.env.REVIEW_DIR
|
||||
for (const key of Object.keys(result.summary)) result.summary[key] = 0
|
||||
result.daily = daily.map((day) => ({ ...day, minutes: 0 }))
|
||||
result.top_titles = result.recent = result.clients = result.methods = []
|
||||
} else if (mode === 'missing_transcoding') {
|
||||
for (const key of Object.keys(result.transcoding)) {
|
||||
if (typeof result.transcoding[key] === 'number') result.transcoding[key] = 0
|
||||
}
|
||||
result.transcoding.hardware = result.transcoding.audio_codecs = []
|
||||
result.transcoding.unknown_video_minutes = result.transcoding.unknown_audio_minutes = 161
|
||||
} else if (mode !== 'ready') result.summary = null
|
||||
return route.fulfill({ json: result })
|
||||
}
|
||||
@@ -73,6 +85,11 @@ const output = process.env.REVIEW_DIR
|
||||
await page.setViewportSize({ width, height: 1000 })
|
||||
await page.goto(base + '/insights')
|
||||
await page.getByRole('heading', { name: 'Your viewing rhythm' }).waitFor()
|
||||
await page.locator('.stats-history').scrollIntoViewIfNeeded()
|
||||
await page.waitForFunction(() => [...document.querySelectorAll('.stats-media-icon img')].length === 2 && [...document.querySelectorAll('.stats-media-icon img')].every((img) => img.complete && img.naturalWidth > 0))
|
||||
await page.evaluate(() => window.scrollTo(0, 0))
|
||||
assert.match(await page.locator('.stats-transcode-metrics').innerText(), /GPU-assisted video[\s\S]*80 min[\s\S]*Audio transcoding[\s\S]*61 min/)
|
||||
assert.match(await page.locator('.stats-transcoding').innerText(), /GPU busy time is not recorded/)
|
||||
assert.notEqual(await page.locator('.stats-period [aria-pressed=true]').evaluate((element) => getComputedStyle(element).backgroundColor), await page.locator('.stats-period [aria-pressed=false]').first().evaluate((element) => getComputedStyle(element).backgroundColor), 'Selected period must be visibly different')
|
||||
await page.locator('.stats-chart-bars button').first().click()
|
||||
assert.match(await page.locator('.stats-chart-detail').innerText(), /minutes/)
|
||||
@@ -87,6 +104,17 @@ const output = process.env.REVIEW_DIR
|
||||
await page.screenshot({ path: path.join(output, `insights-${width}.png`), fullPage: true })
|
||||
}
|
||||
}
|
||||
brokenArtwork = true
|
||||
await page.reload()
|
||||
await page.locator('.stats-history').scrollIntoViewIfNeeded()
|
||||
await page.getByText('MV', { exact: true }).waitFor()
|
||||
await page.waitForFunction(() => document.querySelectorAll('.stats-media-icon img').length === 0)
|
||||
brokenArtwork = false
|
||||
mode = 'missing_transcoding'
|
||||
await page.reload()
|
||||
await page.getByText('Some stream details are missing:', { exact: false }).waitFor()
|
||||
assert.equal(await page.locator('.stats-transcoding').getByText('Not recorded', { exact: true }).count(), 3)
|
||||
mode = 'ready'
|
||||
await page.getByRole('button', { name: '90 days', exact: true }).click()
|
||||
await page.getByRole('heading', { name: 'Your viewing rhythm' }).waitFor()
|
||||
assert.equal(periods.at(-1), 90)
|
||||
@@ -124,6 +152,6 @@ const output = process.env.REVIEW_DIR
|
||||
assert(mutations.some((request) => request.body?.jellystat_base_url === 'http://jellystat:3001'))
|
||||
assert(mutations.filter((request) => request.body).every((request) => !Object.hasOwn(request.body, 'jellystat_api_key')), 'An unchanged secret must be preserved')
|
||||
assert.deepEqual(errors, [])
|
||||
console.log('Insights browser checks passed: desktop/mobile, chart, period, requests, empty/setup/unlinked/error/auth states, settings save/test, preserved secret.')
|
||||
console.log('Insights browser checks passed: desktop/mobile, posters/fallback, transcode totals/missing metadata, chart, period, requests, empty/setup/unlinked/error/auth states, settings save/test, preserved secret.')
|
||||
} finally { await browser.close() }
|
||||
})().catch((error) => { console.error(error); process.exit(1) })
|
||||
|
||||
Reference in New Issue
Block a user