oko 0.1.14

Fully local home security system.
Documentation
use serde::{Deserialize, Serialize};
use sqlx::{Result, SqlitePool};
use time::{macros::format_description, OffsetDateTime};

use crate::db::VideoCameraView;

use super::Model;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Video {
    pub video_id: i64,
    pub camera_id: Option<i64>,
    pub file_path: String,
    pub start_time: OffsetDateTime,
    pub end_time: Option<OffsetDateTime>,
    pub file_size: Option<i64>,
}

pub struct Default {
    pub video_id: i64,
    pub end_time: Option<OffsetDateTime>,
    pub file_name_format: &'static [time::format_description::BorrowedFormatItem<'static>],
}

impl Default {
    #[allow(clippy::unused_self)]
    pub fn start_time(&self) -> OffsetDateTime {
        OffsetDateTime::now_utc()
    }
}

impl Model for Video {
    type Default = Default;
    const DEFAULT: Default = Default {
        video_id: -1,
        end_time: None,
        file_name_format: format_description!(
            "[year]-[month]-[day]_[hour]-[minute]-[second]_[subsecond digits:9]Z"
        ),
    };

    async fn create_using_self(&mut self, pool: &SqlitePool) -> Result<()> {
        let result = sqlx::query!(
            r#"
            INSERT INTO videos (camera_id, file_path, start_time, end_time, file_size)
            VALUES (?, ?, ?, ?, ?)
            RETURNING video_id
            "#,
            self.camera_id,
            self.file_path,
            self.start_time,
            self.end_time,
            self.file_size
        )
        .fetch_one(pool)
        .await?;

        self.video_id = result.video_id;

        Ok(())
    }

    async fn get_using_id(pool: &SqlitePool, id: i64) -> Result<Self> {
        sqlx::query_as!(
            Video,
            r#"
            SELECT video_id, camera_id, file_path, start_time, end_time, file_size
            FROM videos WHERE video_id = ?
            "#,
            id
        )
        .fetch_one(pool)
        .await
    }

    async fn update_using_self(&self, pool: &SqlitePool) -> Result<()> {
        sqlx::query!(
            r#"
            UPDATE videos
            SET camera_id = ?, end_time = ?, file_size = ?
            WHERE video_id = ?
            RETURNING video_id
            "#,
            self.camera_id,
            self.end_time,
            self.file_size,
            self.video_id
        )
        .fetch_one(pool)
        .await?;

        Ok(())
    }

    async fn delete_using_id(pool: &SqlitePool, id: i64) -> Result<()> {
        sqlx::query!(
            r#"
            DELETE
            FROM videos
            WHERE video_id = ?
            RETURNING video_id
            "#,
            id
        )
        .fetch_one(pool)
        .await?;

        Ok(())
    }
}

impl Video {
    pub async fn list_for_camera(
        db: &sqlx::Pool<sqlx::Sqlite>,
        camera_id: i64,
    ) -> Result<Vec<VideoCameraView>> {
        sqlx::query_as!(
            VideoCameraView,
            r#"
            SELECT v.video_id, v.camera_id, c.name as camera_name, v.file_path, v.file_size
            FROM videos v
            JOIN cameras c ON v.camera_id = c.camera_id
            WHERE c.camera_id = ?
            "#,
            camera_id
        )
        .fetch_all(db)
        .await
    }
}

#[allow(clippy::unwrap_used)]
#[cfg(test)]
mod tests {
    use super::*;

    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras", "videos")))]
    async fn create(pool: SqlitePool) -> Result<()> {
        let mut video = Video {
            video_id: Video::DEFAULT.video_id,
            camera_id: Some(1),
            file_path: "/path/to/video.mp4".to_string(),
            start_time: Video::DEFAULT.start_time(),
            end_time: Video::DEFAULT.end_time,
            file_size: Some(1024),
        };

        video.create_using_self(&pool).await?;

        assert_eq!(video.video_id, 3);

        let returned_video = Video::get_using_id(&pool, 3).await?;

        assert_eq!(returned_video.camera_id, video.camera_id);
        assert_eq!(returned_video.file_path, video.file_path);
        assert_eq!(returned_video.start_time, video.start_time);
        assert_eq!(returned_video.end_time, video.end_time);
        assert_eq!(returned_video.file_size, video.file_size);

        Ok(())
    }

    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras", "videos")))]
    async fn get(pool: SqlitePool) -> Result<(), Box<dyn std::error::Error>> {
        let video_id = 1;
        let returned_video = Video::get_using_id(&pool, video_id).await?;

        assert_eq!(returned_video.video_id, video_id);
        assert_eq!(returned_video.camera_id, Some(1));
        assert_eq!(
            returned_video.file_path,
            "/home/piotrpdev/oko/backend/videos/1.mp4"
        );
        assert_eq!(
            returned_video.start_time,
            OffsetDateTime::from_unix_timestamp(1_729_479_512)?
        );
        assert_eq!(returned_video.file_size, Some(6_762_403));

        Ok(())
    }

    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras", "videos")))]
    async fn update(pool: SqlitePool) -> Result<()> {
        let old_video = Video::get_using_id(&pool, 1).await?;

        let updated_video = Video {
            video_id: old_video.video_id,
            camera_id: Some(1),
            file_path: old_video.file_path,
            start_time: old_video.start_time,
            end_time: Some(OffsetDateTime::now_utc()),
            file_size: Some(2048),
        };

        let updated = updated_video.update_using_self(&pool).await;
        assert!(updated.is_ok());

        let returned_video = Video::get_using_id(&pool, old_video.video_id).await?;
        assert_eq!(returned_video.camera_id, updated_video.camera_id);
        assert_eq!(returned_video.file_path, updated_video.file_path);
        assert_eq!(returned_video.start_time, updated_video.start_time);
        assert_eq!(returned_video.end_time, updated_video.end_time);
        assert_eq!(returned_video.file_size, updated_video.file_size);

        Ok(())
    }

    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras", "videos")))]
    async fn delete(pool: SqlitePool) -> Result<()> {
        let video_id = 1;
        let deleted = Video::delete_using_id(&pool, video_id).await;
        assert!(deleted.is_ok());

        let returned_video_result = Video::get_using_id(&pool, video_id).await;
        assert!(returned_video_result.is_err());

        let impossible_deleted = Video::delete_using_id(&pool, video_id).await;

        assert!(impossible_deleted.is_err());

        Ok(())
    }

    #[sqlx::test(fixtures(path = "../../fixtures", scripts("cameras", "videos")))]
    async fn list_for_camera(pool: SqlitePool) -> Result<()> {
        let camera_id = 1;
        let returned_videos = Video::list_for_camera(&pool, camera_id).await?;

        assert_eq!(returned_videos.len(), 1);
        assert_eq!(returned_videos.first().unwrap().video_id, 1);
        assert_eq!(returned_videos.first().unwrap().camera_id, Some(1));
        assert_eq!(returned_videos.first().unwrap().camera_name, "Front Door");
        assert_eq!(
            returned_videos.first().unwrap().file_path,
            "/home/piotrpdev/oko/backend/videos/1.mp4"
        );
        assert_eq!(returned_videos.first().unwrap().file_size, Some(6_762_403));

        Ok(())
    }
}