Skip to main content

backbone_bucket/application/service/
video_thumbnail_service.rs

1//! Video Thumbnail Processor Service
2//!
3//! Hand-written — NOT generated. This file is safe from regeneration.
4
5use std::sync::Arc;
6
7use chrono::Utc;
8use uuid::Uuid;
9
10use super::error::{ServiceError, ServiceResult};
11use crate::domain::entity::{
12    ProcessingJob, ProcessingJobType, JobStatus, Thumbnail, ThumbnailSize,
13};
14use crate::infrastructure::persistence::{
15    ProcessingJobRepository, ThumbnailRepository, StoredFileRepository,
16};
17
18const VIDEO_MIME_PREFIXES: &[&str] = &["video/"];
19
20const DEFAULT_VIDEO_THUMBNAIL_SIZES: &[(ThumbnailSize, i32, i32)] = &[
21    (ThumbnailSize::Small, 64, 64),
22    (ThumbnailSize::Medium, 128, 128),
23    (ThumbnailSize::Large, 256, 256),
24];
25
26pub struct VideoThumbnailService {
27    job_repo: Arc<ProcessingJobRepository>,
28    thumb_repo: Arc<ThumbnailRepository>,
29    file_repo: Arc<StoredFileRepository>,
30}
31
32impl VideoThumbnailService {
33    pub fn new(
34        job_repo: Arc<ProcessingJobRepository>,
35        thumb_repo: Arc<ThumbnailRepository>,
36        file_repo: Arc<StoredFileRepository>,
37    ) -> Self {
38        Self { job_repo, thumb_repo, file_repo }
39    }
40
41    pub async fn enqueue(&self, file_id: Uuid, priority: Option<i32>) -> ServiceResult<ProcessingJob> {
42        let file = self.file_repo
43            .find_by_id(&file_id.to_string())
44            .await
45            .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
46            .ok_or(ServiceError::NotFound)?;
47
48        if !is_video(&file.mime_type) {
49            return Err(ServiceError::Validation(format!(
50                "File {} is not a video (mime: {})", file_id, file.mime_type
51            )));
52        }
53
54        // TODO: job_repo.has_active_job — implement custom repository method
55        let _ = &ProcessingJobType::VideoThumbnail;
56
57        let input = serde_json::json!({
58            "sizes": DEFAULT_VIDEO_THUMBNAIL_SIZES.iter().map(|(size, w, h)| {
59                serde_json::json!({ "size": format!("{:?}", size), "width": w, "height": h })
60            }).collect::<Vec<_>>(),
61        });
62
63        let job = ProcessingJob::builder()
64            .file_id(file_id)
65            .job_type(ProcessingJobType::VideoThumbnail)
66            .status(JobStatus::Pending)
67            .priority(priority.unwrap_or(0))
68            .input_data(input)
69            .retry_count(0)
70            .max_retries(3)
71            .build()
72            .map_err(|e| ServiceError::Validation(e))?;
73
74        let created = self.job_repo
75            .create(&job)
76            .await
77            .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?;
78
79        Ok(created)
80    }
81
82    pub async fn record_thumbnail(
83        &self,
84        file_id: Uuid,
85        size: ThumbnailSize,
86        width: i32,
87        height: i32,
88        storage_key: &str,
89        size_bytes: i64,
90        generation_time_ms: Option<i32>,
91    ) -> ServiceResult<Thumbnail> {
92        let thumb = Thumbnail::builder()
93            .file_id(file_id)
94            .size(size)
95            .width(width)
96            .height(height)
97            .storage_key(storage_key.to_string())
98            .mime_type("image/webp".to_string())
99            .format("webp".to_string())
100            .quality(80)
101            .size_bytes(size_bytes)
102            .generated_at(Utc::now())
103            .generation_time_ms(generation_time_ms.unwrap_or(0))
104            .source_version(1)
105            .is_stale(false)
106            .build()
107            .map_err(|e| ServiceError::Validation(e))?;
108
109        let created = self.thumb_repo
110            .create(&thumb)
111            .await
112            .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?;
113
114        Ok(created)
115    }
116
117    pub async fn complete_job(&self, job_id: Uuid, _file_id: Uuid) -> ServiceResult<ProcessingJob> {
118        let mut job = self.find_job(job_id).await?;
119
120        let result = serde_json::json!({
121            "completed_at": Utc::now().to_rfc3339(),
122            "thumbnails_generated": true,
123        });
124
125        job.status = JobStatus::Completed;
126        job.result_data = Some(result);
127        job.completed_at = Some(Utc::now());
128        job.metadata.touch();
129
130        let id_str = job.id.to_string();
131        let updated = self.job_repo
132            .update(&id_str, &job)
133            .await
134            .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
135            .ok_or(ServiceError::NotFound)?;
136
137        // TODO: file_repo.update_processing_status — implement custom method
138
139        Ok(updated)
140    }
141
142    pub async fn fail_job(&self, job_id: Uuid, error_message: &str) -> ServiceResult<ProcessingJob> {
143        let mut job = self.find_job(job_id).await?;
144
145        job.status = JobStatus::Failed;
146        job.error_message = Some(error_message.to_string());
147        job.completed_at = Some(Utc::now());
148        job.metadata.touch();
149
150        let id_str = job.id.to_string();
151        self.job_repo
152            .update(&id_str, &job)
153            .await
154            .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
155            .ok_or(ServiceError::NotFound)
156    }
157
158    pub async fn get_thumbnails(&self, _file_id: Uuid) -> ServiceResult<Vec<Thumbnail>> {
159        // TODO: thumb_repo.find_by_file_id — implement custom repository method
160        Ok(vec![])
161    }
162
163    pub async fn regenerate(&self, file_id: Uuid) -> ServiceResult<ProcessingJob> {
164        // TODO: thumb_repo.mark_stale_by_file — implement custom method
165        self.enqueue(file_id, Some(1)).await
166    }
167
168    async fn find_job(&self, job_id: Uuid) -> ServiceResult<ProcessingJob> {
169        self.job_repo
170            .find_by_id(&job_id.to_string())
171            .await
172            .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
173            .ok_or(ServiceError::NotFound)
174    }
175}
176
177fn is_video(mime_type: &str) -> bool {
178    VIDEO_MIME_PREFIXES.iter().any(|prefix| mime_type.starts_with(prefix))
179}