Refactor to split client from server
This commit is contained in:
Generated
+41
-7
@@ -1700,7 +1700,7 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "musicfs"
|
name = "musicfs-client"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@@ -1712,24 +1712,58 @@ dependencies = [
|
|||||||
"http",
|
"http",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
"musicfs-core",
|
||||||
|
"musicfs-proto",
|
||||||
"notify",
|
"notify",
|
||||||
"prost",
|
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"symphonia",
|
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"time",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tonic",
|
"tonic",
|
||||||
"tonic-health",
|
"tracing",
|
||||||
"tonic-prost",
|
"twox-hash",
|
||||||
"tonic-reflection",
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "musicfs-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"symphonia",
|
||||||
|
"tempfile",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"twox-hash",
|
"twox-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "musicfs-proto"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"prost",
|
||||||
|
"tonic",
|
||||||
|
"tonic-prost",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "musicfs-server"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"async-trait",
|
||||||
|
"clap",
|
||||||
|
"musicfs-core",
|
||||||
|
"musicfs-proto",
|
||||||
|
"notify",
|
||||||
|
"tempfile",
|
||||||
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tonic",
|
||||||
|
"tonic-health",
|
||||||
|
"tonic-reflection",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nix"
|
name = "nix"
|
||||||
version = "0.29.0"
|
version = "0.29.0"
|
||||||
|
|||||||
+35
-41
@@ -1,57 +1,51 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "musicfs"
|
members = [
|
||||||
|
"crates/musicfs-proto",
|
||||||
|
"crates/musicfs-core",
|
||||||
|
"crates/musicfs-server",
|
||||||
|
"crates/musicfs-client",
|
||||||
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
# `cargo run` (and devenv's `cargo run -- ...`) picks the FUSE binary, not
|
|
||||||
# musicfs-server, when two [[bin]] targets exist.
|
|
||||||
default-run = "musicfs"
|
|
||||||
|
|
||||||
[lib]
|
[workspace.dependencies]
|
||||||
name = "musicfs"
|
musicfs-proto = { path = "crates/musicfs-proto" }
|
||||||
path = "src/lib.rs"
|
musicfs-core = { path = "crates/musicfs-core" }
|
||||||
|
|
||||||
[[bin]]
|
prost = "0.14"
|
||||||
name = "musicfs"
|
tonic = "0.14"
|
||||||
path = "src/main.rs"
|
tonic-prost = "0.14"
|
||||||
|
tonic-health = "0.14"
|
||||||
|
tonic-reflection = "0.14"
|
||||||
|
|
||||||
[[bin]]
|
|
||||||
name = "musicfs-server"
|
|
||||||
path = "src/bin/musicfs-server.rs"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
clap = { version = "4.6.1", features = ["derive"] }
|
|
||||||
fuser = "0.17.0"
|
|
||||||
libc = "0.2.186"
|
|
||||||
notify = "8.2.0"
|
|
||||||
time = "0.3.49"
|
|
||||||
symphonia = { version = "0.5", default-features = false, features = [
|
symphonia = { version = "0.5", default-features = false, features = [
|
||||||
"aac", "alac", "flac", "mp3", "ogg", "vorbis", "wav"
|
"aac", "alac", "flac", "mp3", "ogg", "vorbis", "wav",
|
||||||
] }
|
] }
|
||||||
twox-hash = "2.1.2"
|
twox-hash = "2.1.2"
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
|
tracing-appender = "0.2.5"
|
||||||
|
|
||||||
sea-orm = { version = "2.0.0-rc", features = [ "sqlx-postgres", "runtime-tokio", "macros" ] }
|
tokio = { version = "1", features = [
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "signal", "fs", "io-util", "sync"] }
|
"macros", "rt-multi-thread", "signal", "fs", "io-util", "sync",
|
||||||
|
] }
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
async-trait = "0.1"
|
async-trait = "0.1"
|
||||||
bytes = "1"
|
clap = { version = "4.6.1", features = ["derive"] }
|
||||||
tonic = "0.14"
|
notify = "8.2.0"
|
||||||
tonic-health = "0.14"
|
|
||||||
tonic-prost = "0.14"
|
|
||||||
tonic-reflection = "0.14"
|
|
||||||
prost = "0.14"
|
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
|
||||||
|
fuser = "0.17.0"
|
||||||
|
libc = "0.2.186"
|
||||||
|
sea-orm = { version = "2.0.0-rc", features = [
|
||||||
|
"sqlx-postgres", "runtime-tokio", "macros",
|
||||||
|
] }
|
||||||
|
log = "0.4"
|
||||||
|
bytes = "1"
|
||||||
http = "1"
|
http = "1"
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||||
|
|
||||||
tracing = "0.1"
|
|
||||||
# For sea_orm's ConnectOptions::sqlx_logging_level, which takes a log::LevelFilter.
|
|
||||||
log = "0.4"
|
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
|
||||||
# Pinned to 0.2.5: Builder::max_log_files (used for retention) landed in the
|
|
||||||
# 0.2.3 Builder API and is current here. An unqualified "0.2" could otherwise
|
|
||||||
# resolve to an older patch without the retention method.
|
|
||||||
tracing-appender = "0.2.5"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
[package]
|
||||||
|
name = "musicfs-client"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
default-run = "musicfs"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
name = "musicfs"
|
||||||
|
path = "src/lib.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "musicfs"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
musicfs-core.workspace = true
|
||||||
|
musicfs-proto.workspace = true
|
||||||
|
fuser.workspace = true
|
||||||
|
libc.workspace = true
|
||||||
|
sea-orm.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
async-trait.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
|
notify.workspace = true
|
||||||
|
tokio-stream.workspace = true
|
||||||
|
tonic.workspace = true
|
||||||
|
bytes.workspace = true
|
||||||
|
http.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
twox-hash.workspace = true
|
||||||
|
chrono.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::{
|
use std::{
|
||||||
hash::Hasher,
|
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
time::{SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
@@ -7,7 +6,6 @@ use std::{
|
|||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
|
|
||||||
use fuser::INodeNo;
|
use fuser::INodeNo;
|
||||||
use twox_hash::XxHash64;
|
|
||||||
|
|
||||||
use crate::music::metadata::MusicMetadata;
|
use crate::music::metadata::MusicMetadata;
|
||||||
use crate::origins::attrs::FileAttrs;
|
use crate::origins::attrs::FileAttrs;
|
||||||
@@ -38,27 +36,6 @@ fn secs_since_epoch(time: SystemTime) -> u64 {
|
|||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hash inputs shared by every Item regardless of origin. Server manifests
|
|
||||||
/// and client Items both feed these same five values through xxhash, so a
|
|
||||||
/// file's hash is identical on both sides — that's what makes `Reconcile`'s
|
|
||||||
/// diff correct.
|
|
||||||
pub fn compute_item_hash(
|
|
||||||
inode: u64,
|
|
||||||
original_path: &[u8],
|
|
||||||
ctime_secs: u64,
|
|
||||||
mtime_secs: u64,
|
|
||||||
crtime_secs: u64,
|
|
||||||
) -> u64 {
|
|
||||||
let seed = 1234;
|
|
||||||
let mut hasher = XxHash64::with_seed(seed);
|
|
||||||
hasher.write_u64(inode);
|
|
||||||
hasher.write(original_path);
|
|
||||||
hasher.write_u64(ctime_secs);
|
|
||||||
hasher.write_u64(mtime_secs);
|
|
||||||
hasher.write_u64(crtime_secs);
|
|
||||||
return hasher.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Item {
|
impl Item {
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new(
|
pub fn new(
|
||||||
@@ -88,13 +65,13 @@ impl Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn compute_hash(&self) -> u64 {
|
pub fn compute_hash(&self) -> u64 {
|
||||||
return compute_item_hash(
|
musicfs_core::compute_item_hash(
|
||||||
self.inode.0,
|
self.inode.0,
|
||||||
self.original_path.as_os_str().as_bytes(),
|
self.original_path.as_os_str().as_bytes(),
|
||||||
secs_since_epoch(self.attrs.ctime),
|
secs_since_epoch(self.attrs.ctime),
|
||||||
secs_since_epoch(self.attrs.mtime),
|
secs_since_epoch(self.attrs.mtime),
|
||||||
secs_since_epoch(self.attrs.crtime),
|
secs_since_epoch(self.attrs.crtime),
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
pub mod db;
|
pub mod db;
|
||||||
pub mod item;
|
pub mod item;
|
||||||
pub mod logging;
|
|
||||||
pub mod music;
|
pub mod music;
|
||||||
pub mod origins;
|
pub mod origins;
|
||||||
pub mod proto;
|
|
||||||
pub mod server;
|
|
||||||
pub mod virtual_dirs;
|
pub mod virtual_dirs;
|
||||||
|
|
||||||
|
pub use musicfs_core::logging;
|
||||||
|
pub use musicfs_proto as proto;
|
||||||
@@ -6,11 +6,93 @@ use sea_orm::entity::prelude::*;
|
|||||||
use crate::db::entities::Model;
|
use crate::db::entities::Model;
|
||||||
use crate::item::Item;
|
use crate::item::Item;
|
||||||
use crate::music::metadata::MusicMetadata;
|
use crate::music::metadata::MusicMetadata;
|
||||||
use crate::music::metadata::db::{
|
|
||||||
artists, music_metadata as music_metadata_entity, other_tags, pictures,
|
|
||||||
};
|
|
||||||
use tracing::{debug, error};
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
mod entities {
|
||||||
|
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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use entities::{artists, music_metadata as music_metadata_entity, other_tags, pictures};
|
||||||
|
|
||||||
pub async fn save_music_metadata(
|
pub async fn save_music_metadata(
|
||||||
inode: i64,
|
inode: i64,
|
||||||
music_metadata: &MusicMetadata,
|
music_metadata: &MusicMetadata,
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod db;
|
||||||
|
pub use musicfs_core::music::{metadata, parse};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
pub mod attrs;
|
pub use musicfs_core::attrs;
|
||||||
pub mod local;
|
pub mod local;
|
||||||
pub mod network;
|
pub mod network;
|
||||||
|
|
||||||
@@ -266,10 +266,8 @@ fn manifest_entry_to_item(
|
|||||||
rdev: 0,
|
rdev: 0,
|
||||||
blksize: 4096,
|
blksize: 4096,
|
||||||
};
|
};
|
||||||
let music_metadata: Option<MusicMetadata> = entry
|
let music_metadata: Option<MusicMetadata> =
|
||||||
.music_metadata
|
entry.music_metadata.clone().map(music_metadata_from_proto);
|
||||||
.clone()
|
|
||||||
.map(|mm| MusicMetadata::from(mm));
|
|
||||||
|
|
||||||
let mut local_path = PathBuf::new();
|
let mut local_path = PathBuf::new();
|
||||||
if let Some(mm) = &music_metadata {
|
if let Some(mm) = &music_metadata {
|
||||||
@@ -303,26 +301,24 @@ fn manifest_entry_to_item(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<crate::proto::MusicMetadata> for MusicMetadata {
|
fn music_metadata_from_proto(mm: crate::proto::MusicMetadata) -> MusicMetadata {
|
||||||
fn from(mm: crate::proto::MusicMetadata) -> Self {
|
MusicMetadata {
|
||||||
return MusicMetadata {
|
artist: mm.artist,
|
||||||
artist: mm.artist,
|
album_artist: mm.album_artist,
|
||||||
album_artist: mm.album_artist,
|
album: mm.album,
|
||||||
album: mm.album,
|
track_number: mm.track_number,
|
||||||
track_number: mm.track_number,
|
track_title: mm.track_title,
|
||||||
track_title: mm.track_title,
|
other_tags: mm.other_tags,
|
||||||
other_tags: mm.other_tags,
|
header: mm.header,
|
||||||
header: mm.header,
|
picture_block_headers: mm.picture_block_headers,
|
||||||
picture_block_headers: mm.picture_block_headers,
|
picture_data_ranges: mm
|
||||||
picture_data_ranges: mm
|
.picture_data_ranges
|
||||||
.picture_data_ranges
|
.into_iter()
|
||||||
.into_iter()
|
.map(|p| (p.offset, p.length))
|
||||||
.map(|p| (p.offset, p.length))
|
.collect(),
|
||||||
.collect(),
|
real_audio_start: mm.real_audio_start,
|
||||||
real_audio_start: mm.real_audio_start,
|
vorbis_comment_offset: mm.vorbis_comment_offset,
|
||||||
vorbis_comment_offset: mm.vorbis_comment_offset,
|
vorbis_comment_length: mm.vorbis_comment_length,
|
||||||
vorbis_comment_length: mm.vorbis_comment_length,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "musicfs-core"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
symphonia.workspace = true
|
||||||
|
twox-hash.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
tracing-subscriber.workspace = true
|
||||||
|
tracing-appender.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
use std::hash::Hasher;
|
||||||
|
|
||||||
|
use twox_hash::XxHash64;
|
||||||
|
|
||||||
|
pub fn compute_item_hash(
|
||||||
|
inode: u64,
|
||||||
|
original_path: &[u8],
|
||||||
|
ctime_secs: u64,
|
||||||
|
mtime_secs: u64,
|
||||||
|
crtime_secs: u64,
|
||||||
|
) -> u64 {
|
||||||
|
let seed = 1234;
|
||||||
|
let mut hasher = XxHash64::with_seed(seed);
|
||||||
|
hasher.write_u64(inode);
|
||||||
|
hasher.write(original_path);
|
||||||
|
hasher.write_u64(ctime_secs);
|
||||||
|
hasher.write_u64(mtime_secs);
|
||||||
|
hasher.write_u64(crtime_secs);
|
||||||
|
hasher.finish()
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
pub mod attrs;
|
||||||
|
pub mod hash;
|
||||||
|
pub mod logging;
|
||||||
|
pub mod music;
|
||||||
|
|
||||||
|
pub use attrs::FileAttrs;
|
||||||
|
pub use hash::compute_item_hash;
|
||||||
|
pub use music::metadata::MusicMetadata;
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
use symphonia::core::meta::{MetadataRevision, StandardTagKey};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct MusicMetadata {
|
||||||
|
pub artist: Vec<String>,
|
||||||
|
pub album_artist: Option<String>,
|
||||||
|
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<Vec<u8>>,
|
||||||
|
pub picture_data_ranges: Vec<(u64, u64)>,
|
||||||
|
pub real_audio_start: u64,
|
||||||
|
pub vorbis_comment_offset: u64,
|
||||||
|
pub vorbis_comment_length: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MusicMetadata {
|
||||||
|
pub fn virtual_size(&self, real_file_size: u64) -> u64 {
|
||||||
|
let pictures_size: u64 = self
|
||||||
|
.picture_block_headers
|
||||||
|
.iter()
|
||||||
|
.zip(self.picture_data_ranges.iter())
|
||||||
|
.map(|(prefix, (_, len))| prefix.len() as u64 + len)
|
||||||
|
.sum();
|
||||||
|
self.header.len() as u64 + pictures_size + (real_file_size - self.real_audio_start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn extract_standard_tags(revision: &MetadataRevision, out: &mut MusicMetadata) {
|
||||||
|
for tag in revision.tags() {
|
||||||
|
let value = tag.value.to_string();
|
||||||
|
match tag.std_key {
|
||||||
|
Some(StandardTagKey::Artist) => out.artist.push(value),
|
||||||
|
Some(StandardTagKey::AlbumArtist) => out.album_artist = Some(value),
|
||||||
|
Some(StandardTagKey::Album) => out.album = value,
|
||||||
|
Some(StandardTagKey::TrackNumber) => {
|
||||||
|
out.track_number = value.parse::<i32>().unwrap_or(0)
|
||||||
|
}
|
||||||
|
Some(StandardTagKey::TrackTitle) => out.track_title = value,
|
||||||
|
_ => out.other_tags.push(format!("{}={}", tag.key, value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn virtual_size_calculation() {
|
||||||
|
let mut metadata = MusicMetadata::default();
|
||||||
|
metadata.header = vec![0u8; 100];
|
||||||
|
metadata.picture_block_headers = vec![vec![0u8; 4]];
|
||||||
|
metadata.picture_data_ranges = vec![(0, 50)];
|
||||||
|
metadata.real_audio_start = 200;
|
||||||
|
let file_size = 1000u64;
|
||||||
|
let virtual_size = metadata.virtual_size(file_size);
|
||||||
|
assert_eq!(virtual_size, 954);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
pub mod db;
|
|
||||||
pub mod encoder;
|
pub mod encoder;
|
||||||
pub mod flac;
|
pub mod flac;
|
||||||
pub mod metadata;
|
pub mod metadata;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
[package]
|
||||||
|
name = "musicfs-proto"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
prost.workspace = true
|
||||||
|
tonic.workspace = true
|
||||||
|
tonic-prost.workspace = true
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
pub mod musicfs {
|
||||||
|
include!("generated/musicfs/musicfs.rs");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use musicfs::{
|
||||||
|
ChangeEvent, FileChunk, GetFileRequest, GetManifestRequest, GetMetadataRequest, InodeHash,
|
||||||
|
ManifestEntry, MusicMetadata, PictureDataRange, ReconcileRequest, ReconcileResponse,
|
||||||
|
SubscribeEventsRequest,
|
||||||
|
music_fs_client::MusicFsClient,
|
||||||
|
music_fs_server::{MusicFs, MusicFsServer},
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
[package]
|
||||||
|
name = "musicfs-server"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
default-run = "musicfs-server"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "musicfs-server"
|
||||||
|
path = "src/bin/musicfs-server.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
musicfs-core.workspace = true
|
||||||
|
musicfs-proto.workspace = true
|
||||||
|
tonic.workspace = true
|
||||||
|
tonic-health.workspace = true
|
||||||
|
tonic-reflection.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
anyhow.workspace = true
|
||||||
|
async-trait.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
|
notify.workspace = true
|
||||||
|
tokio-stream.workspace = true
|
||||||
|
tracing.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile.workspace = true
|
||||||
@@ -2,8 +2,8 @@ use std::{net::SocketAddr, path::PathBuf};
|
|||||||
|
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use musicfs::logging::{LogConfig, init};
|
use musicfs_core::logging::{LogConfig, init};
|
||||||
use musicfs::server::{
|
use musicfs_server::server::{
|
||||||
state::ServerState,
|
state::ServerState,
|
||||||
transport::{self, TransportArgs},
|
transport::{self, TransportArgs},
|
||||||
watcher::ServerWatcher,
|
watcher::ServerWatcher,
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod server;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::item::compute_item_hash;
|
use musicfs_core::compute_item_hash;
|
||||||
use crate::music::metadata::MusicMetadata;
|
use musicfs_core::music::metadata::MusicMetadata;
|
||||||
|
|
||||||
/// One row of the in-memory manifest: every field the client needs to
|
/// One row of the in-memory manifest: every field the client needs to
|
||||||
/// reconstruct an `Item` whose hash matches the server's hash.
|
/// reconstruct an `Item` whose hash matches the server's hash.
|
||||||
@@ -8,9 +8,9 @@ use std::{
|
|||||||
|
|
||||||
use std::os::unix::fs::MetadataExt;
|
use std::os::unix::fs::MetadataExt;
|
||||||
|
|
||||||
use crate::music::parse::parse_music_metadata_for_path;
|
|
||||||
use crate::origins::attrs::FileAttrs;
|
|
||||||
use crate::server::manifest::ManifestEntry;
|
use crate::server::manifest::ManifestEntry;
|
||||||
|
use musicfs_core::FileAttrs;
|
||||||
|
use musicfs_core::music::parse::parse_music_metadata_for_path;
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
/// Server-side entry for one file. Built once on startup from a directory
|
/// Server-side entry for one file. Built once on startup from a directory
|
||||||
@@ -20,7 +20,7 @@ pub struct FileEntry {
|
|||||||
pub abs_path: PathBuf,
|
pub abs_path: PathBuf,
|
||||||
pub rel_path: String,
|
pub rel_path: String,
|
||||||
pub attrs: FileAttrs,
|
pub attrs: FileAttrs,
|
||||||
pub music_metadata: Option<crate::music::metadata::MusicMetadata>,
|
pub music_metadata: Option<musicfs_core::music::metadata::MusicMetadata>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -10,19 +10,19 @@ use async_trait::async_trait;
|
|||||||
use tokio::sync::broadcast::Receiver;
|
use tokio::sync::broadcast::Receiver;
|
||||||
use tonic::{Request, Response, Status, transport::Server};
|
use tonic::{Request, Response, Status, transport::Server};
|
||||||
|
|
||||||
use crate::music::metadata::MusicMetadata;
|
use crate::server::manifest::ManifestEntry as DomainManifestEntry;
|
||||||
use crate::proto as proto_types;
|
use crate::server::state::ServerState;
|
||||||
use crate::proto::MusicFs as MusicFsTrait;
|
use crate::server::transport::{MusicTransport, TransportArgs};
|
||||||
use crate::proto::{
|
use crate::server::watcher::{ChangeEvent, ChangeKind};
|
||||||
|
use musicfs_core::music::metadata::MusicMetadata;
|
||||||
|
use musicfs_proto as proto_types;
|
||||||
|
use musicfs_proto::MusicFs as MusicFsTrait;
|
||||||
|
use musicfs_proto::{
|
||||||
ChangeEvent as ProtoChangeEvent, FileChunk, GetFileRequest, GetManifestRequest,
|
ChangeEvent as ProtoChangeEvent, FileChunk, GetFileRequest, GetManifestRequest,
|
||||||
GetMetadataRequest, InodeHash, ManifestEntry, MusicFsServer,
|
GetMetadataRequest, InodeHash, ManifestEntry, MusicFsServer,
|
||||||
MusicMetadata as ProtoMusicMetadata, PictureDataRange, ReconcileRequest, ReconcileResponse,
|
MusicMetadata as ProtoMusicMetadata, PictureDataRange, ReconcileRequest, ReconcileResponse,
|
||||||
SubscribeEventsRequest,
|
SubscribeEventsRequest,
|
||||||
};
|
};
|
||||||
use crate::server::manifest::ManifestEntry as DomainManifestEntry;
|
|
||||||
use crate::server::state::ServerState;
|
|
||||||
use crate::server::transport::{MusicTransport, TransportArgs};
|
|
||||||
use crate::server::watcher::{ChangeEvent, ChangeKind};
|
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
pub struct GrpcTransport {
|
pub struct GrpcTransport {
|
||||||
@@ -53,11 +53,11 @@ impl MusicTransport for GrpcTransport {
|
|||||||
|
|
||||||
// v1alpha covers older clients; v1 is the current standard.
|
// v1alpha covers older clients; v1 is the current standard.
|
||||||
let reflection_v1 = tonic_reflection::server::Builder::configure()
|
let reflection_v1 = tonic_reflection::server::Builder::configure()
|
||||||
.register_encoded_file_descriptor_set(crate::proto::musicfs::FILE_DESCRIPTOR_SET)
|
.register_encoded_file_descriptor_set(musicfs_proto::musicfs::FILE_DESCRIPTOR_SET)
|
||||||
.register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
|
.register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
|
||||||
.build_v1()?;
|
.build_v1()?;
|
||||||
let reflection_v1alpha = tonic_reflection::server::Builder::configure()
|
let reflection_v1alpha = tonic_reflection::server::Builder::configure()
|
||||||
.register_encoded_file_descriptor_set(crate::proto::musicfs::FILE_DESCRIPTOR_SET)
|
.register_encoded_file_descriptor_set(musicfs_proto::musicfs::FILE_DESCRIPTOR_SET)
|
||||||
.register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
|
.register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET)
|
||||||
.build_v1alpha()?;
|
.build_v1alpha()?;
|
||||||
|
|
||||||
@@ -298,31 +298,29 @@ impl From<DomainManifestEntry> for ManifestEntry {
|
|||||||
mtime: entry.mtime,
|
mtime: entry.mtime,
|
||||||
ctime: entry.ctime,
|
ctime: entry.ctime,
|
||||||
crtime: entry.crtime,
|
crtime: entry.crtime,
|
||||||
music_metadata: entry.music_metadata.map(Into::into),
|
music_metadata: entry.music_metadata.map(music_metadata_to_proto),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<MusicMetadata> for ProtoMusicMetadata {
|
fn music_metadata_to_proto(mm: MusicMetadata) -> ProtoMusicMetadata {
|
||||||
fn from(mm: MusicMetadata) -> Self {
|
ProtoMusicMetadata {
|
||||||
return ProtoMusicMetadata {
|
artist: mm.artist,
|
||||||
artist: mm.artist,
|
album_artist: mm.album_artist,
|
||||||
album_artist: mm.album_artist,
|
album: mm.album,
|
||||||
album: mm.album,
|
track_number: mm.track_number,
|
||||||
track_number: mm.track_number,
|
track_title: mm.track_title,
|
||||||
track_title: mm.track_title,
|
other_tags: mm.other_tags,
|
||||||
other_tags: mm.other_tags,
|
header: mm.header,
|
||||||
header: mm.header,
|
picture_block_headers: mm.picture_block_headers,
|
||||||
picture_block_headers: mm.picture_block_headers,
|
picture_data_ranges: mm
|
||||||
picture_data_ranges: mm
|
.picture_data_ranges
|
||||||
.picture_data_ranges
|
.into_iter()
|
||||||
.into_iter()
|
.map(|(offset, length)| PictureDataRange { offset, length })
|
||||||
.map(|(offset, length)| PictureDataRange { offset, length })
|
.collect(),
|
||||||
.collect(),
|
real_audio_start: mm.real_audio_start,
|
||||||
real_audio_start: mm.real_audio_start,
|
vorbis_comment_offset: mm.vorbis_comment_offset,
|
||||||
vorbis_comment_offset: mm.vorbis_comment_offset,
|
vorbis_comment_length: mm.vorbis_comment_length,
|
||||||
vorbis_comment_length: mm.vorbis_comment_length,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
+4
-4
@@ -42,7 +42,7 @@
|
|||||||
processes.musicfs = {
|
processes.musicfs = {
|
||||||
after = [ "devenv:processes:postgres" ];
|
after = [ "devenv:processes:postgres" ];
|
||||||
exec = lib.mkDefault ''
|
exec = lib.mkDefault ''
|
||||||
cargo run -- \
|
cargo run -p musicfs-client -- \
|
||||||
--source /home/fujin/Music \
|
--source /home/fujin/Music \
|
||||||
--mountpoint /tmp/rust-fuse \
|
--mountpoint /tmp/rust-fuse \
|
||||||
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
|
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
processes.musicfs = {
|
processes.musicfs = {
|
||||||
after = [ "devenv:processes:postgres" ];
|
after = [ "devenv:processes:postgres" ];
|
||||||
exec = ''
|
exec = ''
|
||||||
cargo run -- \
|
cargo run -p musicfs-client -- \
|
||||||
--source /home/fujin/Music \
|
--source /home/fujin/Music \
|
||||||
--mountpoint /tmp/rust-fuse \
|
--mountpoint /tmp/rust-fuse \
|
||||||
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
|
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
processes.musicfs = {
|
processes.musicfs = {
|
||||||
after = [ "devenv:processes:postgres" ];
|
after = [ "devenv:processes:postgres" ];
|
||||||
exec = ''
|
exec = ''
|
||||||
cargo run -- \
|
cargo run -p musicfs-client -- \
|
||||||
--source http://10.185.226.145:50051 \
|
--source http://10.185.226.145:50051 \
|
||||||
--mountpoint /tmp/rust-fuse \
|
--mountpoint /tmp/rust-fuse \
|
||||||
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
|
--database "postgresql://fujin@localhost/musicfs?host=$PGHOST"
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
processes.musicfs = {
|
processes.musicfs = {
|
||||||
after = [ "devenv:processes:postgres" ];
|
after = [ "devenv:processes:postgres" ];
|
||||||
exec = ''
|
exec = ''
|
||||||
cargo run -- \
|
cargo run -p musicfs-client -- \
|
||||||
--source http://127.0.0.1:50061 \
|
--source http://127.0.0.1:50061 \
|
||||||
--mountpoint ./target/e2e/mnt \
|
--mountpoint ./target/e2e/mnt \
|
||||||
--database "postgresql://fujin@localhost/musicfs_e2e?host=$PGHOST" \
|
--database "postgresql://fujin@localhost/musicfs_e2e?host=$PGHOST" \
|
||||||
|
|||||||
+2
-2
@@ -1,8 +1,8 @@
|
|||||||
version: v2
|
version: v2
|
||||||
plugins:
|
plugins:
|
||||||
- remote: buf.build/community/neoeinstein-prost
|
- remote: buf.build/community/neoeinstein-prost
|
||||||
out: ../src/proto/generated
|
out: ../crates/musicfs-proto/src/generated
|
||||||
opt:
|
opt:
|
||||||
- file_descriptor_set
|
- file_descriptor_set
|
||||||
- remote: buf.build/community/neoeinstein-tonic
|
- remote: buf.build/community/neoeinstein-tonic
|
||||||
out: ../src/proto/generated
|
out: ../crates/musicfs-proto/src/generated
|
||||||
|
|||||||
@@ -65,9 +65,19 @@ stop_postgres() {
|
|||||||
# ──────────────────────── VM management ────────────────────────────────
|
# ──────────────────────── VM management ────────────────────────────────
|
||||||
vm_ip() { incus list "$VM_NAME" -f csv -c 4 2>/dev/null | head -1 | awk '{print $1}'; }
|
vm_ip() { incus list "$VM_NAME" -f csv -c 4 2>/dev/null | head -1 | awk '{print $1}'; }
|
||||||
|
|
||||||
|
detect_e2e_ip() {
|
||||||
|
local bridge_cidr
|
||||||
|
bridge_cidr="$(incus network get incusbr0 ipv4.address 2>/dev/null || true)"
|
||||||
|
if [[ -z "$bridge_cidr" ]]; then echo ""; return; fi
|
||||||
|
local gateway="${bridge_cidr%%/*}"
|
||||||
|
echo "${gateway%.*}.146"
|
||||||
|
}
|
||||||
|
|
||||||
start_vm() {
|
start_vm() {
|
||||||
log "starting VM '$VM_NAME' (port $PORT)…"
|
log "starting VM '$VM_NAME' (port $PORT)…"
|
||||||
|
local e2e_ip; e2e_ip="$(detect_e2e_ip)"
|
||||||
VM_NAME="$VM_NAME" \
|
VM_NAME="$VM_NAME" \
|
||||||
|
VM_IP="$e2e_ip" \
|
||||||
MUSIC_SOURCE="$MUSIC_DIR" \
|
MUSIC_SOURCE="$MUSIC_DIR" \
|
||||||
LISTEN_PORT="$PORT" \
|
LISTEN_PORT="$PORT" \
|
||||||
RUST_LOG=debug \
|
RUST_LOG=debug \
|
||||||
|
|||||||
@@ -62,9 +62,21 @@ stop_postgres() {
|
|||||||
# ──────────────────────── VM management ────────────────────────────────
|
# ──────────────────────── VM management ────────────────────────────────
|
||||||
vm_ip() { incus list "$VM_NAME" -f csv -c 4 2>/dev/null | head -1 | awk '{print $1}'; }
|
vm_ip() { incus list "$VM_NAME" -f csv -c 4 2>/dev/null | head -1 | awk '{print $1}'; }
|
||||||
|
|
||||||
|
detect_e2e_ip() {
|
||||||
|
local bridge_cidr
|
||||||
|
bridge_cidr="$(incus network get incusbr0 ipv4.address 2>/dev/null || true)"
|
||||||
|
if [[ -z "$bridge_cidr" ]]; then echo ""; return; fi
|
||||||
|
local gateway="${bridge_cidr%%/*}"
|
||||||
|
echo "${gateway%.*}.146"
|
||||||
|
}
|
||||||
|
|
||||||
start_vm() {
|
start_vm() {
|
||||||
log "starting VM '$VM_NAME' (port $PORT)…"
|
log "starting VM '$VM_NAME' (port $PORT)…"
|
||||||
|
# e2e VM uses .146 to avoid colliding with the dev VM (.145)
|
||||||
|
local e2e_ip
|
||||||
|
e2e_ip="$(detect_e2e_ip)"
|
||||||
VM_NAME="$VM_NAME" \
|
VM_NAME="$VM_NAME" \
|
||||||
|
VM_IP="$e2e_ip" \
|
||||||
MUSIC_SOURCE="$MUSIC_DIR" \
|
MUSIC_SOURCE="$MUSIC_DIR" \
|
||||||
LISTEN_PORT="$PORT" \
|
LISTEN_PORT="$PORT" \
|
||||||
RUST_LOG=debug \
|
RUST_LOG=debug \
|
||||||
|
|||||||
+1
-5
@@ -179,11 +179,7 @@ wait_for_vm() {
|
|||||||
deploy() {
|
deploy() {
|
||||||
local bundle; bundle="$(build_bundle)"
|
local bundle; bundle="$(build_bundle)"
|
||||||
log "deploying bundle to $VM_NAME:$SERVER_DIR ..."
|
log "deploying bundle to $VM_NAME:$SERVER_DIR ..."
|
||||||
# Stop first: overwriting a running binary fails with ETXTBSY ("text file busy").
|
incus exec "$VM_NAME" -- systemctl stop "$UNIT_NAME" 2>/dev/null || true
|
||||||
if incus exec "$VM_NAME" -- systemctl is-active --quiet "$UNIT_NAME" 2>/dev/null; then
|
|
||||||
log "stopping $UNIT_NAME (binary in use)..."
|
|
||||||
incus exec "$VM_NAME" -- systemctl stop "$UNIT_NAME"
|
|
||||||
fi
|
|
||||||
incus exec "$VM_NAME" -- mkdir -p "$SERVER_DIR/lib" "$LOG_DIR"
|
incus exec "$VM_NAME" -- mkdir -p "$SERVER_DIR/lib" "$LOG_DIR"
|
||||||
incus file push "$bundle/server" "$VM_NAME$SERVER_DIR/server" --mode=0755
|
incus file push "$bundle/server" "$VM_NAME$SERVER_DIR/server" --mode=0755
|
||||||
# Ship the bundled libs one by one (robust across incus file-push versions).
|
# Ship the bundled libs one by one (robust across incus file-push versions).
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
use symphonia::core::meta::{MetadataRevision, StandardTagKey};
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
|
||||||
pub struct MusicMetadata {
|
|
||||||
pub artist: Vec<String>,
|
|
||||||
pub album_artist: Option<String>,
|
|
||||||
pub album: String,
|
|
||||||
pub track_number: i32,
|
|
||||||
pub track_title: String,
|
|
||||||
pub other_tags: Vec<String>,
|
|
||||||
pub header: Vec<u8>,
|
|
||||||
/// Per-externalized-region reinsert prefix kept in memory. FLAC stores its
|
|
||||||
/// 4-byte PICTURE block header here; MP3 keeps whole frames in the original
|
|
||||||
/// file and stores an empty prefix.
|
|
||||||
pub picture_block_headers: Vec<Vec<u8>>,
|
|
||||||
pub picture_data_ranges: Vec<(u64, u64)>,
|
|
||||||
pub real_audio_start: u64,
|
|
||||||
pub vorbis_comment_offset: u64,
|
|
||||||
pub vorbis_comment_length: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MusicMetadata {
|
|
||||||
/// Size of the virtual file: rebuilt header + externalized regions + the
|
|
||||||
/// original audio tail. Format-agnostic.
|
|
||||||
pub fn virtual_size(&self, real_file_size: u64) -> u64 {
|
|
||||||
let pictures_size: u64 = self
|
|
||||||
.picture_block_headers
|
|
||||||
.iter()
|
|
||||||
.zip(self.picture_data_ranges.iter())
|
|
||||||
.map(|(prefix, (_, len))| prefix.len() as u64 + len)
|
|
||||||
.sum();
|
|
||||||
self.header.len() as u64 + pictures_size + (real_file_size - self.real_audio_start)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Map symphonia's normalized tags onto the shared `MusicMetadata` fields.
|
|
||||||
/// Used by every format parser so the field mapping lives in one place.
|
|
||||||
pub(crate) fn extract_standard_tags(revision: &MetadataRevision, out: &mut MusicMetadata) {
|
|
||||||
for tag in revision.tags() {
|
|
||||||
let value = tag.value.to_string();
|
|
||||||
match tag.std_key {
|
|
||||||
Some(StandardTagKey::Artist) => out.artist.push(value),
|
|
||||||
Some(StandardTagKey::AlbumArtist) => out.album_artist = Some(value),
|
|
||||||
Some(StandardTagKey::Album) => out.album = value,
|
|
||||||
Some(StandardTagKey::TrackNumber) => {
|
|
||||||
out.track_number = value.parse::<i32>().unwrap_or(0)
|
|
||||||
}
|
|
||||||
Some(StandardTagKey::TrackTitle) => out.track_title = value,
|
|
||||||
_ => out.other_tags.push(format!("{}={}", tag.key, value)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn virtual_size_calculation() {
|
|
||||||
let mut metadata = MusicMetadata::default();
|
|
||||||
metadata.header = vec![0u8; 100];
|
|
||||||
metadata.picture_block_headers = vec![vec![0u8; 4]];
|
|
||||||
metadata.picture_data_ranges = vec![(0, 50)];
|
|
||||||
metadata.real_audio_start = 200;
|
|
||||||
let file_size = 1000u64;
|
|
||||||
let virtual_size = metadata.virtual_size(file_size);
|
|
||||||
// 100 (header) + 4 (prefix) + 50 (picture data) + 800 (audio tail)
|
|
||||||
assert_eq!(virtual_size, 954);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
//! Generated protobuf + tonic bindings for the musicfs service.
|
|
||||||
//!
|
|
||||||
//! `buf generate` writes prost messages to `generated/musicfs/musicfs.rs` and
|
|
||||||
//! the tonic service to `generated/musicfs/musicfs.tonic.rs`. The prost plugin
|
|
||||||
//! appends `include!("musicfs.tonic.rs")` to the end of `musicfs.rs`, so
|
|
||||||
//! including that one file here also pulls in the service. The only glue buf
|
|
||||||
//! does not generate is the module named after the proto package (`musicfs`),
|
|
||||||
//! which we provide below before re-exporting the types callers use.
|
|
||||||
pub mod musicfs {
|
|
||||||
include!("generated/musicfs/musicfs.rs");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use musicfs::{
|
|
||||||
ChangeEvent, FileChunk, GetFileRequest, GetManifestRequest, GetMetadataRequest, InodeHash,
|
|
||||||
ManifestEntry, MusicMetadata, PictureDataRange, ReconcileRequest, ReconcileResponse,
|
|
||||||
SubscribeEventsRequest,
|
|
||||||
music_fs_client::MusicFsClient,
|
|
||||||
music_fs_server::{MusicFs, MusicFsServer},
|
|
||||||
};
|
|
||||||
Reference in New Issue
Block a user