oko/db/
video.rs

1use serde::{Deserialize, Serialize};
2use sqlx::{Result, SqlitePool};
3use time::{macros::format_description, OffsetDateTime};
4
5use crate::db::VideoCameraView;
6
7use super::Model;
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct Video {
11    pub video_id: i64,
12    pub camera_id: Option<i64>,
13    pub file_path: String,
14    pub start_time: OffsetDateTime,
15    pub end_time: Option<OffsetDateTime>,
16    pub file_size: Option<i64>,
17}
18
19pub struct Default {
20    pub video_id: i64,
21    pub end_time: Option<OffsetDateTime>,
22    pub file_name_format: &'static [time::format_description::BorrowedFormatItem<'static>],
23}
24
25impl Default {
26    #[allow(clippy::unused_self)]
27    pub fn start_time(&self) -> OffsetDateTime {
28        OffsetDateTime::now_utc()
29    }
30}
31
32impl Model for Video {
33    type Default = Default;
34    const DEFAULT: Default = Default {
35        video_id: -1,
36        end_time: None,
37        file_name_format: format_description!(
38            "[year]-[month]-[day]_[hour]-[minute]-[second]_[subsecond digits:9]Z"
39        ),
40    };
41
42    async fn create_using_self(&mut self, pool: &SqlitePool) -> Result<()> {
43        let result = sqlx::query!(
44            r#"
45            INSERT INTO videos (camera_id, file_path, start_time, end_time, file_size)
46            VALUES (?, ?, ?, ?, ?)
47            RETURNING video_id
48            "#,
49            self.camera_id,
50            self.file_path,
51            self.start_time,
52            self.end_time,
53            self.file_size
54        )
55        .fetch_one(pool)
56        .await?;
57
58        self.video_id = result.video_id;
59
60        Ok(())
61    }
62
63    async fn get_using_id(pool: &SqlitePool, id: i64) -> Result<Self> {
64        sqlx::query_as!(
65            Video,
66            r#"
67            SELECT video_id, camera_id, file_path, start_time, end_time, file_size
68            FROM videos WHERE video_id = ?
69            "#,
70            id
71        )
72        .fetch_one(pool)
73        .await
74    }
75
76    async fn update_using_self(&self, pool: &SqlitePool) -> Result<()> {
77        sqlx::query!(
78            r#"
79            UPDATE videos
80            SET camera_id = ?, end_time = ?, file_size = ?
81            WHERE video_id = ?
82            RETURNING video_id
83            "#,
84            self.camera_id,
85            self.end_time,
86            self.file_size,
87            self.video_id
88        )
89        .fetch_one(pool)
90        .await?;
91
92        Ok(())
93    }
94
95    async fn delete_using_id(pool: &SqlitePool, id: i64) -> Result<()> {
96        sqlx::query!(
97            r#"
98            DELETE
99            FROM videos
100            WHERE video_id = ?
101            RETURNING video_id
102            "#,
103            id
104        )
105        .fetch_one(pool)
106        .await?;
107
108        Ok(())
109    }
110}
111
112impl Video {
113    pub async fn list_for_camera(
114        db: &sqlx::Pool<sqlx::Sqlite>,
115        camera_id: i64,
116    ) -> Result<Vec<VideoCameraView>> {
117        sqlx::query_as!(
118            VideoCameraView,
119            r#"
120            SELECT v.video_id, v.camera_id, c.name as camera_name, v.file_path, v.file_size
121            FROM videos v
122            JOIN cameras c ON v.camera_id = c.camera_id
123            WHERE c.camera_id = ?
124            "#,
125            camera_id
126        )
127        .fetch_all(db)
128        .await
129    }
130}
131
132#[allow(clippy::unwrap_used)]
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras", "videos")))]
138    async fn create(pool: SqlitePool) -> Result<()> {
139        let mut video = Video {
140            video_id: Video::DEFAULT.video_id,
141            camera_id: Some(1),
142            file_path: "/path/to/video.mp4".to_string(),
143            start_time: Video::DEFAULT.start_time(),
144            end_time: Video::DEFAULT.end_time,
145            file_size: Some(1024),
146        };
147
148        video.create_using_self(&pool).await?;
149
150        assert_eq!(video.video_id, 3);
151
152        let returned_video = Video::get_using_id(&pool, 3).await?;
153
154        assert_eq!(returned_video.camera_id, video.camera_id);
155        assert_eq!(returned_video.file_path, video.file_path);
156        assert_eq!(returned_video.start_time, video.start_time);
157        assert_eq!(returned_video.end_time, video.end_time);
158        assert_eq!(returned_video.file_size, video.file_size);
159
160        Ok(())
161    }
162
163    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras", "videos")))]
164    async fn get(pool: SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
165        let video_id = 1;
166        let returned_video = Video::get_using_id(&pool, video_id).await?;
167
168        assert_eq!(returned_video.video_id, video_id);
169        assert_eq!(returned_video.camera_id, Some(1));
170        assert_eq!(
171            returned_video.file_path,
172            "/home/piotrpdev/oko/backend/videos/1.mp4"
173        );
174        assert_eq!(
175            returned_video.start_time,
176            OffsetDateTime::from_unix_timestamp(1_729_479_512)?
177        );
178        assert_eq!(returned_video.file_size, Some(6_762_403));
179
180        Ok(())
181    }
182
183    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras", "videos")))]
184    async fn update(pool: SqlitePool) -> Result<()> {
185        let old_video = Video::get_using_id(&pool, 1).await?;
186
187        let updated_video = Video {
188            video_id: old_video.video_id,
189            camera_id: Some(1),
190            file_path: old_video.file_path,
191            start_time: old_video.start_time,
192            end_time: Some(OffsetDateTime::now_utc()),
193            file_size: Some(2048),
194        };
195
196        let updated = updated_video.update_using_self(&pool).await;
197        assert!(updated.is_ok());
198
199        let returned_video = Video::get_using_id(&pool, old_video.video_id).await?;
200        assert_eq!(returned_video.camera_id, updated_video.camera_id);
201        assert_eq!(returned_video.file_path, updated_video.file_path);
202        assert_eq!(returned_video.start_time, updated_video.start_time);
203        assert_eq!(returned_video.end_time, updated_video.end_time);
204        assert_eq!(returned_video.file_size, updated_video.file_size);
205
206        Ok(())
207    }
208
209    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras", "videos")))]
210    async fn delete(pool: SqlitePool) -> Result<()> {
211        let video_id = 1;
212        let deleted = Video::delete_using_id(&pool, video_id).await;
213        assert!(deleted.is_ok());
214
215        let returned_video_result = Video::get_using_id(&pool, video_id).await;
216        assert!(returned_video_result.is_err());
217
218        let impossible_deleted = Video::delete_using_id(&pool, video_id).await;
219
220        assert!(impossible_deleted.is_err());
221
222        Ok(())
223    }
224
225    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras", "videos")))]
226    async fn list_for_camera(pool: SqlitePool) -> Result<()> {
227        let camera_id = 1;
228        let returned_videos = Video::list_for_camera(&pool, camera_id).await?;
229
230        assert_eq!(returned_videos.len(), 1);
231        assert_eq!(returned_videos.first().unwrap().video_id, 1);
232        assert_eq!(returned_videos.first().unwrap().camera_id, Some(1));
233        assert_eq!(returned_videos.first().unwrap().camera_name, "Front Door");
234        assert_eq!(
235            returned_videos.first().unwrap().file_path,
236            "/home/piotrpdev/oko/backend/videos/1.mp4"
237        );
238        assert_eq!(returned_videos.first().unwrap().file_size, Some(6_762_403));
239
240        Ok(())
241    }
242}