Initial commit
This commit is contained in:
commit
adec590fa2
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,24 @@
|
|||
[package]
|
||||
name = "eleanor"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Agatha Lovelace <agatha@technogothic.net>"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
adler = "1.0.2"
|
||||
dirs = "4.0.0"
|
||||
lofty = "0.7.3"
|
||||
miette = { version = "5.2.0", features = ["fancy"] }
|
||||
mime = "0.3.16"
|
||||
mime_guess = "2.0.4"
|
||||
paris = { version = "1.5.13", features = ["macros"] }
|
||||
sea-orm = { version = "0.9.1", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros"] }
|
||||
sea-orm-migration = "^0.9.0"
|
||||
sea-query = "0.26.2"
|
||||
serde = { version = "1.0.142", features = ["derive"] }
|
||||
symphonia = { version = "0.5.1", features = ["flac", "mp3", "vorbis", "ogg", "wav"] }
|
||||
tokio = { version = "1.20.1", features = ["full"] }
|
||||
toml = "0.5.9"
|
||||
walkdir = "2.3.2"
|
|
@ -0,0 +1,75 @@
|
|||
use std::{fs::File, io::Write};
|
||||
|
||||
use super::utils::config_dir;
|
||||
use miette::{miette, IntoDiagnostic, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Determines if the files will be loaded from a local path or remotely
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(untagged)]
|
||||
pub enum SourceKind {
|
||||
/// Path to a directory
|
||||
Local { path: String },
|
||||
/// Remote server address
|
||||
Remote { address: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Source {
|
||||
pub id: u8,
|
||||
pub name: String,
|
||||
#[serde(flatten)]
|
||||
pub source: SourceKind,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct Config {
|
||||
pub cache_expire_days: usize,
|
||||
pub crossfade: bool,
|
||||
pub crossfade_duration: u8,
|
||||
pub song_change_notification: bool,
|
||||
pub volume: f32,
|
||||
pub sources: Vec<Source>,
|
||||
}
|
||||
impl Config {
|
||||
pub fn read_config() -> Result<Self> {
|
||||
let file = config_dir()
|
||||
.and_then(|v| Some(v.join("settings.toml")))
|
||||
.ok_or(miette!("Configuration file not found"))?;
|
||||
|
||||
let contents = std::fs::read_to_string(file).into_diagnostic()?;
|
||||
|
||||
toml::from_str(&contents).into_diagnostic()
|
||||
}
|
||||
|
||||
pub fn write_config(config: &Config) -> Result<()> {
|
||||
let contents = toml::to_string(config).into_diagnostic()?;
|
||||
|
||||
let path = config_dir()
|
||||
.and_then(|v| Some(v.join("settings.toml")))
|
||||
.ok_or(miette!("Configuration file not found"))?;
|
||||
|
||||
File::create(path)
|
||||
.and_then(|mut v| v.write_all(contents.as_bytes()))
|
||||
.into_diagnostic()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
cache_expire_days: 30,
|
||||
crossfade: false,
|
||||
crossfade_duration: 5,
|
||||
song_change_notification: false,
|
||||
volume: 0.5,
|
||||
sources: vec![Source {
|
||||
id: 0,
|
||||
name: "Music".into(),
|
||||
source: SourceKind::Local {
|
||||
path: "/home/agatha/Music/local".into(),
|
||||
},
|
||||
}],
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,189 @@
|
|||
use std::{
|
||||
ffi::{OsStr, OsString},
|
||||
fs::File,
|
||||
hash::Hasher,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use super::{
|
||||
config::{Config, Source, SourceKind},
|
||||
model::library,
|
||||
};
|
||||
use adler::Adler32;
|
||||
use lofty::{read_from_path, Accessor, AudioFile};
|
||||
use miette::{miette, IntoDiagnostic, Result};
|
||||
use paris::{success, warn};
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, Set};
|
||||
use symphonia::{
|
||||
core::{
|
||||
io::MediaSourceStream,
|
||||
meta::{Limit, MetadataOptions},
|
||||
probe::Hint,
|
||||
},
|
||||
default::get_probe,
|
||||
};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum IndexMode {
|
||||
Purge,
|
||||
New,
|
||||
Initial,
|
||||
}
|
||||
|
||||
pub async fn index_source(source: Source, mode: IndexMode, db: &DatabaseConnection) -> Result<()> {
|
||||
let mut existing: Vec<OsString> = vec![];
|
||||
|
||||
// Force reindex source
|
||||
if mode == IndexMode::Purge {
|
||||
warn!("Overwriting source {}", source.id);
|
||||
|
||||
library::Entity::delete_many()
|
||||
.filter(library::Column::SourceId.eq(source.id))
|
||||
.exec(db)
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
// Only index new songs
|
||||
} else if mode == IndexMode::New {
|
||||
existing = library::Entity::find()
|
||||
.filter(library::Column::SourceId.eq(source.id))
|
||||
.column(library::Column::Filename)
|
||||
.all(db)
|
||||
.await
|
||||
.into_diagnostic()?
|
||||
.into_iter()
|
||||
.map(|v| v.filename.into())
|
||||
.collect();
|
||||
}
|
||||
|
||||
match source.source {
|
||||
SourceKind::Local { path } => {
|
||||
for file in WalkDir::new(path)
|
||||
.into_iter()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|e| !e.file_type().is_dir())
|
||||
.filter(|e| {
|
||||
mime_guess::from_path(e.path())
|
||||
.first()
|
||||
.and_then(|v| Some(v.type_() == mime::AUDIO))
|
||||
.unwrap_or(false)
|
||||
})
|
||||
{
|
||||
if mode == IndexMode::New {
|
||||
if existing.contains(&file.file_name().into()) {
|
||||
continue;
|
||||
};
|
||||
}
|
||||
|
||||
let audio = read_from_path(file.path(), true).into_diagnostic()?;
|
||||
|
||||
let tags = audio.first_tag();
|
||||
let properties = audio.properties();
|
||||
|
||||
let hash = hash_file(file.path())?;
|
||||
|
||||
let song: library::ActiveModel = library::ActiveModel {
|
||||
path: Set(file
|
||||
.path()
|
||||
.parent()
|
||||
.and_then(Path::to_str)
|
||||
.ok_or(miette!("Couldn't get path for file {:?}", file))?
|
||||
.to_string()),
|
||||
filename: Set(file
|
||||
.file_name()
|
||||
.to_str()
|
||||
.ok_or(miette!("Couldn't get filename for file {:?}", file))?
|
||||
.to_string()),
|
||||
source_id: Set(source.id.into()),
|
||||
hash: Set(hash.try_into().into_diagnostic()?),
|
||||
artist: Set(tags
|
||||
.and_then(|t| t.artist())
|
||||
.and_then(|t| Some(t.to_string()))),
|
||||
name: Set(tags
|
||||
.and_then(|t| t.title())
|
||||
.and_then(|t| Some(t.to_string()))),
|
||||
album: Set(tags
|
||||
.and_then(|t| t.album())
|
||||
.and_then(|t| Some(t.to_string()))),
|
||||
genres: Set(tags
|
||||
.and_then(|t| t.genre())
|
||||
.and_then(|t| Some(t.to_string()))),
|
||||
track: Set(tags.and_then(|t| t.track()).and_then(|t| Some(t as i32))),
|
||||
year: Set(tags.and_then(|t| t.year()).and_then(|t| Some(t as i32))),
|
||||
duration: Set(properties
|
||||
.duration()
|
||||
.as_millis()
|
||||
.try_into()
|
||||
.into_diagnostic()?),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
library::Entity::insert(song)
|
||||
.on_conflict(
|
||||
sea_query::OnConflict::column(library::Column::Filename)
|
||||
.do_nothing()
|
||||
.to_owned(),
|
||||
)
|
||||
.exec(db)
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
}
|
||||
}
|
||||
SourceKind::Remote { address } => {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
|
||||
success!("Indexed source {} in {:?} mode", source.id, mode);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn index_initial(db: &DatabaseConnection) -> Result<()> {
|
||||
let sources = Config::read_config()?.sources;
|
||||
|
||||
for source in sources {
|
||||
index_source(source, IndexMode::Initial, db).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn index_new(db: &DatabaseConnection) -> Result<()> {
|
||||
let sources = Config::read_config()?.sources;
|
||||
|
||||
for source in sources {
|
||||
index_source(source, IndexMode::New, db).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn hash_file(path: &Path) -> Result<u64> {
|
||||
let file = Box::new(File::open(path).into_diagnostic()?);
|
||||
|
||||
let probe = get_probe();
|
||||
|
||||
let ext = path.extension().and_then(OsStr::to_str).unwrap_or("");
|
||||
|
||||
let source = MediaSourceStream::new(file, Default::default());
|
||||
let mut data = probe
|
||||
.format(
|
||||
&Hint::new().with_extension(ext),
|
||||
source,
|
||||
&Default::default(),
|
||||
&MetadataOptions {
|
||||
limit_metadata_bytes: Limit::Maximum(0),
|
||||
limit_visual_bytes: Limit::Maximum(0),
|
||||
},
|
||||
)
|
||||
.into_diagnostic()?
|
||||
.format;
|
||||
|
||||
let mut adler = Adler32::new();
|
||||
|
||||
while let Ok(packet) = data.next_packet() {
|
||||
adler.write(&packet.data);
|
||||
}
|
||||
|
||||
Ok(adler.finish())
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
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(Song::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Song::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Song::Path).string().not_null())
|
||||
.col(
|
||||
ColumnDef::new(Song::Filename)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Song::SourceId).integer().not_null())
|
||||
.col(ColumnDef::new(Song::Hash).integer().not_null().unique_key())
|
||||
.col(ColumnDef::new(Song::Artist).string())
|
||||
.col(ColumnDef::new(Song::Name).string())
|
||||
.col(ColumnDef::new(Song::Album).string())
|
||||
.col(ColumnDef::new(Song::Duration).integer().not_null())
|
||||
.col(ColumnDef::new(Song::Genres).string())
|
||||
.col(ColumnDef::new(Song::Track).integer())
|
||||
.col(ColumnDef::new(Song::Year).integer())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Song::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// A Table containing every indexed song
|
||||
#[derive(Iden)]
|
||||
pub enum Song {
|
||||
#[iden = "library"]
|
||||
Table,
|
||||
/// Id of the song
|
||||
Id,
|
||||
Path,
|
||||
Filename,
|
||||
/// Refers to the sources defined in the configuration file and determines if file is remote
|
||||
SourceId,
|
||||
/// A hash of the song's samples as Vec<f32>
|
||||
Hash,
|
||||
Artist,
|
||||
Name,
|
||||
Album,
|
||||
Duration,
|
||||
/// Comma separated list of genres
|
||||
Genres,
|
||||
/// Number of the track in the album
|
||||
Track,
|
||||
Year,
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
use super::{m20220803_000001_create_library::Song, m20220803_000001_create_playlists::Playlist};
|
||||
|
||||
#[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(PlaylistEntry::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(PlaylistEntry::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(PlaylistEntry::PlaylistId)
|
||||
.integer()
|
||||
.not_null(),
|
||||
)
|
||||
.col(ColumnDef::new(PlaylistEntry::SongHash).integer().not_null())
|
||||
.col(ColumnDef::new(PlaylistEntry::Ordinal).integer())
|
||||
.col(ColumnDef::new(PlaylistEntry::AddedDate).integer())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk-playlist-id")
|
||||
.from(PlaylistEntry::Table, PlaylistEntry::PlaylistId)
|
||||
.to(Playlist::Table, Playlist::Id),
|
||||
)
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("fk-song-hash")
|
||||
.from(PlaylistEntry::Table, PlaylistEntry::SongHash)
|
||||
.to(Song::Table, Song::Hash),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(PlaylistEntry::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// A Table containing mappings between playlist and song
|
||||
#[derive(Iden)]
|
||||
pub enum PlaylistEntry {
|
||||
#[iden = "playlist_entries"]
|
||||
Table,
|
||||
Id,
|
||||
/// Id of playlist containing a song
|
||||
PlaylistId,
|
||||
/// Id of the song in the playlist
|
||||
SongHash,
|
||||
/// Position of song in playlist
|
||||
Ordinal,
|
||||
/// Date when the song was added
|
||||
AddedDate,
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
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(Playlist::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Playlist::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Playlist::Name).string())
|
||||
.col(ColumnDef::new(Playlist::SortOrder).string())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Playlist::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
/// A Table containing created playlists
|
||||
#[derive(Iden)]
|
||||
pub enum Playlist {
|
||||
#[iden = "playlists"]
|
||||
Table,
|
||||
/// Playlist Id
|
||||
Id,
|
||||
Name,
|
||||
SortOrder,
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
mod m20220803_000001_create_library;
|
||||
mod m20220803_000001_create_playlist_entries;
|
||||
mod m20220803_000001_create_playlists;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigratorTrait for Migrator {
|
||||
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
|
||||
vec![
|
||||
Box::new(m20220803_000001_create_library::Migration),
|
||||
Box::new(m20220803_000001_create_playlists::Migration),
|
||||
Box::new(m20220803_000001_create_playlist_entries::Migration),
|
||||
]
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
pub mod config;
|
||||
pub mod fetching;
|
||||
mod migrator;
|
||||
pub mod model;
|
||||
pub mod playback;
|
||||
pub mod utils;
|
||||
|
||||
use std::fs::{create_dir_all, File};
|
||||
|
||||
use miette::{miette, IntoDiagnostic, Result};
|
||||
use migrator::Migrator;
|
||||
use paris::success;
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
use self::{config::Config, utils::config_dir};
|
||||
|
||||
/// Create the necessary files on first run
|
||||
pub fn create_app_data() -> Result<()> {
|
||||
let path = config_dir().ok_or(miette!("Configuration directory does not exist"))?;
|
||||
|
||||
// Create Eleanor's config directory
|
||||
create_dir_all(&path).into_diagnostic()?;
|
||||
|
||||
File::create(&path.join("eleanor.db")).into_diagnostic()?;
|
||||
Config::write_config(&Default::default())?;
|
||||
success!("Created configuration file");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run unapplied migrations
|
||||
pub async fn prepare_db(db: &sea_orm::DatabaseConnection) -> Result<()> {
|
||||
Migrator::up(db, None).await.into_diagnostic()?;
|
||||
|
||||
success!("Applied migrations");
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.1
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "library")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub path: String,
|
||||
pub filename: String,
|
||||
pub source_id: i32,
|
||||
pub hash: u64,
|
||||
pub artist: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub album: Option<String>,
|
||||
pub duration: u64,
|
||||
pub genres: Option<String>,
|
||||
pub track: Option<i32>,
|
||||
pub year: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::playlist_entries::Entity")]
|
||||
PlaylistEntries,
|
||||
}
|
||||
|
||||
impl Related<super::playlist_entries::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::PlaylistEntries.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -0,0 +1,7 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.1
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod library;
|
||||
pub mod playlist_entries;
|
||||
pub mod playlists;
|
|
@ -0,0 +1,48 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.1
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "playlist_entries")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub playlist_id: i32,
|
||||
pub song_hash: i32,
|
||||
pub ordinal: Option<i32>,
|
||||
pub added_date: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::library::Entity",
|
||||
from = "Column::SongHash",
|
||||
to = "super::library::Column::Hash",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
)]
|
||||
Library,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::playlists::Entity",
|
||||
from = "Column::PlaylistId",
|
||||
to = "super::playlists::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
)]
|
||||
Playlists,
|
||||
}
|
||||
|
||||
impl Related<super::library::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Library.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::playlists::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Playlists.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -0,0 +1,26 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.1
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "playlists")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub name: Option<String>,
|
||||
pub sort_order: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::playlist_entries::Entity")]
|
||||
PlaylistEntries,
|
||||
}
|
||||
|
||||
impl Related<super::playlist_entries::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::PlaylistEntries.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -0,0 +1,5 @@
|
|||
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.1
|
||||
|
||||
pub use super::library::Entity as Library;
|
||||
pub use super::playlist_entries::Entity as PlaylistEntries;
|
||||
pub use super::playlists::Entity as Playlists;
|
|
@ -0,0 +1,18 @@
|
|||
use miette::{miette, Result};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn config_dir() -> Option<PathBuf> {
|
||||
dirs::config_dir().and_then(|v| Some(v.join("eleanor")))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn cache_dir() -> Option<PathBuf> {
|
||||
dirs::cache_dir().and_then(|v| Some(v.join("eleanor")))
|
||||
}
|
||||
|
||||
/// If no files have been created in the config directory, the app is running for the first time
|
||||
pub fn is_first_run() -> Result<bool> {
|
||||
let path = config_dir().ok_or(miette!("Configuration directory not found"))?;
|
||||
|
||||
Ok(!path.exists())
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
use backend::{
|
||||
create_app_data,
|
||||
fetching::{index_initial, index_new},
|
||||
prepare_db,
|
||||
utils::{config_dir, is_first_run},
|
||||
};
|
||||
use miette::{ensure, miette, IntoDiagnostic, Result};
|
||||
use paris::info;
|
||||
use sea_orm::{Database, DatabaseConnection};
|
||||
use sea_orm_migration::SchemaManager;
|
||||
|
||||
mod backend;
|
||||
mod gui;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// First, make sure that the app's files exist
|
||||
let first_run = is_first_run()?;
|
||||
if first_run {
|
||||
info!("No previous configuration found; Starting first run process");
|
||||
create_app_data()?;
|
||||
}
|
||||
|
||||
// Create a database connection
|
||||
let db: DatabaseConnection = Database::connect(&format!(
|
||||
"sqlite://{}/eleanor.db?mode=rwc",
|
||||
config_dir()
|
||||
.ok_or(miette!("Configuration directory not found"))?
|
||||
.display()
|
||||
))
|
||||
.await
|
||||
.into_diagnostic()?;
|
||||
|
||||
// Run migrations
|
||||
prepare_db(&db).await?;
|
||||
|
||||
let schema_manager = SchemaManager::new(&db);
|
||||
|
||||
ensure!(
|
||||
schema_manager
|
||||
.has_table("library")
|
||||
.await
|
||||
.into_diagnostic()?,
|
||||
miette!("Running migrations failed")
|
||||
);
|
||||
|
||||
if first_run {
|
||||
index_initial(&db).await?;
|
||||
} else {
|
||||
// Index only new songs
|
||||
index_new(&db).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
Loading…
Reference in New Issue