Refactor
This commit is contained in:
+374
-313
@@ -44,6 +44,33 @@ struct LocalItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl LocalItem {
|
impl LocalItem {
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn new(
|
||||||
|
inode: INodeNo,
|
||||||
|
parent_inode: INodeNo,
|
||||||
|
name: String,
|
||||||
|
original_path: PathBuf,
|
||||||
|
local_path: PathBuf,
|
||||||
|
file_type: FileType,
|
||||||
|
metadata: fs::Metadata,
|
||||||
|
music_metadata: Option<MusicMetadata>,
|
||||||
|
) -> LocalItem {
|
||||||
|
let mut item = LocalItem {
|
||||||
|
inode,
|
||||||
|
parent_inode,
|
||||||
|
name,
|
||||||
|
original_path,
|
||||||
|
local_path,
|
||||||
|
file_type,
|
||||||
|
metadata,
|
||||||
|
music_metadata,
|
||||||
|
hash: 0,
|
||||||
|
};
|
||||||
|
item.hash = item.compute_hash();
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
fn compute_hash(&self) -> u64 {
|
fn compute_hash(&self) -> u64 {
|
||||||
let seed = 1234;
|
let seed = 1234;
|
||||||
let mut hasher = XxHash64::with_seed(seed);
|
let mut hasher = XxHash64::with_seed(seed);
|
||||||
@@ -82,11 +109,12 @@ pub struct LocalOrigin {
|
|||||||
|
|
||||||
impl std::fmt::Debug for LocalOrigin {
|
impl std::fmt::Debug for LocalOrigin {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("LocalOrigin")
|
return f
|
||||||
|
.debug_struct("LocalOrigin")
|
||||||
.field("source", &self.source)
|
.field("source", &self.source)
|
||||||
.field("destination", &self.destination)
|
.field("destination", &self.destination)
|
||||||
.field("files", &self.files)
|
.field("files", &self.files)
|
||||||
.finish()
|
.finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,12 +137,31 @@ impl LocalOrigin {
|
|||||||
.map(|e| (e.inode, e))
|
.map(|e| (e.inode, e))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
LocalOrigin::sync_items_to_db(&snapshot, &db_items, &client).await;
|
||||||
|
LocalOrigin::restore_music_metadata_from_db(&mut snapshot, &db_items, &client).await;
|
||||||
|
LocalOrigin::restore_virtual_paths(&mut snapshot, &db_items, Path::new(&source));
|
||||||
|
|
||||||
|
let local_origin = LocalOrigin {
|
||||||
|
source: source.into(),
|
||||||
|
destination: destination.into(),
|
||||||
|
files: Arc::new(Mutex::new(snapshot)),
|
||||||
|
client,
|
||||||
|
};
|
||||||
|
//dbg!(&local_origin);
|
||||||
|
return Ok(local_origin);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn sync_items_to_db(
|
||||||
|
snapshot: &BTreeMap<INodeNo, LocalItem>,
|
||||||
|
db_items: &std::collections::HashMap<i64, Model>,
|
||||||
|
client: &sea_orm::DatabaseConnection,
|
||||||
|
) {
|
||||||
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![];
|
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 => {
|
None => {
|
||||||
@@ -140,13 +187,13 @@ impl LocalOrigin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !to_insert.is_empty() {
|
if !to_insert.is_empty() {
|
||||||
Entity::insert_many(to_insert).exec(&client).await.unwrap();
|
Entity::insert_many(to_insert).exec(client).await.unwrap();
|
||||||
}
|
}
|
||||||
for model in to_update {
|
for model in to_update {
|
||||||
model.update(&client).await.unwrap();
|
model.update(client).await.unwrap();
|
||||||
}
|
}
|
||||||
for ino in to_delete {
|
for ino in to_delete {
|
||||||
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 {
|
for ino_i64 in &to_save_music {
|
||||||
@@ -155,14 +202,23 @@ impl LocalOrigin {
|
|||||||
.get(&ino)
|
.get(&ino)
|
||||||
.and_then(|item| item.music_metadata.as_ref())
|
.and_then(|item| item.music_metadata.as_ref())
|
||||||
{
|
{
|
||||||
LocalOrigin::save_music_metadata(*ino_i64, music_metadata, &client)
|
LocalOrigin::save_music_metadata(*ino_i64, music_metadata, client)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn restore_music_metadata_from_db(
|
||||||
|
snapshot: &mut BTreeMap<INodeNo, LocalItem>,
|
||||||
|
db_items: &std::collections::HashMap<i64, Model>,
|
||||||
|
client: &sea_orm::DatabaseConnection,
|
||||||
|
) {
|
||||||
|
use artists::Column as ArtCol;
|
||||||
|
use music_metadata_entity::Column as MmCol;
|
||||||
|
use other_tags::Column as OtCol;
|
||||||
|
use pictures::Column as PicCol;
|
||||||
|
|
||||||
// For unchanged FLAC items, restore music_metadata from DB
|
|
||||||
// (overrides the fresh FLAC parse so that writes made in a previous session survive restarts)
|
|
||||||
let unchanged_music_inodes: Vec<i64> = db_items
|
let unchanged_music_inodes: Vec<i64> = db_items
|
||||||
.values()
|
.values()
|
||||||
.filter_map(|db_item| {
|
.filter_map(|db_item| {
|
||||||
@@ -176,112 +232,116 @@ impl LocalOrigin {
|
|||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !unchanged_music_inodes.is_empty() {
|
if unchanged_music_inodes.is_empty() {
|
||||||
use artists::Column as ArtCol;
|
return;
|
||||||
use music_metadata_entity::Column as MmCol;
|
|
||||||
use other_tags::Column as OtCol;
|
|
||||||
use pictures::Column as PicCol;
|
|
||||||
|
|
||||||
let mm_rows: std::collections::HashMap<i64, music_metadata_entity::Model> =
|
|
||||||
music_metadata_entity::Entity::find()
|
|
||||||
.filter(MmCol::Inode.is_in(unchanged_music_inodes.clone()))
|
|
||||||
.all(&client)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.into_iter()
|
|
||||||
.map(|m| (m.inode, m))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut artists_by_inode: std::collections::HashMap<i64, Vec<String>> =
|
|
||||||
std::collections::HashMap::new();
|
|
||||||
for row in artists::Entity::find()
|
|
||||||
.filter(ArtCol::Inode.is_in(unchanged_music_inodes.clone()))
|
|
||||||
.all(&client)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
artists_by_inode
|
|
||||||
.entry(row.inode)
|
|
||||||
.or_default()
|
|
||||||
.push(row.artist);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut other_tags_by_inode: std::collections::HashMap<i64, Vec<(i32, String)>> =
|
|
||||||
std::collections::HashMap::new();
|
|
||||||
for row in other_tags::Entity::find()
|
|
||||||
.filter(OtCol::Inode.is_in(unchanged_music_inodes.clone()))
|
|
||||||
.all(&client)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
other_tags_by_inode
|
|
||||||
.entry(row.inode)
|
|
||||||
.or_default()
|
|
||||||
.push((row.position, row.tag));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pictures_by_inode: std::collections::HashMap<i64, Vec<pictures::Model>> =
|
|
||||||
std::collections::HashMap::new();
|
|
||||||
for row in pictures::Entity::find()
|
|
||||||
.filter(PicCol::Inode.is_in(unchanged_music_inodes))
|
|
||||||
.all(&client)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
{
|
|
||||||
pictures_by_inode.entry(row.inode).or_default().push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (inode, mm_row) in mm_rows {
|
|
||||||
let ino = INodeNo(inode as u64);
|
|
||||||
let Some(item) = snapshot.get_mut(&ino) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut sorted_tags = other_tags_by_inode.remove(&inode).unwrap_or_default();
|
|
||||||
sorted_tags.sort_by_key(|(pos, _)| *pos);
|
|
||||||
|
|
||||||
let mut sorted_pics = pictures_by_inode.remove(&inode).unwrap_or_default();
|
|
||||||
sorted_pics.sort_by_key(|p| p.position);
|
|
||||||
|
|
||||||
let picture_block_headers: Vec<[u8; 4]> = sorted_pics
|
|
||||||
.iter()
|
|
||||||
.map(|p| {
|
|
||||||
let mut hdr = [0u8; 4];
|
|
||||||
let len = p.block_header.len().min(4);
|
|
||||||
hdr[..len].copy_from_slice(&p.block_header[..len]);
|
|
||||||
hdr
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let picture_data_ranges: Vec<(u64, u64)> = sorted_pics
|
|
||||||
.iter()
|
|
||||||
.map(|p| (p.data_offset as u64, p.data_length as u64))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut music_metadata = MusicMetadata {
|
|
||||||
artist: artists_by_inode.remove(&inode).unwrap_or_default(),
|
|
||||||
album: mm_row.album,
|
|
||||||
track_number: mm_row.track_number,
|
|
||||||
track_title: mm_row.track_title,
|
|
||||||
other_tags: sorted_tags.into_iter().map(|(_, tag)| tag).collect(),
|
|
||||||
header: mm_row.header,
|
|
||||||
picture_block_headers,
|
|
||||||
picture_data_ranges,
|
|
||||||
real_audio_start: mm_row.real_audio_start as u64,
|
|
||||||
vorbis_comment_offset: 0,
|
|
||||||
vorbis_comment_length: 0,
|
|
||||||
};
|
|
||||||
music_metadata.find_vorbis_offsets();
|
|
||||||
item.music_metadata = Some(music_metadata);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// For unchanged items (hash matches), restore virtual paths from DB
|
let mm_rows: std::collections::HashMap<i64, music_metadata_entity::Model> =
|
||||||
// so that renames performed in a previous session are preserved.
|
music_metadata_entity::Entity::find()
|
||||||
|
.filter(MmCol::Inode.is_in(unchanged_music_inodes.clone()))
|
||||||
|
.all(client)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.map(|m| (m.inode, m))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut artists_by_inode: std::collections::HashMap<i64, Vec<String>> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
for row in artists::Entity::find()
|
||||||
|
.filter(ArtCol::Inode.is_in(unchanged_music_inodes.clone()))
|
||||||
|
.all(client)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
artists_by_inode
|
||||||
|
.entry(row.inode)
|
||||||
|
.or_default()
|
||||||
|
.push(row.artist);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut other_tags_by_inode: std::collections::HashMap<i64, Vec<(i32, String)>> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
for row in other_tags::Entity::find()
|
||||||
|
.filter(OtCol::Inode.is_in(unchanged_music_inodes.clone()))
|
||||||
|
.all(client)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
other_tags_by_inode
|
||||||
|
.entry(row.inode)
|
||||||
|
.or_default()
|
||||||
|
.push((row.position, row.tag));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pictures_by_inode: std::collections::HashMap<i64, Vec<pictures::Model>> =
|
||||||
|
std::collections::HashMap::new();
|
||||||
|
for row in pictures::Entity::find()
|
||||||
|
.filter(PicCol::Inode.is_in(unchanged_music_inodes))
|
||||||
|
.all(client)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
pictures_by_inode.entry(row.inode).or_default().push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (inode, mm_row) in mm_rows {
|
||||||
|
let ino = INodeNo(inode as u64);
|
||||||
|
let Some(item) = snapshot.get_mut(&ino) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut sorted_tags = other_tags_by_inode.remove(&inode).unwrap_or_default();
|
||||||
|
sorted_tags.sort_by_key(|(pos, _)| *pos);
|
||||||
|
|
||||||
|
let mut sorted_pics = pictures_by_inode.remove(&inode).unwrap_or_default();
|
||||||
|
sorted_pics.sort_by_key(|p| p.position);
|
||||||
|
|
||||||
|
let picture_block_headers: Vec<[u8; 4]> = sorted_pics
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
let mut hdr = [0u8; 4];
|
||||||
|
let len = p.block_header.len().min(4);
|
||||||
|
hdr[..len].copy_from_slice(&p.block_header[..len]);
|
||||||
|
hdr
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let picture_data_ranges: Vec<(u64, u64)> = sorted_pics
|
||||||
|
.iter()
|
||||||
|
.map(|p| (p.data_offset as u64, p.data_length as u64))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut music_metadata = MusicMetadata {
|
||||||
|
artist: artists_by_inode.remove(&inode).unwrap_or_default(),
|
||||||
|
album: mm_row.album,
|
||||||
|
track_number: mm_row.track_number,
|
||||||
|
track_title: mm_row.track_title,
|
||||||
|
other_tags: sorted_tags.into_iter().map(|(_, tag)| tag).collect(),
|
||||||
|
header: mm_row.header,
|
||||||
|
picture_block_headers,
|
||||||
|
picture_data_ranges,
|
||||||
|
real_audio_start: mm_row.real_audio_start as u64,
|
||||||
|
vorbis_comment_offset: 0,
|
||||||
|
vorbis_comment_length: 0,
|
||||||
|
};
|
||||||
|
music_metadata.find_vorbis_offsets();
|
||||||
|
item.music_metadata = Some(music_metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_virtual_paths(
|
||||||
|
snapshot: &mut BTreeMap<INodeNo, LocalItem>,
|
||||||
|
db_items: &std::collections::HashMap<i64, Model>,
|
||||||
|
source: &Path,
|
||||||
|
) {
|
||||||
let restorations: Vec<(INodeNo, String, PathBuf)> = db_items
|
let restorations: Vec<(INodeNo, String, PathBuf)> = db_items
|
||||||
.values()
|
.values()
|
||||||
.filter_map(|db_item| {
|
.filter_map(|db_item| {
|
||||||
let ino = INodeNo(db_item.inode as u64);
|
let ino = INodeNo(db_item.inode as u64);
|
||||||
|
if ino == INodeNo::ROOT {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
snapshot
|
snapshot
|
||||||
.get(&ino)
|
.get(&ino)
|
||||||
.filter(|item| item.hash as i64 == db_item.hash)
|
.filter(|item| item.hash as i64 == db_item.hash)
|
||||||
@@ -296,7 +356,7 @@ impl LocalOrigin {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
for (_, _, local_path) in &restorations {
|
for (_, _, local_path) in &restorations {
|
||||||
LocalOrigin::ensure_virtual_dirs(local_path, Path::new(&source), &mut snapshot);
|
LocalOrigin::ensure_virtual_dirs(local_path, source, snapshot);
|
||||||
}
|
}
|
||||||
for (ino, name, local_path) in restorations {
|
for (ino, name, local_path) in restorations {
|
||||||
if let Some(item) = snapshot.get_mut(&ino) {
|
if let Some(item) = snapshot.get_mut(&ino) {
|
||||||
@@ -305,15 +365,6 @@ impl LocalOrigin {
|
|||||||
item.local_path = local_path;
|
item.local_path = local_path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let local_origin = LocalOrigin {
|
|
||||||
source: source.into(),
|
|
||||||
destination: destination.into(),
|
|
||||||
files: Arc::new(Mutex::new(snapshot)),
|
|
||||||
client,
|
|
||||||
};
|
|
||||||
//dbg!(&local_origin);
|
|
||||||
Ok(local_origin)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_music_metadata(
|
async fn save_music_metadata(
|
||||||
@@ -380,7 +431,7 @@ impl LocalOrigin {
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_fileset(
|
fn fill_fileset(
|
||||||
@@ -413,25 +464,20 @@ impl LocalOrigin {
|
|||||||
) -> Result<BTreeMap<INodeNo, LocalItem>, io::Error> {
|
) -> Result<BTreeMap<INodeNo, LocalItem>, io::Error> {
|
||||||
let mut map = BTreeMap::new();
|
let mut map = BTreeMap::new();
|
||||||
|
|
||||||
let local_root = LocalItem {
|
let local_root = LocalItem::new(
|
||||||
inode: INodeNo::ROOT,
|
INodeNo::ROOT,
|
||||||
parent_inode: INodeNo::ROOT,
|
INodeNo::ROOT,
|
||||||
name: "/".to_string(),
|
"/".to_string(),
|
||||||
original_path: source.to_path_buf(),
|
source.to_path_buf(),
|
||||||
local_path: destination.to_path_buf(),
|
destination.to_path_buf(),
|
||||||
file_type: FileType::Directory,
|
FileType::Directory,
|
||||||
metadata: fs::metadata(source)?,
|
fs::metadata(source)?,
|
||||||
music_metadata: None,
|
None,
|
||||||
hash: 0,
|
);
|
||||||
};
|
|
||||||
let local_root = LocalItem {
|
|
||||||
hash: local_root.compute_hash(),
|
|
||||||
..local_root
|
|
||||||
};
|
|
||||||
map.insert(INodeNo::ROOT, local_root);
|
map.insert(INodeNo::ROOT, local_root);
|
||||||
|
|
||||||
LocalOrigin::read_into_map(source, destination, &mut map)?;
|
LocalOrigin::read_into_map(source, destination, &mut map)?;
|
||||||
Ok(map)
|
return Ok(map);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_into_map(
|
fn read_into_map(
|
||||||
@@ -466,21 +512,16 @@ impl LocalOrigin {
|
|||||||
|
|
||||||
let inode = INodeNo(metadata.ino());
|
let inode = INodeNo(metadata.ino());
|
||||||
let parent_inode = LocalOrigin::ensure_virtual_dirs(&local_path, source, map);
|
let parent_inode = LocalOrigin::ensure_virtual_dirs(&local_path, source, map);
|
||||||
let local_item = LocalItem {
|
let local_item = LocalItem::new(
|
||||||
inode,
|
inode,
|
||||||
parent_inode,
|
parent_inode,
|
||||||
name,
|
name,
|
||||||
original_path: item_path.clone(),
|
item_path.clone(),
|
||||||
local_path,
|
local_path,
|
||||||
file_type,
|
file_type,
|
||||||
metadata,
|
metadata,
|
||||||
music_metadata,
|
music_metadata,
|
||||||
hash: 0,
|
);
|
||||||
};
|
|
||||||
let local_item = LocalItem {
|
|
||||||
hash: local_item.compute_hash(),
|
|
||||||
..local_item
|
|
||||||
};
|
|
||||||
|
|
||||||
map.insert(inode, local_item);
|
map.insert(inode, local_item);
|
||||||
|
|
||||||
@@ -488,7 +529,7 @@ impl LocalOrigin {
|
|||||||
LocalOrigin::read_into_map(&item_path, destination, map)?;
|
LocalOrigin::read_into_map(&item_path, destination, map)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn local_file_to_file_attr(local_file: &LocalItem) -> FileAttr {
|
fn local_file_to_file_attr(local_file: &LocalItem) -> FileAttr {
|
||||||
@@ -508,11 +549,11 @@ impl LocalOrigin {
|
|||||||
ino: local_file.inode,
|
ino: local_file.inode,
|
||||||
size,
|
size,
|
||||||
blocks: metadata.blocks(),
|
blocks: metadata.blocks(),
|
||||||
atime: atime,
|
atime,
|
||||||
mtime: mtime,
|
mtime,
|
||||||
ctime: ctime,
|
ctime,
|
||||||
crtime: crtime,
|
crtime,
|
||||||
kind: kind,
|
kind,
|
||||||
perm: metadata.permissions().mode() as u16,
|
perm: metadata.permissions().mode() as u16,
|
||||||
nlink: metadata.nlink() as u32,
|
nlink: metadata.nlink() as u32,
|
||||||
uid: metadata.uid(),
|
uid: metadata.uid(),
|
||||||
@@ -524,10 +565,19 @@ impl LocalOrigin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn file_type(file_type: FileType) -> fuser::FileType {
|
fn file_type(file_type: FileType) -> fuser::FileType {
|
||||||
return match file_type {
|
match file_type {
|
||||||
FileType::Directory => fuser::FileType::Directory,
|
FileType::Directory => fuser::FileType::Directory,
|
||||||
FileType::File => fuser::FileType::RegularFile,
|
FileType::File => fuser::FileType::RegularFile,
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_db_blocking<F: std::future::Future>(future: F) -> F::Output {
|
||||||
|
return tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_io()
|
||||||
|
.enable_time()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(future);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_bytes_at(path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
|
fn read_bytes_at(path: &Path, offset: u64, len: usize) -> io::Result<Vec<u8>> {
|
||||||
@@ -536,11 +586,82 @@ impl LocalOrigin {
|
|||||||
let mut buf = vec![0u8; len];
|
let mut buf = vec![0u8; len];
|
||||||
let n = f.read(&mut buf)?;
|
let n = f.read(&mut buf)?;
|
||||||
buf.truncate(n);
|
buf.truncate(n);
|
||||||
Ok(buf)
|
|
||||||
|
return Ok(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assemble_flac_read(
|
||||||
|
original_path: &Path,
|
||||||
|
header: &[u8],
|
||||||
|
pic_hdrs: &[[u8; 4]],
|
||||||
|
pic_ranges: &[(u64, u64)],
|
||||||
|
real_audio_start: u64,
|
||||||
|
offset: u64,
|
||||||
|
size: u32,
|
||||||
|
) -> io::Result<Vec<u8>> {
|
||||||
|
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 {
|
||||||
|
return Ok(header[offset as usize..end as usize].to_vec());
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
let bytes = LocalOrigin::read_bytes_at(
|
||||||
|
original_path,
|
||||||
|
real_off,
|
||||||
|
(data_end - data_start) as usize,
|
||||||
|
)?;
|
||||||
|
buf.extend_from_slice(&bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
let bytes =
|
||||||
|
LocalOrigin::read_bytes_at(original_path, real_off, (end - audio_start) as usize)?;
|
||||||
|
buf.extend_from_slice(&bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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()
|
||||||
|
.filter(|c| matches!(c, std::path::Component::Normal(_)))
|
||||||
|
.collect();
|
||||||
if components.len() <= 1 {
|
if components.len() <= 1 {
|
||||||
return INodeNo::ROOT;
|
return INodeNo::ROOT;
|
||||||
}
|
}
|
||||||
@@ -551,13 +672,15 @@ impl LocalOrigin {
|
|||||||
}
|
}
|
||||||
parent_path.push_str(comp.as_os_str().to_str().unwrap_or_default());
|
parent_path.push_str(comp.as_os_str().to_str().unwrap_or_default());
|
||||||
}
|
}
|
||||||
LocalOrigin::virtual_inode(&parent_path)
|
|
||||||
|
return LocalOrigin::virtual_inode(&parent_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn virtual_inode(path: &str) -> INodeNo {
|
fn virtual_inode(path: &str) -> INodeNo {
|
||||||
let mut hasher = XxHash64::with_seed(5678);
|
let mut hasher = XxHash64::with_seed(5678);
|
||||||
hasher.write(path.as_bytes());
|
hasher.write(path.as_bytes());
|
||||||
INodeNo(hasher.finish() | (1u64 << 63))
|
|
||||||
|
return INodeNo(hasher.finish() | (1u64 << 63));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_virtual_dirs(
|
fn ensure_virtual_dirs(
|
||||||
@@ -565,7 +688,10 @@ impl LocalOrigin {
|
|||||||
source: &Path,
|
source: &Path,
|
||||||
map: &mut BTreeMap<INodeNo, LocalItem>,
|
map: &mut BTreeMap<INodeNo, LocalItem>,
|
||||||
) -> INodeNo {
|
) -> INodeNo {
|
||||||
let components: Vec<_> = local_path.components().collect();
|
let components: Vec<_> = local_path
|
||||||
|
.components()
|
||||||
|
.filter(|c| matches!(c, std::path::Component::Normal(_)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
if components.len() <= 1 {
|
if components.len() <= 1 {
|
||||||
return INodeNo::ROOT;
|
return INodeNo::ROOT;
|
||||||
@@ -584,22 +710,16 @@ impl LocalOrigin {
|
|||||||
let virt_ino = LocalOrigin::virtual_inode(¤t_path);
|
let virt_ino = LocalOrigin::virtual_inode(¤t_path);
|
||||||
|
|
||||||
if !map.contains_key(&virt_ino) {
|
if !map.contains_key(&virt_ino) {
|
||||||
let metadata = fs::metadata(source).unwrap();
|
let virt_item = LocalItem::new(
|
||||||
let virt_item = LocalItem {
|
virt_ino,
|
||||||
inode: virt_ino,
|
current_parent,
|
||||||
parent_inode: current_parent,
|
comp_str.to_string(),
|
||||||
name: comp_str.to_string(),
|
source.to_path_buf(),
|
||||||
original_path: source.to_path_buf(),
|
PathBuf::from(¤t_path),
|
||||||
local_path: PathBuf::from(¤t_path),
|
FileType::Directory,
|
||||||
file_type: FileType::Directory,
|
fs::metadata(source).unwrap(),
|
||||||
metadata,
|
None,
|
||||||
music_metadata: None,
|
);
|
||||||
hash: 0,
|
|
||||||
};
|
|
||||||
let virt_item = LocalItem {
|
|
||||||
hash: virt_item.compute_hash(),
|
|
||||||
..virt_item
|
|
||||||
};
|
|
||||||
map.insert(virt_ino, virt_item);
|
map.insert(virt_ino, virt_item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,16 +835,11 @@ impl Filesystem for LocalOrigin {
|
|||||||
if let Some(music_metadata) = updated_music_metadata {
|
if let Some(music_metadata) = updated_music_metadata {
|
||||||
let client = self.client.clone();
|
let client = self.client.clone();
|
||||||
let ino_i64 = ino.0 as i64;
|
let ino_i64 = ino.0 as i64;
|
||||||
tokio::runtime::Builder::new_current_thread()
|
LocalOrigin::run_db_blocking(async move {
|
||||||
.enable_io()
|
LocalOrigin::save_music_metadata(ino_i64, &music_metadata, &client)
|
||||||
.enable_time()
|
.await
|
||||||
.build()
|
.unwrap();
|
||||||
.unwrap()
|
});
|
||||||
.block_on(async move {
|
|
||||||
LocalOrigin::save_music_metadata(ino_i64, &music_metadata, &client)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reply.written(written);
|
reply.written(written);
|
||||||
@@ -769,26 +884,38 @@ impl Filesystem for LocalOrigin {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if offset == 0 {
|
// Offsets are 1-based cursors: "." = 1, ".." = 2, real entries = 3+.
|
||||||
let _ = reply.add(ino, 0, fuser::FileType::Directory, &Path::new("."));
|
// The kernel passes back the offset of the last entry it received;
|
||||||
let _ = reply.add(
|
// we return entries with offset > that value.
|
||||||
parent_inode,
|
if offset < 1 {
|
||||||
1,
|
if reply.add(ino, 1, fuser::FileType::Directory, ".") {
|
||||||
fuser::FileType::Directory,
|
reply.ok();
|
||||||
&Path::new(".."),
|
return;
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if offset < 2 {
|
||||||
|
if reply.add(parent_inode, 2, fuser::FileType::Directory, "..") {
|
||||||
|
reply.ok();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let skip_count = if offset <= 2 {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(offset - 2) as usize
|
||||||
|
};
|
||||||
|
|
||||||
for (i, (key, value)) in files
|
for (i, (key, value)) in files
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|(_, v)| v.parent_inode == ino && v.inode != ino)
|
.filter(|(_, v)| v.parent_inode == ino && v.inode != ino)
|
||||||
.skip(offset as usize)
|
.skip(skip_count)
|
||||||
.enumerate()
|
.enumerate()
|
||||||
{
|
{
|
||||||
let entry_offset: u64 = 2 + i as u64;
|
let entry_offset = (skip_count + i + 3) as u64;
|
||||||
let file_type: fuser::FileType = LocalOrigin::file_type(value.file_type);
|
let file_type: fuser::FileType = LocalOrigin::file_type(value.file_type);
|
||||||
let buffer_full: bool = reply.add(*key, entry_offset, file_type, &value.name);
|
if reply.add(*key, entry_offset, file_type, &value.name) {
|
||||||
if buffer_full {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -859,23 +986,18 @@ impl Filesystem for LocalOrigin {
|
|||||||
|
|
||||||
drop(files);
|
drop(files);
|
||||||
|
|
||||||
tokio::runtime::Builder::new_current_thread()
|
LocalOrigin::run_db_blocking(async move {
|
||||||
.enable_time()
|
use sea_orm::ActiveValue::Set;
|
||||||
.enable_io()
|
ActiveModel {
|
||||||
.build()
|
inode: Set(inode_i64),
|
||||||
.unwrap()
|
name: Set(new_name_owned),
|
||||||
.block_on(async move {
|
local_path: Set(new_local_path_str),
|
||||||
use sea_orm::ActiveValue::Set;
|
..Default::default()
|
||||||
ActiveModel {
|
}
|
||||||
inode: Set(inode_i64),
|
.update(&client)
|
||||||
name: Set(new_name_owned),
|
.await
|
||||||
local_path: Set(new_local_path_str),
|
.unwrap();
|
||||||
..Default::default()
|
});
|
||||||
}
|
|
||||||
.update(&client)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
});
|
|
||||||
|
|
||||||
reply.ok();
|
reply.ok();
|
||||||
}
|
}
|
||||||
@@ -901,98 +1023,41 @@ impl Filesystem for LocalOrigin {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let original_path = item.original_path.clone();
|
let original_path = item.original_path.clone();
|
||||||
let mm = item
|
let flac = item
|
||||||
.music_metadata
|
.music_metadata
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.filter(|mm| mm.real_audio_start > 0);
|
.filter(|mm| mm.real_audio_start > 0)
|
||||||
let is_flac = mm.is_some();
|
.map(|mm| {
|
||||||
let header = mm.map(|mm| mm.header.clone()).unwrap_or_default();
|
(
|
||||||
let pic_hdrs = mm
|
mm.header.clone(),
|
||||||
.map(|mm| mm.picture_block_headers.clone())
|
mm.picture_block_headers.clone(),
|
||||||
.unwrap_or_default();
|
mm.picture_data_ranges.clone(),
|
||||||
let pic_ranges = mm
|
mm.real_audio_start,
|
||||||
.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);
|
drop(files);
|
||||||
|
|
||||||
if !is_flac {
|
// Non-FLAC files are streamed straight from the real file.
|
||||||
|
let Some((header, pic_hdrs, pic_ranges, real_audio_start)) = flac else {
|
||||||
match LocalOrigin::read_bytes_at(&original_path, offset, size as usize) {
|
match LocalOrigin::read_bytes_at(&original_path, offset, size as usize) {
|
||||||
Ok(bytes) => reply.data(&bytes),
|
Ok(bytes) => reply.data(&bytes),
|
||||||
Err(_) => reply.error(Errno::EIO),
|
Err(_) => reply.error(Errno::EIO),
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match LocalOrigin::assemble_flac_read(
|
||||||
|
&original_path,
|
||||||
|
&header,
|
||||||
|
&pic_hdrs,
|
||||||
|
&pic_ranges,
|
||||||
|
real_audio_start,
|
||||||
|
offset,
|
||||||
|
size,
|
||||||
|
) {
|
||||||
|
Ok(bytes) => reply.data(&bytes),
|
||||||
|
Err(_) => reply.error(Errno::EIO),
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
||||||
@@ -1004,7 +1069,7 @@ impl Filesystem for LocalOrigin {
|
|||||||
) {
|
) {
|
||||||
println!("lookup(parent={}, name={})", parent, name.display());
|
println!("lookup(parent={}, name={})", parent, name.display());
|
||||||
|
|
||||||
return match self
|
match self
|
||||||
.files
|
.files
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1018,7 +1083,7 @@ impl Filesystem for LocalOrigin {
|
|||||||
reply.entry(&ttl, &attr, Generation(0));
|
reply.entry(&ttl, &attr, Generation(0));
|
||||||
}
|
}
|
||||||
None => reply.error(Errno::ENOENT),
|
None => reply.error(Errno::ENOENT),
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1113,23 +1178,19 @@ impl From<(LocalItemEntity, fs::Metadata)> for LocalItem {
|
|||||||
fn from((entity, metadata): (LocalItemEntity, fs::Metadata)) -> Self {
|
fn from((entity, metadata): (LocalItemEntity, fs::Metadata)) -> Self {
|
||||||
let local_path = PathBuf::from(&entity.local_path);
|
let local_path = PathBuf::from(&entity.local_path);
|
||||||
let parent_inode = LocalOrigin::parent_inode_from_path(&local_path);
|
let parent_inode = LocalOrigin::parent_inode_from_path(&local_path);
|
||||||
let local_item = LocalItem {
|
let file_type = match entity.file_type.as_str() {
|
||||||
inode: INodeNo(entity.inode as u64),
|
"directory" => FileType::Directory,
|
||||||
parent_inode,
|
_ => FileType::File,
|
||||||
name: entity.name,
|
|
||||||
original_path: PathBuf::from(entity.original_path),
|
|
||||||
local_path,
|
|
||||||
file_type: match entity.file_type.as_str() {
|
|
||||||
"directory" => FileType::Directory,
|
|
||||||
_ => FileType::File,
|
|
||||||
},
|
|
||||||
metadata,
|
|
||||||
music_metadata: None,
|
|
||||||
hash: 0,
|
|
||||||
};
|
};
|
||||||
LocalItem {
|
LocalItem::new(
|
||||||
hash: local_item.compute_hash(),
|
INodeNo(entity.inode as u64),
|
||||||
..local_item
|
parent_inode,
|
||||||
}
|
entity.name,
|
||||||
|
PathBuf::from(entity.original_path),
|
||||||
|
local_path,
|
||||||
|
file_type,
|
||||||
|
metadata,
|
||||||
|
None,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-19
@@ -11,6 +11,20 @@ use symphonia::core::{
|
|||||||
probe::Hint,
|
probe::Hint,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const BLOCK_PADDING: u8 = 1;
|
||||||
|
const BLOCK_VORBIS_COMMENT: u8 = 4;
|
||||||
|
const BLOCK_PICTURE: u8 = 6;
|
||||||
|
const BLOCK_LAST_FLAG: u8 = 0x80;
|
||||||
|
const BLOCK_TYPE_MASK: u8 = 0x7f;
|
||||||
|
|
||||||
|
/// Parse a 4-byte FLAC metadata block header into (is_last, block_type, data_length).
|
||||||
|
fn read_block_header(hdr: &[u8; 4]) -> (bool, u8, u64) {
|
||||||
|
let is_last = (hdr[0] & BLOCK_LAST_FLAG) != 0;
|
||||||
|
let block_type = hdr[0] & BLOCK_TYPE_MASK;
|
||||||
|
let length = u32::from_be_bytes([0, hdr[1], hdr[2], hdr[3]]) as u64;
|
||||||
|
(is_last, block_type, length)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub struct MusicMetadata {
|
pub struct MusicMetadata {
|
||||||
pub artist: Vec<String>,
|
pub artist: Vec<String>,
|
||||||
@@ -94,10 +108,8 @@ impl MusicMetadata {
|
|||||||
if cursor.read_exact(&mut hdr).is_err() {
|
if cursor.read_exact(&mut hdr).is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let is_last = (hdr[0] & 0x80) != 0;
|
let (is_last, block_type, length) = read_block_header(&hdr);
|
||||||
let block_type = hdr[0] & 0x7f;
|
if block_type == BLOCK_VORBIS_COMMENT {
|
||||||
let length = u32::from_be_bytes([0, hdr[1], hdr[2], hdr[3]]) as u64;
|
|
||||||
if block_type == 4 {
|
|
||||||
self.vorbis_comment_offset = cursor.position();
|
self.vorbis_comment_offset = cursor.position();
|
||||||
self.vorbis_comment_length = length;
|
self.vorbis_comment_length = length;
|
||||||
return;
|
return;
|
||||||
@@ -146,12 +158,10 @@ fn parse_flac(path: &Path) -> Option<FlacParsed> {
|
|||||||
loop {
|
loop {
|
||||||
let mut hdr = [0u8; 4];
|
let mut hdr = [0u8; 4];
|
||||||
f.read_exact(&mut hdr).ok()?;
|
f.read_exact(&mut hdr).ok()?;
|
||||||
let is_last = (hdr[0] & 0x80) != 0;
|
let (is_last, block_type, length) = read_block_header(&hdr);
|
||||||
let block_type = hdr[0] & 0x7f;
|
|
||||||
let length = u32::from_be_bytes([0, hdr[1], hdr[2], hdr[3]]) as u64;
|
|
||||||
pos += 4;
|
pos += 4;
|
||||||
|
|
||||||
if block_type == 6 {
|
if block_type == BLOCK_PICTURE {
|
||||||
// PICTURE: keep block header (we'll fix is_last later), record data range
|
// PICTURE: keep block header (we'll fix is_last later), record data range
|
||||||
picture_block_headers.push(hdr);
|
picture_block_headers.push(hdr);
|
||||||
picture_data_ranges.push((pos, length));
|
picture_data_ranges.push((pos, length));
|
||||||
@@ -159,8 +169,8 @@ fn parse_flac(path: &Path) -> Option<FlacParsed> {
|
|||||||
} else {
|
} else {
|
||||||
let mut data = vec![0u8; length as usize];
|
let mut data = vec![0u8; length as usize];
|
||||||
f.read_exact(&mut data).ok()?;
|
f.read_exact(&mut data).ok()?;
|
||||||
// Skip VORBIS_COMMENT (type 4) — we rebuild it
|
// Skip VORBIS_COMMENT — we rebuild it
|
||||||
if block_type != 4 {
|
if block_type != BLOCK_VORBIS_COMMENT {
|
||||||
other_blocks.push((block_type, data));
|
other_blocks.push((block_type, data));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -173,7 +183,7 @@ fn parse_flac(path: &Path) -> Option<FlacParsed> {
|
|||||||
|
|
||||||
// Fix is_last on the last picture block header: it must be 1 when audio follows
|
// 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() {
|
if let Some(last_hdr) = picture_block_headers.last_mut() {
|
||||||
last_hdr[0] = 0x80 | (last_hdr[0] & 0x7f);
|
last_hdr[0] = BLOCK_LAST_FLAG | (last_hdr[0] & BLOCK_TYPE_MASK);
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(FlacParsed {
|
Some(FlacParsed {
|
||||||
@@ -191,8 +201,8 @@ fn build_flac_header(blocks: Vec<(u8, Vec<u8>)>, metadata: &MusicMetadata) -> (V
|
|||||||
let vorbis_len = vorbis.len() as u64;
|
let vorbis_len = vorbis.len() as u64;
|
||||||
|
|
||||||
let mut patched: Vec<(u8, Vec<u8>)> = blocks;
|
let mut patched: Vec<(u8, Vec<u8>)> = blocks;
|
||||||
patched.push((4, vorbis));
|
patched.push((BLOCK_VORBIS_COMMENT, vorbis));
|
||||||
patched.push((1, vec![0u8; PADDING_SIZE])); // PADDING — allows metaflac in-place writes
|
patched.push((BLOCK_PADDING, vec![0u8; PADDING_SIZE])); // allows metaflac in-place writes
|
||||||
|
|
||||||
let vorbis_idx = patched.len() - 2;
|
let vorbis_idx = patched.len() - 2;
|
||||||
let has_pictures = !metadata.picture_data_ranges.is_empty();
|
let has_pictures = !metadata.picture_data_ranges.is_empty();
|
||||||
@@ -203,7 +213,7 @@ fn build_flac_header(blocks: Vec<(u8, Vec<u8>)>, metadata: &MusicMetadata) -> (V
|
|||||||
let mut vorbis_offset = 0u64;
|
let mut vorbis_offset = 0u64;
|
||||||
for (i, (block_type, data)) in patched.iter().enumerate() {
|
for (i, (block_type, data)) in patched.iter().enumerate() {
|
||||||
let is_last_block = i == last && !has_pictures;
|
let is_last_block = i == last && !has_pictures;
|
||||||
let flag: u8 = if is_last_block { 0x80 } else { 0x00 };
|
let flag: u8 = if is_last_block { BLOCK_LAST_FLAG } else { 0x00 };
|
||||||
let length = data.len() as u32;
|
let length = data.len() as u32;
|
||||||
if i == vorbis_idx {
|
if i == vorbis_idx {
|
||||||
vorbis_offset = out.len() as u64 + 4; // data starts after 4-byte block header
|
vorbis_offset = out.len() as u64 + 4; // data starts after 4-byte block header
|
||||||
@@ -273,14 +283,12 @@ fn extract_non_vorbis_blocks(header: &[u8]) -> Vec<(u8, Vec<u8>)> {
|
|||||||
if cursor.read_exact(&mut hdr).is_err() {
|
if cursor.read_exact(&mut hdr).is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let is_last = (hdr[0] & 0x80) != 0;
|
let (is_last, block_type, length) = read_block_header(&hdr);
|
||||||
let block_type = hdr[0] & 0x7f;
|
let mut data = vec![0u8; length as usize];
|
||||||
let length = u32::from_be_bytes([0, hdr[1], hdr[2], hdr[3]]) as usize;
|
|
||||||
let mut data = vec![0u8; length];
|
|
||||||
if cursor.read_exact(&mut data).is_err() {
|
if cursor.read_exact(&mut data).is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if block_type != 4 && block_type != 1 {
|
if block_type != BLOCK_VORBIS_COMMENT && block_type != BLOCK_PADDING {
|
||||||
blocks.push((block_type, data));
|
blocks.push((block_type, data));
|
||||||
}
|
}
|
||||||
if is_last {
|
if is_last {
|
||||||
|
|||||||
Reference in New Issue
Block a user