Skip to main content

systemprompt_cli/commands/core/content/
popular.rs

1//! `core content popular` command over a parsed duration window.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use super::types::{ContentSummary, PopularOutput};
7use crate::cli_settings::CliConfig;
8use crate::shared::CommandOutput;
9use anyhow::{Result, anyhow};
10use clap::Args;
11use systemprompt_content::ContentRepository;
12use systemprompt_database::DbPool;
13use systemprompt_identifiers::SourceId;
14
15use crate::context::CommandContext;
16
17fn parse_duration(s: &str) -> Result<i64> {
18    let s = s.trim().to_lowercase();
19    if s.ends_with('d') {
20        s[..s.len() - 1]
21            .parse::<i64>()
22            .map_err(|_e| anyhow!("Invalid duration format: {}", s))
23    } else if s.ends_with('w') {
24        s[..s.len() - 1]
25            .parse::<i64>()
26            .map(|w| w * 7)
27            .map_err(|_e| anyhow!("Invalid duration format: {}", s))
28    } else {
29        s.parse::<i64>().map_err(|_e| {
30            anyhow!(
31                "Invalid duration format: {}. Use '7d', '30d', '1w', etc.",
32                s
33            )
34        })
35    }
36}
37
38#[derive(Debug, Args)]
39pub struct PopularArgs {
40    #[arg(help = "Source ID")]
41    pub source: String,
42
43    #[arg(long, default_value = "30d", help = "Time period (e.g., 7d, 30d, 1w)")]
44    pub since: String,
45
46    #[arg(long, default_value = "10")]
47    pub limit: i64,
48}
49
50pub async fn execute(args: PopularArgs, ctx: &CommandContext) -> Result<CommandOutput> {
51    execute_with_pool(args, &ctx.db_pool().await?, &ctx.cli).await
52}
53
54pub async fn execute_with_pool(
55    args: PopularArgs,
56    pool: &DbPool,
57    _config: &CliConfig,
58) -> Result<CommandOutput> {
59    let repo = ContentRepository::new(pool)?;
60
61    let source = SourceId::new(args.source.clone());
62    let days_i64 = parse_duration(&args.since)?;
63    let days = i32::try_from(days_i64).map_err(|_e| anyhow!("Duration too large"))?;
64
65    let content_ids = repo
66        .get_popular_content_ids(&source, days, args.limit)
67        .await?;
68
69    let mut items = Vec::with_capacity(content_ids.len());
70    for id in content_ids {
71        if let Some(content) = repo.get_by_id(&id).await? {
72            items.push(ContentSummary {
73                id: content.id,
74                slug: content.slug,
75                title: content.title,
76                kind: content.kind,
77                source_id: content.source_id,
78                category_id: content.category_id,
79                published_at: Some(content.published_at),
80            });
81        }
82    }
83
84    let output = PopularOutput {
85        items,
86        source_id: source,
87        days: days_i64,
88    };
89
90    Ok(
91        CommandOutput::table_of(vec!["id", "title", "kind", "published_at"], &output.items)
92            .with_title("Popular Content"),
93    )
94}