feat(uploads): show batch format counts and quality ranges
ci / ts / ts (packages + web) (push) Failing after 5m56s
ci / module boundaries / boundaries (push) Successful in 5s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m39s

This commit is contained in:
mrleo1nid committed 2026-10-29 17:41:28 +03:00
1 parent e78cab6ce6
commit 1e3a149aef
7 files changed
+63 -1

No files matched your search

+4 -1
View File
@@ -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(' · '),
};
}