Cross platform track paths

This commit is contained in:
Elias Projahn 2025-03-30 10:52:27 +02:00
parent 0149da36e6
commit 83789709ad
7 changed files with 102 additions and 17 deletions

View file

@ -0,0 +1 @@
UPDATE tracks SET path = (SELECT group_concat(value, '/') FROM json_each(tracks.path));

View file

@ -0,0 +1 @@
UPDATE tracks SET path = '["' || replace(path, '/', '","') || '"]';

View file

@ -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,
}) })
} }

View file

@ -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()
}
}

View file

@ -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();

View file

@ -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("/"))
}

View file

@ -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()