diff --git a/metadata/audiobook.v b/metadata/audiobook.v index e96b82f..1b6b3e5 100644 --- a/metadata/audiobook.v +++ b/metadata/audiobook.v @@ -64,6 +64,8 @@ pub: pub struct WriteReport { pub: in_place bool + // Переписан только тэг в начале файла (новый поместился на место старого), аудио не тронуто + header bool // Почему не удалось переписать на месте (пусто, если на месте) fallback string } @@ -75,6 +77,10 @@ mut: audio_start u64 } +// Запас в конце тэга при полной перезаписи: следующие правки (длиннее описание, другие названия) +// поместятся на место старого тэга, и аудио переписывать не придётся +const book_tag_padding = 4096 + // Фреймы, которые редактор пересобирает сам, остальные переносятся без изменений const managed_frames = ['TIT2', 'TALB', 'TPE1', 'TPE2', 'TCOM', 'TIT1', 'TRCK', 'TYER', 'TDRC', 'TCON', 'COMM', 'TLAN'] @@ -558,7 +564,7 @@ fn build_tag(parsed ParsedTag, t BookTags, opts WriteOptions) []u8 { frames << apic_frame(c, version) } - frames << []u8{len: tag_padding} + frames << []u8{len: book_tag_padding} mut out := [u8(`I`), `D`, `3`, version, 0, 0] out << encode_header_size(u64(frames.len)) @@ -574,6 +580,16 @@ fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) !WriteReport parsed := parse_id3v2(path)! tag := build_tag(parsed, t, opts) + // Новый тэг помещается на место старого — переписать только его, аудио не трогать + if fitted := fit_tag(parsed, tag) { + if write_header(path, fitted) { + return WriteReport{ + in_place: true + header: true + } + } + } + tmp_path := path + '.tagtmp' mut buf := []u8{len: 1024 * 1024} { @@ -624,6 +640,34 @@ fn write_book_tags_impl(path string, t BookTags, opts WriteOptions) !WriteReport } } +// Тэг (без запаса), дополненный нулями ровно до размера старого тэга; none — не помещается +fn fit_tag(parsed ParsedTag, tag []u8) ?[]u8 { + if parsed.version !in [u8(3), 4] || parsed.audio_start < 10 { + return none + } + 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 +} + +// Перезапись начала файла тем же числом байт; false — файл не открылся на запись +fn write_header(path string, data []u8) bool { + mut f := os.open_file(path, 'r+') or { return false } + defer { + f.close() + } + f.write(data) or { return false } + f.flush() + return true +} + // Содержимое from — в существующий файл to (с обрезкой), не заменяя сам файл fn copy_back(from string, to string, mut buf []u8) ! { mut src := os.open(from)! diff --git a/test/audiobook_test.v b/test/audiobook_test.v index 59a0278..59bcf8a 100644 --- a/test/audiobook_test.v +++ b/test/audiobook_test.v @@ -151,3 +151,52 @@ fn test_write_keeps_inode() { t := metadata.read_book_tags(path) or { panic(err) } assert t.tags.title == 'Тот же файл' } + +// Повторная запись: тэг помещается на место старого — переписывается только он, аудио не меняется +fn test_rewrite_header_only() { + path := tmp_copy('test_id3v23.mp3') + defer { + os.rm(path) or {} + } + original := os.read_bytes(path) or { panic(err) } + tail := 2048 + audio := original[original.len - tail..].clone() + + // Описание больше старого тэга — полная перезапись, в тэге остаётся запас + first := metadata.write_book_tags_report(path, metadata.BookTags{ + title: 'Первая запись' + comment: 'описание '.repeat(300) + }) or { panic(err) } + assert first.in_place + assert !first.header + size := os.file_size(path) + before := os.stat(path) or { panic(err) } + + // Правка в пределах запаса — только тэг + second := metadata.write_book_tags_report(path, metadata.BookTags{ + title: 'Вторая запись, длиннее первой' + album: 'Книга' + comment: 'описание '.repeat(300) + 'и ещё немного' + }) or { panic(err) } + assert second.header + assert os.file_size(path) == size + after := os.stat(path) or { panic(err) } + assert after.inode == before.inode + t := metadata.read_book_tags(path) or { panic(err) } + assert t.tags.title == 'Вторая запись, длиннее первой' + assert t.tags.album == 'Книга' + assert t.tags.comment.ends_with('и ещё немного') + now := os.read_bytes(path) or { panic(err) } + assert now[now.len - tail..] == audio + + // Не помещается — снова полная перезапись + third := metadata.write_book_tags_report(path, metadata.BookTags{ + title: 'Третья' + comment: 'длинное описание '.repeat(1000) + }) or { panic(err) } + assert !third.header + t3 := metadata.read_book_tags(path) or { panic(err) } + assert t3.tags.title == 'Третья' + end := os.read_bytes(path) or { panic(err) } + assert end[end.len - tail..] == audio +}