mirror of
https://github.com/johrpan/musicus.git
synced 2025-10-26 03:47:23 +01:00
Cross platform track paths
This commit is contained in:
parent
0149da36e6
commit
83789709ad
7 changed files with 102 additions and 17 deletions
1
migrations/2025-03-30-065511_json_paths/down.sql
Normal file
1
migrations/2025-03-30-065511_json_paths/down.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
UPDATE tracks SET path = (SELECT group_concat(value, '/') FROM json_each(tracks.path));
|
||||||
1
migrations/2025-03-30-065511_json_paths/up.sql
Normal file
1
migrations/2025-03-30-065511_json_paths/up.sql
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
UPDATE tracks SET path = '["' || replace(path, '/', '","') || '"]';
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! This module contains higher-level models combining information from
|
//! This module contains higher-level models combining information from
|
||||||
//! multiple database tables.
|
//! multiple database tables.
|
||||||
|
|
||||||
use std::{collections::HashSet, fmt::Display};
|
use std::{collections::HashSet, fmt::Display, path::PathBuf};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
|
@ -61,7 +61,7 @@ pub struct EnsemblePerformer {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Track {
|
pub struct Track {
|
||||||
pub track_id: String,
|
pub track_id: String,
|
||||||
pub path: String,
|
pub path: PathBuf,
|
||||||
pub works: Vec<Work>,
|
pub works: Vec<Work>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -405,7 +405,7 @@ impl Track {
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
track_id: data.track_id,
|
track_id: data.track_id,
|
||||||
path: data.path,
|
path: data.path.0,
|
||||||
works,
|
works,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,19 @@
|
||||||
//! This module contains structs that are one-to-one representations of the
|
//! This module contains structs that are one-to-one representations of the
|
||||||
//! tables in the database schema.
|
//! tables in the database schema.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, Result};
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use diesel::{prelude::*, sqlite::Sqlite};
|
use diesel::{
|
||||||
|
backend::Backend,
|
||||||
|
deserialize::{FromSql, FromSqlRow},
|
||||||
|
expression::AsExpression,
|
||||||
|
prelude::*,
|
||||||
|
serialize::{IsNull, Output, ToSql},
|
||||||
|
sql_types::Text,
|
||||||
|
sqlite::Sqlite,
|
||||||
|
};
|
||||||
use gtk::glib::{self, Boxed};
|
use gtk::glib::{self, Boxed};
|
||||||
|
|
||||||
use super::{schema::*, TranslatedString};
|
use super::{schema::*, TranslatedString};
|
||||||
|
|
@ -131,7 +142,7 @@ pub struct Track {
|
||||||
pub recording_index: i32,
|
pub recording_index: i32,
|
||||||
pub medium_id: Option<String>,
|
pub medium_id: Option<String>,
|
||||||
pub medium_index: Option<i32>,
|
pub medium_index: Option<i32>,
|
||||||
pub path: String,
|
pub path: PathBufWrapper,
|
||||||
pub created_at: NaiveDateTime,
|
pub created_at: NaiveDateTime,
|
||||||
pub edited_at: NaiveDateTime,
|
pub edited_at: NaiveDateTime,
|
||||||
pub last_used_at: NaiveDateTime,
|
pub last_used_at: NaiveDateTime,
|
||||||
|
|
@ -183,3 +194,59 @@ pub struct AlbumMedium {
|
||||||
pub medium_id: String,
|
pub medium_id: String,
|
||||||
pub sequence_number: i32,
|
pub sequence_number: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(AsExpression, FromSqlRow, Clone, Debug)]
|
||||||
|
#[diesel(sql_type = Text)]
|
||||||
|
pub struct PathBufWrapper(pub PathBuf);
|
||||||
|
|
||||||
|
impl ToSql<Text, Sqlite> for PathBufWrapper
|
||||||
|
where
|
||||||
|
String: ToSql<Text, Sqlite>,
|
||||||
|
{
|
||||||
|
fn to_sql(&self, out: &mut Output<Sqlite>) -> diesel::serialize::Result {
|
||||||
|
out.set_value(serde_json::to_string(
|
||||||
|
&self
|
||||||
|
.0
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
p.to_str()
|
||||||
|
.ok_or_else(|| anyhow!("Path contains invalid UTF-8"))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<&str>>>()?,
|
||||||
|
)?);
|
||||||
|
|
||||||
|
Ok(IsNull::No)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<DB> FromSql<Text, DB> for PathBufWrapper
|
||||||
|
where
|
||||||
|
DB: Backend,
|
||||||
|
String: FromSql<Text, DB>,
|
||||||
|
{
|
||||||
|
fn from_sql(bytes: DB::RawValue<'_>) -> diesel::deserialize::Result<Self> {
|
||||||
|
Ok(PathBufWrapper(
|
||||||
|
serde_json::from_str::<Vec<String>>(&String::from_sql(bytes)?)?
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PathBuf> for PathBufWrapper {
|
||||||
|
fn from(value: PathBuf) -> Self {
|
||||||
|
PathBufWrapper(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PathBufWrapper> for PathBuf {
|
||||||
|
fn from(value: PathBufWrapper) -> Self {
|
||||||
|
value.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRef<Path> for PathBufWrapper {
|
||||||
|
fn as_ref(&self) -> &Path {
|
||||||
|
self.0.as_ref()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ impl TracksEditorTrackRow {
|
||||||
|
|
||||||
obj.set_subtitle(&match &track_data.location {
|
obj.set_subtitle(&match &track_data.location {
|
||||||
TrackLocation::Undefined => String::new(),
|
TrackLocation::Undefined => String::new(),
|
||||||
TrackLocation::Library(track) => track.path.clone(),
|
TrackLocation::Library(track) => track.path.to_string_lossy().into_owned(),
|
||||||
TrackLocation::System(path) => {
|
TrackLocation::System(path) => {
|
||||||
let format_string = gettext("Import from {}");
|
let format_string = gettext("Import from {}");
|
||||||
let file_name = path.file_name().unwrap().to_str().unwrap();
|
let file_name = path.file_name().unwrap().to_str().unwrap();
|
||||||
|
|
|
||||||
|
|
@ -1632,9 +1632,7 @@ impl Library {
|
||||||
|
|
||||||
let mut to_path = PathBuf::from(self.folder());
|
let mut to_path = PathBuf::from(self.folder());
|
||||||
to_path.push(&filename);
|
to_path.push(&filename);
|
||||||
let library_path = filename
|
let library_path = PathBuf::from(filename);
|
||||||
.into_string()
|
|
||||||
.or(Err(anyhow!("Filename contains invalid Unicode.")))?;
|
|
||||||
|
|
||||||
fs::copy(path, to_path)?;
|
fs::copy(path, to_path)?;
|
||||||
|
|
||||||
|
|
@ -1644,7 +1642,7 @@ impl Library {
|
||||||
recording_index,
|
recording_index,
|
||||||
medium_id: None,
|
medium_id: None,
|
||||||
medium_index: None,
|
medium_index: None,
|
||||||
path: library_path,
|
path: library_path.into(),
|
||||||
created_at: now,
|
created_at: now,
|
||||||
edited_at: now,
|
edited_at: now,
|
||||||
last_used_at: now,
|
last_used_at: now,
|
||||||
|
|
@ -1822,7 +1820,7 @@ fn write_zip(
|
||||||
|
|
||||||
// Include all tracks that are part of the library.
|
// Include all tracks that are part of the library.
|
||||||
for (index, track) in tracks.into_iter().enumerate() {
|
for (index, track) in tracks.into_iter().enumerate() {
|
||||||
add_file_to_zip(&mut zip, &library_folder, &track.path)?;
|
add_file_to_zip(&mut zip, &library_folder, &path_to_zip(&track.path)?)?;
|
||||||
|
|
||||||
// Ignore if the reveiver has been dropped.
|
// Ignore if the reveiver has been dropped.
|
||||||
let _ = sender.send_blocking(ProcessMsg::Progress((index + 1) as f64 / n_tracks as f64));
|
let _ = sender.send_blocking(ProcessMsg::Progress((index + 1) as f64 / n_tracks as f64));
|
||||||
|
|
@ -1833,7 +1831,6 @@ fn write_zip(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Cross-platform paths?
|
|
||||||
fn add_file_to_zip(
|
fn add_file_to_zip(
|
||||||
zip: &mut ZipWriter<BufWriter<File>>,
|
zip: &mut ZipWriter<BufWriter<File>>,
|
||||||
library_folder: impl AsRef<Path>,
|
library_folder: impl AsRef<Path>,
|
||||||
|
|
@ -2064,9 +2061,8 @@ fn import_from_zip(
|
||||||
|
|
||||||
let n_tracks = tracks.len();
|
let n_tracks = tracks.len();
|
||||||
|
|
||||||
// TODO: Cross-platform paths?
|
|
||||||
for (index, track) in tracks.into_iter().enumerate() {
|
for (index, track) in tracks.into_iter().enumerate() {
|
||||||
let library_track_file_path = library_folder.as_ref().join(Path::new(&track.path));
|
let library_track_file_path = library_folder.as_ref().join(&track.path);
|
||||||
|
|
||||||
// Skip tracks that are already present.
|
// Skip tracks that are already present.
|
||||||
if !fs::exists(&library_track_file_path)? {
|
if !fs::exists(&library_track_file_path)? {
|
||||||
|
|
@ -2074,7 +2070,7 @@ fn import_from_zip(
|
||||||
fs::create_dir_all(parent)?;
|
fs::create_dir_all(parent)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let archive_track_file = archive.by_name(&track.path)?;
|
let archive_track_file = archive.by_name(&path_to_zip(&track.path)?)?;
|
||||||
let library_track_file = File::create(library_track_file_path)?;
|
let library_track_file = File::create(library_track_file_path)?;
|
||||||
|
|
||||||
std::io::copy(
|
std::io::copy(
|
||||||
|
|
@ -2160,3 +2156,23 @@ async fn download_tmp_file(
|
||||||
|
|
||||||
Ok(file)
|
Ok(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Convert a path to a ZIP path. ZIP files use "/" as the path separator
|
||||||
|
/// regardless of the current platform.
|
||||||
|
fn path_to_zip(path: impl AsRef<Path>) -> Result<String> {
|
||||||
|
Ok(path
|
||||||
|
.as_ref()
|
||||||
|
.iter()
|
||||||
|
.map(|p| {
|
||||||
|
p.to_str()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow!(
|
||||||
|
"Path \"{}\"contains invalid UTF-8",
|
||||||
|
path.as_ref().to_string_lossy()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.map(|s| s.to_owned())
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<String>>>()?
|
||||||
|
.join("/"))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Cell, OnceCell, RefCell},
|
cell::{Cell, OnceCell, RefCell},
|
||||||
path::PathBuf,
|
path::{Path, PathBuf},
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
|
@ -473,7 +473,7 @@ impl Player {
|
||||||
self.append(playlist)
|
self.append(playlist)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn library_path_to_file_path(&self, path: &str) -> String {
|
fn library_path_to_file_path(&self, path: impl AsRef<Path>) -> String {
|
||||||
PathBuf::from(self.library().unwrap().folder())
|
PathBuf::from(self.library().unwrap().folder())
|
||||||
.join(path)
|
.join(path)
|
||||||
.to_str()
|
.to_str()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue