From dc13ae43b8dc7cad90a13af52381301541998a73 Mon Sep 17 00:00:00 2001 From: K1ndWha1e Date: Sun, 11 Oct 2026 11:26:29 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=B5=D0=BD=D1=8C=D1=88=D0=B5=20=D0=BF?= =?UTF-8?q?=D0=B0=D0=BC=D1=8F=D1=82=D0=B8=20=D0=BD=D0=B0=20=D1=82=D1=8D?= =?UTF-8?q?=D0=B3=D0=B0=D1=85=20=D1=81=20=D0=BE=D0=B1=D0=BB=D0=BE=D0=B6?= =?UTF-8?q?=D0=BA=D0=BE=D0=B9:=20=D1=84=D1=80=D0=B5=D0=B9=D0=BC=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BF=D0=BE=D0=BB=D0=BD=D1=8F=D0=B5=D1=82=D1=81=D1=8F=20?= =?UTF-8?q?=D0=BF=D1=80=D1=8F=D0=BC=D0=BE=20=D0=B2=20=D0=BC=D0=B0=D1=81?= =?UTF-8?q?=D1=81=D0=B8=D0=B2=D0=B5,=20=D1=82=D1=8D=D0=B3=20=D1=81=D0=BE?= =?UTF-8?q?=D0=B1=D0=B8=D1=80=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=B2=20?= =?UTF-8?q?=D0=B1=D1=83=D1=84=D0=B5=D1=80=20=D0=BD=D1=83=D0=B6=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D1=80=D0=B0=D0=B7=D0=BC=D0=B5=D1=80=D0=B0,=20?= =?UTF-8?q?=C2=AB=D1=82=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20=D1=82=D1=8D=D0=B3?= =?UTF-8?q?=C2=BB=20=D0=BF=D0=B8=D1=88=D0=B5=D1=82=D1=81=D1=8F=20=D0=B1?= =?UTF-8?q?=D0=B5=D0=B7=20=D0=BA=D0=BE=D0=BF=D0=B8=D0=B8,=20=D0=B1=D1=83?= =?UTF-8?q?=D1=84=D0=B5=D1=80=D1=8B=20=E2=80=94=20free()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parsed.frames << frame клонировал срезы буфера (raw, apic) — у обложки 3 МБ это +5,7 МБ на файл. Замер на тэге 2,9 МБ: чтение 8,6 → 2,9 МБ выделений, запись 11,6 → 5,8 МБ. Co-Authored-By: Claude Opus 5.5 --- metadata/audiobook.v | 92 ++++++++++++++++++++++++++++++-------------- 1 file changed, 64 insertions(+), 28 deletions(-) diff --git a/metadata/audiobook.v b/metadata/audiobook.v index 5e9a3a4..ee85f78 100644 --- a/metadata/audiobook.v +++ b/metadata/audiobook.v @@ -75,6 +75,9 @@ mut: version u8 frames []Frame audio_start u64 + // Байты тэга из файла: фреймы — срезы этого буфера. После записи / чтения — free(), чтобы тэг + // с обложкой в несколько МБ не оседал в куче (при параллельной записи — по копии на поток) + raw []u8 } // Запас в конце тэга при полной перезаписи: следующие правки (длиннее описание, другие названия) @@ -136,6 +139,7 @@ fn parse_id3v2(path string) !ParsedTag { } bytes = unsafe { clean } } + parsed.raw = bytes mut pos := u64(10) // Расширенный заголовок @@ -170,9 +174,10 @@ fn parse_id3v2(path string) !ParsedTag { pos = end continue } - mut frame := Frame{} - frame.fill(bytes, pos, version) - parsed.frames << frame + // Фрейм заполняется уже в массиве: `parsed.frames << frame` скопировал бы его срезы буфера + // (V клонирует срезы при копировании структуры в массив) — у обложки это ещё два её размера + parsed.frames << Frame{} + parsed.frames[parsed.frames.len - 1].fill(bytes, pos, version) pos = end } return parsed @@ -290,7 +295,11 @@ fn (frame Frame) apic_value() (string, u8, []u8) { // Чтение тегов файла. Если ID3v2 нет — смотрим ID3v1 fn read_book_tags_impl(path string) !FileTags { - parsed := parse_id3v2(path)! + mut parsed := parse_id3v2(path)! + // Тексты уже декодированы в свои строки — байты тэга больше не нужны + defer { + unsafe { parsed.raw.free() } + } mut res := FileTags{ version: parsed.version } @@ -539,7 +548,17 @@ fn is_managed(frame Frame) bool { // Сборка тэга целиком (заголовок + фреймы + запас нулей) fn build_tag(parsed ParsedTag, t BookTags, opts WriteOptions) []u8 { version := if parsed.version in [u8(3), 4] { parsed.version } else { u8(3) } - mut frames := []u8{} + // Сразу нужной ёмкости: тэг с обложкой — это мегабайты, а рост массива удвоениями оставлял + // в куче копии на каждом шаге. Текстовые фреймы — с запасом + mut capacity := 10 + book_tag_padding + 4096 + t.comment.len * 2 + for frame in parsed.frames { + capacity += frame.raw.len + } + if c := opts.cover { + capacity += c.data.len + 64 + } + mut frames := []u8{cap: capacity} + frames << [u8(`I`), `D`, `3`, version, 0, 0, 0, 0, 0, 0] // Порядок как в большинстве редакторов: текстовые сверху, картинка в конце pairs := [ @@ -585,10 +604,12 @@ fn build_tag(parsed ParsedTag, t BookTags, opts WriteOptions) []u8 { frames << []u8{len: book_tag_padding} - mut out := [u8(`I`), `D`, `3`, version, 0, 0] - out << encode_header_size(u64(frames.len)) - out << frames - return out + // Размер тэга (без 10 байт заголовка) — на своё место в заголовке + size := encode_header_size(u64(frames.len - 10)) + for i in 0 .. 4 { + frames[6 + i] = size[i] + } + return frames } // Запись тэгов: новый тэг + аудио из исходного файла собираются во временном файле, @@ -596,12 +617,20 @@ fn build_tag(parsed ParsedTag, t BookTags, opts WriteOptions) []u8 { // Audiobookshelf опознаёт файлы по inode, и замена файла новым путает его библиотеку. // Файл без ID3v2 тоже поддерживается — тэг просто ставится в начало fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) !WriteReport { - parsed := parse_id3v2(path)! - tag := build_tag(parsed, t, opts) + mut parsed := parse_id3v2(path)! + mut tag := build_tag(parsed, t, opts) + // Старый и новый тэг (с обложкой — мегабайты) сразу сборщику: при параллельной записи иначе + // куча вырастает на несколько копий на поток и сборщик её системе не возвращает + defer { + unsafe { + parsed.raw.free() + tag.free() + } + } // Новый тэг помещается на место старого — переписать только его, аудио не трогать - if fitted := fit_tag(parsed, tag) { - if write_header(path, fitted) { + if tag_fits(parsed, tag) { + if write_header(path, parsed, tag) { return WriteReport{ in_place: true header: true @@ -611,6 +640,9 @@ fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) !WriteReport tmp_path := path + '.tagtmp' mut buf := []u8{len: 1024 * 1024} + defer { + unsafe { buf.free() } + } { mut src := os.open(path)! defer { @@ -659,30 +691,34 @@ fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) !WriteReport } } -// Тэг (без запаса), дополненный нулями ровно до размера старого тэга; none — не помещается -fn fit_tag(parsed ParsedTag, tag []u8) ?[]u8 { +// Новый тэг (без запаса) помещается на место старого +fn tag_fits(parsed ParsedTag, tag []u8) bool { if parsed.version !in [u8(3), 4] || parsed.audio_start < 10 { - return none + return false } body := tag.len - book_tag_padding - if body < 10 || u64(body) > parsed.audio_start { - return none - } - mut out := []u8{cap: int(parsed.audio_start)} - out << tag[..6] - out << encode_header_size(parsed.audio_start - 10) - out << tag[10..body] - out << []u8{len: int(parsed.audio_start) - body} - return out + return body >= 10 && u64(body) <= parsed.audio_start } -// Перезапись начала файла тем же числом байт; false — файл не открылся на запись -fn write_header(path string, data []u8) bool { +// Перезапись начала файла тем же числом байт: тэг без запаса, размер в заголовке — как у старого +// тэга, остаток до начала аудио — нулями. Пишется прямо из буфера тэга, без копии; +// false — файл не открылся на запись +fn write_header(path string, parsed ParsedTag, tag []u8) bool { + body := tag.len - book_tag_padding mut f := os.open_file(path, 'r+') or { return false } defer { f.close() } - f.write(data) or { return false } + f.write(tag[..6]) or { return false } + f.write(encode_header_size(parsed.audio_start - 10)) or { return false } + f.write(tag[10..body]) or { return false } + mut left := int(parsed.audio_start) - body + zeros := []u8{len: if left < 65536 { left } else { 65536 }} + for left > 0 { + n := if left < zeros.len { left } else { zeros.len } + f.write(zeros[..n]) or { return false } + left -= n + } f.flush() return true }