Меньше памяти на тэгах с обложкой: фрейм заполняется прямо в массиве, тэг собирается в буфер нужного размера, «только тэг» пишется без копии, буферы — free()
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
71b838e7d4
commit
dc13ae43b8
1 file changed
+64
-28
+64
-28
@@ -75,6 +75,9 @@ mut:
|
|||||||
version u8
|
version u8
|
||||||
frames []Frame
|
frames []Frame
|
||||||
audio_start u64
|
audio_start u64
|
||||||
|
// Байты тэга из файла: фреймы — срезы этого буфера. После записи / чтения — free(), чтобы тэг
|
||||||
|
// с обложкой в несколько МБ не оседал в куче (при параллельной записи — по копии на поток)
|
||||||
|
raw []u8
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запас в конце тэга при полной перезаписи: следующие правки (длиннее описание, другие названия)
|
// Запас в конце тэга при полной перезаписи: следующие правки (длиннее описание, другие названия)
|
||||||
@@ -136,6 +139,7 @@ fn parse_id3v2(path string) !ParsedTag {
|
|||||||
}
|
}
|
||||||
bytes = unsafe { clean }
|
bytes = unsafe { clean }
|
||||||
}
|
}
|
||||||
|
parsed.raw = bytes
|
||||||
|
|
||||||
mut pos := u64(10)
|
mut pos := u64(10)
|
||||||
// Расширенный заголовок
|
// Расширенный заголовок
|
||||||
@@ -170,9 +174,10 @@ fn parse_id3v2(path string) !ParsedTag {
|
|||||||
pos = end
|
pos = end
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
mut frame := Frame{}
|
// Фрейм заполняется уже в массиве: `parsed.frames << frame` скопировал бы его срезы буфера
|
||||||
frame.fill(bytes, pos, version)
|
// (V клонирует срезы при копировании структуры в массив) — у обложки это ещё два её размера
|
||||||
parsed.frames << frame
|
parsed.frames << Frame{}
|
||||||
|
parsed.frames[parsed.frames.len - 1].fill(bytes, pos, version)
|
||||||
pos = end
|
pos = end
|
||||||
}
|
}
|
||||||
return parsed
|
return parsed
|
||||||
@@ -290,7 +295,11 @@ fn (frame Frame) apic_value() (string, u8, []u8) {
|
|||||||
|
|
||||||
// Чтение тегов файла. Если ID3v2 нет — смотрим ID3v1
|
// Чтение тегов файла. Если ID3v2 нет — смотрим ID3v1
|
||||||
fn read_book_tags_impl(path string) !FileTags {
|
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{
|
mut res := FileTags{
|
||||||
version: parsed.version
|
version: parsed.version
|
||||||
}
|
}
|
||||||
@@ -539,7 +548,17 @@ fn is_managed(frame Frame) bool {
|
|||||||
// Сборка тэга целиком (заголовок + фреймы + запас нулей)
|
// Сборка тэга целиком (заголовок + фреймы + запас нулей)
|
||||||
fn build_tag(parsed ParsedTag, t BookTags, opts WriteOptions) []u8 {
|
fn build_tag(parsed ParsedTag, t BookTags, opts WriteOptions) []u8 {
|
||||||
version := if parsed.version in [u8(3), 4] { parsed.version } else { u8(3) }
|
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 := [
|
pairs := [
|
||||||
@@ -585,10 +604,12 @@ fn build_tag(parsed ParsedTag, t BookTags, opts WriteOptions) []u8 {
|
|||||||
|
|
||||||
frames << []u8{len: book_tag_padding}
|
frames << []u8{len: book_tag_padding}
|
||||||
|
|
||||||
mut out := [u8(`I`), `D`, `3`, version, 0, 0]
|
// Размер тэга (без 10 байт заголовка) — на своё место в заголовке
|
||||||
out << encode_header_size(u64(frames.len))
|
size := encode_header_size(u64(frames.len - 10))
|
||||||
out << frames
|
for i in 0 .. 4 {
|
||||||
return out
|
frames[6 + i] = size[i]
|
||||||
|
}
|
||||||
|
return frames
|
||||||
}
|
}
|
||||||
|
|
||||||
// Запись тэгов: новый тэг + аудио из исходного файла собираются во временном файле,
|
// Запись тэгов: новый тэг + аудио из исходного файла собираются во временном файле,
|
||||||
@@ -596,12 +617,20 @@ fn build_tag(parsed ParsedTag, t BookTags, opts WriteOptions) []u8 {
|
|||||||
// Audiobookshelf опознаёт файлы по inode, и замена файла новым путает его библиотеку.
|
// Audiobookshelf опознаёт файлы по inode, и замена файла новым путает его библиотеку.
|
||||||
// Файл без ID3v2 тоже поддерживается — тэг просто ставится в начало
|
// Файл без ID3v2 тоже поддерживается — тэг просто ставится в начало
|
||||||
fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) !WriteReport {
|
fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) !WriteReport {
|
||||||
parsed := parse_id3v2(path)!
|
mut parsed := parse_id3v2(path)!
|
||||||
tag := build_tag(parsed, t, opts)
|
mut tag := build_tag(parsed, t, opts)
|
||||||
|
// Старый и новый тэг (с обложкой — мегабайты) сразу сборщику: при параллельной записи иначе
|
||||||
|
// куча вырастает на несколько копий на поток и сборщик её системе не возвращает
|
||||||
|
defer {
|
||||||
|
unsafe {
|
||||||
|
parsed.raw.free()
|
||||||
|
tag.free()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Новый тэг помещается на место старого — переписать только его, аудио не трогать
|
// Новый тэг помещается на место старого — переписать только его, аудио не трогать
|
||||||
if fitted := fit_tag(parsed, tag) {
|
if tag_fits(parsed, tag) {
|
||||||
if write_header(path, fitted) {
|
if write_header(path, parsed, tag) {
|
||||||
return WriteReport{
|
return WriteReport{
|
||||||
in_place: true
|
in_place: true
|
||||||
header: true
|
header: true
|
||||||
@@ -611,6 +640,9 @@ fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) !WriteReport
|
|||||||
|
|
||||||
tmp_path := path + '.tagtmp'
|
tmp_path := path + '.tagtmp'
|
||||||
mut buf := []u8{len: 1024 * 1024}
|
mut buf := []u8{len: 1024 * 1024}
|
||||||
|
defer {
|
||||||
|
unsafe { buf.free() }
|
||||||
|
}
|
||||||
{
|
{
|
||||||
mut src := os.open(path)!
|
mut src := os.open(path)!
|
||||||
defer {
|
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 {
|
if parsed.version !in [u8(3), 4] || parsed.audio_start < 10 {
|
||||||
return none
|
return false
|
||||||
}
|
}
|
||||||
body := tag.len - book_tag_padding
|
body := tag.len - book_tag_padding
|
||||||
if body < 10 || u64(body) > parsed.audio_start {
|
return 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Перезапись начала файла тем же числом байт; 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 }
|
mut f := os.open_file(path, 'r+') or { return false }
|
||||||
defer {
|
defer {
|
||||||
f.close()
|
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()
|
f.flush()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user