Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
095499b0fe | ||
|
|
6161a7faaa | ||
|
|
4c9184aeb2 |
No files matched your search
+119
-31
@@ -183,6 +183,82 @@ 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']
|
||||
|
||||
// Чтение тэга по фреймам без обложки: картинка в мегабайты с диска не читается, второе значение —
|
||||
// есть ли она. texts_only — только текстовые фреймы для read_book_tags (несколько КБ на файл),
|
||||
// иначе все фреймы, кроме обложки, — для записи, когда обложка заменяется или удаляется.
|
||||
// Тэг с unsynchronisation (v2.3) по фреймам не прочитать — он читается целиком, как parse_id3v2
|
||||
fn parse_id3v2_lazy(path string, texts_only bool) !(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 && (!texts_only || 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 +371,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_lazy(path, true)!
|
||||
// Тексты уже декодированы в свои строки — байты тэга больше не нужны
|
||||
defer {
|
||||
unsafe { parsed.raw.free() }
|
||||
}
|
||||
mut res := FileTags{
|
||||
version: parsed.version
|
||||
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 +426,6 @@ fn read_book_tags_impl(path string) !FileTags {
|
||||
else {}
|
||||
}
|
||||
}
|
||||
'APIC' {
|
||||
res.has_cover = true
|
||||
}
|
||||
else {}
|
||||
}
|
||||
}
|
||||
@@ -520,18 +595,21 @@ fn comm_frame(text string, lang string, version u8) []u8 {
|
||||
return out
|
||||
}
|
||||
|
||||
fn apic_frame(cover Cover, version u8) []u8 {
|
||||
mut body := [u8(0)]
|
||||
body << normalize_mime(cover.mime, cover.data).bytes()
|
||||
body << 0
|
||||
// Тип 3 — передняя обложка
|
||||
body << 3
|
||||
// Пустое описание в ISO-8859-1
|
||||
body << 0
|
||||
body << cover.data
|
||||
mut out := frame_header('APIC', body.len, version)
|
||||
out << body
|
||||
return out
|
||||
// Фрейм обложки — сразу в конец out: картинка в мегабайты копируется один раз, без промежуточных буферов
|
||||
fn write_apic(mut out []u8, cover Cover, version u8) {
|
||||
mime := normalize_mime(cover.mime, cover.data)
|
||||
// Кодировка, mime с нулём, тип 3 (передняя обложка), пустое описание в ISO-8859-1
|
||||
size := 1 + mime.len + 1 + 1 + 1 + cover.data.len
|
||||
out << frame_header('APIC', size, version)
|
||||
out << u8(0)
|
||||
out << mime.bytes()
|
||||
out << [u8(0), 3, 0]
|
||||
out << cover.data
|
||||
}
|
||||
|
||||
// Сколько байт займёт фрейм обложки
|
||||
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 {
|
||||
@@ -545,17 +623,26 @@ fn is_managed(frame Frame) bool {
|
||||
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 {
|
||||
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
|
||||
for frame in parsed.frames {
|
||||
capacity += frame.raw.len
|
||||
if kept_frame(frame, drop_cover) {
|
||||
capacity += frame.raw.len
|
||||
}
|
||||
}
|
||||
if c := opts.cover {
|
||||
capacity += c.data.len + 64
|
||||
if c := new_cover {
|
||||
capacity += apic_frame_len(c)
|
||||
}
|
||||
mut frames := []u8{cap: capacity}
|
||||
frames << [u8(`I`), `D`, `3`, version, 0, 0, 0, 0, 0, 0]
|
||||
@@ -588,18 +675,13 @@ fn build_tag(parsed ParsedTag, t BookTags, opts WriteOptions) []u8 {
|
||||
frames << comm_frame(t.comment.trim_space(), 'rus', version)
|
||||
}
|
||||
|
||||
new_cover := opts.cover
|
||||
for frame in parsed.frames {
|
||||
if is_managed(frame) {
|
||||
continue
|
||||
if kept_frame(frame, drop_cover) {
|
||||
frames << frame.raw
|
||||
}
|
||||
if frame.name == 'APIC' && (new_cover != none || opts.remove_cover) {
|
||||
continue
|
||||
}
|
||||
frames << frame.raw
|
||||
}
|
||||
if c := new_cover {
|
||||
frames << apic_frame(c, version)
|
||||
write_apic(mut frames, c, version)
|
||||
}
|
||||
|
||||
frames << []u8{len: book_tag_padding}
|
||||
@@ -617,7 +699,13 @@ fn build_tag(parsed ParsedTag, t BookTags, opts WriteOptions) []u8 {
|
||||
// Audiobookshelf опознаёт файлы по inode, и замена файла новым путает его библиотеку.
|
||||
// Файл без ID3v2 тоже поддерживается — тэг просто ставится в начало
|
||||
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)
|
||||
// Старый и новый тэг (с обложкой — мегабайты) сразу сборщику: при параллельной записи иначе
|
||||
// куча вырастает на несколько копий на поток и сборщик её системе не возвращает
|
||||
|
||||
+33
-29
@@ -4,23 +4,23 @@ import encoding.iconv
|
||||
|
||||
@[heap]
|
||||
pub struct Frame {
|
||||
pub mut:
|
||||
data string
|
||||
mut:
|
||||
prev_size u64
|
||||
cur_size u64
|
||||
start u64
|
||||
end u64
|
||||
encoding_name string
|
||||
name string
|
||||
encoding_name_idx u8
|
||||
addition []u8
|
||||
encoded_data []u8
|
||||
apic ?[]u8
|
||||
encoded_size []u8
|
||||
modified bool
|
||||
// Фрейм целиком в том виде, в каком он был прочитан из файла
|
||||
raw []u8
|
||||
pub mut:
|
||||
data string
|
||||
mut:
|
||||
prev_size u64
|
||||
cur_size u64
|
||||
start u64
|
||||
end u64
|
||||
encoding_name string
|
||||
name string
|
||||
encoding_name_idx u8
|
||||
addition []u8
|
||||
encoded_data []u8
|
||||
apic ?[]u8
|
||||
encoded_size []u8
|
||||
modified bool
|
||||
// Фрейм целиком в том виде, в каком он был прочитан из файла
|
||||
raw []u8
|
||||
}
|
||||
|
||||
// Во фрейме первый байт указывает кодировку 0 - ISO-8859-1 (Latin-1), 1 - UTF-16LE
|
||||
@@ -45,15 +45,15 @@ fn (mut frame Frame) fill(bytes []u8, start_position u64, idv3_version u8) {
|
||||
|
||||
// Содержимое фрейма
|
||||
frame.encoding_name = match bytes[frame.start + 10] {
|
||||
0 { "ISO-8859-1" }
|
||||
1 { "UTF-16LE" }
|
||||
2 { "UTF-16BE" }
|
||||
else { "UTF-8" }
|
||||
0 { 'ISO-8859-1' }
|
||||
1 { 'UTF-16LE' }
|
||||
2 { 'UTF-16BE' }
|
||||
else { 'UTF-8' }
|
||||
}
|
||||
|
||||
frame.encoding_name_idx = u8(bytes[frame.start + 10])
|
||||
|
||||
if frame.name.to_lower() == "apic" {
|
||||
if frame.name.to_lower() == 'apic' {
|
||||
// Дополнительные байты с информацией (MIME)
|
||||
frame.addition = bytes[frame.start + 10..frame.start + 24]
|
||||
// Фрейм целиком вместе с заголовком, чтобы записать его обратно без изменений
|
||||
@@ -65,13 +65,13 @@ fn (mut frame Frame) fill(bytes []u8, start_position u64, idv3_version u8) {
|
||||
// UTF-16 с BOM (01): 2 байта маркера, по ним определяется порядок байт
|
||||
if frame.encoding_name_idx == 1 && data_start + 2 <= frame.end {
|
||||
if bytes[data_start] == 0xFE && bytes[data_start + 1] == 0xFF {
|
||||
frame.encoding_name = "UTF-16BE"
|
||||
frame.encoding_name = 'UTF-16BE'
|
||||
}
|
||||
data_start += 2
|
||||
}
|
||||
|
||||
// Замыкающие нули не относятся к данным
|
||||
frame.data = (iconv.encoding_to_vstring(bytes[data_start..frame.end], frame.encoding_name) or { "" }).all_before('\x00')
|
||||
frame.data = (iconv.encoding_to_vstring(bytes[data_start..frame.end], frame.encoding_name) or { '' }).all_before('\x00')
|
||||
|
||||
frame.addition = bytes[frame.start..frame.start + 10]
|
||||
}
|
||||
@@ -86,10 +86,10 @@ pub fn (mut frame Frame) init(tag_name string, idv3_version u8) {
|
||||
fn (mut frame Frame) set_unicode_encoding(idv3_version u8) {
|
||||
// В v2.3 нет UTF-8, там Юникод только UTF-16 с BOM
|
||||
if idv3_version == 3 {
|
||||
frame.encoding_name = "UTF-16LE"
|
||||
frame.encoding_name = 'UTF-16LE'
|
||||
frame.encoding_name_idx = 1
|
||||
} else {
|
||||
frame.encoding_name = "UTF-8"
|
||||
frame.encoding_name = 'UTF-8'
|
||||
frame.encoding_name_idx = 3
|
||||
}
|
||||
}
|
||||
@@ -123,13 +123,17 @@ pub fn (mut frame Frame) encode_frame(idv3_version u8) {
|
||||
// Не удалось закодировать в исходной кодировке фрейма, переходим на Юникод
|
||||
frame.set_unicode_encoding(idv3_version)
|
||||
iconv.vstring_to_encoding(frame.data, frame.encoding_name) or {
|
||||
panic("не удалось закодировать фрейм ${frame.name}: ${err}")
|
||||
panic('не удалось закодировать фрейм ${frame.name}: ${err}')
|
||||
}
|
||||
}
|
||||
|
||||
// Для UTF-16 с BOM (01) перед текстом нужен маркер порядка байт
|
||||
frame.encoded_data = if frame.encoding_name_idx == 1 {
|
||||
mut bom := if frame.encoding_name == "UTF-16BE" { [u8(0xFE), 0xFF] } else { [u8(0xFF), 0xFE] }
|
||||
mut bom := if frame.encoding_name == 'UTF-16BE' {
|
||||
[u8(0xFE), 0xFF]
|
||||
} else {
|
||||
[u8(0xFF), 0xFE]
|
||||
}
|
||||
bom << encoded_data
|
||||
bom
|
||||
} else {
|
||||
@@ -174,7 +178,7 @@ pub fn (mut frame Frame) get_encoded_frame_tag(idv3_version u8) []u8 {
|
||||
pub fn (mut frame Frame) to_bytes(idv3_version u8) []u8 {
|
||||
// Картинка и нетронутые фреймы пишутся как были прочитаны,
|
||||
// чтобы не потерять то, что не разбирается (язык в COMM, значение в TXXX, бинарные фреймы)
|
||||
if frame.raw.len > 0 && (!frame.modified || frame.name == "APIC") {
|
||||
if frame.raw.len > 0 && (!frame.modified || frame.name == 'APIC') {
|
||||
return frame.raw
|
||||
}
|
||||
|
||||
|
||||
+152
-30
@@ -152,45 +152,167 @@ enum Genre as u8 {
|
||||
jpop = 146
|
||||
synthpop = 147
|
||||
|
||||
unknown = 255
|
||||
unknown = 255
|
||||
}
|
||||
|
||||
const genres_name = [
|
||||
'Blues', 'Classic Rock', 'Country', 'Dance', 'Disco', 'Funk', 'Grunge',
|
||||
'Hip-Hop', 'Jazz', 'Metal', 'New Age', 'Oldies', 'Other', 'Pop', 'R&B',
|
||||
'Rap', 'Reggae', 'Rock', 'Techno', 'Industrial', 'Alternative', 'Ska',
|
||||
'Death Metal', 'Pranks', 'Soundtrack', 'Euro-Techno', 'Ambient',
|
||||
'Trip-Hop', 'Vocal', 'Jazz+Funk', 'Fusion', 'Trance', 'Classical',
|
||||
'Instrumental', 'Acid', 'House', 'Game', 'Sound Clip', 'Gospel',
|
||||
'Noise', 'AlternRock', 'Bass', 'Soul', 'Punk', 'Space', 'Meditative',
|
||||
'Instrumental Pop', 'Instrumental Rock', 'Ethnic', 'Gothic', 'Darkwave',
|
||||
'Techno-Industrial', 'Electronic', 'Pop-Folk', 'Eurodance', 'Dream',
|
||||
'Southern Rock', 'Comedy', 'Cult', 'Gangsta', 'Top 40', 'Christian Rap',
|
||||
'Pop/Funk', 'Jungle', 'Native American', 'Cabaret', 'New Wave',
|
||||
'Psychadelic', 'Rave', 'Showtunes', 'Trailer', 'Lo-Fi', 'Tribal',
|
||||
'Acid Punk', 'Acid Jazz', 'Polka', 'Retro', 'Musical', 'Rock & Roll',
|
||||
'Blues',
|
||||
'Classic Rock',
|
||||
'Country',
|
||||
'Dance',
|
||||
'Disco',
|
||||
'Funk',
|
||||
'Grunge',
|
||||
'Hip-Hop',
|
||||
'Jazz',
|
||||
'Metal',
|
||||
'New Age',
|
||||
'Oldies',
|
||||
'Other',
|
||||
'Pop',
|
||||
'R&B',
|
||||
'Rap',
|
||||
'Reggae',
|
||||
'Rock',
|
||||
'Techno',
|
||||
'Industrial',
|
||||
'Alternative',
|
||||
'Ska',
|
||||
'Death Metal',
|
||||
'Pranks',
|
||||
'Soundtrack',
|
||||
'Euro-Techno',
|
||||
'Ambient',
|
||||
'Trip-Hop',
|
||||
'Vocal',
|
||||
'Jazz+Funk',
|
||||
'Fusion',
|
||||
'Trance',
|
||||
'Classical',
|
||||
'Instrumental',
|
||||
'Acid',
|
||||
'House',
|
||||
'Game',
|
||||
'Sound Clip',
|
||||
'Gospel',
|
||||
'Noise',
|
||||
'AlternRock',
|
||||
'Bass',
|
||||
'Soul',
|
||||
'Punk',
|
||||
'Space',
|
||||
'Meditative',
|
||||
'Instrumental Pop',
|
||||
'Instrumental Rock',
|
||||
'Ethnic',
|
||||
'Gothic',
|
||||
'Darkwave',
|
||||
'Techno-Industrial',
|
||||
'Electronic',
|
||||
'Pop-Folk',
|
||||
'Eurodance',
|
||||
'Dream',
|
||||
'Southern Rock',
|
||||
'Comedy',
|
||||
'Cult',
|
||||
'Gangsta',
|
||||
'Top 40',
|
||||
'Christian Rap',
|
||||
'Pop/Funk',
|
||||
'Jungle',
|
||||
'Native American',
|
||||
'Cabaret',
|
||||
'New Wave',
|
||||
'Psychadelic',
|
||||
'Rave',
|
||||
'Showtunes',
|
||||
'Trailer',
|
||||
'Lo-Fi',
|
||||
'Tribal',
|
||||
'Acid Punk',
|
||||
'Acid Jazz',
|
||||
'Polka',
|
||||
'Retro',
|
||||
'Musical',
|
||||
'Rock & Roll',
|
||||
'Hard Rock',
|
||||
// Winamp
|
||||
'Folk', 'Folk-Rock', 'National Folk', 'Swing', 'Fast Fusion', 'Bebob',
|
||||
'Latin', 'Revival', 'Celtic', 'Bluegrass', 'Avantgarde', 'Gothic Rock',
|
||||
'Progressive Rock', 'Psychedelic Rock', 'Symphonic Rock', 'Slow Rock',
|
||||
'Big Band', 'Chorus', 'Easy Listening', 'Acoustic', 'Humour', 'Speech',
|
||||
'Chanson', 'Opera', 'Chamber Music', 'Sonata', 'Symphony', 'Booty Bass',
|
||||
'Primus', 'Porn Groove', 'Satire', 'Slow Jam', 'Club', 'Tango', 'Samba',
|
||||
'Folklore', 'Ballad', 'Power Ballad', 'Rhythmic Soul', 'Freestyle', 'Duet',
|
||||
'Punk Ballad', 'Drum Solo', 'A Cappella', 'Euro-House', 'Dance Hall',
|
||||
'Goa', 'Drum & Bass', 'Club-House', 'Hardcore', 'Terror', 'Indie',
|
||||
'BritPop', 'Negerpunk', 'Polsk Punk', 'Beat', 'Christian Gangsta',
|
||||
'Heavy Metal', 'Black Metal', 'Crossover', 'Contemporary Christian',
|
||||
'Christian Rock', 'Merengue', 'Salsa', 'Thrash Metal', 'Anime', 'JPop',
|
||||
'Synthpop'
|
||||
'Folk',
|
||||
'Folk-Rock',
|
||||
'National Folk',
|
||||
'Swing',
|
||||
'Fast Fusion',
|
||||
'Bebob',
|
||||
'Latin',
|
||||
'Revival',
|
||||
'Celtic',
|
||||
'Bluegrass',
|
||||
'Avantgarde',
|
||||
'Gothic Rock',
|
||||
'Progressive Rock',
|
||||
'Psychedelic Rock',
|
||||
'Symphonic Rock',
|
||||
'Slow Rock',
|
||||
'Big Band',
|
||||
'Chorus',
|
||||
'Easy Listening',
|
||||
'Acoustic',
|
||||
'Humour',
|
||||
'Speech',
|
||||
'Chanson',
|
||||
'Opera',
|
||||
'Chamber Music',
|
||||
'Sonata',
|
||||
'Symphony',
|
||||
'Booty Bass',
|
||||
'Primus',
|
||||
'Porn Groove',
|
||||
'Satire',
|
||||
'Slow Jam',
|
||||
'Club',
|
||||
'Tango',
|
||||
'Samba',
|
||||
'Folklore',
|
||||
'Ballad',
|
||||
'Power Ballad',
|
||||
'Rhythmic Soul',
|
||||
'Freestyle',
|
||||
'Duet',
|
||||
'Punk Ballad',
|
||||
'Drum Solo',
|
||||
'A Cappella',
|
||||
'Euro-House',
|
||||
'Dance Hall',
|
||||
'Goa',
|
||||
'Drum & Bass',
|
||||
'Club-House',
|
||||
'Hardcore',
|
||||
'Terror',
|
||||
'Indie',
|
||||
'BritPop',
|
||||
'Negerpunk',
|
||||
'Polsk Punk',
|
||||
'Beat',
|
||||
'Christian Gangsta',
|
||||
'Heavy Metal',
|
||||
'Black Metal',
|
||||
'Crossover',
|
||||
'Contemporary Christian',
|
||||
'Christian Rock',
|
||||
'Merengue',
|
||||
'Salsa',
|
||||
'Thrash Metal',
|
||||
'Anime',
|
||||
'JPop',
|
||||
'Synthpop',
|
||||
]
|
||||
|
||||
pub fn (genre Genre) str() string{
|
||||
pub fn (genre Genre) str() string {
|
||||
match genre {
|
||||
.unknown {
|
||||
return "Unknown"
|
||||
} else {
|
||||
return 'Unknown'
|
||||
}
|
||||
else {
|
||||
return genres_name[u8(genre)]
|
||||
}
|
||||
}
|
||||
|
||||
+15
-15
@@ -4,11 +4,10 @@ import os { File }
|
||||
import encoding.iconv
|
||||
import utils
|
||||
|
||||
|
||||
// Определить кодировку utf-8 или cp-1251
|
||||
@[inline]
|
||||
fn decode_bytes(bytes []u8) string {
|
||||
format := if utils.is_utf8(bytes) { "UTF-8" } else { "CP1251" }
|
||||
format := if utils.is_utf8(bytes) { 'UTF-8' } else { 'CP1251' }
|
||||
text := (iconv.encoding_to_vstring(bytes, format) or { "Can't decode" }).all_before('\x00')
|
||||
|
||||
return text
|
||||
@@ -22,10 +21,11 @@ pub fn (mut tag ID3v1) get_metadata(last_bytes []u8) {
|
||||
tag.album = decode_bytes(last_bytes[63..92])
|
||||
tag.year = decode_bytes(last_bytes[93..97])
|
||||
tag.comment = decode_bytes(last_bytes[97..125])
|
||||
tag.track = if last_bytes[125] == u8(0) {last_bytes[126].str()} else { "" }
|
||||
tag.track = if last_bytes[125] == u8(0) { last_bytes[126].str() } else { '' }
|
||||
|
||||
genre := Genre.from(last_bytes[127..128][0]) or { Genre.unknown }
|
||||
tag.genre = genre.str()}
|
||||
tag.genre = genre.str()
|
||||
}
|
||||
|
||||
// Конвертировать ID3 v2.3 в ID3 v1
|
||||
pub fn (id3v1 ID3v1) to_v23() ID3v23 {
|
||||
@@ -60,7 +60,7 @@ fn cut_or_trim(data string, capacity i8) []u8 {
|
||||
mut final_data := []u8{len: capacity, init: 0}
|
||||
|
||||
if data.len > 0 {
|
||||
bytes_data := iconv.vstring_to_encoding(data, "CP1251") or { []u8{len: capacity} }
|
||||
bytes_data := iconv.vstring_to_encoding(data, 'CP1251') or { []u8{len: capacity} }
|
||||
|
||||
diff := capacity - bytes_data.len
|
||||
if diff > 0 {
|
||||
@@ -79,7 +79,7 @@ fn cut_or_trim(data string, capacity i8) []u8 {
|
||||
pub fn (mut id3v1 ID3v1) to_bytes() []u8 {
|
||||
mut bytes := []u8{cap: 128}
|
||||
|
||||
bytes << "TAG".bytes()
|
||||
bytes << 'TAG'.bytes()
|
||||
bytes << cut_or_trim(id3v1.title, i8(30))
|
||||
bytes << cut_or_trim(id3v1.artist, i8(30))
|
||||
bytes << cut_or_trim(id3v1.album, i8(30))
|
||||
@@ -87,7 +87,7 @@ pub fn (mut id3v1 ID3v1) to_bytes() []u8 {
|
||||
bytes << cut_or_trim(id3v1.comment, i8(28))
|
||||
|
||||
// bytes << cut_or_trim(id3v1.track, i8(2))
|
||||
bytes << if id3v1.track.len != 0 { [u8(0), id3v1.track.u8()]} else { [u8(1), u8(0)] }
|
||||
bytes << if id3v1.track.len != 0 { [u8(0), id3v1.track.u8()] } else { [u8(1), u8(0)] }
|
||||
|
||||
id_genre := Genre.from(id3v1.genre.to_lower()) or { Genre.unknown }
|
||||
bytes << u8(id_genre) // cut_or_trim([u8(id_genre)], i8(2))
|
||||
@@ -100,15 +100,15 @@ pub fn (mut id3v1 ID3v1) to_bytes() []u8 {
|
||||
// Добавление изменённого тэга
|
||||
pub fn (mut id3v1 ID3v1) insert(mut audio File, position i8) []u8 {
|
||||
// if !id3v1.embedded {
|
||||
bytes_tag := id3v1.to_bytes()
|
||||
// Уcтановка указателя на последний байт
|
||||
audio.seek(0, os.SeekMode.end) or { panic(err) }
|
||||
// Положение указателя
|
||||
size := audio.tell() or { panic(err) }
|
||||
// Чтение всех байта, кроме последних 128
|
||||
mut all_bytes := audio.read_bytes(int(size))#[..position].clone()
|
||||
bytes_tag := id3v1.to_bytes()
|
||||
// Уcтановка указателя на последний байт
|
||||
audio.seek(0, os.SeekMode.end) or { panic(err) }
|
||||
// Положение указателя
|
||||
size := audio.tell() or { panic(err) }
|
||||
// Чтение всех байта, кроме последних 128
|
||||
mut all_bytes := audio.read_bytes(int(size))#[..position].clone()
|
||||
|
||||
all_bytes << bytes_tag
|
||||
all_bytes << bytes_tag
|
||||
// }
|
||||
|
||||
return all_bytes
|
||||
|
||||
+6
-8
@@ -27,7 +27,6 @@ import os { File }
|
||||
|
||||
const start_idx = 10
|
||||
|
||||
|
||||
pub fn (mut tag ID3v23) add_frame(frame Frame) {
|
||||
name := EnumID3v23.from(frame.name.to_lower()) or { EnumID3v23.unknown }
|
||||
|
||||
@@ -84,7 +83,6 @@ pub fn (mut id3v23 ID3v23) encode_embed_tags() {
|
||||
encode_embed_tags[ID3v23](mut id3v23)
|
||||
}
|
||||
|
||||
|
||||
// Заполнение тэга
|
||||
pub fn (mut id3v23 ID3v23) fill_tags(mut audio File) {
|
||||
dump_data[ID3v23](mut id3v23, mut audio)
|
||||
@@ -93,12 +91,12 @@ pub fn (mut id3v23 ID3v23) fill_tags(mut audio File) {
|
||||
// Конвертировать ID3 v2.3 в ID3 v1
|
||||
pub fn (id3v23 ID3v23) to_v1() ID3v1 {
|
||||
return ID3v1{
|
||||
genre: id3v23.tcon.data
|
||||
title: id3v23.tit2.data
|
||||
artist: id3v23.tpe2.data
|
||||
album: id3v23.talb.data
|
||||
track: id3v23.trck.data.split("/")[0]
|
||||
year: id3v23.tyer.data
|
||||
genre: id3v23.tcon.data
|
||||
title: id3v23.tit2.data
|
||||
artist: id3v23.tpe2.data
|
||||
album: id3v23.talb.data
|
||||
track: id3v23.trck.data.split('/')[0]
|
||||
year: id3v23.tyer.data
|
||||
comment: id3v23.txxx.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ module metadata
|
||||
|
||||
import os { File }
|
||||
|
||||
|
||||
pub fn (mut tag ID3v24) add_frame(frame Frame) {
|
||||
name := EnumID3v24.from(frame.name.to_lower()) or { EnumID3v24.unknown }
|
||||
|
||||
@@ -43,7 +42,6 @@ pub fn (mut tag ID3v24) add_frame(frame Frame) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@[inline]
|
||||
fn get_size_v24(size_bytes []u8) u64 {
|
||||
return (u64(size_bytes[0]) << 21) | (u64(size_bytes[1]) << 14) | (u64(size_bytes[2]) << 7) | u64(size_bytes[3])
|
||||
@@ -77,7 +75,6 @@ pub fn (id3v24 ID3v24) to_v1() ID3v1 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Добавление аудио после нового тэга
|
||||
pub fn (mut id3v24 ID3v24) insert_tags_into_audio(mut audio File) ! {
|
||||
insert_tags_into_audio[ID3v24](mut id3v24, mut audio)!
|
||||
|
||||
+90
-93
@@ -1,109 +1,106 @@
|
||||
module metadata
|
||||
|
||||
|
||||
@[heap; minify]
|
||||
pub struct ID3v1 {
|
||||
pub mut:
|
||||
// Жанр
|
||||
genre string
|
||||
// Название аудиодорожки
|
||||
title string
|
||||
// Актёр озучания
|
||||
artist string
|
||||
// Название альбома
|
||||
album string
|
||||
// Записывается как 2/5, что значит 2 трэк из 5
|
||||
track string
|
||||
// Год записи, может int нужно
|
||||
year string
|
||||
// Комментарий
|
||||
comment string
|
||||
// Присутвует ли тэг
|
||||
embedded bool
|
||||
pub mut:
|
||||
// Жанр
|
||||
genre string
|
||||
// Название аудиодорожки
|
||||
title string
|
||||
// Актёр озучания
|
||||
artist string
|
||||
// Название альбома
|
||||
album string
|
||||
// Записывается как 2/5, что значит 2 трэк из 5
|
||||
track string
|
||||
// Год записи, может int нужно
|
||||
year string
|
||||
// Комментарий
|
||||
comment string
|
||||
// Присутвует ли тэг
|
||||
embedded bool
|
||||
}
|
||||
|
||||
@[heap; minify]
|
||||
pub struct ID3v23 {
|
||||
pub mut:
|
||||
// Размер тэга
|
||||
size u64
|
||||
// Название аудиодорожки
|
||||
tit2 Frame
|
||||
// Автор
|
||||
tpe1 Frame
|
||||
// Исполнитель
|
||||
tpe2 Frame
|
||||
// Название альбома
|
||||
talb Frame
|
||||
// Год выпуска
|
||||
tyer Frame
|
||||
// Номер трека
|
||||
trck Frame
|
||||
// Жанр
|
||||
tcon Frame
|
||||
// Композитор (автор)
|
||||
tcop Frame
|
||||
// Обложка
|
||||
apic Frame
|
||||
// Комментарий
|
||||
txxx Frame
|
||||
// Затычка
|
||||
unknown Frame
|
||||
// Остальные фреймы
|
||||
others []Frame
|
||||
encoded_header []u8
|
||||
// Присутвует ли тэг
|
||||
embedded bool
|
||||
// Версия тэга
|
||||
version u8 = 3
|
||||
title string = "ID3"
|
||||
pub mut:
|
||||
// Размер тэга
|
||||
size u64
|
||||
// Название аудиодорожки
|
||||
tit2 Frame
|
||||
// Автор
|
||||
tpe1 Frame
|
||||
// Исполнитель
|
||||
tpe2 Frame
|
||||
// Название альбома
|
||||
talb Frame
|
||||
// Год выпуска
|
||||
tyer Frame
|
||||
// Номер трека
|
||||
trck Frame
|
||||
// Жанр
|
||||
tcon Frame
|
||||
// Композитор (автор)
|
||||
tcop Frame
|
||||
// Обложка
|
||||
apic Frame
|
||||
// Комментарий
|
||||
txxx Frame
|
||||
// Затычка
|
||||
unknown Frame
|
||||
// Остальные фреймы
|
||||
others []Frame
|
||||
encoded_header []u8
|
||||
// Присутвует ли тэг
|
||||
embedded bool
|
||||
// Версия тэга
|
||||
version u8 = 3
|
||||
title string = 'ID3'
|
||||
}
|
||||
|
||||
@[heap; minify]
|
||||
pub struct ID3v24 {
|
||||
pub mut:
|
||||
// Размер тэга
|
||||
size u64
|
||||
// Название аудиодорожки
|
||||
tit2 Frame
|
||||
// Автор
|
||||
tpe1 Frame
|
||||
// Исполнитель
|
||||
tpe2 Frame
|
||||
// Название альбома
|
||||
talb Frame
|
||||
// Год выпуска
|
||||
tdrc Frame
|
||||
// Номер трека
|
||||
trck Frame
|
||||
// Жанр
|
||||
tcon Frame
|
||||
// Композитор (автор)
|
||||
tcop Frame
|
||||
// Обложка
|
||||
apic Frame
|
||||
// Комментарий
|
||||
txxx Frame
|
||||
// Затычка
|
||||
modified_frames []Frame
|
||||
// Остальные фреймы
|
||||
others []Frame
|
||||
// Версия тэга
|
||||
version u8 = 4
|
||||
// Присутвует ли тэг
|
||||
embedded bool
|
||||
// Закодированный хэдер
|
||||
encoded_header []u8
|
||||
title string = "ID3"
|
||||
pub mut:
|
||||
// Размер тэга
|
||||
size u64
|
||||
// Название аудиодорожки
|
||||
tit2 Frame
|
||||
// Автор
|
||||
tpe1 Frame
|
||||
// Исполнитель
|
||||
tpe2 Frame
|
||||
// Название альбома
|
||||
talb Frame
|
||||
// Год выпуска
|
||||
tdrc Frame
|
||||
// Номер трека
|
||||
trck Frame
|
||||
// Жанр
|
||||
tcon Frame
|
||||
// Композитор (автор)
|
||||
tcop Frame
|
||||
// Обложка
|
||||
apic Frame
|
||||
// Комментарий
|
||||
txxx Frame
|
||||
// Затычка
|
||||
modified_frames []Frame
|
||||
// Остальные фреймы
|
||||
others []Frame
|
||||
// Версия тэга
|
||||
version u8 = 4
|
||||
// Присутвует ли тэг
|
||||
embedded bool
|
||||
// Закодированный хэдер
|
||||
encoded_header []u8
|
||||
title string = 'ID3'
|
||||
}
|
||||
|
||||
|
||||
pub struct Audio {
|
||||
pub mut:
|
||||
id3v1 ID3v1
|
||||
id3v23 ID3v23
|
||||
id3v24 ID3v24
|
||||
path string
|
||||
extension string = ".mp3"
|
||||
pub mut:
|
||||
id3v1 ID3v1
|
||||
id3v23 ID3v23
|
||||
id3v24 ID3v24
|
||||
path string
|
||||
extension string = '.mp3'
|
||||
}
|
||||
|
||||
+7
-8
@@ -2,12 +2,11 @@ module metadata
|
||||
|
||||
import os
|
||||
|
||||
|
||||
pub fn (mut audio Audio) detect_tags(mut data os.File) {
|
||||
// ? --- Проверка версии ID3v1 ---
|
||||
last_bytes := get_bytes(mut data, -128, 3)
|
||||
|
||||
if last_bytes[0..3].bytestr() == "TAG" {
|
||||
if last_bytes[0..3].bytestr() == 'TAG' {
|
||||
audio.id3v1.embedded = true
|
||||
}
|
||||
// ===============================
|
||||
@@ -15,7 +14,7 @@ pub fn (mut audio Audio) detect_tags(mut data os.File) {
|
||||
// ? --- Проверка версии ID3v23.3 ---
|
||||
first_ten_bytes := get_bytes(mut data, 0, 4)
|
||||
|
||||
if first_ten_bytes[..3].bytestr() == "ID3" {
|
||||
if first_ten_bytes[..3].bytestr() == 'ID3' {
|
||||
if first_ten_bytes[3] == 3 {
|
||||
audio.id3v23.embedded = true
|
||||
} else if first_ten_bytes[3] == 4 {
|
||||
@@ -110,7 +109,7 @@ pub fn encode_embed_tags[T](mut id3v2 T) {
|
||||
// Читаются только заполненные тэги
|
||||
if id3v2.$(field.name).cur_size > 0 {
|
||||
// Фрейма не было в файле и init не вызывался: имя берётся из поля структуры
|
||||
if id3v2.$(field.name).name == "" && field.name.len == 4 {
|
||||
if id3v2.$(field.name).name == '' && field.name.len == 4 {
|
||||
id3v2.$(field.name).init(field.name, id3v2.version)
|
||||
}
|
||||
|
||||
@@ -140,8 +139,8 @@ pub fn encode_embed_tags[T](mut id3v2 T) {
|
||||
// Добавляет аудио из файла после нового тэга в encoded_header, старый тэг пропускается
|
||||
pub fn insert_tags_into_audio[T](mut id3v2 T, mut audio os.File) ! {
|
||||
header := get_bytes(mut audio, 0, 10)
|
||||
if header.len < 10 || header[..3].bytestr() != "ID3" {
|
||||
return error("в файле нет тэга ID3v2")
|
||||
if header.len < 10 || header[..3].bytestr() != 'ID3' {
|
||||
return error('в файле нет тэга ID3v2')
|
||||
}
|
||||
|
||||
// Размер старого тэга берётся из файла, id3v2.size уже хранит размер нового.
|
||||
@@ -155,7 +154,7 @@ pub fn insert_tags_into_audio[T](mut id3v2 T, mut audio os.File) ! {
|
||||
audio.seek(0, .end)!
|
||||
file_size := u64(audio.tell()!)
|
||||
if audio_start > file_size {
|
||||
return error("размер тэга (${audio_start}) больше размера файла (${file_size})")
|
||||
return error('размер тэга (${audio_start}) больше размера файла (${file_size})')
|
||||
}
|
||||
|
||||
// Аудио читается сразу в конец encoded_header, без промежуточной копии всего файла
|
||||
@@ -168,7 +167,7 @@ pub fn insert_tags_into_audio[T](mut id3v2 T, mut audio os.File) ! {
|
||||
for read < sound_len {
|
||||
n := unsafe { audio.read_into_ptr(&u8(id3v2.encoded_header.data) + header_len + read, sound_len - read)! }
|
||||
if n == 0 {
|
||||
return error("файл закончился раньше ожидаемого")
|
||||
return error('файл закончился раньше ожидаемого')
|
||||
}
|
||||
read += n
|
||||
}
|
||||
|
||||
@@ -82,6 +82,74 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
// Замена обложки: в памяти только новый тэг — старая обложка не читается, новая копируется один раз
|
||||
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 тэг добавляется в начало
|
||||
fn test_write_without_id3v2() {
|
||||
path := tmp_copy('test_id3v1.mp3')
|
||||
|
||||
+8
-9
@@ -3,7 +3,7 @@ import metadata
|
||||
|
||||
// Проверка чтения id3v1
|
||||
fn test_idv3v1() {
|
||||
mut audio := os.open("./data/test_id3v1.mp3") or { panic(err) }
|
||||
mut audio := os.open('./data/test_id3v1.mp3') or { panic(err) }
|
||||
defer {
|
||||
audio.close()
|
||||
}
|
||||
@@ -17,13 +17,12 @@ fn test_idv3v1() {
|
||||
last_bytes := metadata.get_bytes(mut audio, -128, 128)
|
||||
wav.id3v1.get_metadata(last_bytes)
|
||||
|
||||
assert wav.id3v1.genre == "Rock"
|
||||
assert wav.id3v1.title == "Test Title"
|
||||
assert wav.id3v1.artist == "Test Artist"
|
||||
assert wav.id3v1.album == "Test Album"
|
||||
assert wav.id3v1.track == "6"
|
||||
assert wav.id3v1.year == "2026"
|
||||
assert wav.id3v1.comment == "Test comment"
|
||||
assert wav.id3v1.genre == 'Rock'
|
||||
assert wav.id3v1.title == 'Test Title'
|
||||
assert wav.id3v1.artist == 'Test Artist'
|
||||
assert wav.id3v1.album == 'Test Album'
|
||||
assert wav.id3v1.track == '6'
|
||||
assert wav.id3v1.year == '2026'
|
||||
assert wav.id3v1.comment == 'Test comment'
|
||||
assert wav.id3v1.embedded == true
|
||||
}
|
||||
|
||||
+70
-70
@@ -3,7 +3,7 @@ import metadata
|
||||
|
||||
// Проверка чтения id3v23
|
||||
fn test_idv3v23() {
|
||||
mut audio := os.open("./data/test_id3v23.mp3") or { panic(err) }
|
||||
mut audio := os.open('./data/test_id3v23.mp3') or { panic(err) }
|
||||
defer {
|
||||
audio.close()
|
||||
}
|
||||
@@ -16,29 +16,29 @@ fn test_idv3v23() {
|
||||
wav.id3v23.get_full_size(mut audio)
|
||||
wav.id3v23.fill_tags(mut audio)
|
||||
|
||||
assert wav.id3v23.tit2.data == "Test Title"
|
||||
assert wav.id3v23.tpe1.data == "Test Author"
|
||||
assert wav.id3v23.tpe2.data == "Test Narator"
|
||||
assert wav.id3v23.talb.data == "Test Album"
|
||||
assert wav.id3v23.tyer.data == "2026"
|
||||
assert wav.id3v23.trck.data == "6/7"
|
||||
assert wav.id3v23.tcon.data == "Rock"
|
||||
assert wav.id3v23.tcop.data == "Test"
|
||||
assert wav.id3v23.txxx.data == "comment"
|
||||
assert wav.id3v23.tit2.data == 'Test Title'
|
||||
assert wav.id3v23.tpe1.data == 'Test Author'
|
||||
assert wav.id3v23.tpe2.data == 'Test Narator'
|
||||
assert wav.id3v23.talb.data == 'Test Album'
|
||||
assert wav.id3v23.tyer.data == '2026'
|
||||
assert wav.id3v23.trck.data == '6/7'
|
||||
assert wav.id3v23.tcon.data == 'Rock'
|
||||
assert wav.id3v23.tcop.data == 'Test'
|
||||
assert wav.id3v23.txxx.data == 'comment'
|
||||
|
||||
id3v1 := wav.id3v23.to_v1()
|
||||
|
||||
assert id3v1.genre == "Rock"
|
||||
assert id3v1.title == "Test Title"
|
||||
assert id3v1.artist == "Test Narator"
|
||||
assert id3v1.album == "Test Album"
|
||||
assert id3v1.track == "6"
|
||||
assert id3v1.year == "2026"
|
||||
assert id3v1.comment == "comment"
|
||||
assert id3v1.genre == 'Rock'
|
||||
assert id3v1.title == 'Test Title'
|
||||
assert id3v1.artist == 'Test Narator'
|
||||
assert id3v1.album == 'Test Album'
|
||||
assert id3v1.track == '6'
|
||||
assert id3v1.year == '2026'
|
||||
assert id3v1.comment == 'comment'
|
||||
}
|
||||
|
||||
fn test_writeidv3v23() {
|
||||
mut audio := os.open("./data/test_id3v23.mp3") or { panic(err) }
|
||||
mut audio := os.open('./data/test_id3v23.mp3') or { panic(err) }
|
||||
defer {
|
||||
audio.close()
|
||||
}
|
||||
@@ -52,23 +52,23 @@ fn test_writeidv3v23() {
|
||||
wav.id3v23.fill_tags(mut audio)
|
||||
|
||||
// Изменение значений тэга
|
||||
wav.id3v23.tit2.set_data("Test Title2")
|
||||
wav.id3v23.tpe1.set_data("Test Author2")
|
||||
wav.id3v23.tpe2.set_data("Test Narator2")
|
||||
wav.id3v23.talb.set_data("Test Album2")
|
||||
wav.id3v23.tyer.set_data("2027")
|
||||
wav.id3v23.trck.set_data("7/7")
|
||||
wav.id3v23.tcon.set_data("Rock2")
|
||||
wav.id3v23.tcop.set_data("Test2")
|
||||
wav.id3v23.txxx.set_data("comment2")
|
||||
wav.id3v23.tit2.set_data('Test Title2')
|
||||
wav.id3v23.tpe1.set_data('Test Author2')
|
||||
wav.id3v23.tpe2.set_data('Test Narator2')
|
||||
wav.id3v23.talb.set_data('Test Album2')
|
||||
wav.id3v23.tyer.set_data('2027')
|
||||
wav.id3v23.trck.set_data('7/7')
|
||||
wav.id3v23.tcon.set_data('Rock2')
|
||||
wav.id3v23.tcop.set_data('Test2')
|
||||
wav.id3v23.txxx.set_data('comment2')
|
||||
|
||||
wav.id3v23.encode_embed_tags()
|
||||
wav.id3v23.insert_tags_into_audio(mut audio) or { panic(err) }
|
||||
|
||||
os.write_bytes("./data/temp_v23.mp3", wav.id3v23.encoded_header) or { panic(err) }
|
||||
os.write_bytes('./data/temp_v23.mp3', wav.id3v23.encoded_header) or { panic(err) }
|
||||
|
||||
// Чтение файла и проверка записи
|
||||
mut temp_audio := os.open("./data/temp_v23.mp3") or { panic(err) }
|
||||
mut temp_audio := os.open('./data/temp_v23.mp3') or { panic(err) }
|
||||
defer {
|
||||
temp_audio.close()
|
||||
}
|
||||
@@ -82,31 +82,31 @@ fn test_writeidv3v23() {
|
||||
temp_wav.id3v23.get_full_size(mut temp_audio)
|
||||
temp_wav.id3v23.fill_tags(mut temp_audio)
|
||||
|
||||
assert temp_wav.id3v23.tit2.data == "Test Title2"
|
||||
assert temp_wav.id3v23.tpe1.data == "Test Author2"
|
||||
assert temp_wav.id3v23.tpe2.data == "Test Narator2"
|
||||
assert temp_wav.id3v23.talb.data == "Test Album2"
|
||||
assert temp_wav.id3v23.tyer.data == "2027"
|
||||
assert temp_wav.id3v23.trck.data == "7/7"
|
||||
assert temp_wav.id3v23.tcon.data == "Rock2"
|
||||
assert temp_wav.id3v23.tcop.data == "Test2"
|
||||
assert temp_wav.id3v23.txxx.data == "comment2"
|
||||
assert temp_wav.id3v23.tit2.data == 'Test Title2'
|
||||
assert temp_wav.id3v23.tpe1.data == 'Test Author2'
|
||||
assert temp_wav.id3v23.tpe2.data == 'Test Narator2'
|
||||
assert temp_wav.id3v23.talb.data == 'Test Album2'
|
||||
assert temp_wav.id3v23.tyer.data == '2027'
|
||||
assert temp_wav.id3v23.trck.data == '7/7'
|
||||
assert temp_wav.id3v23.tcon.data == 'Rock2'
|
||||
assert temp_wav.id3v23.tcop.data == 'Test2'
|
||||
assert temp_wav.id3v23.txxx.data == 'comment2'
|
||||
|
||||
// Проверяется прочитанный с диска тэг, а не тот, что в памяти
|
||||
id3v1 := temp_wav.id3v23.to_v1()
|
||||
|
||||
assert id3v1.genre == "Rock2"
|
||||
assert id3v1.title == "Test Title2"
|
||||
assert id3v1.artist == "Test Narator2"
|
||||
assert id3v1.album == "Test Album2"
|
||||
assert id3v1.track == "7"
|
||||
assert id3v1.year == "2027"
|
||||
assert id3v1.comment == "comment2"
|
||||
assert id3v1.genre == 'Rock2'
|
||||
assert id3v1.title == 'Test Title2'
|
||||
assert id3v1.artist == 'Test Narator2'
|
||||
assert id3v1.album == 'Test Album2'
|
||||
assert id3v1.track == '7'
|
||||
assert id3v1.year == '2027'
|
||||
assert id3v1.comment == 'comment2'
|
||||
}
|
||||
|
||||
// Проверка чтения id3v24
|
||||
fn test_idv3v24() {
|
||||
mut audio := os.open("./data/test_id3v24.mp3") or { panic(err) }
|
||||
mut audio := os.open('./data/test_id3v24.mp3') or { panic(err) }
|
||||
defer {
|
||||
audio.close()
|
||||
}
|
||||
@@ -119,30 +119,30 @@ fn test_idv3v24() {
|
||||
wav.id3v24.get_full_size(mut audio)
|
||||
wav.id3v24.fill_tags(mut audio)
|
||||
|
||||
assert wav.id3v24.tit2.data == "Test Title"
|
||||
assert wav.id3v24.tpe1.data == "Test Author"
|
||||
assert wav.id3v24.tpe2.data == "Test Narator"
|
||||
assert wav.id3v24.talb.data == "Test Album"
|
||||
assert wav.id3v24.tdrc.data == "2026"
|
||||
assert wav.id3v24.trck.data == "6/7"
|
||||
assert wav.id3v24.tcon.data == "Rock"
|
||||
assert wav.id3v24.tcop.data == "Test"
|
||||
assert wav.id3v24.txxx.data == "comment"
|
||||
assert wav.id3v24.tit2.data == 'Test Title'
|
||||
assert wav.id3v24.tpe1.data == 'Test Author'
|
||||
assert wav.id3v24.tpe2.data == 'Test Narator'
|
||||
assert wav.id3v24.talb.data == 'Test Album'
|
||||
assert wav.id3v24.tdrc.data == '2026'
|
||||
assert wav.id3v24.trck.data == '6/7'
|
||||
assert wav.id3v24.tcon.data == 'Rock'
|
||||
assert wav.id3v24.tcop.data == 'Test'
|
||||
assert wav.id3v24.txxx.data == 'comment'
|
||||
|
||||
id3v1 := wav.id3v24.to_v1()
|
||||
|
||||
assert id3v1.genre == "Rock"
|
||||
assert id3v1.title == "Test Title"
|
||||
assert id3v1.artist == "Test Narator"
|
||||
assert id3v1.album == "Test Album"
|
||||
assert id3v1.track == "6"
|
||||
assert id3v1.year == "2026"
|
||||
assert id3v1.comment == "comment"
|
||||
assert id3v1.genre == 'Rock'
|
||||
assert id3v1.title == 'Test Title'
|
||||
assert id3v1.artist == 'Test Narator'
|
||||
assert id3v1.album == 'Test Album'
|
||||
assert id3v1.track == '6'
|
||||
assert id3v1.year == '2026'
|
||||
assert id3v1.comment == 'comment'
|
||||
}
|
||||
|
||||
// Кириллица во фрейме с кодировкой ISO-8859-1 не должна превращаться в '?'
|
||||
fn test_write_unicode_into_latin1_frame() {
|
||||
mut audio := os.open("./data/test_id3v23.mp3") or { panic(err) }
|
||||
mut audio := os.open('./data/test_id3v23.mp3') or { panic(err) }
|
||||
defer {
|
||||
audio.close()
|
||||
}
|
||||
@@ -152,11 +152,11 @@ fn test_write_unicode_into_latin1_frame() {
|
||||
wav.id3v23.fill_tags(mut audio)
|
||||
old_size := int(wav.id3v23.size)
|
||||
|
||||
wav.id3v23.tit2.set_data("Глава 2")
|
||||
wav.id3v23.tit2.set_data('Глава 2')
|
||||
wav.id3v23.encode_embed_tags()
|
||||
wav.id3v23.insert_tags_into_audio(mut audio) or { panic(err) }
|
||||
|
||||
temp_path := os.join_path(os.temp_dir(), "id3_editor_unicode_test.mp3")
|
||||
temp_path := os.join_path(os.temp_dir(), 'id3_editor_unicode_test.mp3')
|
||||
os.write_bytes(temp_path, wav.id3v23.encoded_header) or { panic(err) }
|
||||
defer {
|
||||
os.rm(temp_path) or {}
|
||||
@@ -171,19 +171,19 @@ fn test_write_unicode_into_latin1_frame() {
|
||||
temp_wav.id3v23.get_full_size(mut temp_audio)
|
||||
temp_wav.id3v23.fill_tags(mut temp_audio)
|
||||
|
||||
assert temp_wav.id3v23.tit2.data == "Глава 2"
|
||||
assert temp_wav.id3v23.tit2.data == 'Глава 2'
|
||||
// Нетронутые фреймы и аудио сохраняются без изменений
|
||||
assert temp_wav.id3v23.tpe1.data == "Test Author"
|
||||
assert temp_wav.id3v23.tpe1.data == 'Test Author'
|
||||
assert temp_wav.id3v23.others.len == 1
|
||||
|
||||
original := os.read_bytes("./data/test_id3v23.mp3") or { panic(err) }
|
||||
original := os.read_bytes('./data/test_id3v23.mp3') or { panic(err) }
|
||||
written := os.read_bytes(temp_path) or { panic(err) }
|
||||
assert written[int(temp_wav.id3v23.size) + 10..] == original[old_size + 10..]
|
||||
}
|
||||
|
||||
// Вставка в файл без тэга ID3v2 возвращает ошибку, а не падает
|
||||
fn test_insert_without_id3v2() {
|
||||
mut audio := os.open("./data/test_id3v1.mp3") or { panic(err) }
|
||||
mut audio := os.open('./data/test_id3v1.mp3') or { panic(err) }
|
||||
defer {
|
||||
audio.close()
|
||||
}
|
||||
@@ -191,7 +191,7 @@ fn test_insert_without_id3v2() {
|
||||
wav.id3v23.encode_embed_tags()
|
||||
|
||||
wav.id3v23.insert_tags_into_audio(mut audio) or {
|
||||
assert err.msg() == "в файле нет тэга ID3v2"
|
||||
assert err.msg() == 'в файле нет тэга ID3v2'
|
||||
return
|
||||
}
|
||||
assert false, 'Ошибка не возвращена'
|
||||
|
||||
Reference in new issue
Block a user