mirror of
https://github.com/tsirysndr/music-player.git
synced 2026-01-08 20:58:07 -05:00
added music library scanner and database migrations
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
.vscode
|
||||
.vscode
|
||||
development.sqlite3
|
||||
1477
Cargo.lock
generated
1477
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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
13
entity/Cargo.toml
Normal 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
15
entity/src/album.rs
Normal 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
14
entity/src/artist.rs
Normal 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
3
entity/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod album;
|
||||
pub mod artist;
|
||||
pub mod track;
|
||||
24
entity/src/track.rs
Normal file
24
entity/src/track.rs
Normal 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
25
migration/Cargo.toml
Normal 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
41
migration/README.md
Normal 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
12
migration/src/lib.rs
Normal 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)]
|
||||
}
|
||||
}
|
||||
97
migration/src/m20220101_000001_create_table.rs
Normal file
97
migration/src/m20220101_000001_create_table.rs
Normal 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
14
migration/src/main.rs
Normal 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
15
scanner/Cargo.toml
Normal 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
57
scanner/src/lib.rs
Normal 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
5
scanner/src/main.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
fn main() {
|
||||
music_player_scanner::scan_directory(|song| {
|
||||
println!("{:?}", song);
|
||||
});
|
||||
}
|
||||
16
scanner/src/types.rs
Normal file
16
scanner/src/types.rs
Normal 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,
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user