mirror of
https://github.com/tsirysndr/music-player.git
synced 2026-01-09 21:28:04 -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
|
/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]
|
[dependencies.music-player-playback]
|
||||||
path = "playback"
|
path = "playback"
|
||||||
|
|
||||||
|
[dependencies.music-player-scanner]
|
||||||
|
path = "scanner"
|
||||||
|
|
||||||
|
[dependencies.music-player-entity]
|
||||||
|
path = "entity"
|
||||||
|
|
||||||
|
[dependencies.music-player-migration]
|
||||||
|
path = "migration"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = "3.2.20"
|
clap = "3.2.20"
|
||||||
cpal = "0.13.0"
|
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]
|
[dependencies.music-player-playback]
|
||||||
path = "../playback"
|
path = "../playback"
|
||||||
|
|
||||||
|
[dependencies.music-player-scanner]
|
||||||
|
path = "../scanner"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
owo-colors = "3.5.0"
|
owo-colors = "3.5.0"
|
||||||
prost = "0.11.0"
|
prost = "0.11.0"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use music_player_scanner::scan_directory;
|
||||||
|
|
||||||
use crate::api::v1alpha1::{
|
use crate::api::v1alpha1::{
|
||||||
library_service_server::LibraryService, GetAlbumDetailsRequest, GetAlbumDetailsResponse,
|
library_service_server::LibraryService, GetAlbumDetailsRequest, GetAlbumDetailsResponse,
|
||||||
GetAlbumsRequest, GetAlbumsResponse, GetArtistDetailsRequest, GetArtistDetailsResponse,
|
GetAlbumsRequest, GetAlbumsResponse, GetArtistDetailsRequest, GetArtistDetailsResponse,
|
||||||
@@ -15,6 +17,7 @@ impl LibraryService for Library {
|
|||||||
&self,
|
&self,
|
||||||
_request: tonic::Request<ScanRequest>,
|
_request: tonic::Request<ScanRequest>,
|
||||||
) -> Result<tonic::Response<ScanResponse>, tonic::Status> {
|
) -> Result<tonic::Response<ScanResponse>, tonic::Status> {
|
||||||
|
scan_directory(|song| {});
|
||||||
let response = ScanResponse {};
|
let response = ScanResponse {};
|
||||||
Ok(tonic::Response::new(response))
|
Ok(tonic::Response::new(response))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user