Coverage for functions \ flipdare \ service \ processor \ _processor_mixin.py: 75%
108 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
« prev ^ index » next coverage.py v7.13.0, created at 2026-05-08 12:22 +1000
1#!/usr/bin/env python
2# Copyright (c) 2026 Flipdare Pty Ltd. All rights reserved.
3#
4# This file is part of Flipdare's proprietary software and contains
5# confidential and copyrighted material. Unauthorised copying,
6# modification, distribution, or use of this file is strictly
7# prohibited without prior written permission from Flipdare Pty Ltd.
8#
9# This software includes third-party components licensed under MIT,
10# BSD, and Apache 2.0 licences. See THIRD_PARTY_NOTICES for details.
11#
14from pathlib import Path
16from google.cloud.storage.bucket import Bucket as StorageBucket
17from flipdare.app_globals import truncate_string
18from flipdare.app_log import LOG
19from flipdare.constants import DOWNLOAD_FILE_DIR, IS_DEBUG
20from flipdare.result.app_result import AppResult
21from flipdare.core.hash_generator import HashGenerator
22from flipdare.core.storage_file_type import StorageFileType
23from flipdare.core.video_optimizer import VideoOptimizer
24from flipdare.generated import ImageModel, StoredFileModel, VideoModel
25from flipdare.util.file_util import FileUtil
26from flipdare.util.firebase_file import FirebaseFile
27from flipdare.backend.app_storage_client import AppStorageClient
30class ProcessorMixin:
32 def __init__(self, bucket: StorageBucket, local_path: Path = DOWNLOAD_FILE_DIR) -> None:
33 self._local_path = local_path
34 self.bucket = bucket
36 @property
37 def local_path(self) -> Path:
38 return self._local_path
40 def optimize_video(self, video_model: VideoModel) -> StoredFileModel | None:
41 # Placeholder for video optimization logic
42 # ffmpeg -i input.mp4 -c:v libx264 -crf 23 -preset medium output.mp4
43 # ffmpeg -i input.mp4 -c:v libx264 -crf 23 -preset medium -vf scale=1080:-1 output.mp4
44 # base resolution on mobile devices is 9x16 or 1080x1920
45 # or 16x9 for landscape
46 if video_model.low is not None:
47 LOG().debug(
48 f"VideoModel: {video_model.source.url} already has low quality version. "
49 "Skipping optimization.",
50 )
51 return None
53 bucket = self.bucket
54 high_gs_url = video_model.source.url
55 uid = video_model.source.uid
56 w = video_model.w
57 h = video_model.h
59 optimizer = VideoOptimizer(
60 bucket=bucket,
61 fire_file=FileUtil.create_file(
62 gs_url=high_gs_url,
63 uid=uid,
64 file_type=StorageFileType.VIDEO,
65 ),
66 width=w,
67 height=h,
68 )
69 ff = optimizer.optimize_file()
70 if ff is None:
71 LOG().error(f"Failed to optimize video for video: {high_gs_url}")
72 return None
74 result = self.upload_file(ff)
75 if not result.is_ok:
76 LOG().error(f"Failed to upload optimized video for video: {high_gs_url}")
77 return None
79 optimized_model = result.generated
80 if optimized_model is None:
81 LOG().error(f"Uploaded optimized video model is None for video: {high_gs_url}")
82 return None
84 return optimized_model
86 def generate_thumbnail(
87 self,
88 uid: str,
89 video_gs_url: str,
90 width: int,
91 height: int,
92 ) -> ImageModel | None:
93 # Placeholder for thumbnail generation logic
94 # and is use as a placeholder before video is downloaded
95 #
96 if IS_DEBUG:
97 LOG().debug(f"Generating thumbnail for video: {video_gs_url}")
99 bucket = self.bucket
100 video_file = FileUtil.create_file(
101 gs_url=video_gs_url,
102 uid=uid,
103 file_type=StorageFileType.VIDEO,
104 )
105 ok = AppStorageClient(bucket).download_video_to_local(video_file)
106 if not ok:
107 LOG().error(f"Failed to download video for video: {video_gs_url}")
108 return None
110 optimizer = VideoOptimizer(bucket=bucket, fire_file=video_file, width=width, height=height)
111 thumbnail_file = optimizer.create_thumbnail_file()
112 if thumbnail_file is None:
113 LOG().error(f"Failed to generate thumbnail for video: {video_gs_url}")
114 return None
116 result = self.upload_file(thumbnail_file)
118 # cleanup temporary files
119 try:
120 Path(thumbnail_file.local_path).unlink()
121 Path(video_file.local_path).unlink()
122 except Exception as e:
123 msg = (
124 f"Failed to cleanup temporary files for thumbnail of content: {video_gs_url}: {e}"
125 )
126 LOG().warning(msg)
128 if not result.is_ok:
129 LOG().error(f"Failed to upload thumbnail for video: {video_gs_url}")
130 return None
132 stored_model = result.generated
133 if stored_model is None:
134 LOG().error(f"Uploaded thumbnail model is None for video: {video_gs_url}")
135 return None
137 return ImageModel(
138 w=width,
139 h=height,
140 source=StoredFileModel(
141 uid=stored_model.uid,
142 file_size=stored_model.file_size,
143 url=stored_model.url,
144 ),
145 )
147 def get_image_hash(self, image_model: ImageModel, width: int, height: int) -> str | None:
148 hash_: str | None
149 uid = image_model.source.uid
150 url = image_model.source.url
152 hash_util = HashGenerator.from_bucket(
153 bucket=self.bucket,
154 url=url,
155 width=width,
156 height=height,
157 )
158 hash_ = hash_util.generate_hash()
159 if hash_ is None:
160 LOG().warning(f"Failed to generate hash for StoredFileModel: {uid} : {url}")
161 return None
163 if IS_DEBUG:
164 LOG().debug(f"Updating hash for StoredFileModel: {uid} to {truncate_string(hash_)}")
166 return hash_
168 def upload_file(self, ff: FirebaseFile) -> AppResult[StoredFileModel]:
169 bucket = self.bucket
170 result = AppResult[StoredFileModel](task_name="UploadFile")
171 remote_path = ff.remote_path
172 local_path = ff.local_path
173 gs_url = ff.gs_url
175 try:
176 if IS_DEBUG:
177 msg = f"Uploading file from {local_path} to {gs_url} ({remote_path})"
178 LOG().debug(msg)
180 blob = bucket.blob(remote_path)
181 blob.upload_from_filename(local_path)
183 file_size = blob.size
184 if file_size is None:
185 LOG().warning(f"Uploaded blob size is None for thumbnail of content: {gs_url}")
186 return result
188 stored_model = StoredFileModel(file_size=file_size, url=gs_url, uid=ff.uid)
190 if IS_DEBUG:
191 msg = (
192 f"Uploaded thumbnail from {local_path} to {remote_path} for content: {gs_url}"
193 )
194 LOG().debug(msg)
196 result.generated = stored_model
197 return result
198 except Exception as e:
199 LOG().error(f"Failed to upload thumbnail for content: {gs_url}: {e}")
200 return result