From a76bbe18f1bc99d4b2aa16f4eea850a5a73d90a1 Mon Sep 17 00:00:00 2001 From: K1ndWha1e Date: Fri, 9 Oct 2026 21:58:08 +0300 Subject: [PATCH] =?UTF-8?q?*=D1=87=D1=82=D0=B5=D0=BD=D0=B8=D0=B5:=20=D0=BF?= =?UTF-8?q?=D1=83=D1=81=D1=82=D1=8B=D0=B5=20=D0=B8=D0=BB=D0=B8=20=D0=B8?= =?UTF-8?q?=D1=81=D0=BF=D0=BE=D1=80=D1=87=D0=B5=D0=BD=D0=BD=D1=8B=D0=B5=20?= =?UTF-8?q?(=C2=AB=EF=BF=BD=C2=BB)=20=D0=BD=D0=B0=D0=B7=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5,=20=D0=B0=D0=BB=D1=8C=D0=B1=D0=BE=D0=BC=20=D0=B8?= =?UTF-8?q?=20=D0=B0=D0=B2=D1=82=D0=BE=D1=80=20=D0=B2=20ID3v2=20=D0=B1?= =?UTF-8?q?=D0=B5=D1=80=D1=83=D1=82=D1=81=D1=8F=20=D0=B8=D0=B7=20ID3v1=20(?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D1=80=D1=8B=D0=B5=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D1=8B=20=D1=85=D1=80=D0=B0=D0=BD=D1=8F=D1=82=20=D1=82=D0=B0?= =?UTF-8?q?=D0=BC=20cp1251)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- metadata/audiobook.v | 19 +++++++++++++++++++ test/audiobook_test.v | 27 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/metadata/audiobook.v b/metadata/audiobook.v index 1b6b3e5..5e9a3a4 100644 --- a/metadata/audiobook.v +++ b/metadata/audiobook.v @@ -348,11 +348,30 @@ fn read_book_tags_impl(path string) !FileTags { if t.series == '' { t.series = tit1 } + // Пустые или испорченные («�») поля — из ID3v1: старые файлы часто хранят там исходный текст в cp1251, + // а в ID3v2 он мог попасть уже испорченным (прочитан не в той кодировке и записан обратно) + if needs_v1(t.title) || needs_v1(t.album) || needs_v1(t.author) { + if v1 := read_v1(path) { + if needs_v1(t.title) && !needs_v1(v1.title) { + t.title = v1.title + } + if needs_v1(t.album) && !needs_v1(v1.album) { + t.album = v1.album + } + if needs_v1(t.author) && !needs_v1(v1.author) { + t.author = v1.author + } + } + } // TPE1 мог быть не задан, тогда автор остаётся из TPE2 res.tags = t return res } +fn needs_v1(s string) bool { + return s.trim_space() == '' || s.contains('\uFFFD') +} + // "(101)" и "(101)Speech" из старых тэгов превращаем в название fn clean_genre(g string) string { if g.starts_with('(') && g.contains(')') { diff --git a/test/audiobook_test.v b/test/audiobook_test.v index 59bcf8a..6a31413 100644 --- a/test/audiobook_test.v +++ b/test/audiobook_test.v @@ -200,3 +200,30 @@ fn test_rewrite_header_only() { end := os.read_bytes(path) or { panic(err) } assert end[end.len - tail..] == audio } + +// Испорченное название в ID3v2 («�») — берётся исходное из ID3v1 (cp1251) +fn test_broken_v2_title_falls_back_to_v1() { + path := tmp_copy('test_id3v23.mp3') + defer { + os.rm(path) or {} + } + metadata.write_book_tags(path, metadata.BookTags{ + title: '\uFFFD\uFFFD\uFFFD 1' + album: 'Книга' + }) or { panic(err) } + // ID3v1 в конце файла: «Глава 1» в cp1251 + mut v1 := []u8{len: 128} + v1[0] = `T` + v1[1] = `A` + v1[2] = `G` + title := [u8(0xc3), 0xeb, 0xe0, 0xe2, 0xe0, 0x20, 0x31] + for i, c in title { + v1[3 + i] = c + } + mut f := os.open_append(path) or { panic(err) } + f.write(v1) or { panic(err) } + f.close() + t := metadata.read_book_tags(path) or { panic(err) } + assert t.tags.title == 'Глава 1' + assert t.tags.album == 'Книга' +}