Read and store flac metadata
This commit is contained in:
+25
-6
@@ -8,13 +8,32 @@ CREATE TABLE items (
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE music_metadata (
|
CREATE TABLE music_metadata (
|
||||||
inode BIGINT PRIMARY KEY REFERENCES items(inode) ON DELETE CASCADE,
|
inode BIGINT PRIMARY KEY REFERENCES items(inode) ON DELETE CASCADE,
|
||||||
track_title TEXT NOT NULL,
|
track_title TEXT NOT NULL,
|
||||||
album TEXT NOT NULL,
|
album TEXT NOT NULL,
|
||||||
track_number INTEGER NOT NULL
|
track_number INTEGER NOT NULL,
|
||||||
|
header BYTEA NOT NULL,
|
||||||
|
real_audio_start BIGINT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE music_metadata_artists (
|
CREATE TABLE music_metadata_artists (
|
||||||
inode BIGINT NOT NULL REFERENCES music_metadata(inode) ON DELETE CASCADE,
|
inode BIGINT NOT NULL REFERENCES music_metadata(inode) ON DELETE CASCADE,
|
||||||
artist TEXT NOT NULL
|
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; [
|
packages = with pkgs; [
|
||||||
git
|
git
|
||||||
just
|
just
|
||||||
|
flac
|
||||||
|
|
||||||
opencode
|
opencode
|
||||||
];
|
];
|
||||||
|
|||||||
+226
-6
@@ -1,8 +1,8 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::BTreeMap,
|
collections::BTreeMap,
|
||||||
fs,
|
fs,
|
||||||
hash::{Hash, Hasher},
|
hash::Hasher,
|
||||||
io,
|
io::{self, Read, Seek, SeekFrom},
|
||||||
os::unix::{
|
os::unix::{
|
||||||
ffi::OsStrExt,
|
ffi::OsStrExt,
|
||||||
fs::{MetadataExt, PermissionsExt},
|
fs::{MetadataExt, PermissionsExt},
|
||||||
@@ -18,8 +18,11 @@ use notify::{Event, EventKind, RecursiveMode, Watcher};
|
|||||||
use sea_orm::entity::prelude::*;
|
use sea_orm::entity::prelude::*;
|
||||||
use twox_hash::XxHash64;
|
use twox_hash::XxHash64;
|
||||||
|
|
||||||
|
use crate::file_watcher::FileWatcher;
|
||||||
use crate::music_metadata::MusicMetadata;
|
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)]
|
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||||
enum FileType {
|
enum FileType {
|
||||||
@@ -109,13 +112,22 @@ impl LocalOrigin {
|
|||||||
let mut to_insert: Vec<ActiveModel> = vec![];
|
let mut to_insert: Vec<ActiveModel> = vec![];
|
||||||
let mut to_update: Vec<ActiveModel> = vec![];
|
let mut to_update: Vec<ActiveModel> = vec![];
|
||||||
let mut to_delete: Vec<i64> = vec![];
|
let mut to_delete: Vec<i64> = vec![];
|
||||||
|
let mut to_save_music: Vec<i64> = vec![];
|
||||||
|
|
||||||
for (ino, item) in &snapshot {
|
for (ino, item) in &snapshot {
|
||||||
let ino_i64 = ino.0 as i64;
|
let ino_i64 = ino.0 as i64;
|
||||||
match db_items.get(&ino_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 => {
|
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();
|
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
|
// For unchanged items (hash matches), restore virtual paths from DB
|
||||||
// so that renames performed in a previous session are preserved.
|
// so that renames performed in a previous session are preserved.
|
||||||
let restorations: Vec<(INodeNo, String, PathBuf)> = db_items
|
let restorations: Vec<(INodeNo, String, PathBuf)> = db_items
|
||||||
@@ -177,6 +201,73 @@ impl LocalOrigin {
|
|||||||
Ok(local_origin)
|
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(
|
fn fill_fileset(
|
||||||
map: &Arc<Mutex<BTreeMap<INodeNo, LocalItem>>>,
|
map: &Arc<Mutex<BTreeMap<INodeNo, LocalItem>>>,
|
||||||
source: &Path,
|
source: &Path,
|
||||||
@@ -293,9 +384,14 @@ impl LocalOrigin {
|
|||||||
let crtime = metadata.created().unwrap_or(SystemTime::UNIX_EPOCH);
|
let crtime = metadata.created().unwrap_or(SystemTime::UNIX_EPOCH);
|
||||||
let kind = LocalOrigin::file_type(local_file.file_type);
|
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 {
|
return FileAttr {
|
||||||
ino: local_file.inode,
|
ino: local_file.inode,
|
||||||
size: metadata.size(),
|
size,
|
||||||
blocks: metadata.blocks(),
|
blocks: metadata.blocks(),
|
||||||
atime: atime,
|
atime: atime,
|
||||||
mtime: mtime,
|
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 {
|
fn parent_inode_from_path(local_path: &Path) -> INodeNo {
|
||||||
let components: Vec<_> = local_path.components().collect();
|
let components: Vec<_> = local_path.components().collect();
|
||||||
if components.len() <= 1 {
|
if components.len() <= 1 {
|
||||||
@@ -540,6 +645,121 @@ impl Filesystem for LocalOrigin {
|
|||||||
reply.ok();
|
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(
|
fn lookup(
|
||||||
&self,
|
&self,
|
||||||
_req: &Request,
|
_req: &Request,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use file_watcher::FileWatcher;
|
use file_watcher::FileWatcher;
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::time::Duration;
|
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::{
|
use symphonia::core::{
|
||||||
formats::FormatOptions,
|
formats::FormatOptions,
|
||||||
@@ -13,9 +17,23 @@ pub struct MusicMetadata {
|
|||||||
pub album: String,
|
pub album: String,
|
||||||
pub track_number: i32,
|
pub track_number: i32,
|
||||||
pub track_title: String,
|
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 {
|
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> {
|
pub fn parse_music_metadata(path: &Path) -> Option<MusicMetadata> {
|
||||||
let src = fs::File::open(path).expect("failed to open media");
|
let src = fs::File::open(path).expect("failed to open media");
|
||||||
let mss = MediaSourceStream::new(Box::new(src), Default::default());
|
let mss = MediaSourceStream::new(Box::new(src), Default::default());
|
||||||
@@ -32,25 +50,228 @@ impl MusicMetadata {
|
|||||||
let metadata: Metadata = format.format.metadata();
|
let metadata: Metadata = format.format.metadata();
|
||||||
let revision: &MetadataRevision = metadata.current().unwrap();
|
let revision: &MetadataRevision = metadata.current().unwrap();
|
||||||
|
|
||||||
let mut music_metadata = MusicMetadata {
|
let mut music_metadata = MusicMetadata::default();
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
for tag in revision.tags() {
|
for tag in revision.tags() {
|
||||||
let tag_type = tag.std_key.expect("no std_key for tag found");
|
|
||||||
let value = tag.value.to_string();
|
let value = tag.value.to_string();
|
||||||
|
match tag.std_key {
|
||||||
match tag_type {
|
Some(StandardTagKey::Artist) => music_metadata.artist.push(value),
|
||||||
StandardTagKey::Artist => music_metadata.artist.push(value),
|
Some(StandardTagKey::Album) => music_metadata.album = value,
|
||||||
StandardTagKey::Album => music_metadata.album = value,
|
Some(StandardTagKey::TrackNumber) => {
|
||||||
StandardTagKey::TrackNumber => {
|
music_metadata.track_number = value.parse::<i32>().unwrap_or(0)
|
||||||
music_metadata.track_number = value.parse::<i32>().unwrap()
|
|
||||||
}
|
}
|
||||||
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