Skip to main content

post_archiver/manager/
author.rs

1use chrono::{DateTime, Utc};
2use rusqlite::params;
3
4use crate::{
5    error::Result,
6    manager::{binded::Binded, PostArchiverConnection},
7    query::FromQuery,
8    Alias, Author, AuthorId, FileMetaId, PlatformId, PostId,
9};
10
11/// Specifies how to update an author's thumbnail.
12#[derive(Debug, Clone)]
13pub enum AuthorThumb {
14    /// Set to an explicit value (or clear with `None`).
15    Set(Option<FileMetaId>),
16    /// Set to the thumb of the most recently updated post associated with this author.
17    ByLatest,
18}
19
20/// Specifies how to update an author's `updated` timestamp.
21#[derive(Debug, Clone)]
22pub enum AuthorUpdated {
23    /// Unconditionally set to this value.
24    Set(DateTime<Utc>),
25    /// Set to the `updated` time of the most recently updated post associated with this author.
26    ByLatest,
27}
28
29/// Builder for updating an author's fields.
30///
31/// Fields left as `None` are not modified.
32#[derive(Debug, Clone, Default)]
33pub struct UpdateAuthor {
34    pub name: Option<String>,
35    pub thumb: Option<AuthorThumb>,
36    pub updated: Option<AuthorUpdated>,
37}
38
39impl UpdateAuthor {
40    /// Set the author's name.
41    pub fn name(mut self, name: String) -> Self {
42        self.name = Some(name);
43        self
44    }
45    /// Set or clear the author's thumbnail.
46    pub fn thumb(mut self, thumb: Option<FileMetaId>) -> Self {
47        self.thumb = Some(AuthorThumb::Set(thumb));
48        self
49    }
50    /// Set the author's thumbnail to the latest post's thumb.
51    pub fn thumb_by_latest(mut self) -> Self {
52        self.thumb = Some(AuthorThumb::ByLatest);
53        self
54    }
55    /// Unconditionally set the updated timestamp.
56    pub fn updated(mut self, updated: DateTime<Utc>) -> Self {
57        self.updated = Some(AuthorUpdated::Set(updated));
58        self
59    }
60    /// Set the updated timestamp to the latest associated post's `updated` time.
61    pub fn updated_by_latest(mut self) -> Self {
62        self.updated = Some(AuthorUpdated::ByLatest);
63        self
64    }
65}
66
67//=============================================================
68// Update / Delete
69//=============================================================
70impl<'a, C: PostArchiverConnection> Binded<'a, AuthorId, C> {
71    /// Get this author's current data from the database.
72    pub fn value(&self) -> Result<Author> {
73        let mut stmt = self
74            .conn()
75            .prepare_cached("SELECT * FROM authors WHERE id = ?")?;
76        Ok(stmt.query_row([self.id()], Author::from_row)?)
77    }
78
79    /// Remove this author from the archive.
80    ///
81    /// This also removes all associated aliases and author-post relationships.
82    pub fn delete(self) -> Result<()> {
83        self.conn()
84            .execute("DELETE FROM authors WHERE id = ?", [self.id()])?;
85        Ok(())
86    }
87
88    /// Apply a batch of field updates to this author in a single SQL statement.
89    ///
90    /// Only fields set on `update` (i.e. `Some(...)`) are written to the database.
91    pub fn update(&self, update: UpdateAuthor) -> Result<()> {
92        use rusqlite::types::ToSql;
93
94        let id = self.id();
95        let mut sets: Vec<&str> = Vec::new();
96        let mut params: Vec<&dyn ToSql> = Vec::new();
97
98        macro_rules! push {
99            ($field:expr, $col:expr) => {
100                if let Some(ref v) = $field {
101                    sets.push($col);
102                    params.push(v);
103                }
104            };
105        }
106
107        push!(update.name, "name = ?");
108
109        match &update.thumb {
110            Some(AuthorThumb::Set(v)) => {
111                sets.push("thumb = ?");
112                params.push(v);
113            }
114            Some(AuthorThumb::ByLatest) => {
115                sets.push("thumb = (SELECT thumb FROM posts WHERE id IN (SELECT post FROM author_posts WHERE author = ?) AND thumb IS NOT NULL ORDER BY updated DESC LIMIT 1)");
116                params.push(&id);
117            }
118            None => {}
119        }
120
121        match &update.updated {
122            Some(AuthorUpdated::Set(v)) => {
123                sets.push("updated = ?");
124                params.push(v);
125            }
126            Some(AuthorUpdated::ByLatest) => {
127                sets.push("updated = (SELECT updated FROM posts WHERE id IN (SELECT post FROM author_posts WHERE author = ?) ORDER BY updated DESC LIMIT 1)");
128                params.push(&id);
129            }
130            None => {}
131        }
132
133        if sets.is_empty() {
134            return Ok(());
135        }
136
137        params.push(&id);
138
139        let sql = format!("UPDATE authors SET {} WHERE id = ?", sets.join(", "));
140        self.conn().execute(&sql, params.as_slice())?;
141        Ok(())
142    }
143}
144
145//=============================================================
146// Relations: Aliases
147//=============================================================
148impl<'a, C: PostArchiverConnection> Binded<'a, AuthorId, C> {
149    /// List all aliases associated with this author.
150    pub fn list_aliases(&self) -> Result<Vec<Alias>> {
151        let mut stmt = self
152            .conn()
153            .prepare_cached("SELECT * FROM author_aliases WHERE target = ?")?;
154        let rows = stmt.query_map([self.id()], Alias::from_row)?;
155        rows.collect::<std::result::Result<_, _>>()
156            .map_err(Into::into)
157    }
158
159    /// Add or update aliases for this author.
160    ///
161    /// # Parameters
162    /// - aliases: Vec of (source, platform, link)
163    pub fn add_aliases(&self, aliases: Vec<(String, PlatformId, Option<String>)>) -> Result<()> {
164        let mut stmt = self.conn().prepare_cached(
165            "INSERT OR REPLACE INTO author_aliases (target, source, platform, link) VALUES (?, ?, ?, ?)",
166        )?;
167        for (source, platform, link) in aliases {
168            stmt.execute(params![self.id(), source, platform, link])?;
169        }
170        Ok(())
171    }
172
173    /// Remove aliases from this author.
174    ///
175    /// # Parameters
176    /// - aliases: &[(source, platform)]
177    pub fn remove_aliases(&self, aliases: &[(String, PlatformId)]) -> Result<()> {
178        let mut stmt = self.conn().prepare_cached(
179            "DELETE FROM author_aliases WHERE target = ? AND source = ? AND platform = ?",
180        )?;
181        for (source, platform) in aliases {
182            stmt.execute(params![self.id(), source, platform])?;
183        }
184        Ok(())
185    }
186
187    /// Set an alias's name.
188    pub fn set_alias_name(&self, alias: &(String, PlatformId), name: String) -> Result<()> {
189        let mut stmt = self.conn().prepare_cached(
190            "UPDATE author_aliases SET source = ? WHERE target = ? AND source = ? AND platform = ?",
191        )?;
192        stmt.execute(params![name, self.id(), alias.0, alias.1])?;
193        Ok(())
194    }
195
196    /// Set an alias's platform.
197    pub fn set_alias_platform(
198        &self,
199        alias: &(String, PlatformId),
200        platform: PlatformId,
201    ) -> Result<()> {
202        let mut stmt = self.conn().prepare_cached(
203            "UPDATE author_aliases SET platform = ? WHERE target = ? AND source = ? AND platform = ?",
204        )?;
205        stmt.execute(params![platform, self.id(), alias.0, alias.1])?;
206        Ok(())
207    }
208
209    /// Set an alias's link.
210    pub fn set_alias_link(&self, alias: &(String, PlatformId), link: Option<String>) -> Result<()> {
211        let mut stmt = self.conn().prepare_cached(
212            "UPDATE author_aliases SET link = ? WHERE target = ? AND source = ? AND platform = ?",
213        )?;
214        stmt.execute(params![link, self.id(), alias.0, alias.1])?;
215        Ok(())
216    }
217}
218
219//=============================================================
220// Relations: Posts
221//=============================================================
222impl<'a, C: PostArchiverConnection> Binded<'a, AuthorId, C> {
223    /// List all post IDs associated with this author.
224    pub fn list_posts(&self) -> Result<Vec<PostId>> {
225        let mut stmt = self
226            .conn()
227            .prepare_cached("SELECT post FROM author_posts WHERE author = ?")?;
228        let rows = stmt.query_map([self.id()], |row| row.get(0))?;
229        rows.collect::<std::result::Result<_, _>>()
230            .map_err(Into::into)
231    }
232}
233
234//=============================================================
235// FindAlias trait (kept for importer compatibility)
236//=============================================================
237pub trait FindAlias {
238    fn source(&self) -> &str;
239    fn platform(&self) -> PlatformId;
240}
241
242impl FindAlias for (&str, PlatformId) {
243    fn source(&self) -> &str {
244        self.0
245    }
246    fn platform(&self) -> PlatformId {
247        self.1
248    }
249}
250
251#[cfg(feature = "importer")]
252impl FindAlias for crate::importer::UnsyncAlias {
253    fn source(&self) -> &str {
254        &self.source
255    }
256    fn platform(&self) -> PlatformId {
257        self.platform
258    }
259}