diff --git a/Cargo.toml b/Cargo.toml index 1cdd18f..5d20d7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,7 +38,7 @@ serde_json = "1.0.104" fastrand = "2.0.0" rfd = { version = "0.12.1", features = ["xdg-portal"], default-features = false } sqlx = { version = "0.8.2", features = ["sqlite", "runtime-tokio", "macros"] } -tokio = { version = "1.32.0", features = ["full"] } +tokio = { version = "1.40.0", features = ["full"] } tracing-subscriber = { version = "0.3.17", features = ["fmt", "std", "chrono", "time", "local-time", "env-filter"] } tracing = "0.1.37" time = { version = "0.3.29", features = ["formatting", "macros"] } diff --git a/src/rust/core/Cargo.toml b/src/rust/core/Cargo.toml index 93d3d2f..2eb7196 100644 --- a/src/rust/core/Cargo.toml +++ b/src/rust/core/Cargo.toml @@ -35,7 +35,7 @@ serde_json = "1.0.104" fastrand = "2.0.0" rfd = { version = "0.12.1", features = ["xdg-portal"], default-features = false } sqlx = { version = "0.8.2", features = ["sqlite", "runtime-tokio", "macros"] } -tokio = { version = "1.32.0", features = ["full"] } +tokio = { version = "1.40.0", features = ["full", "macros"] } tracing-subscriber = { version = "0.3.17", features = ["fmt", "std", "chrono", "time", "local-time", "env-filter"] } tracing = "0.1.37" time = { version = "0.3.29", features = ["formatting", "macros"] } diff --git a/src/rust/core/file.rs b/src/rust/core/file.rs index 9192ff3..8a9aa9e 100644 --- a/src/rust/core/file.rs +++ b/src/rust/core/file.rs @@ -1,39 +1,350 @@ -use color_eyre::eyre::{eyre, Result}; -use sqlx::{query, sqlite::SqliteRow, Error, FromRow}; +use tar::{Archive, Builder}; use tracing::error; +use zstd::Encoder; +use std::{fs::{self, File}, future::Future, iter, path::{Path, PathBuf}}; +use color_eyre::eyre::{eyre, Result}; +use serde_json::Value; +use sqlx::{query, query_as, FromRow, SqliteConnection}; +use crate::{images::{get_image_from_db, Image}, kinds::ServiceItemKind, model::get_db, presentations::{get_presentation_from_db, PresKind, Presentation}, service_items::ServiceItem, slides::Background, songs::{get_song_from_db, Song}, videos::{get_video_from_db, Video}}; -use crate::{kinds::ServiceItemKind, model::get_db, service_items::ServiceItem, slides::TextAlignment, songs::Song}; +pub async fn save(list: Vec, path: impl AsRef) -> Result<()> { + let path = path.as_ref(); + let save_file = File::create(path)?; + let mut db = get_db().await; + let json = process_service_items(&list, &mut db).await?; + let archive = store_service_items(&list, &mut db, &save_file, &json).await?; + Ok(()) +} - -pub fn save(list: Vec) -> Result<()> { - let mut db = get_db(); - let rt = tokio::runtime::Runtime::new().unwrap(); - for item in list { +async fn store_service_items(items: &Vec, db: &mut SqliteConnection, save_file: &File, json: &Value) -> Result<()> { + let encoder = Encoder::new(save_file, 3).unwrap(); + let mut tar = Builder::new(encoder); + let mut temp_dir = dirs::data_dir().unwrap(); + temp_dir.push("lumina"); + let mut s: String = + iter::repeat_with(fastrand::alphanumeric) + .take(5) + .collect(); + s.insert_str(0, "temp_"); + temp_dir.push(s); + fs::create_dir_all(&temp_dir)?; + let service_file = temp_dir.join("serviceitems.json"); + fs::File::create(&service_file)?; + match fs::File::options().read(true).write(true).open(service_file) { + Ok(f) => { + serde_json::to_writer_pretty(f, json)?; + }, + Err(e) => error!("There were problems making a file i guess: {e}"), + }; + for item in items { + let background; + let audio: Option; match item.kind { ServiceItemKind::Song => { - rt.block_on(async { - let result = query(r#"SELECT vorder as "verse_order!", fontSize as "font_size!: i32", backgroundType as "background_type!", horizontalTextAlignment as "horizontal_text_alignment!", verticalTextAlignment as "vertical_text_alignment!", title as "title!", font as "font!", background as "background!", lyrics as "lyrics!", ccli as "ccli!", author as "author!", audio as "audio!", id as "id: i32" from songs where id = $1"#).bind(item.database_id).fetch_one(&mut db).await; - process_song(result); - }); + let song = get_song_from_db(item.database_id, db).await?; + background = song.background; + audio = song.audio; }, ServiceItemKind::Image => { - todo!() + let image = get_image_from_db(item.database_id, db).await?; + background = Some(Background::try_from(image.path)?); + audio = None; }, ServiceItemKind::Video => { - todo!() + let video = get_video_from_db(item.database_id, db).await?; + background = Some(Background::try_from(video.path)?); + audio = None; }, ServiceItemKind::Presentation(_) => { + let presentation = get_presentation_from_db(item.database_id, db).await?; + background = Some(Background::try_from(presentation.path)?); + audio = None; + }, + ServiceItemKind::Content => { todo!() }, + }; + if let Some(file) = audio { + let audio_file = temp_dir.join(file.file_name().expect("Audio file couldn't be added to temp_dir")); + match fs::File::create(&audio_file) { + Ok(_) => Ok(fs::copy(file, &audio_file)?), + Err(e) => Err(eyre!("Couldn't create audio file: {e}")), + }?; + }; + if let Some(file) = background { + let background_file = temp_dir.join(file.path.file_name().expect("Background file couldn't be added to temp_dir")); + match fs::File::create(&background_file) { + Ok(_) => Ok(fs::copy(file.path, &background_file)?), + Err(e) => Err(eyre!("Couldn't create background file: {e}")), + }?; + } + } + Ok(()) +} + +async fn clear_temp_dir(temp_dir: &Path) -> Result<()> { + todo!() +} + +async fn process_service_items(items: &Vec, db: &mut SqliteConnection) -> Result { + let mut values: Vec = vec![]; + for item in items { + match item.kind { + ServiceItemKind::Song => { + let value = process_song(item.database_id, db).await?; + values.push(value); + }, + ServiceItemKind::Image => { + let value = process_image(item.database_id, db).await?; + values.push(value); + }, + ServiceItemKind::Video => { + let value = process_video(item.database_id, db).await?; + values.push(value); + }, + ServiceItemKind::Presentation(_) => { + let value = process_presentation(item.database_id, db).await?; + values.push(value); + }, ServiceItemKind::Content => { todo!() }, } } - Ok(()) + let json = Value::from(values); + Ok(json) } -fn process_song(song_result: Result) -> Result<()> { - let song = Song::from_row(&song_result?)?; - Ok(()) +async fn process_song(database_id: i32, db: &mut SqliteConnection) -> Result { + let song = get_song_from_db(database_id, db).await?; + let song_json = serde_json::to_value(&song)?; + let kind_json = serde_json::to_value(ServiceItemKind::Song)?; + let json = serde_json::json!({"item": song_json, "kind": kind_json}); + Ok(json) +} + +async fn process_image(database_id: i32, db: &mut SqliteConnection) -> Result { + let image = get_image_from_db(database_id, db).await?; + let image_json = serde_json::to_value(&image)?; + let kind_json = serde_json::to_value(ServiceItemKind::Image)?; + let json = serde_json::json!({"item": image_json, "kind": kind_json}); + Ok(json) +} + +async fn process_video(database_id: i32, db: &mut SqliteConnection) -> Result { + let video = get_video_from_db(database_id, db).await?; + let video_json = serde_json::to_value(&video)?; + let kind_json = serde_json::to_value(ServiceItemKind::Video)?; + let json = serde_json::json!({"item": video_json, "kind": kind_json}); + Ok(json) +} + +async fn process_presentation(database_id: i32, db: &mut SqliteConnection) -> Result { + let presentation = get_presentation_from_db(database_id, db).await?; + let presentation_json = serde_json::to_value(&presentation)?; + let kind_json = match presentation.kind { + PresKind::Html => serde_json::to_value(ServiceItemKind::Presentation(PresKind::Html))?, + PresKind::Pdf => serde_json::to_value(ServiceItemKind::Presentation(PresKind::Pdf))?, + PresKind::Generic => serde_json::to_value(ServiceItemKind::Presentation(PresKind::Generic))?, + }; + let json = serde_json::json!({"item": presentation_json, "kind": kind_json}); + Ok(json) +} + +#[cfg(test)] +mod test { + use std::path::PathBuf; + + use fs::canonicalize; + use sqlx::Connection; + use pretty_assertions::{assert_eq, assert_ne}; + use tracing::debug; + use super::*; + + async fn get_db() -> SqliteConnection { + let mut data = dirs::data_local_dir().unwrap(); + data.push("lumina"); + data.push("library-db.sqlite3"); + let mut db_url = String::from("sqlite://"); + db_url.push_str(data.to_str().unwrap()); + SqliteConnection::connect(&db_url) + .await + .expect("problems") + } + + #[tokio::test(flavor = "current_thread")] + async fn test_process_song() { + let mut db = get_db().await; + let result = process_song(7, &mut db).await; + let json_song_file = PathBuf::from("./test/test_song.json"); + if let Ok(path) = canonicalize(json_song_file) { + debug!(file = ?&path); + if let Ok(s) = fs::read_to_string(path) { + debug!(s); + match result { + Ok(json) => assert_eq!(json.to_string(), s), + Err(e) => panic!("There was an error in processing the song: {e}"), + } + } else { + panic!("String wasn't read from file"); + } + } else { + panic!("Cannot find absolute path to test_song.json"); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn test_process_image() { + let mut db = get_db().await; + let result = process_image(3, &mut db).await; + let json_image_file = PathBuf::from("./test/test_image.json"); + if let Ok(path) = canonicalize(json_image_file) { + debug!(file = ?&path); + if let Ok(s) = fs::read_to_string(path) { + debug!(s); + match result { + Ok(json) => assert_eq!(json.to_string(), s), + Err(e) => panic!("There was an error in processing the image: {e}"), + } + } else { + panic!("String wasn't read from file"); + } + } else { + panic!("Cannot find absolute path to test_image.json"); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn test_process_video() { + let mut db = get_db().await; + let result = process_video(73, &mut db).await; + let json_video_file = PathBuf::from("./test/test_video.json"); + if let Ok(path) = canonicalize(json_video_file) { + debug!(file = ?&path); + if let Ok(s) = fs::read_to_string(path) { + debug!(s); + match result { + Ok(json) => assert_eq!(json.to_string(), s), + Err(e) => panic!("There was an error in processing the video: {e}"), + } + } else { + panic!("String wasn't read from file"); + } + } else { + panic!("Cannot find absolute path to test_video.json"); + } + } + + #[tokio::test(flavor = "current_thread")] + async fn test_process_presentation() { + let mut db = get_db().await; + let result = process_presentation(54, &mut db).await; + let json_presentation_file = PathBuf::from("./test/test_presentation.json"); + if let Ok(path) = canonicalize(json_presentation_file) { + debug!(file = ?&path); + if let Ok(s) = fs::read_to_string(path) { + debug!(s); + match result { + Ok(json) => assert_eq!(json.to_string(), s), + Err(e) => panic!("There was an error in processing the presentation: {e}"), + } + } else { + panic!("String wasn't read from file"); + } + } else { + panic!("Cannot find absolute path to test_presentation.json"); + } + } + + fn get_items() -> Vec { + let items = vec![ + ServiceItem { + database_id: 7, + kind: ServiceItemKind::Song, + id: 0, + }, + ServiceItem { + database_id: 54, + kind: ServiceItemKind::Presentation(PresKind::Html), + id: 0, + }, + ServiceItem { + database_id: 73, + kind: ServiceItemKind::Video, + id: 0, + }, + ]; + items + } + + #[tokio::test] + async fn test_service_items() { + let mut db = get_db().await; + let items = get_items(); + let json_item_file = PathBuf::from("./test/test_service_items.json"); + let result = process_service_items(&items, &mut db).await; + if let Ok(path) = canonicalize(json_item_file) { + if let Ok(s) = fs::read_to_string(path) { + match result { + Ok(strings) => assert_eq!(strings.to_string(), s), + Err(e) => panic!("There was an error: {e}"), + } + } + } + } + + // #[tokio::test] + // async fn test_save() { + // let path = PathBuf::from("~/dev/lumina/src/rust/core/test.pres"); + // let list = get_items(); + // match save(list, path).await { + // Ok(_) => assert!(true), + // Err(e) => panic!("There was an error: {e}"), + // } + // } + + #[tokio::test] + async fn test_store() { + let path = PathBuf::from("/home/chris/dev/lumina/src/rust/core/test.pres"); + let save_file = match File::create(path) { + Ok(f) => f, + Err(e) => panic!("Couldn't create save_file: {e}"), + }; + let mut db = get_db().await; + let list = get_items(); + if let Ok(json) = process_service_items(&list, &mut db).await { + println!("{:?}", json); + match store_service_items(&list, &mut db, &save_file, &json).await { + Ok(_) => assert!(true), + Err(e) => panic!("There was an error: {e}"), + } + } else { + panic!("There was an error getting the json value"); + } + } + + // #[tokio::test] + // async fn test_things() { + // let mut temp_dir = dirs::data_dir().unwrap(); + // temp_dir.push("lumina"); + // let mut s: String = + // iter::repeat_with(fastrand::alphanumeric) + // .take(5) + // .collect(); + // s.insert_str(0, "temp_"); + // temp_dir.push(s); + // let _ = fs::create_dir_all(&temp_dir); + // let mut db = get_db().await; + // let service_file = temp_dir.join("serviceitems.json"); + // let list = get_items(); + // if let Ok(json) = process_service_items(&list, &mut db).await { + // let _ = fs::File::create(&service_file); + // match fs::write(service_file, json.to_string()) { + // Ok(_) => assert!(true), + // Err(e) => panic!("There was an error: {e}"), + // } + // } else { + // panic!("There was an error getting the json value"); + // } + // } } diff --git a/src/rust/core/images.rs b/src/rust/core/images.rs index 26d2316..96a74a0 100644 --- a/src/rust/core/images.rs +++ b/src/rust/core/images.rs @@ -1,6 +1,7 @@ use crate::model::Model; +use color_eyre::eyre::Result; use serde::{Deserialize, Serialize}; -use sqlx::query_as; +use sqlx::{query_as, SqliteConnection}; use std::path::PathBuf; use tracing::error; @@ -28,6 +29,11 @@ impl Model { } } + +pub async fn get_image_from_db(database_id: i32, db: &mut SqliteConnection) -> Result { + Ok(query_as!(Image, r#"SELECT title as "title!", filePath as "path!", id as "id: i32" from images where id = ?"#, database_id).fetch_one(db).await?) +} + #[cfg(test)] mod test { use super::*; diff --git a/src/rust/core/kinds.rs b/src/rust/core/kinds.rs index 7c45a57..3660b78 100644 --- a/src/rust/core/kinds.rs +++ b/src/rust/core/kinds.rs @@ -1,8 +1,10 @@ use std::{error::Error, fmt::Display}; +use serde::{Deserialize, Serialize}; + use crate::presentations::PresKind; -#[derive(Debug, Clone, Default, PartialEq, Eq)] +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum ServiceItemKind { #[default] Song, diff --git a/src/rust/core/model.rs b/src/rust/core/model.rs index f8972a2..bb94d12 100644 --- a/src/rust/core/model.rs +++ b/src/rust/core/model.rs @@ -58,23 +58,25 @@ impl Default for Model { fn default() -> Self { Self { items: vec![], - db: get_db(), + db: { + let rt = tokio::runtime::Runtime::new().unwrap(); + rt.block_on(async { + get_db().await + }) + } } } } -pub fn get_db() -> SqliteConnection { - let rt = tokio::runtime::Runtime::new().unwrap(); +pub async fn get_db() -> SqliteConnection { let mut data = dirs::data_local_dir().unwrap(); data.push("lumina"); data.push("library-db.sqlite3"); let mut db_url = String::from("sqlite://"); db_url.push_str(data.to_str().unwrap()); - rt.block_on(async { - SqliteConnection::connect(&db_url) - .await - .expect("problems") - }) + SqliteConnection::connect(&db_url) + .await + .expect("problems") } pub trait Modeling { diff --git a/src/rust/core/presentations.rs b/src/rust/core/presentations.rs index a67adc1..8f9b33b 100644 --- a/src/rust/core/presentations.rs +++ b/src/rust/core/presentations.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; - +use color_eyre::eyre::Result; use serde::{Deserialize, Serialize}; -use sqlx::query; +use sqlx::{prelude::FromRow, query, sqlite::SqliteRow, Row, SqliteConnection}; use tracing::error; use crate::model::Model; @@ -35,6 +35,24 @@ impl Presentation { } } +impl FromRow<'_, SqliteRow> for Presentation { + fn from_row(row: &SqliteRow) -> sqlx::Result { + Ok(Self { + id: row.try_get(0)?, + title: row.try_get(1)?, + path: PathBuf::from({ + let string: String = row.try_get(2)?; + string + }), + kind: if row.try_get(3)? { + PresKind::Html + } else { + PresKind::Pdf + }, + }) + } +} + impl Model { pub fn load_from_db(&mut self) { let rt = tokio::runtime::Runtime::new().unwrap(); @@ -61,6 +79,11 @@ impl Model { } } +pub async fn get_presentation_from_db(database_id: i32, db: &mut SqliteConnection) -> Result { + let row = query(r#"SELECT id as "id: i32", title, filePath as "path", html from presentations where id = $1"#).bind(database_id).fetch_one(db).await?; + Ok(Presentation::from_row(&row)?) +} + #[cfg(test)] mod test { use super::*; diff --git a/src/rust/core/slides.rs b/src/rust/core/slides.rs index a03db94..726ffae 100644 --- a/src/rust/core/slides.rs +++ b/src/rust/core/slides.rs @@ -1,4 +1,4 @@ -use std::{error::Error, fmt::Display, path::PathBuf}; +use std::{error::Error, fmt::Display, path::{Path, PathBuf}}; use color_eyre::eyre::{eyre, Result}; use serde::{Deserialize, Serialize}; @@ -26,8 +26,8 @@ pub enum TextAlignment { #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct Background { - path: PathBuf, - kind: BackgroundKind, + pub path: PathBuf, + pub kind: BackgroundKind, } impl TryFrom for Background { @@ -36,11 +36,11 @@ impl TryFrom for Background { let value = value.trim_start_matches("file://"); let path = PathBuf::from(value); if !path.exists() { - return Err(ParseError::NonBackgroundFile) + return Err(ParseError::DoesNotExist) } let extension = value.rsplit_once('.').unwrap_or_default(); match extension.1 { - "jpg" | "png" | "webp" => Ok(Self { + "jpg" | "png" | "webp" | "html" => Ok(Self { path, kind: BackgroundKind::Image, }), @@ -62,7 +62,7 @@ impl TryFrom for Background { .to_str() .unwrap_or_default(); match extension { - "jpg" | "png" | "webp" => Ok(Self { + "jpg" | "png" | "webp" | "html" => Ok(Self { path: value, kind: BackgroundKind::Image, }), @@ -75,9 +75,24 @@ impl TryFrom for Background { } } +impl TryFrom<&str> for Background { + type Error = ParseError; + fn try_from(value: &str) -> Result { + Ok(Self::try_from(String::from(value))?) + } +} + +impl TryFrom<&Path> for Background { + type Error = ParseError; + fn try_from(value: &Path) -> Result { + Ok(Self::try_from(PathBuf::from(value))?) + } +} + #[derive(Debug)] pub enum ParseError { NonBackgroundFile, + DoesNotExist, } impl Error for ParseError {} @@ -113,6 +128,9 @@ impl Display for ParseError { Self::NonBackgroundFile => { "The file is not a recognized image or video type" } + Self::DoesNotExist => { + "This file doesn't exist" + } }; write!(f, "Error: {message}") } diff --git a/src/rust/core/songs.rs b/src/rust/core/songs.rs index 6446cc3..d259563 100644 --- a/src/rust/core/songs.rs +++ b/src/rust/core/songs.rs @@ -2,7 +2,7 @@ use std::{collections::HashMap, path::PathBuf}; use color_eyre::eyre::{eyre, Context, Result}; use serde::{Deserialize, Serialize}; -use sqlx::{query, query_as, sqlite::SqliteRow, FromRow, Row}; +use sqlx::{query, query_as, sqlite::SqliteRow, FromRow, Row, SqliteConnection}; use tracing::{debug, error}; use crate::{ @@ -76,16 +76,9 @@ impl FromRow<'_, SqliteRow> for Song { } -pub fn get_song_from_db(index: i32) -> Result { - let mut db = get_db(); - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(async { - let result = query(r#"SELECT vorder as "verse_order!", fontSize as "font_size!: i32", backgroundType as "background_type!", horizontalTextAlignment as "horizontal_text_alignment!", verticalTextAlignment as "vertical_text_alignment!", title as "title!", font as "font!", background as "background!", lyrics as "lyrics!", ccli as "ccli!", author as "author!", audio as "audio!", id as "id: i32" from songs where id = $1"#).bind(index).fetch_one(&mut db).await; - match result { - Ok(record) => Ok(Song::from_row(&record)?), - Err(e) => Err(eyre!("There was an error getting the song from the db: {e}")), - } - }) +pub async fn get_song_from_db(index: i32, db: &mut SqliteConnection) -> Result { + let row = query(r#"SELECT vorder as "verse_order!", fontSize as "font_size!: i32", backgroundType as "background_type!", horizontalTextAlignment as "horizontal_text_alignment!", verticalTextAlignment as "vertical_text_alignment!", title as "title!", font as "font!", background as "background!", lyrics as "lyrics!", ccli as "ccli!", author as "author!", audio as "audio!", id as "id: i32" from songs where id = $1"#).bind(index).fetch_one(db).await?; + Ok(Song::from_row(&row)?) } @@ -290,10 +283,10 @@ You saved my soul" } } - #[test] - pub fn test_song_from_db() { + #[tokio::test] + pub async fn test_song_from_db() { let song = test_song(); - let result = get_song_from_db(7); + let result = get_song_from_db(7, &mut get_db().await).await; match result { Ok(db_song) => assert_eq!(song, db_song), Err(e) => assert!(false, "{e}"), @@ -340,7 +333,7 @@ You saved my soul" "E1".to_string(), "E2".to_string(), ]), - background: Some(Background::try_from("file:///home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg".to_string()).unwrap()), + background: Some(Background::try_from("file:///home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg").unwrap()), text_alignment: Some(TextAlignment::MiddleCenter), font: Some("Quicksand Bold".to_string()), font_size: Some(60) diff --git a/src/rust/core/test/test_image.json b/src/rust/core/test/test_image.json new file mode 100644 index 0000000..05139fd --- /dev/null +++ b/src/rust/core/test/test_image.json @@ -0,0 +1 @@ +{"item":{"id":3,"path":"file:///home/chris/nc/tfc/Photos/NVTFC/nccq5.png","title":"nccq5"},"kind":"Image"} \ No newline at end of file diff --git a/src/rust/core/test/test_presentation.json b/src/rust/core/test/test_presentation.json new file mode 100644 index 0000000..051fcd4 --- /dev/null +++ b/src/rust/core/test/test_presentation.json @@ -0,0 +1 @@ +{"item":{"id":54,"kind":"Html","path":"file:///home/chris/docs/notes/lessons/20240327T133649--12-isaiah-and-jesus__lesson_project_tfc.html","title":"20240327T133649--12-isaiah-and-jesus__lesson_project_tfc"},"kind":{"Presentation":"Html"}} \ No newline at end of file diff --git a/src/rust/core/test/test_service_items.json b/src/rust/core/test/test_service_items.json new file mode 100644 index 0000000..c9baccc --- /dev/null +++ b/src/rust/core/test/test_service_items.json @@ -0,0 +1 @@ +[{"item":{"audio":"file:///home/chris/music/North Point InsideOut/Nothing Ordinary, Pt. 1 (Live)/05 Death Was Arrested (feat. Seth Condrey).mp3","author":"North Point Worship","background":{"kind":"Image","path":"/home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg"},"ccli":null,"font":"Quicksand Bold","font_size":60,"id":7,"lyrics":"Intro 1\nDeath Was Arrested\nNorth Point Worship\n\nVerse 1\nAlone in my sorrow\nAnd dead in my sin\n\nLost without hope\nWith no place to begin\n\nYour love made a way\nTo let mercy come in\n\nWhen death was arrested\nAnd my life began\n\nVerse 2\nAsh was redeemed\nOnly beauty remains\n\nMy orphan heart\nWas given a name\n\nMy mourning grew quiet,\nMy feet rose to dance\n\nWhen death was arrested\nAnd my life began\n\nChorus 1\nOh, Your grace so free,\nWashes over me\n\nYou have made me new,\nNow life begins with You\n\nIt's Your endless love,\nPouring down on us\n\nYou have made us new,\nNow life begins with You\n\nVerse 3\nReleased from my chains,\nI'm a prisoner no more\n\nMy shame was a ransom\nHe faithfully bore\n\nHe cancelled my debt and\nHe called me His friend\n\nWhen death was arrested\nAnd my life began\n\nVerse 4\nOur Savior displayed\nOn a criminal's cross\n\nDarkness rejoiced as though\nHeaven had lost\n\nBut then Jesus arose\nWith our freedom in hand\n\nThat's when death was arrested\nAnd my life began\n\nThat's when death was arrested\nAnd my life began\n\nBridge 1\nOh, we're free, free,\nForever we're free\n\nCome join the song\nOf all the redeemed\n\nYes, we're free, free,\nForever amen\n\nWhen death was arrested\nAnd my life began\n\nOh, we're free, free,\nForever we're free\n\nCome join the song\nOf all the redeemed\n\nYes, we're free, free,\nForever amen\n\nWhen death was arrested\nAnd my life began\n\nEnding 1\nWhen death was arrested\nAnd my life began\n\nThat's when death was arrested\nAnd my life began","text_alignment":"MiddleCenter","title":"Death Was Arrested","verse_order":["I1","V1","V2","C1","V3","C1","V4","C1","B1","B1","E1","E2"]},"kind":"Song"},{"item":{"id":54,"kind":"Html","path":"file:///home/chris/docs/notes/lessons/20240327T133649--12-isaiah-and-jesus__lesson_project_tfc.html","title":"20240327T133649--12-isaiah-and-jesus__lesson_project_tfc"},"kind":{"Presentation":"Html"}},{"item":{"end_time":0.0,"id":73,"looping":false,"path":"/home/chris/.local/share/librepresenter/ytdl/Getting started with Tokio. The ultimate starter guide to writing async Rust..webm","start_time":0.0,"title":"Getting started with Tokio. The ultimate starter guide to writing async Rust."},"kind":"Video"}] \ No newline at end of file diff --git a/src/rust/core/test/test_song.json b/src/rust/core/test/test_song.json new file mode 100644 index 0000000..f6ee120 --- /dev/null +++ b/src/rust/core/test/test_song.json @@ -0,0 +1 @@ +{"item":{"audio":"file:///home/chris/music/North Point InsideOut/Nothing Ordinary, Pt. 1 (Live)/05 Death Was Arrested (feat. Seth Condrey).mp3","author":"North Point Worship","background":{"kind":"Image","path":"/home/chris/nc/tfc/openlp/CMG - Bright Mountains 01.jpg"},"ccli":null,"font":"Quicksand Bold","font_size":60,"id":7,"lyrics":"Intro 1\nDeath Was Arrested\nNorth Point Worship\n\nVerse 1\nAlone in my sorrow\nAnd dead in my sin\n\nLost without hope\nWith no place to begin\n\nYour love made a way\nTo let mercy come in\n\nWhen death was arrested\nAnd my life began\n\nVerse 2\nAsh was redeemed\nOnly beauty remains\n\nMy orphan heart\nWas given a name\n\nMy mourning grew quiet,\nMy feet rose to dance\n\nWhen death was arrested\nAnd my life began\n\nChorus 1\nOh, Your grace so free,\nWashes over me\n\nYou have made me new,\nNow life begins with You\n\nIt's Your endless love,\nPouring down on us\n\nYou have made us new,\nNow life begins with You\n\nVerse 3\nReleased from my chains,\nI'm a prisoner no more\n\nMy shame was a ransom\nHe faithfully bore\n\nHe cancelled my debt and\nHe called me His friend\n\nWhen death was arrested\nAnd my life began\n\nVerse 4\nOur Savior displayed\nOn a criminal's cross\n\nDarkness rejoiced as though\nHeaven had lost\n\nBut then Jesus arose\nWith our freedom in hand\n\nThat's when death was arrested\nAnd my life began\n\nThat's when death was arrested\nAnd my life began\n\nBridge 1\nOh, we're free, free,\nForever we're free\n\nCome join the song\nOf all the redeemed\n\nYes, we're free, free,\nForever amen\n\nWhen death was arrested\nAnd my life began\n\nOh, we're free, free,\nForever we're free\n\nCome join the song\nOf all the redeemed\n\nYes, we're free, free,\nForever amen\n\nWhen death was arrested\nAnd my life began\n\nEnding 1\nWhen death was arrested\nAnd my life began\n\nThat's when death was arrested\nAnd my life began","text_alignment":"MiddleCenter","title":"Death Was Arrested","verse_order":["I1","V1","V2","C1","V3","C1","V4","C1","B1","B1","E1","E2"]},"kind":"Song"} \ No newline at end of file diff --git a/src/rust/core/test/test_video.json b/src/rust/core/test/test_video.json new file mode 100644 index 0000000..c4fa821 --- /dev/null +++ b/src/rust/core/test/test_video.json @@ -0,0 +1 @@ +{"item":{"end_time":0.0,"id":73,"looping":false,"path":"/home/chris/.local/share/librepresenter/ytdl/Getting started with Tokio. The ultimate starter guide to writing async Rust..webm","start_time":0.0,"title":"Getting started with Tokio. The ultimate starter guide to writing async Rust."},"kind":"Video"} \ No newline at end of file diff --git a/src/rust/core/videos.rs b/src/rust/core/videos.rs index fc41387..c7cdb3d 100644 --- a/src/rust/core/videos.rs +++ b/src/rust/core/videos.rs @@ -1,6 +1,7 @@ use crate::model::Model; +use color_eyre::eyre::Result; use serde::{Deserialize, Serialize}; -use sqlx::query_as; +use sqlx::{query_as, SqliteConnection}; use std::path::PathBuf; use tracing::error; @@ -31,6 +32,12 @@ impl Model