Чтение тэгов без обложки: с диска только нужные текстовые фреймы, у APIC — лишь признак

read_book_tags читал тэг целиком вместе с картинкой (у книг с обложкой 3 МБ — 3 МБ на файл
при каждом открытии книги). Теперь фреймы читаются по одному, обложка и ненужные фреймы
пропускаются; тэг с unsynchronisation (v2.3) — целиком, как раньше. Тест: на тэге с обложкой
3 МБ выделяется меньше 1 МБ, теги и has_cover те же.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
K1ndWha1eandClaude Opus 5.5 committed 2026-10-11 17:26:44 +03:00
1 parent dc13ae43b8
commit 4c9184aeb2
2 files changed
+112 -5

No files matched your search

+79 -5
View File
@@ -183,6 +183,81 @@ fn parse_id3v2(path string) !ParsedTag {
return parsed
}
// Фреймы, которые нужны чтению тэгов книги (read_book_tags)
const read_frames = ['TIT2', 'TALB', 'TPE1', 'TPE2', 'TCOM', 'TIT1', 'TRCK', 'TYER', 'TDRC', 'TCON',
'TLAN', 'COMM', 'TXXX']
// Чтение для read_book_tags: с диска берутся только нужные текстовые фреймы, обложка и прочие фреймы
// пропускаются — у тэга с обложкой в мегабайты читается несколько КБ. Второе значение — есть ли обложка.
// Тэг с unsynchronisation (v2.3) по фреймам не прочитать — он читается целиком, как parse_id3v2
fn parse_id3v2_texts(path string) !(ParsedTag, bool) {
mut f := os.open(path)!
defer {
f.close()
}
mut head := []u8{len: 10}
n := f.read_bytes_into(0, mut head) or { 0 }
if n < 10 || head[..3].bytestr() != 'ID3' {
return ParsedTag{}, false
}
version := head[3]
flags := head[5]
if version !in [u8(3), 4] || (version == 3 && flags & 0x80 != 0) {
parsed := parse_id3v2(path)!
return parsed, parsed.frames.any(it.name == 'APIC')
}
tag_size := get_size_v24(head[6..10])
mut parsed := ParsedTag{
version: version
audio_start: tag_size + 10 + if version == 4 && flags & 0x10 != 0 { u64(10) } else { 0 }
}
tag_end := tag_size + 10
if tag_end > os.file_size(path) {
return error('размер тэга (${tag_size}) больше размера файла')
}
mut pos := u64(10)
// Расширенный заголовок
if flags & 0x40 != 0 {
if f.read_bytes_into(pos, mut head)! < 4 {
return parsed, false
}
pos += if version == 3 { get_size_v23(head[..4]) + 4 } else { get_size_v24(head[..4]) }
}
mut has_cover := false
for pos + 10 <= tag_end {
if f.read_bytes_into(pos, mut head)! < 10 || head[0] == 0 {
break
}
id := head[..4].bytestr()
if !is_frame_id(id) {
break
}
size := if version == 3 { get_size_v23(head[4..8]) } else { get_size_v24(head[4..8]) }
end := pos + 10 + size
if end > tag_end {
break
}
if id == 'APIC' {
// Как в parse_id3v2: слишком короткий APIC — не обложка
has_cover = has_cover || size >= 14
} else if size > 0 && id in read_frames {
// Каждый нужный фрейм — в свой маленький буфер
mut buf := []u8{len: int(size + 10)}
if f.read_bytes_into(pos, mut buf)! < buf.len {
break
}
parsed.frames << Frame{}
parsed.frames[parsed.frames.len - 1].fill(buf, 0, version)
}
pos = end
}
return parsed, has_cover
}
@[inline]
fn is_frame_id(id string) bool {
for c in id {
@@ -295,18 +370,20 @@ fn (frame Frame) apic_value() (string, u8, []u8) {
// Чтение тегов файла. Если ID3v2 нет — смотрим ID3v1
fn read_book_tags_impl(path string) !FileTags {
mut parsed := parse_id3v2(path)!
// Обложка не читается — только отмечается, есть ли она
mut parsed, has_cover := parse_id3v2_texts(path)!
// Тексты уже декодированы в свои строки — байты тэга больше не нужны
defer {
unsafe { parsed.raw.free() }
}
mut res := FileTags{
version: parsed.version
has_cover: has_cover
}
size := os.file_size(path)
res.audio_size = if size > parsed.audio_start { size - parsed.audio_start } else { 0 }
if parsed.frames.len == 0 {
if parsed.frames.len == 0 && !has_cover {
res.tags = read_v1(path) or { BookTags{} }
return res
}
@@ -348,9 +425,6 @@ fn read_book_tags_impl(path string) !FileTags {
else {}
}
}
'APIC' {
res.has_cover = true
}
else {}
}
}
+33
View File
@@ -82,6 +82,39 @@ fn test_roundtrip_v24() {
check_roundtrip('test_id3v24.mp3')
}
// Чтение тэгов не загружает обложку: у тэга с картинкой 3 МБ — только текстовые фреймы
fn test_read_skips_cover() {
for name in ['test_id3v23.mp3', 'test_id3v24.mp3'] {
path := tmp_copy(name)
defer {
os.rm(path) or {}
}
mut data := []u8{len: 3 * 1024 * 1024, init: u8(index % 251)}
data[0] = 0xFF
data[1] = 0xD8
tags := metadata.BookTags{
title: 'Глава 1'
album: 'Тень ветра'
author: 'Сафон'
series: 'Кладбище забытых книг'
}
metadata.write_book_tags(path, tags, cover: metadata.Cover{
mime: 'image/jpeg'
data: data
}) or { panic(err) }
before := u64(gc_heap_usage().total_bytes)
t := metadata.read_book_tags(path) or { panic(err) }
allocated := u64(gc_heap_usage().total_bytes) - before
assert t.has_cover
assert t.tags.title == 'Глава 1'
assert t.tags.album == 'Тень ветра'
assert t.tags.series == 'Кладбище забытых книг'
assert allocated < 1024 * 1024, '${name}: при чтении выделено ${allocated} Б'
// Обложку по-прежнему отдаёт read_cover
assert (metadata.read_cover(path) or { panic(err) }).data.len == data.len
}
}
// В файл без ID3v2 тэг добавляется в начало
fn test_write_without_id3v2() {
path := tmp_copy('test_id3v1.mp3')