Read and store flac metadata
This commit is contained in:
+25
-6
@@ -8,13 +8,32 @@ CREATE TABLE items (
|
||||
);
|
||||
|
||||
CREATE TABLE music_metadata (
|
||||
inode BIGINT PRIMARY KEY REFERENCES items(inode) ON DELETE CASCADE,
|
||||
track_title TEXT NOT NULL,
|
||||
album TEXT NOT NULL,
|
||||
track_number INTEGER NOT NULL
|
||||
inode BIGINT PRIMARY KEY REFERENCES items(inode) ON DELETE CASCADE,
|
||||
track_title TEXT NOT NULL,
|
||||
album TEXT NOT NULL,
|
||||
track_number INTEGER NOT NULL,
|
||||
header BYTEA NOT NULL,
|
||||
real_audio_start BIGINT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE music_metadata_artists (
|
||||
inode BIGINT NOT NULL REFERENCES music_metadata(inode) ON DELETE CASCADE,
|
||||
artist TEXT NOT NULL
|
||||
inode BIGINT NOT NULL REFERENCES music_metadata(inode) ON DELETE CASCADE,
|
||||
artist TEXT NOT NULL,
|
||||
PRIMARY KEY (inode, artist)
|
||||
);
|
||||
|
||||
CREATE TABLE music_metadata_other_tags (
|
||||
inode BIGINT NOT NULL REFERENCES music_metadata(inode) ON DELETE CASCADE,
|
||||
position INTEGER NOT NULL,
|
||||
tag TEXT NOT NULL,
|
||||
PRIMARY KEY (inode, position)
|
||||
);
|
||||
|
||||
CREATE TABLE music_metadata_pictures (
|
||||
inode BIGINT NOT NULL REFERENCES music_metadata(inode) ON DELETE CASCADE,
|
||||
position INTEGER NOT NULL,
|
||||
block_header BYTEA NOT NULL,
|
||||
data_offset BIGINT NOT NULL,
|
||||
data_length BIGINT NOT NULL,
|
||||
PRIMARY KEY (inode, position)
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
packages = with pkgs; [
|
||||
git
|
||||
just
|
||||
flac
|
||||
|
||||
opencode
|
||||
];
|
||||
|
||||
+226
-6
@@ -1,8 +1,8 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
fs,
|
||||
hash::{Hash, Hasher},
|
||||
io,
|
||||
hash::Hasher,
|
||||
io::{self, Read, Seek, SeekFrom},
|
||||
os::unix::{
|
||||
ffi::OsStrExt,
|
||||
fs::{MetadataExt, PermissionsExt},
|
||||
@@ -18,8 +18,11 @@ use notify::{Event, EventKind, RecursiveMode, Watcher};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use twox_hash::XxHash64;
|
||||
|
||||
use crate::file_watcher::FileWatcher;
|
||||
use crate::music_metadata::MusicMetadata;
|
||||
use crate::{file_watcher::FileWatcher, music_metadata};
|
||||
use crate::music_metadata::db::{
|
||||
artists, music_metadata as music_metadata_entity, other_tags, pictures,
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
enum FileType {
|
||||
@@ -109,13 +112,22 @@ impl LocalOrigin {
|
||||
let mut to_insert: Vec<ActiveModel> = vec![];
|
||||
let mut to_update: Vec<ActiveModel> = vec![];
|
||||
let mut to_delete: Vec<i64> = vec![];
|
||||
let mut to_save_music: Vec<i64> = vec![];
|
||||
|
||||
for (ino, item) in &snapshot {
|
||||
let ino_i64 = ino.0 as i64;
|
||||
match db_items.get(&ino_i64) {
|
||||
None => to_insert.push(ActiveModel::from(item)),
|
||||
None => {
|
||||
to_insert.push(ActiveModel::from(item));
|
||||
if item.music_metadata.is_some() {
|
||||
to_save_music.push(ino_i64);
|
||||
}
|
||||
}
|
||||
Some(db_item) if db_item.hash != item.hash as i64 => {
|
||||
to_update.push(ActiveModel::from(item))
|
||||
to_update.push(ActiveModel::from(item));
|
||||
if item.music_metadata.is_some() {
|
||||
to_save_music.push(ino_i64);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -137,6 +149,18 @@ impl LocalOrigin {
|
||||
Entity::delete_by_id(ino).exec(&client).await.unwrap();
|
||||
}
|
||||
|
||||
for ino_i64 in &to_save_music {
|
||||
let ino = INodeNo(*ino_i64 as u64);
|
||||
if let Some(music_metadata) = snapshot
|
||||
.get(&ino)
|
||||
.and_then(|item| item.music_metadata.as_ref())
|
||||
{
|
||||
LocalOrigin::save_music_metadata(*ino_i64, music_metadata, &client)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// For unchanged items (hash matches), restore virtual paths from DB
|
||||
// so that renames performed in a previous session are preserved.
|
||||
let restorations: Vec<(INodeNo, String, PathBuf)> = db_items
|
||||
@@ -177,6 +201,73 @@ impl LocalOrigin {
|
||||
Ok(local_origin)
|
||||
}
|
||||
|
||||
async fn save_music_metadata(
|
||||
inode: i64,
|
||||
music_metadata: &MusicMetadata,
|
||||
client: &sea_orm::DatabaseConnection,
|
||||
) -> Result<(), sea_orm::DbErr> {
|
||||
use sea_orm::ActiveValue::Set;
|
||||
|
||||
music_metadata_entity::Entity::delete_by_id(inode)
|
||||
.exec(client)
|
||||
.await?;
|
||||
|
||||
music_metadata_entity::Entity::insert(music_metadata_entity::ActiveModel {
|
||||
inode: Set(inode),
|
||||
track_title: Set(music_metadata.track_title.clone()),
|
||||
album: Set(music_metadata.album.clone()),
|
||||
track_number: Set(music_metadata.track_number),
|
||||
header: Set(music_metadata.header.clone()),
|
||||
real_audio_start: Set(music_metadata.real_audio_start as i64),
|
||||
})
|
||||
.exec(client)
|
||||
.await?;
|
||||
|
||||
if !music_metadata.artist.is_empty() {
|
||||
artists::Entity::insert_many(music_metadata.artist.iter().map(|a| {
|
||||
artists::ActiveModel {
|
||||
inode: Set(inode),
|
||||
artist: Set(a.clone()),
|
||||
}
|
||||
}))
|
||||
.exec(client)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !music_metadata.other_tags.is_empty() {
|
||||
other_tags::Entity::insert_many(music_metadata.other_tags.iter().enumerate().map(
|
||||
|(pos, tag)| other_tags::ActiveModel {
|
||||
inode: Set(inode),
|
||||
position: Set(pos as i32),
|
||||
tag: Set(tag.clone()),
|
||||
},
|
||||
))
|
||||
.exec(client)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !music_metadata.picture_block_headers.is_empty() {
|
||||
pictures::Entity::insert_many(
|
||||
music_metadata
|
||||
.picture_block_headers
|
||||
.iter()
|
||||
.zip(music_metadata.picture_data_ranges.iter())
|
||||
.enumerate()
|
||||
.map(|(pos, (hdr, (offset, len)))| pictures::ActiveModel {
|
||||
inode: Set(inode),
|
||||
position: Set(pos as i32),
|
||||
block_header: Set(hdr.to_vec()),
|
||||
data_offset: Set(*offset as i64),
|
||||
data_length: Set(*len as i64),
|
||||
}),
|
||||
)
|
||||
.exec(client)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fill_fileset(
|
||||
map: &Arc<Mutex<BTreeMap<INodeNo, LocalItem>>>,
|
||||
source: &Path,
|
||||
@@ -293,9 +384,14 @@ impl LocalOrigin {
|
||||
let crtime = metadata.created().unwrap_or(SystemTime::UNIX_EPOCH);
|
||||
let kind = LocalOrigin::file_type(local_file.file_type);
|
||||
|
||||
let size = match &local_file.music_metadata {
|
||||
Some(mm) if mm.real_audio_start > 0 => mm.virtual_size(metadata.size()),
|
||||
_ => metadata.size(),
|
||||
};
|
||||
|
||||
return FileAttr {
|
||||
ino: local_file.inode,
|
||||
size: metadata.size(),
|
||||
size,
|
||||
blocks: metadata.blocks(),
|
||||
atime: atime,
|
||||
mtime: mtime,
|
||||
@@ -319,6 +415,15 @@ impl LocalOrigin {
|
||||
};
|
||||
}
|
||||
|
||||
fn read_bytes_at(path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
|
||||
let mut f = fs::File::open(path)?;
|
||||
f.seek(SeekFrom::Start(offset))?;
|
||||
let mut buf = vec![0u8; len];
|
||||
let n = f.read(&mut buf)?;
|
||||
buf.truncate(n);
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
fn parent_inode_from_path(local_path: &Path) -> INodeNo {
|
||||
let components: Vec<_> = local_path.components().collect();
|
||||
if components.len() <= 1 {
|
||||
@@ -540,6 +645,121 @@ impl Filesystem for LocalOrigin {
|
||||
reply.ok();
|
||||
}
|
||||
|
||||
fn read(
|
||||
&self,
|
||||
_req: &Request,
|
||||
ino: INodeNo,
|
||||
_fh: fuser::FileHandle,
|
||||
offset: u64,
|
||||
size: u32,
|
||||
_flags: fuser::OpenFlags,
|
||||
_lock_owner: Option<fuser::LockOwner>,
|
||||
reply: fuser::ReplyData,
|
||||
) {
|
||||
let files = self.files.lock().unwrap();
|
||||
let item = match files.get(&ino) {
|
||||
Some(f) => f,
|
||||
None => {
|
||||
reply.error(Errno::ENOENT);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let original_path = item.original_path.clone();
|
||||
let mm = item
|
||||
.music_metadata
|
||||
.as_ref()
|
||||
.filter(|mm| mm.real_audio_start > 0);
|
||||
let is_flac = mm.is_some();
|
||||
let header = mm.map(|mm| mm.header.clone()).unwrap_or_default();
|
||||
let pic_hdrs = mm
|
||||
.map(|mm| mm.picture_block_headers.clone())
|
||||
.unwrap_or_default();
|
||||
let pic_ranges = mm
|
||||
.map(|mm| mm.picture_data_ranges.clone())
|
||||
.unwrap_or_default();
|
||||
let real_audio_start = mm.map(|mm| mm.real_audio_start).unwrap_or(0);
|
||||
drop(files);
|
||||
|
||||
if !is_flac {
|
||||
match LocalOrigin::read_bytes_at(&original_path, offset, size as usize) {
|
||||
Ok(bytes) => reply.data(&bytes),
|
||||
Err(_) => reply.error(Errno::EIO),
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let end = offset + size as u64;
|
||||
let header_end = header.len() as u64;
|
||||
|
||||
// Pure header read — most common for tag readers
|
||||
if end <= header_end {
|
||||
reply.data(&header[offset as usize..end as usize]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Multi-region read: walk [header][pic_hdr+pic_data]...[audio] and collect bytes
|
||||
let mut buf = Vec::with_capacity(size as usize);
|
||||
|
||||
if offset < header_end {
|
||||
buf.extend_from_slice(&header[offset as usize..]);
|
||||
}
|
||||
|
||||
let mut virt_pos = header_end;
|
||||
for (pic_hdr, (data_real_offset, data_len)) in pic_hdrs.iter().zip(pic_ranges.iter()) {
|
||||
let pic_hdr_end = virt_pos + 4;
|
||||
let pic_end = pic_hdr_end + data_len;
|
||||
|
||||
if end <= virt_pos {
|
||||
break;
|
||||
}
|
||||
if offset >= pic_end {
|
||||
virt_pos = pic_end;
|
||||
continue;
|
||||
}
|
||||
|
||||
let hdr_from = (offset.max(virt_pos) - virt_pos) as usize;
|
||||
let hdr_to = ((end.min(pic_hdr_end)) - virt_pos) as usize;
|
||||
if hdr_from < hdr_to {
|
||||
buf.extend_from_slice(&pic_hdr[hdr_from..hdr_to.min(4)]);
|
||||
}
|
||||
|
||||
let data_start = offset.max(pic_hdr_end);
|
||||
let data_end = end.min(pic_end);
|
||||
if data_start < data_end {
|
||||
let real_off = data_real_offset + (data_start - pic_hdr_end);
|
||||
match LocalOrigin::read_bytes_at(
|
||||
&original_path,
|
||||
real_off,
|
||||
(data_end - data_start) as usize,
|
||||
) {
|
||||
Ok(bytes) => buf.extend_from_slice(&bytes),
|
||||
Err(_) => {
|
||||
reply.error(Errno::EIO);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
virt_pos = pic_end;
|
||||
}
|
||||
|
||||
let audio_start = offset.max(virt_pos);
|
||||
if audio_start < end {
|
||||
let real_off = real_audio_start + (audio_start - virt_pos);
|
||||
match LocalOrigin::read_bytes_at(&original_path, real_off, (end - audio_start) as usize)
|
||||
{
|
||||
Ok(bytes) => buf.extend_from_slice(&bytes),
|
||||
Err(_) => {
|
||||
reply.error(Errno::EIO);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
reply.data(&buf);
|
||||
}
|
||||
|
||||
fn lookup(
|
||||
&self,
|
||||
_req: &Request,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use clap::Parser;
|
||||
use file_watcher::FileWatcher;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
+235
-14
@@ -1,4 +1,8 @@
|
||||
use std::{fs, path::Path};
|
||||
use std::{
|
||||
fs,
|
||||
io::{Read, Seek, SeekFrom},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use symphonia::core::{
|
||||
formats::FormatOptions,
|
||||
@@ -13,9 +17,23 @@ pub struct MusicMetadata {
|
||||
pub album: String,
|
||||
pub track_number: i32,
|
||||
pub track_title: String,
|
||||
pub other_tags: Vec<String>,
|
||||
pub header: Vec<u8>,
|
||||
pub picture_block_headers: Vec<[u8; 4]>,
|
||||
pub picture_data_ranges: Vec<(u64, u64)>,
|
||||
pub real_audio_start: u64,
|
||||
}
|
||||
|
||||
impl MusicMetadata {
|
||||
pub fn virtual_size(&self, real_file_size: u64) -> u64 {
|
||||
let pictures_size: u64 = self
|
||||
.picture_data_ranges
|
||||
.iter()
|
||||
.map(|(_, len)| 4 + len)
|
||||
.sum();
|
||||
self.header.len() as u64 + pictures_size + (real_file_size - self.real_audio_start)
|
||||
}
|
||||
|
||||
pub fn parse_music_metadata(path: &Path) -> Option<MusicMetadata> {
|
||||
let src = fs::File::open(path).expect("failed to open media");
|
||||
let mss = MediaSourceStream::new(Box::new(src), Default::default());
|
||||
@@ -32,25 +50,228 @@ impl MusicMetadata {
|
||||
let metadata: Metadata = format.format.metadata();
|
||||
let revision: &MetadataRevision = metadata.current().unwrap();
|
||||
|
||||
let mut music_metadata = MusicMetadata {
|
||||
..Default::default()
|
||||
};
|
||||
let mut music_metadata = MusicMetadata::default();
|
||||
|
||||
for tag in revision.tags() {
|
||||
let tag_type = tag.std_key.expect("no std_key for tag found");
|
||||
let value = tag.value.to_string();
|
||||
|
||||
match tag_type {
|
||||
StandardTagKey::Artist => music_metadata.artist.push(value),
|
||||
StandardTagKey::Album => music_metadata.album = value,
|
||||
StandardTagKey::TrackNumber => {
|
||||
music_metadata.track_number = value.parse::<i32>().unwrap()
|
||||
match tag.std_key {
|
||||
Some(StandardTagKey::Artist) => music_metadata.artist.push(value),
|
||||
Some(StandardTagKey::Album) => music_metadata.album = value,
|
||||
Some(StandardTagKey::TrackNumber) => {
|
||||
music_metadata.track_number = value.parse::<i32>().unwrap_or(0)
|
||||
}
|
||||
StandardTagKey::TrackTitle => music_metadata.track_title = value,
|
||||
_ => {}
|
||||
Some(StandardTagKey::TrackTitle) => music_metadata.track_title = value,
|
||||
_ => music_metadata
|
||||
.other_tags
|
||||
.push(format!("{}={}", tag.key, value)),
|
||||
}
|
||||
}
|
||||
|
||||
return Some(music_metadata);
|
||||
if let Some(parsed) = parse_flac(path) {
|
||||
music_metadata.real_audio_start = parsed.audio_start;
|
||||
music_metadata.picture_block_headers = parsed.picture_block_headers;
|
||||
music_metadata.picture_data_ranges = parsed.picture_data_ranges;
|
||||
music_metadata.header = build_flac_header(parsed.other_blocks, &music_metadata);
|
||||
}
|
||||
|
||||
Some(music_metadata)
|
||||
}
|
||||
}
|
||||
|
||||
struct FlacParsed {
|
||||
other_blocks: Vec<(u8, Vec<u8>)>,
|
||||
picture_block_headers: Vec<[u8; 4]>,
|
||||
picture_data_ranges: Vec<(u64, u64)>,
|
||||
audio_start: u64,
|
||||
}
|
||||
|
||||
fn parse_flac(path: &Path) -> Option<FlacParsed> {
|
||||
let mut f = fs::File::open(path).ok()?;
|
||||
|
||||
let mut magic = [0u8; 4];
|
||||
f.read_exact(&mut magic).ok()?;
|
||||
if &magic != b"fLaC" {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut other_blocks: Vec<(u8, Vec<u8>)> = vec![];
|
||||
let mut picture_block_headers: Vec<[u8; 4]> = vec![];
|
||||
let mut picture_data_ranges: Vec<(u64, u64)> = vec![];
|
||||
let mut pos = 4u64;
|
||||
|
||||
loop {
|
||||
let mut hdr = [0u8; 4];
|
||||
f.read_exact(&mut hdr).ok()?;
|
||||
let is_last = (hdr[0] & 0x80) != 0;
|
||||
let block_type = hdr[0] & 0x7f;
|
||||
let length = u32::from_be_bytes([0, hdr[1], hdr[2], hdr[3]]) as u64;
|
||||
pos += 4;
|
||||
|
||||
if block_type == 6 {
|
||||
// PICTURE: keep block header (we'll fix is_last later), record data range
|
||||
picture_block_headers.push(hdr);
|
||||
picture_data_ranges.push((pos, length));
|
||||
f.seek(SeekFrom::Current(length as i64)).ok()?;
|
||||
} else {
|
||||
let mut data = vec![0u8; length as usize];
|
||||
f.read_exact(&mut data).ok()?;
|
||||
// Skip VORBIS_COMMENT (type 4) — we rebuild it
|
||||
if block_type != 4 {
|
||||
other_blocks.push((block_type, data));
|
||||
}
|
||||
}
|
||||
|
||||
pos += length;
|
||||
if is_last {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix is_last on the last picture block header: it must be 1 when audio follows
|
||||
if let Some(last_hdr) = picture_block_headers.last_mut() {
|
||||
last_hdr[0] = 0x80 | (last_hdr[0] & 0x7f);
|
||||
}
|
||||
|
||||
Some(FlacParsed {
|
||||
other_blocks,
|
||||
picture_block_headers,
|
||||
picture_data_ranges,
|
||||
audio_start: pos,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_flac_header(blocks: Vec<(u8, Vec<u8>)>, metadata: &MusicMetadata) -> Vec<u8> {
|
||||
let vorbis = build_vorbis_comment(metadata);
|
||||
|
||||
let mut patched: Vec<(u8, Vec<u8>)> = blocks;
|
||||
patched.push((4, vorbis));
|
||||
|
||||
let has_pictures = !metadata.picture_data_ranges.is_empty();
|
||||
let mut out = Vec::new();
|
||||
out.extend_from_slice(b"fLaC");
|
||||
|
||||
let last = patched.len() - 1;
|
||||
for (i, (block_type, data)) in patched.iter().enumerate() {
|
||||
// is_last only if this is the final block AND no picture blocks follow
|
||||
let is_last_block = i == last && !has_pictures;
|
||||
let flag: u8 = if is_last_block { 0x80 } else { 0x00 };
|
||||
let length = data.len() as u32;
|
||||
out.push(flag | block_type);
|
||||
out.push((length >> 16) as u8);
|
||||
out.push((length >> 8) as u8);
|
||||
out.push(length as u8);
|
||||
out.extend_from_slice(data);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
pub mod db {
|
||||
pub mod music_metadata {
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "music_metadata")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub inode: i64,
|
||||
pub track_title: String,
|
||||
pub album: String,
|
||||
pub track_number: i32,
|
||||
#[sea_orm(column_type = "Blob")]
|
||||
pub header: Vec<u8>,
|
||||
pub real_audio_start: i64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
|
||||
pub mod artists {
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "music_metadata_artists")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub inode: i64,
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub artist: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
|
||||
pub mod other_tags {
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "music_metadata_other_tags")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub inode: i64,
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub position: i32,
|
||||
pub tag: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
|
||||
pub mod pictures {
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "music_metadata_pictures")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub inode: i64,
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub position: i32,
|
||||
#[sea_orm(column_type = "Blob")]
|
||||
pub block_header: Vec<u8>,
|
||||
pub data_offset: i64,
|
||||
pub data_length: i64,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_vorbis_comment(metadata: &MusicMetadata) -> Vec<u8> {
|
||||
let vendor = b"musicfs";
|
||||
let mut out = Vec::new();
|
||||
|
||||
out.extend_from_slice(&(vendor.len() as u32).to_le_bytes());
|
||||
out.extend_from_slice(vendor);
|
||||
|
||||
let mut comments: Vec<String> = vec![
|
||||
format!("TITLE={}", metadata.track_title),
|
||||
format!("ALBUM={}", metadata.album),
|
||||
format!("TRACKNUMBER={}", metadata.track_number),
|
||||
];
|
||||
for artist in &metadata.artist {
|
||||
comments.push(format!("ARTIST={}", artist));
|
||||
}
|
||||
comments.extend(metadata.other_tags.iter().cloned());
|
||||
|
||||
out.extend_from_slice(&(comments.len() as u32).to_le_bytes());
|
||||
for comment in &comments {
|
||||
let bytes = comment.as_bytes();
|
||||
out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
|
||||
out.extend_from_slice(bytes);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user