From 6f0b086f7830f27a7d938bc03db0c73295d745e2 Mon Sep 17 00:00:00 2001 From: Benedikt Peetz Date: Tue, 26 Aug 2025 20:20:15 +0200 Subject: feat(yt/commands/subs): Implement disabling subscriptions This allows for a softer version of `unsubscribe`, as the subscription can be enabled again. --- crates/yt/src/commands/subscriptions/implm.rs | 42 +++++++++++++++++++--- crates/yt/src/commands/subscriptions/mod.rs | 26 ++++++++++++-- crates/yt/src/commands/update/implm/mod.rs | 2 +- crates/yt/src/storage/db/get/subscription.rs | 16 +++++++++ crates/yt/src/storage/db/insert/subscription.rs | 33 +++++++++++++++++ crates/yt/src/storage/db/subscription.rs | 6 ++-- crates/yt/src/storage/migrate/mod.rs | 14 ++++++-- .../yt/src/storage/migrate/sql/6_Five_to_Six.sql | 12 +++++++ 8 files changed, 140 insertions(+), 11 deletions(-) create mode 100644 crates/yt/src/storage/migrate/sql/6_Five_to_Six.sql diff --git a/crates/yt/src/commands/subscriptions/implm.rs b/crates/yt/src/commands/subscriptions/implm.rs index 31b714e..1e2e545 100644 --- a/crates/yt/src/commands/subscriptions/implm.rs +++ b/crates/yt/src/commands/subscriptions/implm.rs @@ -12,7 +12,7 @@ use std::str::FromStr; use crate::{ app::App, - commands::subscriptions::SubscriptionCommand, + commands::subscriptions::{SubscriptionCommand, SubscriptionStatus}, storage::db::{ insert::{Operations, subscription::Operation}, subscription::{Subscription, Subscriptions, check_url}, @@ -55,11 +55,41 @@ impl SubscriptionCommand { .await .with_context(|| format!("Failed to unsubscribe from {name:?}"))?; } - SubscriptionCommand::List {} => { + SubscriptionCommand::SetStatus { name, new_status } => { + let mut present_subscriptions = Subscriptions::get(app).await?; + + let mut ops = Operations::new("Subscribe: Set Status"); + if let Some(subscription) = present_subscriptions.0.remove(&name) { + subscription.set_is_active( + match new_status { + SubscriptionStatus::Active => true, + SubscriptionStatus::Inactive => false, + }, + &mut ops, + ); + } else { + bail!("Couldn't find subscription: '{}'", &name); + } + ops.commit(app) + .await + .with_context(|| format!("Failed to change status of {name:?}"))?; + } + SubscriptionCommand::List { active } => { let all_subs = Subscriptions::get(app).await?; + let all_subs = if active { + all_subs.remove_inactive() + } else { + all_subs + }; + for (key, val) in all_subs.0 { - println!("{}: '{}'", key, val.url); + println!( + "{}: '{}' ({})", + key, + val.url, + if val.is_active { "active" } else { "inactive" } + ); } } SubscriptionCommand::Export {} => { @@ -245,7 +275,11 @@ async fn actual_subscribe( ); } - let sub = Subscription { name, url }; + let sub = Subscription { + name, + url, + is_active: true, + }; sub.add(ops); diff --git a/crates/yt/src/commands/subscriptions/mod.rs b/crates/yt/src/commands/subscriptions/mod.rs index edd41c6..6a16a9a 100644 --- a/crates/yt/src/commands/subscriptions/mod.rs +++ b/crates/yt/src/commands/subscriptions/mod.rs @@ -10,7 +10,7 @@ use std::path::PathBuf; -use clap::Subcommand; +use clap::{Subcommand, ValueEnum}; use clap_complete::ArgValueCompleter; use url::Url; @@ -41,6 +41,18 @@ pub(super) enum SubscriptionCommand { name: String, }, + /// Change the status of an subscription. + /// + /// An active subscription will be updated in `yt update`, while an inactive one will not. + SetStatus { + /// The human readable name of the subscription + #[arg(add = ArgValueCompleter::new(complete_subscription))] + name: String, + + /// What should this subscription be considered now? + new_status: SubscriptionStatus, + }, + /// Import a bunch of URLs as subscriptions. Import { /// The file containing the URLs. Will use Stdin otherwise. @@ -58,5 +70,15 @@ pub(super) enum SubscriptionCommand { Export {}, /// List all subscriptions - List {}, + List { + /// Only show active subscriptions + #[arg(short, long, default_value_t = false)] + active: bool, + }, +} + +#[derive(ValueEnum, Debug, Clone, Copy)] +pub(in crate::commands) enum SubscriptionStatus { + Active, + Inactive, } diff --git a/crates/yt/src/commands/update/implm/mod.rs b/crates/yt/src/commands/update/implm/mod.rs index 53c7415..10626ac 100644 --- a/crates/yt/src/commands/update/implm/mod.rs +++ b/crates/yt/src/commands/update/implm/mod.rs @@ -28,7 +28,7 @@ impl UpdateCommand { subscriptions: subscription_names_to_update, } = self; - let mut all_subs = Subscriptions::get(app).await?; + let mut all_subs = Subscriptions::get(app).await?.remove_inactive(); let max_backlog = max_backlog.unwrap_or(app.config.update.max_backlog); diff --git a/crates/yt/src/storage/db/get/subscription.rs b/crates/yt/src/storage/db/get/subscription.rs index 16a6e8b..1d0b660 100644 --- a/crates/yt/src/storage/db/get/subscription.rs +++ b/crates/yt/src/storage/db/get/subscription.rs @@ -39,6 +39,13 @@ impl Subscriptions { Subscription::new( sub.name, Url::parse(&sub.url).expect("It was an URL, when we inserted it."), + if sub.is_active == 1 { + true + } else if sub.is_active == 0 { + false + } else { + unreachable!("These are the only two options") + }, ), ) }) @@ -46,4 +53,13 @@ impl Subscriptions { Ok(Subscriptions(subscriptions)) } + + pub(crate) fn remove_inactive(self) -> Self { + Self( + self.0 + .into_iter() + .filter(|(_, sub)| sub.is_active) + .collect(), + ) + } } diff --git a/crates/yt/src/storage/db/insert/subscription.rs b/crates/yt/src/storage/db/insert/subscription.rs index d25a209..54409a9 100644 --- a/crates/yt/src/storage/db/insert/subscription.rs +++ b/crates/yt/src/storage/db/insert/subscription.rs @@ -21,6 +21,10 @@ use sqlx::query; pub(crate) enum Operation { Add(Subscription), Remove(Subscription), + SetIsActive { + target: Subscription, + is_active: bool, + }, } impl Committable for Operation { @@ -72,6 +76,26 @@ impl Committable for Operation { Ok(()) } + Operation::SetIsActive { target, is_active } => { + query!( + " + UPDATE subscriptions + SET is_active = ? + WHERE name = ?; + ", + is_active, + target.name + ) + .execute(txn) + .await?; + + println!( + "Marked '{}' as '{}'", + target.name, + if is_active { "active" } else { "inactive" } + ); + Ok(()) + } } } } @@ -84,6 +108,15 @@ impl Subscription { pub(crate) fn remove(self, ops: &mut Operations) { ops.push(Operation::Remove(self)); } + + pub(crate) fn set_is_active(self, is_active: bool, ops: &mut Operations) { + if self.is_active != is_active { + ops.push(Operation::SetIsActive { + target: self, + is_active, + }); + } + } } impl Subscriptions { diff --git a/crates/yt/src/storage/db/subscription.rs b/crates/yt/src/storage/db/subscription.rs index c111b52..403938e 100644 --- a/crates/yt/src/storage/db/subscription.rs +++ b/crates/yt/src/storage/db/subscription.rs @@ -23,12 +23,14 @@ pub(crate) struct Subscription { /// The URL this subscription subscribes to pub(crate) url: Url, + + pub(crate) is_active: bool, } impl Subscription { #[must_use] - pub(crate) fn new(name: String, url: Url) -> Self { - Self { name, url } + pub(crate) fn new(name: String, url: Url, is_active: bool) -> Self { + Self { name, url, is_active } } } diff --git a/crates/yt/src/storage/migrate/mod.rs b/crates/yt/src/storage/migrate/mod.rs index 418c893..c5187ee 100644 --- a/crates/yt/src/storage/migrate/mod.rs +++ b/crates/yt/src/storage/migrate/mod.rs @@ -97,8 +97,11 @@ pub(crate) enum DbVersion { /// Introduced: 2025-07-20. Five, + + /// Introduced: 2025-08-26. + Six, } -const CURRENT_VERSION: DbVersion = DbVersion::Five; +const CURRENT_VERSION: DbVersion = DbVersion::Six; async fn add_error_context( function: impl Future>, @@ -151,6 +154,7 @@ impl DbVersion { DbVersion::Three => 3, DbVersion::Four => 4, DbVersion::Five => 5, + DbVersion::Six => 6, DbVersion::Empty => unreachable!("A empty version does not have an associated integer"), } @@ -164,6 +168,7 @@ impl DbVersion { (3, "yt") => Ok(DbVersion::Three), (4, "yt") => Ok(DbVersion::Four), (5, "yt") => Ok(DbVersion::Five), + (6, "yt") => Ok(DbVersion::Six), (0, other) => bail!("Db version is Zero, but got unknown namespace: '{other}'"), (1, other) => bail!("Db version is One, but got unknown namespace: '{other}'"), @@ -171,6 +176,7 @@ impl DbVersion { (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}'"), + (6, other) => bail!("Db version is Six, 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}')"), @@ -208,8 +214,12 @@ impl DbVersion { make_upgrade! {app, Self::Four, Self::Five, "./sql/5_Four_to_Five.sql"} } - // This is the current_version Self::Five => { + make_upgrade! {app, Self::Five, Self::Six, "./sql/6_Five_to_Six.sql"} + } + + // This is the current_version + Self::Six => { assert_eq!(self, CURRENT_VERSION); assert_eq!(self, get_version(app).await?); Ok(()) diff --git a/crates/yt/src/storage/migrate/sql/6_Five_to_Six.sql b/crates/yt/src/storage/migrate/sql/6_Five_to_Six.sql new file mode 100644 index 0000000..6a2cbcc --- /dev/null +++ b/crates/yt/src/storage/migrate/sql/6_Five_to_Six.sql @@ -0,0 +1,12 @@ +-- yt - A fully featured command line YouTube client +-- +-- Copyright (C) 2025 Benedikt Peetz +-- 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 . + +ALTER TABLE subscriptions +ADD COLUMN is_active INTEGER NOT NULL DEFAULT 1 CHECK (is_active IN (0, 1)); -- cgit 1.4.1