первый коммит

This commit is contained in:
K1ndWha1e committed 2026-07-11 20:46:19 +03:00
commit 81034325a1
13 files changed
+810

No files matched your search

+72
View File
@@ -0,0 +1,72 @@
module metadata
import encoding.iconv
@[heap]
pub struct Frame {
mut:
size u64
start u64
end u64
apic ?[]u8
coded_size ?[]u64
addition []u8
encoding string
text string
name string
}
// Во врейме первый байт указывает кодировку 0 - ISO-8859-1 (Latin-1), 1 - UTF-16LE
@[inline]
fn (mut frame Frame) fill_frame(bytes []u8, start_position u64, idv3_version u8) {
// Начало фрейма
frame.start = start_position
// Название фрейма
frame.name = bytes[frame.start..frame.start + 4].bytestr()
// Размер фрейма
frame.size = if idv3_version == 3 {
get_size_v23(bytes[frame.start+4..frame.start+8])
} else {
get_size_v24(bytes[frame.start+4..frame.start+8])
}
// Конец фрейма
frame.end = frame.start + frame.size + 10
// Содержимое фрейма
frame.encoding = match bytes[frame.start+10] {
0 { "ISO-8859-1" }
1 { "UTF-16LE" }
2 { "UTF-16BE" }
else { "UTF-8" }
}
if frame.name.to_lower() == "apic" {
// Дополнительные байты с информацией (MIME)
frame.addition = bytes[frame.start+10..frame.start+24]
frame.apic = bytes[frame.start+24..frame.end]
} else {
// Выбор шага в записимости от версии
stride := if idv3_version == 3 { 13 } else { 11 }
frame.text = iconv.encoding_to_vstring(bytes[frame.start+u64(stride)..frame.end], frame.encoding) or { "" }
frame.addition = bytes[frame.start..frame.start + 10]
}
}
// Кодируем длину фрейма с нужным сдвигом
@[inline]
pub fn (mut frame Frame) encode_size(idv3_version u8) []u64 {
stride := if idv3_version == 3 {
[u64(24), u64(16), u64(8), u64(0xFF)]
} else {
[u64(21), u64(14), u64(7), u64(0x7F)]
}
return [
(frame.size >> stride[0]) & stride[3],
(frame.size >> stride[1]) & stride[3],
(frame.size >> stride[2]) & stride[3],
frame.size & stride[3]
]
}
+197
View File
@@ -0,0 +1,197 @@
module metadata
@[heap]
enum Genre as u8 {
blues = 0
classic_rock = 1
country = 2
dance = 3
disco = 4
funk = 5
grunge = 6
hip_hop = 7
jazz = 8
metal = 9
new_age = 10
oldies = 11
other = 12
pop = 13
r_and_b = 14
rap = 15
reggae = 16
rock = 17
techno = 18
industrial = 19
alternative = 20
ska = 21
death_metal = 22
pranks = 23
soundtrack = 24
euro_techno = 25
ambient = 26
trip_hop = 27
vocal = 28
jazz_funk = 29
fusion = 30
trance = 31
classical = 32
instrumental = 33
acid = 34
house = 35
game = 36
sound_clip = 37
gospel = 38
noise = 39
alternrock = 40
bass = 41
soul = 42
punk = 43
space = 44
meditative = 45
instrumental_pop = 46
instrumental_rock = 47
ethnic = 48
gothic = 49
darkwave = 50
techno_industrial = 51
electronic = 52
pop_folk = 53
eurodance = 54
dream = 55
southern_rock = 56
comedy = 57
cult = 58
gangsta = 59
top_40 = 60
christian_rap = 61
pop_funk = 62
jungle = 63
native_american = 64
cabaret = 65
new_wave = 66
psychadelic = 67
rave = 68
showtunes = 69
trailer = 70
lo_fi = 71
tribal = 72
acid_punk = 73
acid_jazz = 74
polka = 75
retro = 76
musical = 77
rock_and_roll = 78
hard_rock = 79
// Winamp Extended Genres
folk = 80
folk_rock = 81
national_folk = 82
swing = 83
fast_fusion = 84
bebob = 85
latin = 86
revival = 87
celtic = 88
bluegrass = 89
avantgarde = 90
gothic_rock = 91
progressive_rock = 92
psychedelic_rock = 93
symphonic_rock = 94
slow_rock = 95
big_band = 96
chorus = 97
easy_listening = 98
acoustic = 99
humour = 100
speech = 101
chanson = 102
opera = 103
chamber_music = 104
sonata = 105
symphony = 106
booty_bass = 107
primus = 108
porn_groove = 109
satire = 110
slow_jam = 111
club = 112
tango = 113
samba = 114
folklore = 115
ballad = 116
power_ballad = 117
rhythmic_soul = 118
freestyle = 119
duet = 120
punk_ballad = 121
drum_solo = 122
a_cappella = 123
euro_house = 124
dance_hall = 125
goa = 126
drum_and_bass = 127
club_house = 128
hardcore = 129
terror = 130
indie = 131
brit_pop = 132
negerpunk = 133
polsk_punk = 134
beat = 135
christian_gangsta = 136
heavy_metal = 137
black_metal = 138
crossover = 139
contemporary_c = 140
christian_rock = 141
merengue = 142
salsa = 143
thrash_metal = 144
anime = 145
jpop = 146
synthpop = 147
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',
'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'
]
pub fn (genre Genre) str() string{
match genre {
.unknown {
return "Unknown"
} else {
return genres_name[u8(genre)]
}
}
}
+110
View File
@@ -0,0 +1,110 @@
module metadata
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" }
text := iconv.encoding_to_vstring(bytes, format) or { "Can't decode" }
return text
}
// Извлечение метаданных из ID3 v1
pub fn (mut tag ID3v1) get_metadata(last_bytes []u8) {
tag.title = decode_bytes(last_bytes[3..33])
tag.artist = decode_bytes(last_bytes[33..63])
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 { "" }
genre := Genre.from(last_bytes[127..128][0]) or { Genre.unknown }
tag.genre = genre.str()}
// Конвертировать ID3 v2.3 в ID3 v1
pub fn (id3v1 ID3v1) to_v23() ID3v23 {
return ID3v23{
tcon: metadata.Frame{
text: id3v1.genre
}
tit2: metadata.Frame{
text: id3v1.title
}
tpe2: metadata.Frame{
text: id3v1.artist
}
talb: metadata.Frame{
text: id3v1.album
}
trck: metadata.Frame{
text: id3v1.track
}
tyer: metadata.Frame{
text: id3v1.year
}
comment: metadata.Frame{
text: id3v1.comment
}
}
}
// По ситуации отрезаем или добиваем нулями
@[inline]
fn cut_or_trim(data string, capacity i8) []u8 {
mut final_data := []u8{len: capacity, init: 0}
bytes_data := iconv.vstring_to_encoding(data, "CP1251") or { []u8{len: capacity} }
diff := capacity - bytes_data.len
if diff > 0 {
for i, d in bytes_data {
final_data[i] = d
}
} else {
final_data = bytes_data.clone()[..capacity]
}
return final_data
}
// Перевести тэг в байты
pub fn (id3v1 ID3v1) to_bytes() []u8 {
mut bytes := []u8{cap: 128}
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))
bytes << cut_or_trim(id3v1.year, i8(4))
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)] }
id_genre := Genre.from(id3v1.genre) or { Genre.unknown }
bytes << u8(id_genre) // cut_or_trim([u8(id_genre)], i8(2))
return bytes
}
// Добавление изменённого тэга
pub fn (id3v1 ID3v1) insert(mut audio File, position i8) []u8 {
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]
all_bytes << bytes_tag
return all_bytes
}
+114
View File
@@ -0,0 +1,114 @@
module metadata
import os { File }
// 00: ISO-8859-1 (1 байт на символ).
// 01: UTF-16 с BOM (2 байта на символ + 2 байта маркер).
// 03: UTF-8 (переменная длина).
// Пример из пункта 3 (13 байт размера):
// Если первый байт данных 01 (UTF-16), то реальный текст занимает:
// 13 - 1 (байт кодировки) - 2 (BOM) = 10 байт.
// В UTF-16 это будет всего 5 символов (так как 1 символ = 2 байта).
enum EnumID3v23 as u8 {
tit2
tpe1
tpe2
talb
tyer
trck
tcon
tcop
apic
comment
unknown
}
const start_idx = 10
pub fn (mut tag ID3v23) add_frame(frame Frame) {
name := EnumID3v23.from(frame.name.to_lower()) or { EnumID3v23.unknown }
match name {
.tit2 {
tag.tit2 = frame
}
.tpe1 {
tag.tpe1 = frame
}
.tpe2 {
tag.tpe2 = frame
}
.talb {
tag.talb = frame
}
.tyer {
tag.tyer = frame
}
.trck {
tag.trck = frame
}
.tcon {
tag.tcon = frame
}
.tcop {
tag.tcop = frame
}
.apic {
tag.apic = frame
}
else {
tag.others << frame
}
}
}
// [ID3v23] Получение размера всего тэга / фрейма
@[inline]
fn get_size_v23(size_bytes []u8) u64 {
return (u64(size_bytes[0]) << 24) | (u64(size_bytes[1]) << 16) | (u64(size_bytes[2]) << 8) | u64(size_bytes[3])
}
pub fn (mut id3v23 ID3v23) get_full_size(mut audio File) {
// Первые 10 байт тэга
first_ten_bytes := get_bytes(mut audio, 0, 10)
id3v23.size = (u64(first_ten_bytes[6]) << 21) | (u64(first_ten_bytes[7]) << 14) | (u64(first_ten_bytes[8]) << 7) | u64(first_ten_bytes[9])
}
pub fn (mut id3v23 ID3v23) fill_tag(mut audio File) {
mut current_frame := &Frame{}
// Самая первая позиция
mut start_position := u64(10)
// Версия IDv3
idv3_version := u8(3)
// Получаем вне возможные байты
all_tag_bytes := get_bytes(mut audio, 10, int(id3v23.size + start_position))
for id3v23.size > start_position {
current_frame.fill_frame(all_tag_bytes, start_position, idv3_version)
start_position = current_frame.end
id3v23.size -= current_frame.size
id3v23.add_frame(current_frame)
}
}
// Конвертировать ID3 v2.3 в ID3 v1
pub fn (id3v23 ID3v23) to_v1() ID3v1 {
return ID3v1{
genre: id3v23.tcon.text
title: id3v23.tit2.text
artist: id3v23.tpe2.text
album: id3v23.talb.text
track: id3v23.trck.text.split("/")[0]
year: id3v23.tyer.text
comment: id3v23.comment.text
}
}
pub fn to_bytes () {
}
+107
View File
@@ -0,0 +1,107 @@
module metadata
import os { File }
enum EnumID3v24 as u8 {
tit2
tpe1
tpe2
talb
tdrc
trck
tcon
tcop
apic
comment
unknown
}
pub fn (mut tag ID3v24) add_frame(frame Frame) {
name := EnumID3v24.from(frame.name.to_lower()) or { EnumID3v24.unknown }
match name {
.tit2 {
tag.tit2 = frame
}
.tpe1 {
tag.tpe1 = frame
}
.tpe2 {
tag.tpe2 = frame
}
.talb {
tag.talb = frame
}
.tdrc {
tag.tdrc = frame
}
.trck {
tag.trck = frame
}
.tcon {
tag.tcon = frame
}
.tcop {
tag.tcop = frame
}
.apic {
tag.apic = frame
}
else {
tag.others << 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])
}
pub fn (mut id3v23 ID3v24) get_full_size(mut audio File) {
// Первые 10 байт тэга
first_ten_bytes := get_bytes(mut audio, 0, 10)
id3v23.size = (u64(first_ten_bytes[6]) << 21) | (u64(first_ten_bytes[7]) << 14) | (u64(first_ten_bytes[8]) << 7) | u64(first_ten_bytes[9])
}
pub fn (mut id3v24 ID3v24) fill_tag(mut audio File) {
mut current_frame := &Frame{}
// Самая первая позиция
mut start_position := u64(10)
// Версия IDv3
idv3_version := u8(4)
// Получаем вне возможные байты
all_tag_bytes := get_bytes(mut audio, 10, int(id3v24.size + start_position))
// TODO Подумать как правильно считать размер
// Скорее всего просто скадлывать байтики, если нуль - вырубать
for id3v24.size > start_position {
// Заполнение кадров
current_frame.fill_frame(all_tag_bytes, start_position, idv3_version)
start_position = current_frame.end
id3v24.size -= current_frame.size
// Добавление кадра
id3v24.add_frame(current_frame)
}
}
// Конвертировать ID3 v2.3 в ID3 v1
pub fn (id3v23 ID3v24) to_v1() ID3v1 {
return ID3v1{
genre: id3v23.tcon.text
title: id3v23.tit2.text
artist: id3v23.tpe2.text
album: id3v23.talb.text
track: id3v23.trck.text.split("/")[0]
year: id3v23.tdrc.text
comment: id3v23.comment.text
}
}
pub fn insert(mut audio File) {
}
+62
View File
@@ -0,0 +1,62 @@
module metadata
@[heap]
pub struct ID3v1 {
pub mut:
genre string// Жанр
title string // Название аудиодорожки
artist string // Актёр озучания
album string // Название альбома
track string // Записывается как 2/5, что значит 2 трэк из 5
year string // Год записи, может int нужно
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 // Обложка
comment Frame // Комментарий
others []Frame
embedded bool // Присутвует ли тэг
}
@[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 // Обложка
comment Frame // Комментарий
others []Frame
embedded bool // Присутвует ли тэг
}
@[heap]
pub struct Audio {
pub mut:
id3v1 ID3v1
id3v23 ID3v23
id3v24 ID3v24
path string
extension string = ".mp3"
}
+44
View File
@@ -0,0 +1,44 @@
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" {
audio.id3v1.embedded = true
}
// ===============================
// ? --- Проверка версии ID3v23.3 ---
first_ten_bytes := get_bytes(mut data, 0, 4)
if first_ten_bytes[..3].bytestr() == "ID3" && first_ten_bytes[3] == 3 {
audio.id3v23.embedded = true
}
// ===============================
// ? --- Проверка версии ID3v23.4 ---
if first_ten_bytes[..3].bytestr() == "ID3" && first_ten_bytes[3] == 4 {
audio.id3v24.embedded = true
}
// ===============================
}
pub fn get_bytes(mut audio os.File, position int, size int) []u8 {
// ? ---------Работа с расположением указателя--------------
// Ставим указатель на 128 байт с конца
if position < 0 {
audio.seek(position, os.SeekMode.end) or { panic(err) }
}
// Получаем номер 128 байта с конца
pos := audio.tell() or { panic(err) }
// Читаем байты с конца
last_bytes := audio.read_bytes_at(size, u64(pos))
audio.seek(0, os.SeekMode.start) or { panic(err) }
// =========================================================
return last_bytes
}