Запись тэга с обложкой: старая обложка не читается, новая копируется один раз
- build_tag: ёмкость — только по переносимым фреймам (заменяемая обложка не в счёт), фрейм обложки пишется прямо в буфер тэга (write_apic) вместо двух промежуточных массивов - замена или удаление обложки: старый тэг читается по фреймам без APIC (parse_id3v2_lazy) - тест: перезапись с обложкой 3 МБ выделяет < 1,5 размера обложки (было 17 МБ, стало 3) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6161a7faaa
commit
095499b0fe
2 files changed
+78
-31
No files matched your search
+45
-31
@@ -187,10 +187,11 @@ fn parse_id3v2(path string) !ParsedTag {
|
|||||||
const read_frames = ['TIT2', 'TALB', 'TPE1', 'TPE2', 'TCOM', 'TIT1', 'TRCK', 'TYER', 'TDRC', 'TCON',
|
const read_frames = ['TIT2', 'TALB', 'TPE1', 'TPE2', 'TCOM', 'TIT1', 'TRCK', 'TYER', 'TDRC', 'TCON',
|
||||||
'TLAN', 'COMM', 'TXXX']
|
'TLAN', 'COMM', 'TXXX']
|
||||||
|
|
||||||
// Чтение для read_book_tags: с диска берутся только нужные текстовые фреймы, обложка и прочие фреймы
|
// Чтение тэга по фреймам без обложки: картинка в мегабайты с диска не читается, второе значение —
|
||||||
// пропускаются — у тэга с обложкой в мегабайты читается несколько КБ. Второе значение — есть ли обложка.
|
// есть ли она. texts_only — только текстовые фреймы для read_book_tags (несколько КБ на файл),
|
||||||
|
// иначе все фреймы, кроме обложки, — для записи, когда обложка заменяется или удаляется.
|
||||||
// Тэг с unsynchronisation (v2.3) по фреймам не прочитать — он читается целиком, как parse_id3v2
|
// Тэг с unsynchronisation (v2.3) по фреймам не прочитать — он читается целиком, как parse_id3v2
|
||||||
fn parse_id3v2_texts(path string) !(ParsedTag, bool) {
|
fn parse_id3v2_lazy(path string, texts_only bool) !(ParsedTag, bool) {
|
||||||
mut f := os.open(path)!
|
mut f := os.open(path)!
|
||||||
defer {
|
defer {
|
||||||
f.close()
|
f.close()
|
||||||
@@ -244,8 +245,8 @@ fn parse_id3v2_texts(path string) !(ParsedTag, bool) {
|
|||||||
if id == 'APIC' {
|
if id == 'APIC' {
|
||||||
// Как в parse_id3v2: слишком короткий APIC — не обложка
|
// Как в parse_id3v2: слишком короткий APIC — не обложка
|
||||||
has_cover = has_cover || size >= 14
|
has_cover = has_cover || size >= 14
|
||||||
} else if size > 0 && id in read_frames {
|
} else if size > 0 && (!texts_only || id in read_frames) {
|
||||||
// Каждый нужный фрейм — в свой маленький буфер
|
// Каждый нужный фрейм — в свой буфер
|
||||||
mut buf := []u8{len: int(size + 10)}
|
mut buf := []u8{len: int(size + 10)}
|
||||||
if f.read_bytes_into(pos, mut buf)! < buf.len {
|
if f.read_bytes_into(pos, mut buf)! < buf.len {
|
||||||
break
|
break
|
||||||
@@ -371,7 +372,7 @@ 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 {
|
||||||
// Обложка не читается — только отмечается, есть ли она
|
// Обложка не читается — только отмечается, есть ли она
|
||||||
mut parsed, has_cover := parse_id3v2_texts(path)!
|
mut parsed, has_cover := parse_id3v2_lazy(path, true)!
|
||||||
// Тексты уже декодированы в свои строки — байты тэга больше не нужны
|
// Тексты уже декодированы в свои строки — байты тэга больше не нужны
|
||||||
defer {
|
defer {
|
||||||
unsafe { parsed.raw.free() }
|
unsafe { parsed.raw.free() }
|
||||||
@@ -594,18 +595,21 @@ fn comm_frame(text string, lang string, version u8) []u8 {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apic_frame(cover Cover, version u8) []u8 {
|
// Фрейм обложки — сразу в конец out: картинка в мегабайты копируется один раз, без промежуточных буферов
|
||||||
mut body := [u8(0)]
|
fn write_apic(mut out []u8, cover Cover, version u8) {
|
||||||
body << normalize_mime(cover.mime, cover.data).bytes()
|
mime := normalize_mime(cover.mime, cover.data)
|
||||||
body << 0
|
// Кодировка, mime с нулём, тип 3 (передняя обложка), пустое описание в ISO-8859-1
|
||||||
// Тип 3 — передняя обложка
|
size := 1 + mime.len + 1 + 1 + 1 + cover.data.len
|
||||||
body << 3
|
out << frame_header('APIC', size, version)
|
||||||
// Пустое описание в ISO-8859-1
|
out << u8(0)
|
||||||
body << 0
|
out << mime.bytes()
|
||||||
body << cover.data
|
out << [u8(0), 3, 0]
|
||||||
mut out := frame_header('APIC', body.len, version)
|
out << cover.data
|
||||||
out << body
|
}
|
||||||
return out
|
|
||||||
|
// Сколько байт займёт фрейм обложки
|
||||||
|
fn apic_frame_len(cover Cover) int {
|
||||||
|
return 10 + 4 + normalize_mime(cover.mime, cover.data).len + cover.data.len
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_managed(frame Frame) bool {
|
fn is_managed(frame Frame) bool {
|
||||||
@@ -619,17 +623,26 @@ fn is_managed(frame Frame) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Старый фрейм переносится в новый тэг как есть: не пересобираемый и не заменяемая обложка
|
||||||
|
fn kept_frame(frame Frame, drop_cover bool) bool {
|
||||||
|
return !is_managed(frame) && !(frame.name == 'APIC' && drop_cover)
|
||||||
|
}
|
||||||
|
|
||||||
// Сборка тэга целиком (заголовок + фреймы + запас нулей)
|
// Сборка тэга целиком (заголовок + фреймы + запас нулей)
|
||||||
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) }
|
||||||
|
new_cover := opts.cover
|
||||||
|
drop_cover := new_cover != none || opts.remove_cover
|
||||||
// Сразу нужной ёмкости: тэг с обложкой — это мегабайты, а рост массива удвоениями оставлял
|
// Сразу нужной ёмкости: тэг с обложкой — это мегабайты, а рост массива удвоениями оставлял
|
||||||
// в куче копии на каждом шаге. Текстовые фреймы — с запасом
|
// в куче копии на каждом шаге. Текстовые фреймы — с запасом; заменяемая обложка не в счёт
|
||||||
mut capacity := 10 + book_tag_padding + 4096 + t.comment.len * 2
|
mut capacity := 10 + book_tag_padding + 4096 + t.comment.len * 2
|
||||||
for frame in parsed.frames {
|
for frame in parsed.frames {
|
||||||
capacity += frame.raw.len
|
if kept_frame(frame, drop_cover) {
|
||||||
|
capacity += frame.raw.len
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if c := opts.cover {
|
if c := new_cover {
|
||||||
capacity += c.data.len + 64
|
capacity += apic_frame_len(c)
|
||||||
}
|
}
|
||||||
mut frames := []u8{cap: capacity}
|
mut frames := []u8{cap: capacity}
|
||||||
frames << [u8(`I`), `D`, `3`, version, 0, 0, 0, 0, 0, 0]
|
frames << [u8(`I`), `D`, `3`, version, 0, 0, 0, 0, 0, 0]
|
||||||
@@ -662,18 +675,13 @@ fn build_tag(parsed ParsedTag, t BookTags, opts WriteOptions) []u8 {
|
|||||||
frames << comm_frame(t.comment.trim_space(), 'rus', version)
|
frames << comm_frame(t.comment.trim_space(), 'rus', version)
|
||||||
}
|
}
|
||||||
|
|
||||||
new_cover := opts.cover
|
|
||||||
for frame in parsed.frames {
|
for frame in parsed.frames {
|
||||||
if is_managed(frame) {
|
if kept_frame(frame, drop_cover) {
|
||||||
continue
|
frames << frame.raw
|
||||||
}
|
}
|
||||||
if frame.name == 'APIC' && (new_cover != none || opts.remove_cover) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
frames << frame.raw
|
|
||||||
}
|
}
|
||||||
if c := new_cover {
|
if c := new_cover {
|
||||||
frames << apic_frame(c, version)
|
write_apic(mut frames, c, version)
|
||||||
}
|
}
|
||||||
|
|
||||||
frames << []u8{len: book_tag_padding}
|
frames << []u8{len: book_tag_padding}
|
||||||
@@ -691,7 +699,13 @@ 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 {
|
||||||
mut parsed := parse_id3v2(path)!
|
// Старая обложка заменяется или удаляется — её с диска не читать, остальные фреймы переносятся как есть
|
||||||
|
mut parsed := ParsedTag{}
|
||||||
|
if opts.cover != none || opts.remove_cover {
|
||||||
|
parsed, _ = parse_id3v2_lazy(path, false)!
|
||||||
|
} else {
|
||||||
|
parsed = parse_id3v2(path)!
|
||||||
|
}
|
||||||
mut tag := build_tag(parsed, t, opts)
|
mut tag := build_tag(parsed, t, opts)
|
||||||
// Старый и новый тэг (с обложкой — мегабайты) сразу сборщику: при параллельной записи иначе
|
// Старый и новый тэг (с обложкой — мегабайты) сразу сборщику: при параллельной записи иначе
|
||||||
// куча вырастает на несколько копий на поток и сборщик её системе не возвращает
|
// куча вырастает на несколько копий на поток и сборщик её системе не возвращает
|
||||||
|
|||||||
@@ -117,6 +117,39 @@ fn test_read_skips_cover() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Замена обложки: в памяти только новый тэг — старая обложка не читается, новая копируется один раз
|
||||||
|
fn test_write_cover_allocations() {
|
||||||
|
path := tmp_copy('test_id3v23.mp3')
|
||||||
|
defer {
|
||||||
|
os.rm(path) or {}
|
||||||
|
}
|
||||||
|
mut data := []u8{len: 3 * 1024 * 1024, init: u8(index % 251)}
|
||||||
|
data[0] = 0xFF
|
||||||
|
data[1] = 0xD8
|
||||||
|
cover := metadata.Cover{
|
||||||
|
mime: 'image/jpeg'
|
||||||
|
data: data
|
||||||
|
}
|
||||||
|
tags := metadata.BookTags{
|
||||||
|
title: 'Глава 1'
|
||||||
|
album: 'Тень ветра'
|
||||||
|
}
|
||||||
|
metadata.write_book_tags(path, tags, cover: cover) or { panic(err) }
|
||||||
|
before := u64(gc_heap_usage().total_bytes)
|
||||||
|
metadata.write_book_tags(path, metadata.BookTags{ ...tags, title: 'Глава 2' },
|
||||||
|
cover: cover
|
||||||
|
) or {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
allocated := u64(gc_heap_usage().total_bytes) - before
|
||||||
|
eprintln(' перезапись с обложкой 3 МБ: выделено ${f64(allocated) / 1048576:.1f} МБ')
|
||||||
|
assert allocated < u64(data.len) * 3 / 2, 'выделено ${allocated} Б'
|
||||||
|
t := metadata.read_book_tags(path) or { panic(err) }
|
||||||
|
assert t.tags.title == 'Глава 2'
|
||||||
|
assert t.has_cover
|
||||||
|
assert (metadata.read_cover(path) or { panic(err) }).data == data
|
||||||
|
}
|
||||||
|
|
||||||
// В файл без ID3v2 тэг добавляется в начало
|
// В файл без ID3v2 тэг добавляется в начало
|
||||||
fn test_write_without_id3v2() {
|
fn test_write_without_id3v2() {
|
||||||
path := tmp_copy('test_id3v1.mp3')
|
path := tmp_copy('test_id3v1.mp3')
|
||||||
|
|||||||
Reference in new issue
Block a user