Files
transcriber/internal/adapter/repo/pocketbase/record_mapping.go
T
av 3a2da3004b приём и чтение записей сведены к одному контракту приложения
- адреса приложения переехали в своё пространство `/app/`, опрос готовности
  убран целиком: рубеж и причину остановки владелец узнаёт карточкой записи,
  текст — отдельным адресом названного вида
- заведена единая точка отображения доменной ошибки и слой, приводящий к той же
  форме отказы библиотеки: тело несёт машиночитаемый код рядом с сообщением
- у записи появились имя файла отправителя, длительность и размер своими
  колонками, а у ленты владельца — свой индекс: без него страница сканировала
  весь архив сервиса
2026-08-15 13:51:23 +03:00

156 lines
7.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package pocketbase
import (
"time"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/types"
"git.vakhrushev.me/av/transcriber/internal/entity"
)
// Отображение аудиозаписи в запись коллекции и обратно живёт одним местом.
//
// Мест стало **два** вместо прежних четырёх: захват больше не перечисляет
// колонки поимённо, а возвращает идентификатор и признак своего захвата.
// Инвариант проекта о колонках очереди этим съёживается и перестаёт расти с
// моделью — иначе каждая новая колонка записи попадала бы под него.
// applyOwnedByPipeline кладёт в запись только те поля, которыми распоряжается
// конвейер. Поля, которые он не меняет никогда — владелец, вход, заголовок,
// краткое описание, темы и адресат ответа, — не трогаются вовсе.
//
// Разрез нужен потому, что шаг держит запись снимком с момента захвата и до
// своего сохранения, а это часы. Всё, что владелец правил в панели за это время,
// безусловная запись снимка стёрла бы молча: ни строки в журнале, ни отказа в
// панели — владелец видел бы успешное сохранение и был бы уверен, что правка на
// месте.
func applyOwnedByPipeline(record *core.Record, r *entity.AudioRecord) {
record.Set("state", r.State)
record.Set("state_entered_at", dateOrEmpty(&r.StateEnteredAt))
record.Set("halted_at", dateOrEmpty(r.HaltedAt))
record.Set("halt_reason", derefString(r.HaltReason))
record.Set("error_text", derefString(r.ErrorText))
record.Set("acquisition_id", derefString(r.AcquisitionID))
record.Set("acquire_expires_at", dateOrEmpty(r.AcquireExpiresAt))
record.Set("delay_time", dateOrEmpty(r.DelayTime))
record.Set("attempts", r.Attempts)
record.Set("original_file", derefString(r.OriginalFileID))
record.Set("normalized_file", derefString(r.NormalizedFileID))
record.Set("transcript_text", derefString(r.TranscriptTextID))
record.Set("literary_text", derefString(r.LiteraryTextID))
record.Set("structure", derefString(r.StructureID))
record.Set("recognition", derefString(r.RecognitionID))
}
// applyToRecord кладёт запись целиком — это заведение, и спорить за поля здесь
// не с кем.
func applyToRecord(record *core.Record, r *entity.AudioRecord) {
applyOwnedByPipeline(record, r)
// Владелец кладётся только здесь, при заведении. В applyOwnedByPipeline его
// нет намеренно: конвейер владельца не назначает и не меняет, а снимок шага,
// записанный поверх, стёр бы его молча.
record.Set("owner", r.OwnerID)
record.Set("source", r.Source)
record.Set("title", derefString(r.Title))
record.Set("brief", derefString(r.Brief))
// Имя файла отправителя, длительность и размер кладёт приём и только он: это
// снимок принятого, и конвейер его не пересчитывает. В applyOwnedByPipeline их
// нет намеренно — снимок шага, записанный поверх, стёр бы их молча.
record.Set("original_filename", derefString(r.OriginalFilename))
record.Set("duration_ms", numberOrZero(r.DurationMs))
record.Set("size_bytes", numberOrZero(r.SizeBytes))
// Темы кладутся при заведении пустыми и конвейером не трогаются: считает их
// языковая модель отдельной задачей. Пишутся здесь ради симметрии с чтением —
// колонка, которую читают и не пишут, ничем не отличима от забытой.
record.Set("topics", r.TopicIDs)
}
func recordToAudioRecord(record *core.Record) *entity.AudioRecord {
return &entity.AudioRecord{
Id: record.Id,
OwnerID: record.GetString("owner"),
Source: record.GetString("source"),
Title: nilIfEmpty(record.GetString("title")),
Brief: nilIfEmpty(record.GetString("brief")),
State: record.GetString("state"),
StateEnteredAt: record.GetDateTime("state_entered_at").Time(),
HaltedAt: timeOrNil(record.GetDateTime("halted_at")),
HaltReason: nilIfEmpty(record.GetString("halt_reason")),
ErrorText: nilIfEmpty(record.GetString("error_text")),
AcquisitionID: nilIfEmpty(record.GetString("acquisition_id")),
AcquireExpiresAt: timeOrNil(record.GetDateTime("acquire_expires_at")),
DelayTime: timeOrNil(record.GetDateTime("delay_time")),
Attempts: record.GetInt("attempts"),
OriginalFileID: nilIfEmpty(record.GetString("original_file")),
NormalizedFileID: nilIfEmpty(record.GetString("normalized_file")),
TranscriptTextID: nilIfEmpty(record.GetString("transcript_text")),
LiteraryTextID: nilIfEmpty(record.GetString("literary_text")),
StructureID: nilIfEmpty(record.GetString("structure")),
RecognitionID: nilIfEmpty(record.GetString("recognition")),
OriginalFilename: nilIfEmpty(record.GetString("original_filename")),
// Имя колонки стоит литералом рядом с `.Get…`, а не уезжает в аргумент
// помощника: сверка колонок в `internal/archrules` ищет именно эту форму, а
// инвариант о колонках компилятор не проверяет.
DurationMs: numberValue(record.GetInt("duration_ms")),
SizeBytes: numberValue(record.GetInt("size_bytes")),
TopicIDs: record.GetStringSlice("topics"),
CreatedAt: record.GetDateTime("created").Time(),
UpdatedAt: record.GetDateTime("updated").Time(),
}
}
func derefString(v *string) string {
if v == nil {
return ""
}
return *v
}
// dateOrEmpty отдаёт пустое значение вместо нулевой даты: пустая колонка даты в
// хранилище это пустая строка, и она же значит «времени нет».
func dateOrEmpty(v *time.Time) any {
if v == nil || v.IsZero() {
return ""
}
date, err := types.ParseDateTime(*v)
if err != nil {
return ""
}
return date
}
// numberOrZero отдаёт ноль вместо отсутствующего числа.
//
// «Неизвестно» числовая колонка хранилища не выражает вовсе: пустое значение она
// не держит и кладёт нулём. Отличимость потребовала бы четвёртой колонки-признака
// либо текстового типа у чисел, и платить за это нечем — обе величины ставит
// приём и ставит всегда. Решение владельца 2026-08-15.
func numberOrZero(v *int64) int64 {
if v == nil {
return 0
}
return *v
}
// numberValue читает колонку числом. Ноль здесь означает ноль — см. numberOrZero.
func numberValue(value int) *int64 {
v := int64(value)
return &v
}
func nilIfEmpty(v string) *string {
if v == "" {
return nil
}
return &v
}
func timeOrNil(v types.DateTime) *time.Time {
if v.IsZero() {
return nil
}
t := v.Time()
return &t
}