feat(uploads): show batch format counts and quality ranges
This commit is contained in:
1 parent
e78cab6ce6
commit
1e3a149aef
7 files changed
+63
-1
No files matched your search
@@ -29,7 +29,8 @@ async function setup(page: Page, scenario: 'queue' | 'template' | 'mine') {
|
||||
{ ...file, id: 'withdrawn', original_name: 'withdrawn.flac', metadata: null, status: 'withdrawn' },
|
||||
{ ...file, id: 'rejected', original_name: 'rejected.flac', metadata: null, status: 'rejected' },
|
||||
];
|
||||
const missing = { ...file, id: 'missing', original_name: 'missing.flac', target_source_id: null, target_source_name: null, target_rel_path: null, status_reason: 'uploads.no_target' };
|
||||
files[0]!.bitrate = 778000;
|
||||
const missing = { ...file, id: 'missing', original_name: 'missing.flac', bitrate: 924000, target_source_id: null, target_source_name: null, target_rel_path: null, status_reason: 'uploads.no_target' };
|
||||
let failOnce = true;
|
||||
const deleted: string[] = [];
|
||||
const original = window.fetch;
|
||||
@@ -96,6 +97,8 @@ async function setup(page: Page, scenario: 'queue' | 'template' | 'mine') {
|
||||
test('queue shows the storage and path, and recalculates after fixing placement', async ({ page }, info) => {
|
||||
await setup(page, 'queue');
|
||||
await expect(page.getByText('Есть обложка', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('2 × FLAC', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('778–924 kbps · 44.1 kHz · 16 bit', { exact: true })).toBeVisible();
|
||||
await expect(page.getByText('Artist/Album/01 - Song.flac', { exact: true })).toBeHidden();
|
||||
await page.getByRole('button', { name: 'Развернуть пачку', exact: true }).click();
|
||||
await expect(page.getByText('Artist/Album/01 - Song.flac', { exact: true })).toBeVisible();
|
||||
|
||||
@@ -3,6 +3,7 @@ import { chevronRightIcon, editIcon, refreshIcon } from '../../shared/ui/icons';
|
||||
import { useId, useState } from 'react';
|
||||
import { needsFormatAttention } from './files';
|
||||
import { AudioPreview } from './AudioPreview';
|
||||
import { batchSummary } from './batchSummary';
|
||||
import { UploadStatusBadge, useReasonText, type UploadFile } from './common';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button, cx, Icon } from '../../shared/ui/ui';
|
||||
@@ -32,6 +33,7 @@ export function BatchCard({
|
||||
const ids = files.map((f) => f.id);
|
||||
const all = ids.every((id) => selected.has(id));
|
||||
const size = files.reduce((n, f) => n + f.size_bytes, 0);
|
||||
const summary = batchSummary(files);
|
||||
return (
|
||||
<section className={u.batch} aria-label={t('uploads.batch_of', { author: first.author.username })}>
|
||||
<div className={u.batchHead}>
|
||||
@@ -44,6 +46,8 @@ export function BatchCard({
|
||||
<b>{first.author.username}</b>
|
||||
<span>{formatDateTime(first.created_at, i18n.language)}</span>
|
||||
<span>{t('uploads.batch_size', { count: files.length, size: formatBytes(size, i18n.language) })}</span>
|
||||
<span>{summary.formats.map(([format, count]) => `${count} × ${format || t('uploads.format_unknown')}`).join(' · ')}</span>
|
||||
{summary.quality && <span>{summary.quality}</span>}
|
||||
<span className={cx(s.badge, first.batch_has_cover ? u.tone_info : u.tone_muted)}>
|
||||
{t(first.batch_has_cover ? 'uploads.batch_cover_present' : 'uploads.batch_cover_absent')}
|
||||
</span>
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { expect, it } from 'vitest';
|
||||
import { batchSummary } from './batchSummary';
|
||||
|
||||
it('counts formats and combines measured quality ranges without repeating equal values', () => {
|
||||
const summary = batchSummary([
|
||||
{ format: 'FLAC', original_name: 'one.flac', bitrate: 778000, sample_rate: 44100, bit_depth: 16 },
|
||||
{ format: 'flac', original_name: 'two.flac', bitrate: 924000, sample_rate: 44100, bit_depth: 16 },
|
||||
{ format: null, original_name: 'three.mp3', bitrate: 320000, sample_rate: 48000, bit_depth: null },
|
||||
]);
|
||||
expect(summary.formats).toEqual([['FLAC', 2], ['MP3', 1]]);
|
||||
expect(summary.quality).toBe('320–924 kbps · 44.1–48 kHz · 16 bit');
|
||||
});
|
||||
|
||||
it('ignores missing and invalid measurements and retains fractional sample rates', () => {
|
||||
expect(batchSummary([
|
||||
{ format: null, original_name: 'unknown', bitrate: 0, sample_rate: null, bit_depth: null },
|
||||
{ format: 'WAV', original_name: 'one.wav', bitrate: null, sample_rate: 11025, bit_depth: 24 },
|
||||
{ format: 'wav', original_name: 'two.wav', bitrate: NaN, sample_rate: 96000, bit_depth: 32 },
|
||||
])).toEqual({ formats: [['WAV', 2], ['', 1]], quality: '11.025–96 kHz · 24–32 bit' });
|
||||
expect(batchSummary([]).quality).toBe('');
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { UploadFile } from './common';
|
||||
import { audioExt } from './files';
|
||||
|
||||
type SummaryFile = Pick<UploadFile, 'format' | 'original_name' | 'bitrate' | 'sample_rate' | 'bit_depth'>;
|
||||
|
||||
function range(values: (number | null)[], divisor: number, decimals: number, unit: string): string | null {
|
||||
const known = values.filter((value): value is number => value !== null && Number.isFinite(value) && value > 0);
|
||||
if (!known.length) return null;
|
||||
const format = (value: number) => String(Number((value / divisor).toFixed(decimals)));
|
||||
const min = format(Math.min(...known));
|
||||
const max = format(Math.max(...known));
|
||||
return `${min === max ? min : `${min}–${max}`} ${unit}`;
|
||||
}
|
||||
|
||||
/** Only measured positive values contribute to quality ranges; absent metadata is not zero. */
|
||||
export function batchSummary(files: SummaryFile[]) {
|
||||
const counts = new Map<string, number>();
|
||||
for (const file of files) {
|
||||
const format = (file.format?.trim() || audioExt(file.original_name)).toUpperCase();
|
||||
counts.set(format, (counts.get(format) ?? 0) + 1);
|
||||
}
|
||||
return {
|
||||
formats: [...counts].sort(([a, ac], [b, bc]) => bc - ac || a.localeCompare(b)),
|
||||
quality: [
|
||||
range(files.map((file) => file.bitrate), 1000, 0, 'kbps'),
|
||||
range(files.map((file) => file.sample_rate), 1000, 3, 'kHz'),
|
||||
range(files.map((file) => file.bit_depth), 1, 0, 'bit'),
|
||||
].filter(Boolean).join(' · '),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user