-
Notifications
You must be signed in to change notification settings - Fork 204
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement file association for tracks
- Loading branch information
Showing
11 changed files
with
318 additions
and
156 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; | ||
use std::path::PathBuf; | ||
use tauri::{AppHandle, Emitter, Manager}; | ||
use tauri_plugin_dialog::{DialogExt, MessageDialogKind}; | ||
|
||
use crate::libs::track::get_track_from_file; | ||
use crate::libs::utils::is_file_valid; | ||
use crate::plugins::database::SUPPORTED_TRACKS_EXTENSIONS; | ||
|
||
use super::events::IPCEvent; | ||
|
||
/** | ||
* Linux + Windows | ||
*/ | ||
#[cfg(any(windows, target_os = "linux"))] | ||
pub fn setup_file_associations(app: tauri::App) { | ||
let mut files = Vec::new(); | ||
|
||
// NOTICE: `args` may include URL protocol (`your-app-protocol://`) | ||
// or arguments (`--`) if your app supports them. | ||
// files may aslo be passed as `file://path/to/file` | ||
for maybe_file in std::env::args().skip(1) { | ||
// skip flags like -f or --flag | ||
if maybe_file.starts_with("-") { | ||
continue; | ||
} | ||
|
||
// handle `file://` path urls and skip other urls | ||
if let Ok(url) = url::Url::parse(&maybe_file) { | ||
if let Ok(path) = url.to_file_path() { | ||
files.push(path); | ||
} | ||
} else { | ||
files.push(PathBuf::from(maybe_file)) | ||
} | ||
} | ||
|
||
handle_file_associations(app.handle().clone(), files); | ||
} | ||
|
||
/** | ||
* macOS | ||
*/ | ||
#[cfg(target_os = "macos")] | ||
pub fn setup_file_associations(app: &AppHandle, event: tauri::RunEvent) { | ||
if let tauri::RunEvent::Opened { urls } = event { | ||
let files = urls | ||
.into_iter() | ||
.filter_map(|url| url.to_file_path().ok()) | ||
.collect::<Vec<_>>(); | ||
|
||
handle_file_associations(app.clone(), files); | ||
} | ||
} | ||
|
||
/** | ||
* Handle the app file association. | ||
* For audio files, it will scan the files, create a queue and play it, *without* adding the tracks to the library. | ||
* For playlists files, not implemented. | ||
*/ | ||
fn handle_file_associations(app_handle: AppHandle, mut files: Vec<PathBuf>) { | ||
files = files | ||
.into_iter() | ||
.filter(|path| is_file_valid(path, &SUPPORTED_TRACKS_EXTENSIONS)) | ||
.collect(); | ||
|
||
// This is for the `asset:` protocol to work, ensuring access to the files | ||
let asset_protocol_scope = app_handle.asset_protocol_scope(); | ||
|
||
for file in &files { | ||
let _ = asset_protocol_scope.allow_file(file); | ||
} | ||
|
||
// Build a list of tracks, without importing them to the library | ||
let queue = files | ||
.par_iter() | ||
.map(|path| get_track_from_file(&path)) | ||
.flatten() | ||
.collect::<Vec<_>>(); | ||
|
||
let window = app_handle.get_webview_window("main").unwrap(); | ||
|
||
match window.emit(IPCEvent::PlaybackStart.as_ref(), queue) { | ||
Ok(_) => (), | ||
Err(err) => app_handle | ||
.dialog() | ||
.message(format!( | ||
"Something went wrong when attempting to play this file: {}", | ||
err | ||
)) | ||
.kind(MessageDialogKind::Error) | ||
.show(|_| {}), | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
pub mod error; | ||
pub mod events; | ||
pub mod file_associations; | ||
pub mod track; | ||
pub mod utils; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
use std::path::PathBuf; | ||
|
||
use bonsaidb::core::schema::Collection; | ||
use lofty::file::{AudioFile, TaggedFileExt}; | ||
use lofty::tag::{Accessor, ItemKey}; | ||
use log::{error, warn}; | ||
use serde::{Deserialize, Serialize}; | ||
use ts_rs::TS; | ||
use uuid::Uuid; | ||
|
||
/** | ||
* Track | ||
* represent a single track, id and path should be unique | ||
*/ | ||
#[derive(Debug, Clone, Serialize, Deserialize, Collection, TS)] | ||
#[collection(name="tracks", primary_key = String)] | ||
#[ts(export, export_to = "../../src/generated/typings/index.ts")] | ||
pub struct Track { | ||
#[natural_id] | ||
pub _id: String, | ||
pub title: String, | ||
pub album: String, | ||
pub artists: Vec<String>, | ||
pub genres: Vec<String>, | ||
pub year: Option<u32>, | ||
pub duration: u32, | ||
pub track: NumberOf, | ||
pub disk: NumberOf, | ||
pub path: PathBuf, | ||
} | ||
|
||
#[derive(Debug, Clone, Serialize, Deserialize, TS)] | ||
#[ts(export, export_to = "../../src/generated/typings/index.ts")] | ||
pub struct NumberOf { | ||
pub no: Option<u32>, | ||
pub of: Option<u32>, | ||
} | ||
|
||
/** | ||
* Generate a Track struct from a Path, or nothing if it is not a valid audio | ||
* file | ||
*/ | ||
pub fn get_track_from_file(path: &PathBuf) -> Option<Track> { | ||
match lofty::read_from_path(&path) { | ||
Ok(tagged_file) => { | ||
let tag = tagged_file.primary_tag()?; | ||
|
||
// IMPROVE ME: Is there a more idiomatic way of doing the following? | ||
let mut artists: Vec<String> = tag | ||
.get_strings(&ItemKey::TrackArtist) | ||
.map(ToString::to_string) | ||
.collect(); | ||
|
||
if artists.is_empty() { | ||
artists = tag | ||
.get_strings(&ItemKey::AlbumArtist) | ||
.map(ToString::to_string) | ||
.collect(); | ||
} | ||
|
||
if artists.is_empty() { | ||
artists = vec!["Unknown Artist".into()]; | ||
} | ||
|
||
let Some(id) = get_track_id_for_path(path) else { | ||
return None; | ||
}; | ||
|
||
Some(Track { | ||
_id: id, | ||
title: tag | ||
.get_string(&ItemKey::TrackTitle) | ||
.unwrap_or("Unknown") | ||
.to_string(), | ||
album: tag | ||
.get_string(&ItemKey::AlbumTitle) | ||
.unwrap_or("Unknown") | ||
.to_string(), | ||
artists, | ||
genres: tag | ||
.get_strings(&ItemKey::Genre) | ||
.map(ToString::to_string) | ||
.collect(), | ||
year: tag.year(), | ||
duration: u32::try_from(tagged_file.properties().duration().as_secs()).unwrap_or(0), | ||
track: NumberOf { | ||
no: tag.track(), | ||
of: tag.track_total(), | ||
}, | ||
disk: NumberOf { | ||
no: tag.disk(), | ||
of: tag.disk_total(), | ||
}, | ||
path: path.to_owned(), | ||
}) | ||
} | ||
Err(err) => { | ||
warn!("Failed to get ID3 tags: \"{}\". File {:?}", err, path); | ||
None | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Generate an ID for a track based on its location. | ||
* | ||
* We leverage UUID v3 on tracks paths to easily retrieve tracks by path. | ||
* This is not great and ideally we should use a DB view instead. One day. | ||
*/ | ||
pub fn get_track_id_for_path(path: &PathBuf) -> Option<String> { | ||
match std::fs::canonicalize(path) { | ||
Ok(canonicalized_path) => { | ||
return Some( | ||
Uuid::new_v3( | ||
&Uuid::NAMESPACE_OID, | ||
canonicalized_path.to_string_lossy().as_bytes(), | ||
) | ||
.to_string(), | ||
); | ||
} | ||
Err(err) => { | ||
error!(r#"ID could not be generated for path {:?}: {}"#, path, err); | ||
return None; | ||
} | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.