feat(core): record ids, file paths, time formatting
Refs #1 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
7cd3811ead
commit
6b2466cf05
48
src/core/naming.ts
Normal file
48
src/core/naming.ts
Normal file
@ -0,0 +1,48 @@
|
||||
export const SOURCE_DIR = 'voice/source';
|
||||
export const INBOX_DIR = 'voice/inbox';
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
|
||||
// Время в id — UTC: порядок id не зависит от пояса устройства.
|
||||
export function makeId(startedAt: Date, random: () => number): string {
|
||||
const d = startedAt;
|
||||
const date = `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}`;
|
||||
const time = `${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}`;
|
||||
let hex = '';
|
||||
for (let i = 0; i < 4; i++) hex += Math.min(15, Math.floor(random() * 16)).toString(16);
|
||||
return `${date}-${time}-${hex}`;
|
||||
}
|
||||
|
||||
const EXT: Record<string, string> = {
|
||||
'audio/mp4': 'm4a',
|
||||
'audio/webm': 'webm',
|
||||
'audio/ogg': 'ogg',
|
||||
};
|
||||
|
||||
export function extFromMime(mime: string): string {
|
||||
const base = mime.split(';')[0].trim().toLowerCase();
|
||||
return EXT[base] ?? 'bin';
|
||||
}
|
||||
|
||||
export function audioPath(id: string, mime: string): string {
|
||||
return `${SOURCE_DIR}/${id}.${extFromMime(mime)}`;
|
||||
}
|
||||
|
||||
// Имя заметки — для человека, поэтому в локальном времени.
|
||||
export function notePath(startedAt: Date, n: number): string {
|
||||
const d = startedAt;
|
||||
const name =
|
||||
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
||||
`${pad(d.getHours())}-${pad(d.getMinutes())}-${pad(d.getSeconds())}`;
|
||||
return `${INBOX_DIR}/${name}${n > 1 ? ` (${n})` : ''}.md`;
|
||||
}
|
||||
|
||||
export function formatDuration(ms: number): string {
|
||||
const total = Math.max(0, Math.round(ms / 1000));
|
||||
return `${Math.floor(total / 60)}:${pad(total % 60)}`;
|
||||
}
|
||||
|
||||
export function formatLocalTime(iso: string): string {
|
||||
const d = new Date(iso);
|
||||
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
79
tests/naming.test.ts
Normal file
79
tests/naming.test.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
audioPath,
|
||||
extFromMime,
|
||||
formatDuration,
|
||||
formatLocalTime,
|
||||
makeId,
|
||||
notePath,
|
||||
} from '../src/core/naming';
|
||||
|
||||
const T = new Date('2026-09-27T11:15:03.000Z'); // 14:15:03 по Москве
|
||||
|
||||
function seq(...values: number[]): () => number {
|
||||
return () => values.shift() ?? 0;
|
||||
}
|
||||
|
||||
describe('test environment', () => {
|
||||
it('runs in UTC+3', () => {
|
||||
expect(T.getHours()).toBe(14);
|
||||
});
|
||||
});
|
||||
|
||||
describe('makeId', () => {
|
||||
it('uses UTC time and 4 hex chars', () => {
|
||||
expect(makeId(T, () => 0)).toBe('20260927-111503-0000');
|
||||
expect(makeId(T, seq(0.99, 0.5, 0.0625, 0.1))).toBe('20260927-111503-f811');
|
||||
});
|
||||
|
||||
it('clamps random() === 1 to f', () => {
|
||||
expect(makeId(T, () => 1)).toBe('20260927-111503-ffff');
|
||||
});
|
||||
|
||||
it('does not depend on the local date', () => {
|
||||
const lateEvening = new Date('2026-09-27T22:30:00.000Z'); // 01:30 28-го по Москве
|
||||
expect(makeId(lateEvening, () => 0)).toBe('20260927-223000-0000');
|
||||
expect(notePath(lateEvening, 1)).toBe('voice/inbox/2026-09-28 01-30-00.md');
|
||||
});
|
||||
});
|
||||
|
||||
describe('extFromMime', () => {
|
||||
it.each([
|
||||
['audio/mp4', 'm4a'],
|
||||
['audio/webm', 'webm'],
|
||||
['audio/webm;codecs=opus', 'webm'],
|
||||
['AUDIO/MP4', 'm4a'],
|
||||
['audio/ogg; codecs=opus', 'ogg'],
|
||||
['audio/x-foo', 'bin'],
|
||||
['', 'bin'],
|
||||
])('%s → %s', (mime, ext) => {
|
||||
expect(extFromMime(mime)).toBe(ext);
|
||||
});
|
||||
});
|
||||
|
||||
describe('paths', () => {
|
||||
it('audioPath', () => {
|
||||
expect(audioPath('20260927-111503-0000', 'audio/mp4')).toBe('voice/source/20260927-111503-0000.m4a');
|
||||
});
|
||||
|
||||
it('notePath uses local time and suffix from 2', () => {
|
||||
expect(notePath(T, 1)).toBe('voice/inbox/2026-09-27 14-15-03.md');
|
||||
expect(notePath(T, 2)).toBe('voice/inbox/2026-09-27 14-15-03 (2).md');
|
||||
expect(notePath(T, 3)).toBe('voice/inbox/2026-09-27 14-15-03 (3).md');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatting', () => {
|
||||
it.each([
|
||||
[0, '0:00'],
|
||||
[47_000, '0:47'],
|
||||
[125_400, '2:05'],
|
||||
[-5, '0:00'],
|
||||
])('formatDuration(%d) = %s', (ms, text) => {
|
||||
expect(formatDuration(ms)).toBe(text);
|
||||
});
|
||||
|
||||
it('formatLocalTime', () => {
|
||||
expect(formatLocalTime('2026-09-27T11:17:03.000Z')).toBe('14:17');
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user