add psychonaut journal import functionality

This commit is contained in:
xqtc 2025-04-25 16:53:14 +02:00
parent 6064e8e1d9
commit 7fd19193f5
Signed by: xqtc
GPG key ID: 2C064D095926D9D1
4 changed files with 329 additions and 30 deletions

128
Cargo.lock generated
View file

@ -17,6 +17,56 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
dependencies = [
"anstyle",
"once_cell",
"windows-sys 0.59.0",
]
[[package]]
name = "autocfg"
version = "1.4.0"
@ -77,6 +127,52 @@ dependencies = [
"windows-link",
]
[[package]]
name = "clap"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@ -180,6 +276,12 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]]
name = "itoa"
version = "1.0.15"
@ -226,9 +328,10 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "meowlog"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"chrono",
"clap",
"inquire",
"serde",
"serde_json",
@ -246,7 +349,7 @@ dependencies = [
"libc",
"log",
"wasi",
"windows-sys",
"windows-sys 0.48.0",
]
[[package]]
@ -449,6 +552,12 @@ dependencies = [
"sqlite3-src",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.27.1"
@ -510,6 +619,12 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -620,6 +735,15 @@ dependencies = [
"windows-targets 0.48.5",
]
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"

View file

@ -2,7 +2,7 @@
name = "meowlog"
description = "CLI tool to log the hypothetical ingestion of substances"
authors = ["xqtc"]
version = "0.1.0"
version = "0.1.1"
edition = "2024"
license = "MIT"
repository = "https://git.heroin.trade/xqtc/meowlog"
@ -16,3 +16,4 @@ serde_json = "1.0"
strum = { version = "0.27.1", features = ["derive"] }
strum_macros = "0.27.1"
sqlite = "0.37.0"
clap = { version = "4.5.37", features = ["derive"] }

138
src/import.rs Normal file
View file

@ -0,0 +1,138 @@
use chrono::{TimeZone, Utc};
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::fs::File;
use std::io::BufReader;
use crate::{IngestionEntry, Unit, db};
pub fn import_psychonaut_journal(journal_path: &str) -> Result<(), Box<dyn Error>> {
println!("Importing journal from: {}", journal_path);
let substances = crate::util::read_substances_from_file()?;
let mut substance_map: HashMap<String, String> = HashMap::new();
let mut aliases_map: HashMap<String, String> = HashMap::new();
for (name, data) in &substances {
substance_map.insert(name.to_lowercase(), name.clone());
if let Some(pretty_name) = data["pretty_name"].as_str() {
substance_map.insert(pretty_name.to_lowercase(), name.clone());
}
if let Some(aliases) = data["aliases"].as_array() {
for alias in aliases {
if let Some(alias_str) = alias.as_str() {
aliases_map.insert(alias_str.to_lowercase(), name.clone());
}
}
}
}
let file = File::open(journal_path)?;
let reader = BufReader::new(file);
let journal: Value = serde_json::from_reader(reader)?;
let conn = db::init_db_conn()?;
db::create_db()?;
let mut unknown_substances: HashSet<String> = HashSet::new();
let mut total_entries = 0;
let mut imported_entries = 0;
if let Some(experiences) = journal["experiences"].as_array() {
for experience in experiences {
if let Some(ingestions) = experience["ingestions"].as_array() {
for ingestion in ingestions {
total_entries += 1;
let substance_name = ingestion["substanceName"].as_str().unwrap_or("Unknown");
let administration_route =
ingestion["administrationRoute"].as_str().unwrap_or("ORAL");
let dose = ingestion["dose"].as_f64().unwrap_or(0.0);
let units = ingestion["units"].as_str().unwrap_or("mg");
let time_ms = ingestion["time"].as_i64().unwrap_or(0);
let ingestion_time = Utc
.timestamp_millis_opt(time_ms)
.single()
.unwrap_or_else(Utc::now)
.with_timezone(&chrono::Local);
let lowercase_name = substance_name.to_lowercase();
let mapped_name = if let Some(name) = substance_map.get(&lowercase_name) {
name.clone()
} else if let Some(name) = aliases_map.get(&lowercase_name) {
name.clone()
} else {
unknown_substances.insert(substance_name.to_string());
substance_name.to_string()
};
let route = match administration_route {
"ORAL" => "Oral",
"INSUFFLATED" => "Insufflated",
"SMOKED" => "Smoked",
"SUBLINGUAL" => "Sublingual",
"BUCCAL" => "Buccal",
"RECTAL" => "Rectal",
"INTRAVENOUS" => "Intravenous",
"INTRAMUSCULAR" => "Intramuscular",
"SUBCUTANEOUS" => "Subcutaneous",
"TRANSDERMAL" => "Transdermal",
_ => "Oral", // Default to oral if unknown
}
.to_string();
let unit = match units.to_lowercase().as_str() {
"µg" | "ug" => Unit::Ug,
"mg" => Unit::Mg,
"g" => Unit::G,
"ml" => Unit::Ml,
_ => {
println!(
"Warning: Unknown unit '{}' for {}, defaulting to mg",
units, substance_name
);
Unit::Mg // Default to mg if unknown
}
};
let entry = IngestionEntry {
substance: mapped_name,
route,
amount: dose,
unit,
ingestion_time,
created_at: Some(chrono::Local::now()),
};
match db::save_ingestion_to_db(&conn, &entry) {
Ok(_) => {
imported_entries += 1;
}
Err(e) => {
println!("Error saving entry for {}: {}", substance_name, e);
}
}
}
}
}
}
println!("Import completed!");
println!("Imported {}/{} entries", imported_entries, total_entries);
if !unknown_substances.is_empty() {
println!("\nWARNING: The following substances couldn't be mapped to the drugs database:");
for substance in unknown_substances {
println!(" - {}", substance);
}
println!("These substances were imported with their original names.");
}
Ok(())
}

View file

@ -1,4 +1,5 @@
use chrono::{DateTime, Local};
use clap::Parser;
use db::create_db;
use serde::{Deserialize, Serialize};
use std::error::Error;
@ -6,32 +7,48 @@ use std::process::exit;
use strum::EnumIter;
use strum::IntoEnumIterator;
use strum_macros::Display;
use util::gather_ingestion_data;
use util::initialize_app_directory;
mod util;
use util::gather_ingestion_data;
use util::initialize_app_directory;
use util::read_substances_from_file;
mod db;
use db::init_db_conn;
mod import;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
#[command(arg_required_else_help = false)]
struct CliArgs {
#[arg(short, long, default_value_t = false)]
import_psychonaut_journal: bool,
#[arg(short = 'j', long, required_if_eq("import_psychonaut_journal", "true"))]
journal_path: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, EnumIter)]
pub enum Command {
pub enum MeowlogCommand {
AddIngestion,
EditIngestion,
ListIngestions,
DeleteIngestion,
ShowSubstanceInfo,
Quit,
}
impl std::fmt::Display for Command {
impl std::fmt::Display for MeowlogCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let command_str = match self {
Command::AddIngestion => "Add",
Command::EditIngestion => "Edit",
Command::ListIngestions => "List",
Command::DeleteIngestion => "Delete",
Command::Quit => "Quit",
MeowlogCommand::AddIngestion => "Add",
MeowlogCommand::EditIngestion => "Edit",
MeowlogCommand::ListIngestions => "List",
MeowlogCommand::DeleteIngestion => "Delete",
MeowlogCommand::ShowSubstanceInfo => "Show substance info",
MeowlogCommand::Quit => "Quit",
};
write!(f, "{}", command_str)
}
@ -57,31 +74,49 @@ pub struct IngestionEntry {
}
fn main() -> Result<(), Box<dyn Error>> {
let substances = match read_substances_from_file() {
Ok(s) => s,
Err(e) => {
eprintln!("Error loading substances file: {}", e);
return Err(e);
let cli_args = CliArgs::parse();
match cli_args.import_psychonaut_journal {
true => {
let _ = initialize_app_directory()?;
let _ = create_db();
if let Some(p) = &cli_args.journal_path {
import::import_psychonaut_journal(p)?;
}
println!(
"Import completed. Run the program without --import-psychonaut-journal to use the CLI."
);
Ok(())
}
};
false => {
let substances = match read_substances_from_file() {
Ok(s) => s,
Err(e) => {
eprintln!("Error loading substances file: {}", e);
return Err(e);
}
};
let _ = initialize_app_directory()?;
let _ = create_db();
let _ = initialize_app_directory()?;
let _ = create_db();
println!("Successfully loaded {} substances", substances.len());
println!("Successfully loaded {} substances", substances.len());
loop {
handle_commands(substances.clone())?;
loop {
handle_commands(substances.clone())?;
}
}
}
}
fn handle_commands(
substances: std::collections::HashMap<String, serde_json::Value>,
) -> Result<(), Box<dyn Error>> {
let command =
inquire::Select::new("What do you want to do?", Command::iter().collect()).prompt()?;
let command = inquire::Select::new("What do you want to do?", MeowlogCommand::iter().collect())
.prompt()?;
match command {
Command::AddIngestion => {
MeowlogCommand::AddIngestion => {
let ingestion = gather_ingestion_data(substances.clone())?;
println!("\nRecorded Information:");
@ -121,14 +156,15 @@ fn handle_commands(
println!("Entry saved successfully");
};
}
Command::ListIngestions => {
MeowlogCommand::ListIngestions => {
util::list_ingestions()?;
}
Command::DeleteIngestion => {
MeowlogCommand::DeleteIngestion => {
util::delete_ingestion_entries(&init_db_conn()?)?;
}
Command::EditIngestion => util::edit_ingestion_entry(&init_db_conn()?)?,
Command::Quit => exit(0),
MeowlogCommand::EditIngestion => util::edit_ingestion_entry(&init_db_conn()?)?,
MeowlogCommand::ShowSubstanceInfo => {}
MeowlogCommand::Quit => exit(0),
}
Ok(())
}