feat: Phase 4 — publisher, qBittorrent watcher, identity CLI
Adds the full writer/publisher stack: NIP-35 event signing and relay delivery, qBittorrent polling with publish-delay queue, lava_torrent .torrent file parsing, TMDB inline lookup before publish, and kindexr-cli identity/publish subcommands.
This commit is contained in:
+112
-1
@@ -1,5 +1,6 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use kindexr::{config, db};
|
||||
use kindexr::{config, db, nostr::signer::Signer, publisher::watcher::build_from_torrent_file};
|
||||
use nostr::ToBech32;
|
||||
use rand::Rng;
|
||||
|
||||
/// kindexr-cli — admin CLI for kindexr
|
||||
@@ -26,6 +27,23 @@ enum Command {
|
||||
#[command(subcommand)]
|
||||
cmd: PublisherCmd,
|
||||
},
|
||||
/// Manage the local signing identity
|
||||
Identity {
|
||||
#[command(subcommand)]
|
||||
cmd: IdentityCmd,
|
||||
},
|
||||
/// Enqueue a .torrent file for publishing
|
||||
Publish {
|
||||
/// Path to a .torrent file (or a directory to scan for .torrent files)
|
||||
#[arg(long)]
|
||||
from: String,
|
||||
/// Category tag to assign
|
||||
#[arg(long, default_value = "")]
|
||||
category: String,
|
||||
/// Override the publish delay in seconds (default: from config)
|
||||
#[arg(long)]
|
||||
delay: Option<u64>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
@@ -38,6 +56,18 @@ enum ApikeyCmd {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum IdentityCmd {
|
||||
/// Generate a new keypair and store it in the DB
|
||||
Init {
|
||||
/// Use an existing nsec instead of generating one
|
||||
#[arg(long)]
|
||||
nsec: Option<String>,
|
||||
},
|
||||
/// Show the current identity
|
||||
Info,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum PublisherCmd {
|
||||
/// List known publishers
|
||||
@@ -139,6 +169,87 @@ async fn main() -> anyhow::Result<()> {
|
||||
println!("muted {pubkey}");
|
||||
}
|
||||
},
|
||||
|
||||
Command::Identity { cmd } => match cmd {
|
||||
IdentityCmd::Init { nsec } => {
|
||||
let (nsec_bech32, pubkey) = match nsec {
|
||||
Some(ref key) => {
|
||||
let signer = Signer::from_nsec(key)?;
|
||||
(key.clone(), signer.pubkey_hex())
|
||||
}
|
||||
None => {
|
||||
let keys = nostr::Keys::generate();
|
||||
let nsec_str = keys.secret_key().to_bech32()?;
|
||||
let signer = Signer::from_nsec(&nsec_str)?;
|
||||
(nsec_str, signer.pubkey_hex())
|
||||
}
|
||||
};
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64;
|
||||
db::upsert_identity(&pool, &pubkey, Some(&nsec_bech32), None, now).await?;
|
||||
println!("pubkey: {pubkey}");
|
||||
println!("nsec: {nsec_bech32}");
|
||||
}
|
||||
IdentityCmd::Info => {
|
||||
match db::get_identity(&pool).await? {
|
||||
None => println!("no identity stored — run `identity init` first"),
|
||||
Some(row) => {
|
||||
println!("pubkey: {}", row.pubkey);
|
||||
println!("nsec set: {}", row.nsec.is_some());
|
||||
println!("bunker_url: {}", row.bunker_url.as_deref().unwrap_or("—"));
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Command::Publish { from, category, delay } => {
|
||||
let delay_secs = delay.unwrap_or(cfg.publisher.publish_delay_secs);
|
||||
let path = std::path::Path::new(&from);
|
||||
let torrent_paths: Vec<String> = if path.is_dir() {
|
||||
std::fs::read_dir(path)?
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.path())
|
||||
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("torrent"))
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.collect()
|
||||
} else {
|
||||
vec![from.clone()]
|
||||
};
|
||||
|
||||
if torrent_paths.is_empty() {
|
||||
println!("no .torrent files found in {from}");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as i64;
|
||||
|
||||
let mut queued = 0usize;
|
||||
let mut skipped = 0usize;
|
||||
for tp in &torrent_paths {
|
||||
let rec = match build_from_torrent_file(tp, &category) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!("skip {tp}: {e}");
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if db::is_queued_or_published(&pool, &rec.info_hash).await? {
|
||||
skipped += 1;
|
||||
continue;
|
||||
}
|
||||
let scheduled_at = now + delay_secs as i64;
|
||||
db::enqueue(&pool, &rec.info_hash, &rec.title, &category, Some(tp.as_str()), now, scheduled_at).await?;
|
||||
println!("queued: {} ({})", rec.title, rec.info_hash);
|
||||
queued += 1;
|
||||
}
|
||||
println!("{queued} queued, {skipped} skipped");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
Reference in New Issue
Block a user