Implement tracks import

This commit is contained in:
Elias Projahn 2025-02-16 16:30:35 +01:00
parent fca6ce841c
commit 740ad0cf0b
3 changed files with 181 additions and 18 deletions

View file

@ -1,6 +1,6 @@
use super::tracks_editor_track_row::{PathType, TracksEditorTrackData}; use super::tracks_editor_track_row::{TrackLocation, TracksEditorTrackData};
use crate::{ use crate::{
db::models::{Recording, Work}, db::models::{Recording, Track, Work},
editor::{ editor::{
recording_editor::MusicusRecordingEditor, recording_editor::MusicusRecordingEditor,
recording_selector_popover::RecordingSelectorPopover, recording_selector_popover::RecordingSelectorPopover,
@ -37,6 +37,7 @@ mod imp {
pub recording: RefCell<Option<Recording>>, pub recording: RefCell<Option<Recording>>,
pub recordings_popover: OnceCell<RecordingSelectorPopover>, pub recordings_popover: OnceCell<RecordingSelectorPopover>,
pub track_rows: RefCell<Vec<TracksEditorTrackRow>>, pub track_rows: RefCell<Vec<TracksEditorTrackRow>>,
pub removed_tracks: RefCell<Vec<Track>>,
#[template_child] #[template_child]
pub recording_row: TemplateChild<adw::ActionRow>, pub recording_row: TemplateChild<adw::ActionRow>,
@ -196,6 +197,9 @@ impl TracksEditor {
self.imp().track_list.remove(&track_row); self.imp().track_list.remove(&track_row);
} }
// Forget previously removed tracks (see above).
self.imp().removed_tracks.borrow_mut().clear();
let tracks = self let tracks = self
.library() .library()
.tracks_for_recording(&recording.recording_id) .tracks_for_recording(&recording.recording_id)
@ -208,8 +212,7 @@ impl TracksEditor {
self.add_track_row( self.add_track_row(
recording.clone(), recording.clone(),
TracksEditorTrackData { TracksEditorTrackData {
track_id: Some(track.track_id), location: TrackLocation::Library(track.clone()),
path: PathType::Library(track.path),
parts: track.works, parts: track.works,
}, },
); );
@ -246,8 +249,7 @@ impl TracksEditor {
self.add_track_row( self.add_track_row(
recording.to_owned(), recording.to_owned(),
TracksEditorTrackData { TracksEditorTrackData {
track_id: None, location: TrackLocation::System(path),
path: PathType::System(path),
parts: next_part, parts: next_part,
}, },
); );
@ -264,6 +266,10 @@ impl TracksEditor {
#[weak(rename_to = this)] #[weak(rename_to = this)]
self, self,
move |row| { move |row| {
if let TrackLocation::Library(track) = row.track_data().location {
this.imp().removed_tracks.borrow_mut().push(track);
}
this.imp().track_list.remove(row); this.imp().track_list.remove(row);
this.imp().track_rows.borrow_mut().retain(|p| p != row); this.imp().track_rows.borrow_mut().retain(|p| p != row);
} }
@ -278,7 +284,39 @@ impl TracksEditor {
#[template_callback] #[template_callback]
fn save(&self) { fn save(&self) {
// TODO for track in self.imp().removed_tracks.borrow_mut().drain(..) {
self.library().delete_track(&track).unwrap();
}
for (index, track_row) in self.imp().track_rows.borrow_mut().drain(..).enumerate() {
let track_data = track_row.track_data();
match track_data.location {
TrackLocation::Undefined => {
log::error!("Failed to save track: Undefined track location.");
}
TrackLocation::Library(track) => self
.library()
.update_track(&track.track_id, index as i32, track_data.parts)
.unwrap(),
TrackLocation::System(path) => {
if let Some(recording) = &*self.imp().recording.borrow() {
self.library()
.import_track(
&path,
&recording.recording_id,
index as i32,
track_data.parts,
)
.unwrap();
} else {
log::error!("Failed to save track: No recording set.");
}
}
}
self.imp().track_list.remove(&track_row);
}
self.navigation().pop(); self.navigation().pop();
} }

View file

@ -1,5 +1,5 @@
use crate::{ use crate::{
db::models::{Recording, Work}, db::models::{Recording, Track, Work},
editor::tracks_editor_parts_popover::TracksEditorPartsPopover, editor::tracks_editor_parts_popover::TracksEditorPartsPopover,
library::MusicusLibrary, library::MusicusLibrary,
}; };
@ -90,10 +90,10 @@ impl TracksEditorTrackRow {
obj.set_activatable(!recording.work.parts.is_empty()); obj.set_activatable(!recording.work.parts.is_empty());
obj.set_subtitle(&match &track_data.path { obj.set_subtitle(&match &track_data.location {
PathType::None => String::new(), TrackLocation::Undefined => String::new(),
PathType::Library(path) => path.to_owned(), TrackLocation::Library(track) => track.path.clone(),
PathType::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();
match formatx!(&format_string, file_name) { match formatx!(&format_string, file_name) {
@ -178,15 +178,14 @@ impl TracksEditorTrackRow {
#[derive(Clone, Default, Debug)] #[derive(Clone, Default, Debug)]
pub struct TracksEditorTrackData { pub struct TracksEditorTrackData {
pub track_id: Option<String>, pub location: TrackLocation,
pub path: PathType,
pub parts: Vec<Work>, pub parts: Vec<Work>,
} }
#[derive(Clone, Default, Debug)] #[derive(Clone, Default, Debug)]
pub enum PathType { pub enum TrackLocation {
#[default] #[default]
None, Undefined,
Library(String), Library(Track),
System(PathBuf), System(PathBuf),
} }

View file

@ -8,13 +8,15 @@ use adw::{
prelude::*, prelude::*,
subclass::prelude::*, subclass::prelude::*,
}; };
use anyhow::Result; use anyhow::{anyhow, Result};
use chrono::prelude::*; use chrono::prelude::*;
use diesel::{dsl::exists, prelude::*, QueryDsl, SqliteConnection}; use diesel::{dsl::exists, prelude::*, QueryDsl, SqliteConnection};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::{ use std::{
cell::{OnceCell, RefCell}, cell::{OnceCell, RefCell},
ffi::OsString,
fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };
@ -1232,6 +1234,130 @@ impl MusicusLibrary {
Ok(()) Ok(())
} }
/// Import a track into the music library.
// TODO: Support mediums, think about albums.
pub fn import_track(
&self,
path: impl AsRef<Path>,
recording_id: &str,
recording_index: i32,
works: Vec<Work>,
) -> Result<()> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
let track_id = db::generate_id();
let now = Local::now().naive_local();
// TODO: Human interpretable filenames?
let mut filename = OsString::from(recording_id);
filename.push("_");
filename.push(OsString::from(format!("{recording_index:02}")));
if let Some(extension) = path.as_ref().extension() {
filename.push(".");
filename.push(extension);
};
let mut to_path = PathBuf::from(self.folder());
to_path.push(&filename);
let library_path = filename
.into_string()
.or(Err(anyhow!("Filename contains invalid Unicode.")))?;
fs::copy(path, to_path)?;
let track_data = tables::Track {
track_id: track_id.clone(),
recording_id: recording_id.to_owned(),
recording_index,
medium_id: None,
medium_index: None,
path: library_path,
created_at: now,
edited_at: now,
last_used_at: now,
last_played_at: None,
};
diesel::insert_into(tracks::table)
.values(&track_data)
.execute(connection)?;
for (index, work) in works.into_iter().enumerate() {
let track_work_data = tables::TrackWork {
track_id: track_id.clone(),
work_id: work.work_id,
sequence_number: index as i32,
};
diesel::insert_into(track_works::table)
.values(&track_work_data)
.execute(connection)?;
}
Ok(())
}
// TODO: Support mediums, think about albums.
pub fn delete_track(&self, track: &Track) -> Result<()> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
diesel::delete(track_works::table)
.filter(track_works::track_id.eq(&track.track_id))
.execute(connection)?;
diesel::delete(tracks::table)
.filter(tracks::track_id.eq(&track.track_id))
.execute(connection)?;
let mut path = PathBuf::from(self.folder());
path.push(&track.path);
fs::remove_file(path)?;
Ok(())
}
// TODO: Support mediums, think about albums.
pub fn update_track(
&self,
track_id: &str,
recording_index: i32,
works: Vec<Work>,
) -> Result<()> {
let mut binding = self.imp().connection.borrow_mut();
let connection = &mut *binding.as_mut().unwrap();
let now = Local::now().naive_local();
diesel::update(tracks::table)
.filter(tracks::track_id.eq(track_id.to_owned()))
.set((
tracks::recording_index.eq(recording_index),
tracks::edited_at.eq(now),
tracks::last_used_at.eq(now),
))
.execute(connection)?;
diesel::delete(track_works::table)
.filter(track_works::track_id.eq(track_id))
.execute(connection)?;
for (index, work) in works.into_iter().enumerate() {
let track_work_data = tables::TrackWork {
track_id: track_id.to_owned(),
work_id: work.work_id,
sequence_number: index as i32,
};
diesel::insert_into(track_works::table)
.values(&track_work_data)
.execute(connection)?;
}
Ok(())
}
pub fn connect_changed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId { pub fn connect_changed<F: Fn(&Self) + 'static>(&self, f: F) -> glib::SignalHandlerId {
self.connect_local("changed", true, move |values| { self.connect_local("changed", true, move |values| {
let obj = values[0].get::<Self>().unwrap(); let obj = values[0].get::<Self>().unwrap();