Чтение тэгов без обложки: с диска только нужные текстовые фреймы, у 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:
1 parent
dc13ae43b8
commit
4c9184aeb2
2 files changed
+113
-6
No files matched your search
+80
-6
@@ -183,6 +183,81 @@ fn parse_id3v2(path string) !ParsedTag {
|
|||||||
return parsed
|
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]
|
@[inline]
|
||||||
fn is_frame_id(id string) bool {
|
fn is_frame_id(id string) bool {
|
||||||
for c in id {
|
for c in id {
|
||||||
@@ -295,18 +370,20 @@ 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 := parse_id3v2(path)!
|
// Обложка не читается — только отмечается, есть ли она
|
||||||
|
mut parsed, has_cover := parse_id3v2_texts(path)!
|
||||||
// Тексты уже декодированы в свои строки — байты тэга больше не нужны
|
// Тексты уже декодированы в свои строки — байты тэга больше не нужны
|
||||||
defer {
|
defer {
|
||||||
unsafe { parsed.raw.free() }
|
unsafe { parsed.raw.free() }
|
||||||
}
|
}
|
||||||
mut res := FileTags{
|
mut res := FileTags{
|
||||||
version: parsed.version
|
version: parsed.version
|
||||||
|
has_cover: has_cover
|
||||||
}
|
}
|
||||||
size := os.file_size(path)
|
size := os.file_size(path)
|
||||||
res.audio_size = if size > parsed.audio_start { size - parsed.audio_start } else { 0 }
|
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{} }
|
res.tags = read_v1(path) or { BookTags{} }
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -348,9 +425,6 @@ fn read_book_tags_impl(path string) !FileTags {
|
|||||||
else {}
|
else {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'APIC' {
|
|
||||||
res.has_cover = true
|
|
||||||
}
|
|
||||||
else {}
|
else {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,39 @@ fn test_roundtrip_v24() {
|
|||||||
check_roundtrip('test_id3v24.mp3')
|
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 тэг добавляется в начало
|
// В файл без 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