about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-07-24 16:01:21 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-07-24 16:03:08 +0200
commitf6eb32ae50a21d0d3b0ed0e992f3871d59966743 (patch)
tree0737fb67ffd779fc907a74d0788ea90e64f20c36
parentfeat(crates/yt/commands/watch/mpv_commands): Hook-up the new show commands (diff)
downloadyt-f6eb32ae50a21d0d3b0ed0e992f3871d59966743.zip
feat(crates/yt/storage/db/insert): Track all inserted operations
-rw-r--r--crates/yt/Cargo.toml2
-rw-r--r--crates/yt/src/storage/db/extractor_hash.rs3
-rw-r--r--crates/yt/src/storage/db/get/mod.rs1
-rw-r--r--crates/yt/src/storage/db/get/txn_log.rs33
-rw-r--r--crates/yt/src/storage/db/insert/mod.rs37
-rw-r--r--crates/yt/src/storage/db/insert/subscription.rs3
-rw-r--r--crates/yt/src/storage/db/insert/video/mod.rs3
-rw-r--r--crates/yt/src/storage/db/mod.rs1
-rw-r--r--crates/yt/src/storage/db/subscription.rs3
-rw-r--r--crates/yt/src/storage/db/txn_log.rs14
-rw-r--r--crates/yt/src/storage/migrate/mod.rs14
-rw-r--r--crates/yt/src/storage/migrate/sql/5_Four_to_Five.sql15
12 files changed, 119 insertions, 10 deletions
diff --git a/crates/yt/Cargo.toml b/crates/yt/Cargo.toml
index 95f8270..14d25a4 100644
--- a/crates/yt/Cargo.toml
+++ b/crates/yt/Cargo.toml
@@ -25,7 +25,7 @@ publish = false
 
 [dependencies]
 anyhow = "1.0.98"
-blake3 = "1.8.2"
+blake3 = {version = "1.8.2", features = ["serde"]}
 chrono = { version = "0.4.41", features = ["now"] }
 chrono-humanize = "0.2.3"
 clap = { version = "4.5.41", features = ["derive"] }
diff --git a/crates/yt/src/storage/db/extractor_hash.rs b/crates/yt/src/storage/db/extractor_hash.rs
index b828348..15b57e7 100644
--- a/crates/yt/src/storage/db/extractor_hash.rs
+++ b/crates/yt/src/storage/db/extractor_hash.rs
@@ -14,6 +14,7 @@ use std::{collections::HashSet, fmt::Display, str::FromStr};
 use anyhow::{Context, Result, bail};
 use blake3::Hash;
 use log::debug;
+use serde::{Deserialize, Serialize};
 use tokio::sync::OnceCell;
 use yt_dlp::{info_json::InfoJson, json_cast, json_get};
 
@@ -21,7 +22,7 @@ use crate::app::App;
 
 static EXTRACTOR_HASH_LENGTH: OnceCell<usize> = OnceCell::const_new();
 
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy, Serialize, Deserialize)]
 pub(crate) struct ExtractorHash {
     hash: Hash,
 }
diff --git a/crates/yt/src/storage/db/get/mod.rs b/crates/yt/src/storage/db/get/mod.rs
index 8ca3075..58f1dd6 100644
--- a/crates/yt/src/storage/db/get/mod.rs
+++ b/crates/yt/src/storage/db/get/mod.rs
@@ -2,3 +2,4 @@ pub(crate) mod subscription;
 pub(crate) mod video;
 pub(crate) mod extractor_hash;
 pub(crate) mod playlist;
+pub(crate) mod txn_log;
diff --git a/crates/yt/src/storage/db/get/txn_log.rs b/crates/yt/src/storage/db/get/txn_log.rs
new file mode 100644
index 0000000..e4a1dcb
--- /dev/null
+++ b/crates/yt/src/storage/db/get/txn_log.rs
@@ -0,0 +1,33 @@
+use crate::{
+    app::App,
+    storage::db::{insert::Committable, txn_log::TxnLog, video::TimeStamp},
+};
+
+use anyhow::Result;
+use sqlx::query;
+
+impl<O: Committable> TxnLog<O> {
+    /// Get the log of all operations that have been performed.
+    pub(crate) async fn get(app: &App) -> Result<Self> {
+        let raw_ops = query!(
+            "
+        SELECT *
+        FROM txn_log
+        ORDER BY timestamp ASC;
+        "
+        )
+        .fetch_all(&app.database)
+        .await?;
+
+        let inner = raw_ops
+            .into_iter()
+            .filter_map(|raw_op| {
+                serde_json::from_str(&raw_op.operation)
+                    .map(|parsed_op| (TimeStamp::from_secs(raw_op.timestamp), parsed_op))
+                    .ok()
+            })
+            .collect();
+
+        Ok(TxnLog::new(inner))
+    }
+}
diff --git a/crates/yt/src/storage/db/insert/mod.rs b/crates/yt/src/storage/db/insert/mod.rs
index a3139ac..60bbc0a 100644
--- a/crates/yt/src/storage/db/insert/mod.rs
+++ b/crates/yt/src/storage/db/insert/mod.rs
@@ -3,15 +3,19 @@ use std::mem;
 use crate::app::App;
 
 use anyhow::Result;
-use log::trace;
-use sqlx::SqliteConnection;
+use chrono::Utc;
+use log::{debug, trace};
+use serde::{Serialize, de::DeserializeOwned};
+use sqlx::{SqliteConnection, query};
 
 pub(crate) mod maintenance;
 pub(crate) mod playlist;
 pub(crate) mod subscription;
 pub(crate) mod video;
 
-pub(crate) trait Committable: Sized + std::fmt::Debug {
+pub(crate) trait Committable:
+    Sized + std::fmt::Debug + Serialize + DeserializeOwned
+{
     async fn commit(self, txn: &mut SqliteConnection) -> Result<()>;
 }
 
@@ -48,6 +52,7 @@ impl<O: Committable> Operations<O> {
 
         for op in ops {
             trace!("Commiting operation: {op:?}");
+            add_operation_to_txn_log(&op, &mut txn).await?;
             op.commit(&mut txn).await?;
         }
 
@@ -72,3 +77,29 @@ impl<O: Committable> Drop for Operations<O> {
         );
     }
 }
+
+async fn add_operation_to_txn_log<O: Committable>(
+    operation: &O,
+    txn: &mut SqliteConnection,
+) -> Result<()> {
+    debug!("Adding operation to txn log: {operation:?}");
+
+    let now = Utc::now().timestamp();
+    let operation = serde_json::to_string(&operation).expect("should be serializable");
+
+    query!(
+        r#"
+        INSERT INTO txn_log (
+            timestamp,
+            operation
+        )
+        VALUES (?, ?);
+        "#,
+        now,
+        operation,
+    )
+    .execute(txn)
+    .await?;
+
+    Ok(())
+}
diff --git a/crates/yt/src/storage/db/insert/subscription.rs b/crates/yt/src/storage/db/insert/subscription.rs
index ba9a3e1..8499966 100644
--- a/crates/yt/src/storage/db/insert/subscription.rs
+++ b/crates/yt/src/storage/db/insert/subscription.rs
@@ -4,9 +4,10 @@ use crate::storage::db::{
 };
 
 use anyhow::Result;
+use serde::{Deserialize, Serialize};
 use sqlx::query;
 
-#[derive(Debug)]
+#[derive(Debug, Serialize, Deserialize)]
 pub(crate) enum Operation {
     Add(Subscription),
     Remove(Subscription),
diff --git a/crates/yt/src/storage/db/insert/video/mod.rs b/crates/yt/src/storage/db/insert/video/mod.rs
index b57f043..4cc7358 100644
--- a/crates/yt/src/storage/db/insert/video/mod.rs
+++ b/crates/yt/src/storage/db/insert/video/mod.rs
@@ -12,6 +12,7 @@ use crate::storage::db::{
 use anyhow::{Context, Result};
 use chrono::Utc;
 use log::debug;
+use serde::{Deserialize, Serialize};
 use sqlx::query;
 use tokio::fs;
 
@@ -21,7 +22,7 @@ const fn is_focused_to_value(is_focused: bool) -> Option<i8> {
     if is_focused { Some(1) } else { None }
 }
 
-#[derive(Debug)]
+#[derive(Debug, Serialize, Deserialize)]
 pub(crate) enum Operation {
     Add {
         description: Option<String>,
diff --git a/crates/yt/src/storage/db/mod.rs b/crates/yt/src/storage/db/mod.rs
index c0e16b0..5da28ed 100644
--- a/crates/yt/src/storage/db/mod.rs
+++ b/crates/yt/src/storage/db/mod.rs
@@ -5,3 +5,4 @@ pub(crate) mod extractor_hash;
 pub(crate) mod subscription;
 pub(crate) mod video;
 pub(crate) mod playlist;
+pub(crate) mod txn_log;
diff --git a/crates/yt/src/storage/db/subscription.rs b/crates/yt/src/storage/db/subscription.rs
index 07e5eec..0d9e160 100644
--- a/crates/yt/src/storage/db/subscription.rs
+++ b/crates/yt/src/storage/db/subscription.rs
@@ -2,10 +2,11 @@ use std::collections::HashMap;
 
 use anyhow::Result;
 use log::debug;
+use serde::{Deserialize, Serialize};
 use url::Url;
 use yt_dlp::{json_cast, options::YoutubeDLOptions};
 
-#[derive(Clone, Debug)]
+#[derive(Clone, Debug, Serialize, Deserialize)]
 pub(crate) struct Subscription {
     /// The human readable name of this subscription
     pub(crate) name: String,
diff --git a/crates/yt/src/storage/db/txn_log.rs b/crates/yt/src/storage/db/txn_log.rs
new file mode 100644
index 0000000..8d6c305
--- /dev/null
+++ b/crates/yt/src/storage/db/txn_log.rs
@@ -0,0 +1,14 @@
+use crate::storage::db::{insert::Committable, video::TimeStamp};
+
+pub(crate) struct TxnLog<O: Committable> {
+    inner: Vec<(TimeStamp, O)>,
+}
+
+impl<O: Committable> TxnLog<O> {
+    pub(crate) fn new(inner: Vec<(TimeStamp, O)>) -> Self {
+        Self { inner }
+    }
+    pub(crate) fn inner(&self) -> &[(TimeStamp, O)] {
+        &self.inner
+    }
+}
diff --git a/crates/yt/src/storage/migrate/mod.rs b/crates/yt/src/storage/migrate/mod.rs
index e2d93bd..418c893 100644
--- a/crates/yt/src/storage/migrate/mod.rs
+++ b/crates/yt/src/storage/migrate/mod.rs
@@ -94,8 +94,11 @@ pub(crate) enum DbVersion {
 
     /// Introduced: 2025-07-05.
     Four,
+
+    /// Introduced: 2025-07-20.
+    Five,
 }
-const CURRENT_VERSION: DbVersion = DbVersion::Four;
+const CURRENT_VERSION: DbVersion = DbVersion::Five;
 
 async fn add_error_context(
     function: impl Future<Output = Result<()>>,
@@ -147,6 +150,7 @@ impl DbVersion {
             DbVersion::Two => 2,
             DbVersion::Three => 3,
             DbVersion::Four => 4,
+            DbVersion::Five => 5,
 
             DbVersion::Empty => unreachable!("A empty version does not have an associated integer"),
         }
@@ -159,12 +163,14 @@ impl DbVersion {
             (2, "yt") => Ok(DbVersion::Two),
             (3, "yt") => Ok(DbVersion::Three),
             (4, "yt") => Ok(DbVersion::Four),
+            (5, "yt") => Ok(DbVersion::Five),
 
             (0, other) => bail!("Db version is Zero, but got unknown namespace: '{other}'"),
             (1, other) => bail!("Db version is One, but got unknown namespace: '{other}'"),
             (2, other) => bail!("Db version is Two, but got unknown namespace: '{other}'"),
             (3, other) => bail!("Db version is Three, but got unknown namespace: '{other}'"),
             (4, other) => bail!("Db version is Four, but got unknown namespace: '{other}'"),
+            (5, other) => bail!("Db version is Five, but got unknown namespace: '{other}'"),
 
             (other, "yt") => bail!("Got unkown version for 'yt' namespace: {other}"),
             (num, nasp) => bail!("Got unkown version number ({num}) and namespace ('{nasp}')"),
@@ -198,8 +204,12 @@ impl DbVersion {
                 make_upgrade! {app, Self::Three, Self::Four, "./sql/4_Three_to_Four.sql"}
             }
 
-            // This is the current_version
             Self::Four => {
+                make_upgrade! {app, Self::Four, Self::Five, "./sql/5_Four_to_Five.sql"}
+            }
+
+            // This is the current_version
+            Self::Five => {
                 assert_eq!(self, CURRENT_VERSION);
                 assert_eq!(self, get_version(app).await?);
                 Ok(())
diff --git a/crates/yt/src/storage/migrate/sql/5_Four_to_Five.sql b/crates/yt/src/storage/migrate/sql/5_Four_to_Five.sql
new file mode 100644
index 0000000..6c4b7cc
--- /dev/null
+++ b/crates/yt/src/storage/migrate/sql/5_Four_to_Five.sql
@@ -0,0 +1,15 @@
+-- yt - A fully featured command line YouTube client
+--
+-- Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+-- SPDX-License-Identifier: GPL-3.0-or-later
+--
+-- This file is part of Yt.
+--
+-- You should have received a copy of the License along with this program.
+-- If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+
+CREATE TABLE txn_log (
+    timestamp INTEGER NOT NULL,
+    operation TEXT    NOT NULL
+) STRICT;