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