added music library scanner and database migrations

This commit is contained in:
Tsiry Sandratraina
2022-09-13 06:17:54 +00:00
parent cdf5957ef1
commit 9b05c80468
20 changed files with 1845 additions and 2 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
DATABASE_URL=sqlite://development.sqlite3

3
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/target
.vscode
.vscode
development.sqlite3

1477
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -23,6 +23,15 @@ path = "server"
[dependencies.music-player-playback]
path = "playback"
[dependencies.music-player-scanner]
path = "scanner"
[dependencies.music-player-entity]
path = "entity"
[dependencies.music-player-migration]
path = "migration"
[dependencies]
clap = "3.2.20"
cpal = "0.13.0"

13
entity/Cargo.toml Normal file
View File

@@ -0,0 +1,13 @@
[package]
name = "music-player-entity"
version = "0.1.0"
edition = "2021"
readme = "README.md"
repository = "https://github.com/tsirysndr/music-player"
license = "MIT"
authors = ["Tsiry Sandratraina <tsiry.sndr@aol.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
sea-orm = { version = "0.9.2", features = ["runtime-tokio-rustls", "sqlx-sqlite"] }

15
entity/src/album.rs Normal file
View File

@@ -0,0 +1,15 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "album")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: String,
pub title: String,
pub artist: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

14
entity/src/artist.rs Normal file
View File

@@ -0,0 +1,14 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "artist")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: String,
pub name: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

3
entity/src/lib.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod album;
pub mod artist;
pub mod track;

24
entity/src/track.rs Normal file
View File

@@ -0,0 +1,24 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "track")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: String,
pub title: String,
pub artist: String,
pub album: String,
pub genre: String,
pub year: Option<i32>,
pub track: Option<i32>,
pub bitrate: Option<i32>,
pub sample_rate: Option<i32>,
pub bit_depth: Option<i32>,
pub channels: Option<i32>,
pub duration: Option<i32>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl ActiveModelBehavior for ActiveModel {}

25
migration/Cargo.toml Normal file
View File

@@ -0,0 +1,25 @@
[package]
name = "music-player-migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
dotenv = "0.15.0"
[dependencies.sea-orm-migration]
version = "^0.9.0"
features = [
# Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
# View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
# e.g.
# "runtime-tokio-rustls", # `ASYNC_RUNTIME` feature
# "sqlx-postgres", # `DATABASE_DRIVER` feature
"runtime-tokio-rustls",
"sqlx-sqlite",
]

41
migration/README.md Normal file
View File

@@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- migrate generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

12
migration/src/lib.rs Normal file
View File

@@ -0,0 +1,12 @@
pub use sea_orm_migration::prelude::*;
mod m20220101_000001_create_table;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![Box::new(m20220101_000001_create_table::Migration)]
}
}

View File

@@ -0,0 +1,97 @@
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Artist::Table)
.if_not_exists()
.col(ColumnDef::new(Artist::Id).string().not_null().primary_key())
.col(ColumnDef::new(Artist::Name).string().not_null())
.to_owned(),
)
.await?;
manager
.create_table(
Table::create()
.table(Album::Table)
.if_not_exists()
.col(ColumnDef::new(Album::Id).string().not_null().primary_key())
.col(ColumnDef::new(Album::Title).string().not_null())
.col(ColumnDef::new(Album::Artist).string().not_null())
.to_owned(),
)
.await?;
manager
.create_table(
Table::create()
.table(Track::Table)
.if_not_exists()
.col(ColumnDef::new(Track::Id).string().not_null().primary_key())
.col(ColumnDef::new(Track::Title).string().not_null())
.col(ColumnDef::new(Track::Artist).string().not_null())
.col(ColumnDef::new(Track::Album).string().not_null())
.col(ColumnDef::new(Track::Genre).string().not_null())
.col(ColumnDef::new(Track::Year).integer())
.col(ColumnDef::new(Track::Track).integer())
.col(ColumnDef::new(Track::Bitrate).integer())
.col(ColumnDef::new(Track::SampleRate).integer())
.col(ColumnDef::new(Track::BitDepth).integer())
.col(ColumnDef::new(Track::Channels).integer())
.col(ColumnDef::new(Track::Duration).integer())
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Artist::Table).if_exists().to_owned())
.await?;
manager
.drop_table(Table::drop().table(Album::Table).if_exists().to_owned())
.await?;
manager
.drop_table(Table::drop().table(Track::Table).if_exists().to_owned())
.await?;
Ok(())
}
}
#[derive(Iden)]
enum Album {
Table,
Id,
Title,
Artist,
}
#[derive(Iden)]
enum Artist {
Table,
Id,
Name,
}
#[derive(Iden)]
enum Track {
Table,
Id,
Title,
Artist,
Album,
Genre,
Year,
Track,
Bitrate,
SampleRate,
BitDepth,
Channels,
Duration,
}

14
migration/src/main.rs Normal file
View File

@@ -0,0 +1,14 @@
use std::fs::File;
use sea_orm_migration::prelude::*;
#[tokio::main]
async fn main() {
File::create(
std::env::var("DATABASE_URL")
.unwrap()
.replace("sqlite://", ""),
)
.expect("Failed to create database file");
cli::run_cli(migration::Migrator).await;
}

15
scanner/Cargo.toml Normal file
View File

@@ -0,0 +1,15 @@
[package]
name = "music-player-scanner"
version = "0.1.0"
edition = "2021"
repository = "https://github.com/tsirysndr/music-player"
license = "MIT"
authors = ["Tsiry Sandratraina <tsiry.sndr@aol.com>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dirs = "4.0.0"
lofty = "0.8.1"
mime_guess = "2.0.4"
walkdir = "2.3.2"

57
scanner/src/lib.rs Normal file
View File

@@ -0,0 +1,57 @@
use lofty::{Accessor, AudioFile, LoftyError, Probe};
use types::Song;
use walkdir::WalkDir;
pub mod types;
pub fn scan_directory<F>(save: F) -> Result<Vec<Song>, LoftyError>
where
F: Fn(&Song),
{
let mut songs: Vec<Song> = Vec::new();
for entry in WalkDir::new(dirs::audio_dir().unwrap())
.follow_links(true)
.into_iter()
.filter_map(|e| e.ok())
{
let path = format!("{}", entry.path().display());
let guess = mime_guess::from_path(&path);
if guess.first_or_octet_stream() == "audio/mpeg" {
match Probe::open(&path)
.expect("ERROR: Bad path provided!")
.read(true)
{
Ok(tagged_file) => {
let tag = match tagged_file.primary_tag() {
Some(primary_tag) => primary_tag,
// If the "primary" tag doesn't exist, we just grab the
// first tag we can find. Realistically, a tag reader would likely
// iterate through the tags to find a suitable one.
None => tagged_file.first_tag().expect("ERROR: No tags found!"),
};
let properties = tagged_file.properties();
let song = Song {
title: tag.title().unwrap_or("None").to_string(),
artist: tag.artist().unwrap_or("None").to_string(),
album: tag.album().unwrap_or("None").to_string(),
genre: tag.genre().unwrap_or("None").to_string(),
year: tag.year(),
track: tag.track(),
bitrate: properties.audio_bitrate(),
sample_rate: properties.sample_rate(),
bit_depth: properties.bit_depth(),
channels: properties.channels(),
duration: properties.duration(),
};
save(&song);
songs.push(song);
}
Err(e) => println!("ERROR: {}", e),
}
}
}
Ok(songs)
}

5
scanner/src/main.rs Normal file
View File

@@ -0,0 +1,5 @@
fn main() {
music_player_scanner::scan_directory(|song| {
println!("{:?}", song);
});
}

16
scanner/src/types.rs Normal file
View File

@@ -0,0 +1,16 @@
use std::time::Duration;
#[derive(Debug)]
pub struct Song {
pub title: String,
pub artist: String,
pub album: String,
pub genre: String,
pub year: Option<u32>,
pub track: Option<u32>,
pub bitrate: Option<u32>,
pub sample_rate: Option<u32>,
pub bit_depth: Option<u8>,
pub channels: Option<u8>,
pub duration: Duration,
}

View File

@@ -14,6 +14,9 @@ keywords = ["tokio", "music", "cli", "daemon", "streaming", "player"]
[dependencies.music-player-playback]
path = "../playback"
[dependencies.music-player-scanner]
path = "../scanner"
[dependencies]
owo-colors = "3.5.0"
prost = "0.11.0"

View File

@@ -1,3 +1,5 @@
use music_player_scanner::scan_directory;
use crate::api::v1alpha1::{
library_service_server::LibraryService, GetAlbumDetailsRequest, GetAlbumDetailsResponse,
GetAlbumsRequest, GetAlbumsResponse, GetArtistDetailsRequest, GetArtistDetailsResponse,
@@ -15,6 +17,7 @@ impl LibraryService for Library {
&self,
_request: tonic::Request<ScanRequest>,
) -> Result<tonic::Response<ScanResponse>, tonic::Status> {
scan_directory(|song| {});
let response = ScanResponse {};
Ok(tonic::Response::new(response))
}