django-nativemojo 0.1.10__py3-none-any.whl → 0.1.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. django_nativemojo-0.1.15.dist-info/METADATA +136 -0
  2. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/RECORD +105 -65
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/__init__.py +5 -0
  5. mojo/apps/account/management/commands/__init__.py +6 -0
  6. mojo/apps/account/management/commands/serializer_admin.py +531 -0
  7. mojo/apps/account/migrations/0004_user_avatar.py +20 -0
  8. mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
  9. mojo/apps/account/models/group.py +25 -7
  10. mojo/apps/account/models/member.py +15 -4
  11. mojo/apps/account/models/user.py +197 -20
  12. mojo/apps/account/rest/group.py +1 -0
  13. mojo/apps/account/rest/user.py +6 -2
  14. mojo/apps/aws/rest/__init__.py +1 -0
  15. mojo/apps/aws/rest/s3.py +64 -0
  16. mojo/apps/fileman/README.md +8 -8
  17. mojo/apps/fileman/backends/base.py +76 -70
  18. mojo/apps/fileman/backends/filesystem.py +86 -86
  19. mojo/apps/fileman/backends/s3.py +200 -108
  20. mojo/apps/fileman/migrations/0001_initial.py +106 -0
  21. mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
  22. mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
  23. mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
  24. mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
  25. mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
  26. mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
  27. mojo/apps/fileman/migrations/0008_file_category.py +18 -0
  28. mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
  29. mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
  30. mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
  31. mojo/apps/fileman/models/__init__.py +1 -5
  32. mojo/apps/fileman/models/file.py +204 -58
  33. mojo/apps/fileman/models/manager.py +161 -31
  34. mojo/apps/fileman/models/rendition.py +118 -0
  35. mojo/apps/fileman/renderer/__init__.py +111 -0
  36. mojo/apps/fileman/renderer/audio.py +403 -0
  37. mojo/apps/fileman/renderer/base.py +205 -0
  38. mojo/apps/fileman/renderer/document.py +404 -0
  39. mojo/apps/fileman/renderer/image.py +222 -0
  40. mojo/apps/fileman/renderer/utils.py +297 -0
  41. mojo/apps/fileman/renderer/video.py +304 -0
  42. mojo/apps/fileman/rest/__init__.py +1 -18
  43. mojo/apps/fileman/rest/upload.py +22 -32
  44. mojo/apps/fileman/signals.py +58 -0
  45. mojo/apps/fileman/tasks.py +254 -0
  46. mojo/apps/fileman/utils/__init__.py +40 -16
  47. mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
  48. mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
  49. mojo/apps/incident/models/__init__.py +1 -0
  50. mojo/apps/incident/models/history.py +36 -0
  51. mojo/apps/incident/models/incident.py +1 -1
  52. mojo/apps/incident/reporter.py +3 -1
  53. mojo/apps/incident/rest/event.py +7 -1
  54. mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
  55. mojo/apps/logit/models/log.py +4 -1
  56. mojo/apps/metrics/utils.py +2 -2
  57. mojo/apps/notify/handlers/ses/message.py +1 -1
  58. mojo/apps/notify/providers/aws.py +2 -2
  59. mojo/apps/tasks/__init__.py +34 -1
  60. mojo/apps/tasks/manager.py +200 -45
  61. mojo/apps/tasks/rest/tasks.py +24 -10
  62. mojo/apps/tasks/runner.py +283 -18
  63. mojo/apps/tasks/task.py +99 -0
  64. mojo/apps/tasks/tq_handlers.py +118 -0
  65. mojo/decorators/auth.py +6 -1
  66. mojo/decorators/http.py +7 -2
  67. mojo/helpers/aws/__init__.py +41 -0
  68. mojo/helpers/aws/ec2.py +804 -0
  69. mojo/helpers/aws/iam.py +748 -0
  70. mojo/helpers/aws/s3.py +451 -11
  71. mojo/helpers/aws/ses.py +483 -0
  72. mojo/helpers/aws/sns.py +461 -0
  73. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  74. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  75. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  76. mojo/helpers/dates.py +18 -0
  77. mojo/helpers/response.py +6 -2
  78. mojo/helpers/settings/__init__.py +2 -0
  79. mojo/helpers/{settings.py → settings/helper.py} +1 -37
  80. mojo/helpers/settings/parser.py +132 -0
  81. mojo/middleware/logging.py +1 -1
  82. mojo/middleware/mojo.py +5 -0
  83. mojo/models/rest.py +261 -46
  84. mojo/models/secrets.py +13 -4
  85. mojo/serializers/__init__.py +100 -0
  86. mojo/serializers/advanced/README.md +363 -0
  87. mojo/serializers/advanced/__init__.py +247 -0
  88. mojo/serializers/advanced/formats/__init__.py +28 -0
  89. mojo/serializers/advanced/formats/csv.py +416 -0
  90. mojo/serializers/advanced/formats/excel.py +516 -0
  91. mojo/serializers/advanced/formats/json.py +239 -0
  92. mojo/serializers/advanced/formats/localizers.py +509 -0
  93. mojo/serializers/advanced/formats/response.py +485 -0
  94. mojo/serializers/advanced/serializer.py +568 -0
  95. mojo/serializers/manager.py +501 -0
  96. mojo/serializers/optimized.py +618 -0
  97. mojo/serializers/settings_example.py +322 -0
  98. mojo/serializers/{models.py → simple.py} +38 -15
  99. testit/helpers.py +21 -4
  100. django_nativemojo-0.1.10.dist-info/METADATA +0 -96
  101. mojo/apps/metrics/rest/db.py +0 -0
  102. mojo/helpers/aws/setup_email.py +0 -0
  103. mojo/ws4redis/README.md +0 -174
  104. mojo/ws4redis/__init__.py +0 -2
  105. mojo/ws4redis/client.py +0 -283
  106. mojo/ws4redis/connection.py +0 -327
  107. mojo/ws4redis/exceptions.py +0 -32
  108. mojo/ws4redis/redis.py +0 -183
  109. mojo/ws4redis/servers/base.py +0 -86
  110. mojo/ws4redis/servers/django.py +0 -171
  111. mojo/ws4redis/servers/uwsgi.py +0 -63
  112. mojo/ws4redis/settings.py +0 -45
  113. mojo/ws4redis/utf8validator.py +0 -128
  114. mojo/ws4redis/websocket.py +0 -403
  115. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/LICENSE +0 -0
  116. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/NOTICE +0 -0
  117. {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.15.dist-info}/WHEEL +0 -0
  118. /mojo/{ws4redis/servers → apps/aws}/__init__.py +0 -0
  119. /mojo/apps/{fileman/models/render.py → aws/models/__init__.py} +0 -0
  120. /mojo/apps/fileman/{rest/__init__ → migrations/__init__.py} +0 -0
@@ -0,0 +1,403 @@
1
+ import os
2
+ import subprocess
3
+ import io
4
+ import logging
5
+ import mimetypes
6
+ import tempfile
7
+ from typing import Dict, Optional, Tuple, Union, BinaryIO, List
8
+ import shutil
9
+ from PIL import Image
10
+
11
+ from mojo.apps.fileman.models import File, FileRendition
12
+ from mojo.apps.fileman.renderer.base import BaseRenderer, RenditionRole
13
+ from mojo.apps.fileman.renderer.utils import get_audio_duration
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ class AudioRenderer(BaseRenderer):
18
+ """
19
+ Renderer for audio files
20
+
21
+ Creates various renditions like thumbnails, previews, and different formats
22
+ """
23
+
24
+ # Audio file categories
25
+ supported_categories = ['audio']
26
+
27
+ # Default rendition definitions with options
28
+ default_renditions = {
29
+ RenditionRole.AUDIO_THUMBNAIL: {
30
+ 'width': 300,
31
+ 'height': 300,
32
+ 'format': 'jpg',
33
+ 'waveform': False, # If true, generates a waveform image instead of using embedded artwork
34
+ },
35
+ RenditionRole.THUMBNAIL: {
36
+ 'width': 200,
37
+ 'height': 200,
38
+ 'format': 'jpg',
39
+ 'waveform': False,
40
+ },
41
+ RenditionRole.AUDIO_PREVIEW: {
42
+ 'bitrate': '128k',
43
+ 'format': 'mp3',
44
+ 'duration': 30, # First 30 seconds as preview
45
+ },
46
+ RenditionRole.AUDIO_MP3: {
47
+ 'bitrate': '192k',
48
+ 'format': 'mp3',
49
+ },
50
+ }
51
+
52
+ def __init__(self, file: File):
53
+ super().__init__(file)
54
+ # Check if ffmpeg is available
55
+ self._check_ffmpeg()
56
+
57
+ def _check_ffmpeg(self):
58
+ """Check if ffmpeg is available in the system"""
59
+ try:
60
+ subprocess.run(["ffmpeg", "-version"],
61
+ stdout=subprocess.PIPE,
62
+ stderr=subprocess.PIPE,
63
+ check=True)
64
+ except (subprocess.SubprocessError, FileNotFoundError):
65
+ logger.warning("ffmpeg is not available. Audio rendering may not work properly.")
66
+
67
+ def _download_original(self) -> Union[str, None]:
68
+ """
69
+ Download the original file to a temporary location
70
+
71
+ Returns:
72
+ str: Path to the downloaded file, or None if download failed
73
+ """
74
+ try:
75
+ file_manager = self.file.file_manager
76
+ backend = file_manager.backend
77
+
78
+ # Get file extension
79
+ _, ext = os.path.splitext(self.file.filename)
80
+ temp_path = self.get_temp_path(ext)
81
+
82
+ # Download file from storage
83
+ with open(temp_path, 'wb') as f:
84
+ backend.download(self.file.storage_file_path, f)
85
+
86
+ return temp_path
87
+ except Exception as e:
88
+ logger.error(f"Failed to download original audio file: {str(e)}")
89
+ return None
90
+
91
+ def _extract_audio_cover(self, source_path: str, width: int, height: int,
92
+ output_format: str) -> Tuple[str, str, int]:
93
+ """
94
+ Extract album artwork from audio file
95
+
96
+ Args:
97
+ source_path: Path to the source audio
98
+ width: Target width
99
+ height: Target height
100
+ output_format: Output format (jpg, png)
101
+
102
+ Returns:
103
+ Tuple[str, str, int]: (Output path, mime type, file size) or (None, None, 0) if extraction failed
104
+ """
105
+ temp_output = self.get_temp_path(f".{output_format}")
106
+
107
+ try:
108
+ # Use ffmpeg to extract album art
109
+ cmd = [
110
+ "ffmpeg",
111
+ "-y", # Overwrite output files
112
+ "-i", source_path, # Input file
113
+ "-an", # No audio
114
+ "-vcodec", "copy", # Copy video codec (album art)
115
+ temp_output # Output file
116
+ ]
117
+
118
+ # Run command, but don't fail if artwork doesn't exist
119
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
120
+
121
+ # Check if the output file exists and has content
122
+ if os.path.exists(temp_output) and os.path.getsize(temp_output) > 0:
123
+ # Resize the image to the requested dimensions
124
+ from PIL import Image
125
+ with Image.open(temp_output) as img:
126
+ img.thumbnail((width, height), Image.Resampling.LANCZOS)
127
+ img.save(temp_output, format=output_format.upper())
128
+
129
+ # Get file size
130
+ file_size = os.path.getsize(temp_output)
131
+
132
+ # Get mime type
133
+ mime_type = mimetypes.guess_type(f"file.{output_format}")[0]
134
+
135
+ return temp_output, mime_type, file_size
136
+ else:
137
+ # If no artwork, create a default audio thumbnail
138
+ return self._create_default_audio_thumbnail(width, height, output_format)
139
+
140
+ except Exception as e:
141
+ logger.error(f"Failed to extract audio cover: {str(e)}")
142
+ return self._create_default_audio_thumbnail(width, height, output_format)
143
+
144
+ def _create_default_audio_thumbnail(self, width: int, height: int,
145
+ output_format: str) -> Tuple[str, str, int]:
146
+ """
147
+ Create a default audio thumbnail when no album art is available
148
+
149
+ Args:
150
+ width: Target width
151
+ height: Target height
152
+ output_format: Output format (jpg, png)
153
+
154
+ Returns:
155
+ Tuple[str, str, int]: (Output path, mime type, file size)
156
+ """
157
+ temp_output = self.get_temp_path(f".{output_format}")
158
+
159
+ try:
160
+ # Create a simple gradient image with a music note icon
161
+ from PIL import Image, ImageDraw, ImageFont
162
+
163
+ # Create a gradient background
164
+ img = Image.new('RGB', (width, height), color=(60, 60, 60))
165
+ draw = ImageDraw.Draw(img)
166
+
167
+ # Draw a music note icon or text
168
+ try:
169
+ # Draw a circle in the center
170
+ circle_x, circle_y = width // 2, height // 2
171
+ circle_radius = min(width, height) // 4
172
+ draw.ellipse(
173
+ (circle_x - circle_radius, circle_y - circle_radius,
174
+ circle_x + circle_radius, circle_y + circle_radius),
175
+ fill=(100, 100, 100)
176
+ )
177
+
178
+ # Draw audio file name
179
+ name, _ = os.path.splitext(self.file.filename)
180
+ draw.text((width//2, height//2), name, fill=(220, 220, 220),
181
+ anchor="mm")
182
+ except Exception:
183
+ # If text drawing fails, just use the background
184
+ pass
185
+
186
+ # Save the image
187
+ img.save(temp_output, format=output_format.upper())
188
+
189
+ # Get file size
190
+ file_size = os.path.getsize(temp_output)
191
+
192
+ # Get mime type
193
+ mime_type = mimetypes.guess_type(f"file.{output_format}")[0]
194
+
195
+ return temp_output, mime_type, file_size
196
+
197
+ except Exception as e:
198
+ logger.error(f"Failed to create default audio thumbnail: {str(e)}")
199
+ # Create a very basic fallback
200
+ img = Image.new('RGB', (width, height), color=(80, 80, 80))
201
+ img.save(temp_output, format=output_format.upper())
202
+ file_size = os.path.getsize(temp_output)
203
+ mime_type = mimetypes.guess_type(f"file.{output_format}")[0]
204
+ return temp_output, mime_type, file_size
205
+
206
+ def _create_waveform_image(self, source_path: str, width: int, height: int,
207
+ output_format: str) -> Tuple[str, str, int]:
208
+ """
209
+ Create a waveform visualization of the audio file
210
+
211
+ Args:
212
+ source_path: Path to the source audio
213
+ width: Target width
214
+ height: Target height
215
+ output_format: Output format (jpg, png)
216
+
217
+ Returns:
218
+ Tuple[str, str, int]: (Output path, mime type, file size)
219
+ """
220
+ temp_output = self.get_temp_path(f".{output_format}")
221
+
222
+ try:
223
+ # Use ffmpeg to generate a waveform image
224
+ cmd = [
225
+ "ffmpeg",
226
+ "-y", # Overwrite output files
227
+ "-i", source_path, # Input file
228
+ "-filter_complex", f"showwavespic=s={width}x{height}:colors=white",
229
+ "-frames:v", "1",
230
+ temp_output # Output file
231
+ ]
232
+
233
+ subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
234
+
235
+ # Get file size
236
+ file_size = os.path.getsize(temp_output)
237
+
238
+ # Get mime type
239
+ mime_type = mimetypes.guess_type(f"file.{output_format}")[0]
240
+
241
+ return temp_output, mime_type, file_size
242
+
243
+ except subprocess.SubprocessError as e:
244
+ logger.error(f"Failed to create audio waveform: {str(e)}")
245
+ return self._create_default_audio_thumbnail(width, height, output_format)
246
+
247
+ def _convert_audio(self, source_path: str, options: Dict) -> Tuple[str, str, int]:
248
+ """
249
+ Convert audio to different format with specified options
250
+
251
+ Args:
252
+ source_path: Path to the source audio
253
+ options: Conversion options
254
+
255
+ Returns:
256
+ Tuple[str, str, int]: (Output path, mime type, file size)
257
+ """
258
+ output_format = options.get('format', 'mp3')
259
+ bitrate = options.get('bitrate', '192k')
260
+ duration = options.get('duration') # Optional duration limit in seconds
261
+
262
+ temp_output = self.get_temp_path(f".{output_format}")
263
+
264
+ try:
265
+ # Build ffmpeg command
266
+ cmd = [
267
+ "ffmpeg",
268
+ "-y", # Overwrite output files
269
+ "-i", source_path, # Input file
270
+ ]
271
+
272
+ # Add duration limit if specified
273
+ if duration:
274
+ cmd.extend(["-t", str(duration)])
275
+
276
+ # Audio settings
277
+ cmd.extend([
278
+ "-b:a", bitrate,
279
+ "-ar", "44100", # Sample rate
280
+ ])
281
+
282
+ # Specific format settings
283
+ if output_format == 'mp3':
284
+ cmd.extend(["-c:a", "libmp3lame"])
285
+ elif output_format == 'ogg':
286
+ cmd.extend(["-c:a", "libvorbis"])
287
+ elif output_format == 'aac' or output_format == 'm4a':
288
+ cmd.extend(["-c:a", "aac"])
289
+
290
+ # Add output file
291
+ cmd.append(temp_output)
292
+
293
+ subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
294
+
295
+ # Get file size
296
+ file_size = os.path.getsize(temp_output)
297
+
298
+ # Get mime type
299
+ mime_type = mimetypes.guess_type(f"file.{output_format}")[0]
300
+
301
+ return temp_output, mime_type, file_size
302
+
303
+ except subprocess.SubprocessError as e:
304
+ logger.error(f"Failed to convert audio: {str(e)}")
305
+ if os.path.exists(temp_output):
306
+ os.unlink(temp_output)
307
+ raise
308
+
309
+ def create_rendition(self, role: str, options: Dict = None) -> Optional[FileRendition]:
310
+ """
311
+ Create an audio rendition for the specified role
312
+
313
+ Args:
314
+ role: The role of the rendition
315
+ options: Additional options for creating the rendition
316
+
317
+ Returns:
318
+ FileRendition: The created rendition, or None if creation failed
319
+ """
320
+ try:
321
+ # Get rendition settings
322
+ settings = self.default_renditions.get(role, {})
323
+ if options:
324
+ settings.update(options)
325
+
326
+ # Download the original file
327
+ source_path = self._download_original()
328
+ if not source_path:
329
+ return None
330
+
331
+ try:
332
+ temp_output = None
333
+ mime_type = None
334
+ file_size = None
335
+
336
+ # Process based on role type
337
+ if role in [RenditionRole.THUMBNAIL, RenditionRole.AUDIO_THUMBNAIL]:
338
+ # Create thumbnail image (either from album art or waveform)
339
+ width = settings.get('width', 300)
340
+ height = settings.get('height', 300)
341
+ output_format = settings.get('format', 'jpg')
342
+ use_waveform = settings.get('waveform', False)
343
+
344
+ if use_waveform:
345
+ temp_output, mime_type, file_size = self._create_waveform_image(
346
+ source_path, width, height, output_format
347
+ )
348
+ else:
349
+ temp_output, mime_type, file_size = self._extract_audio_cover(
350
+ source_path, width, height, output_format
351
+ )
352
+
353
+ # Set filename
354
+ name, _ = os.path.splitext(self.file.filename)
355
+ filename = f"{name}_{role}.{output_format}"
356
+ category = 'image' # Thumbnails are images
357
+
358
+ else:
359
+ # Create audio rendition
360
+ temp_output, mime_type, file_size = self._convert_audio(
361
+ source_path, settings
362
+ )
363
+
364
+ # Set filename
365
+ name, _ = os.path.splitext(self.file.filename)
366
+ output_format = settings.get('format', 'mp3')
367
+ filename = f"{name}_{role}.{output_format}"
368
+ category = 'audio'
369
+
370
+ # Save to storage
371
+ file_manager = self.file.file_manager
372
+ backend = file_manager.backend
373
+ storage_path = os.path.join(
374
+ os.path.dirname(self.file.storage_file_path),
375
+ filename
376
+ )
377
+
378
+ # Upload to storage
379
+ with open(temp_output, 'rb') as f:
380
+ backend.save(f, storage_path, mime_type)
381
+
382
+ # Create rendition record
383
+ rendition = self._create_rendition_object(
384
+ role=role,
385
+ filename=filename,
386
+ storage_path=storage_path,
387
+ content_type=mime_type,
388
+ category=category,
389
+ file_size=file_size
390
+ )
391
+
392
+ return rendition
393
+
394
+ finally:
395
+ # Clean up temporary files
396
+ if source_path and os.path.exists(source_path):
397
+ os.unlink(source_path)
398
+ if temp_output and os.path.exists(temp_output):
399
+ os.unlink(temp_output)
400
+
401
+ except Exception as e:
402
+ logger.error(f"Failed to create audio rendition '{role}': {str(e)}")
403
+ return None
@@ -0,0 +1,205 @@
1
+ import os
2
+ import logging
3
+ from abc import ABC, abstractmethod
4
+ from typing import Dict, List, Optional, Tuple, Any, Union
5
+ from django.conf import settings
6
+ from mojo.apps.fileman.models import File, FileRendition
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ class RenditionRole:
11
+ """
12
+ Predefined roles for file renditions
13
+ """
14
+ # Common roles
15
+ ORIGINAL = 'original'
16
+ THUMBNAIL = 'thumbnail'
17
+ PREVIEW = 'preview'
18
+
19
+ # Image-specific roles
20
+ THUMBNAIL_SM = 'thumbnail_sm'
21
+ THUMBNAIL_MD = 'thumbnail_md'
22
+ THUMBNAIL_LG = 'thumbnail_lg'
23
+ SQUARE_SM = 'square_sm'
24
+ SQUARE_MD = 'square_md'
25
+ SQUARE_LG = 'square_lg'
26
+
27
+ # Video-specific roles
28
+ VIDEO_THUMBNAIL = 'video_thumbnail'
29
+ VIDEO_PREVIEW = 'video_preview'
30
+ VIDEO_MP4 = 'video_mp4'
31
+ VIDEO_WEBM = 'video_webm'
32
+
33
+ # Document-specific roles
34
+ DOCUMENT_THUMBNAIL = 'document_thumbnail'
35
+ DOCUMENT_PREVIEW = 'document_preview'
36
+ DOCUMENT_PDF = 'document_pdf'
37
+
38
+ # Audio-specific roles
39
+ AUDIO_THUMBNAIL = 'audio_thumbnail'
40
+ AUDIO_PREVIEW = 'audio_preview'
41
+ AUDIO_MP3 = 'audio_mp3'
42
+
43
+
44
+ class BaseRenderer(ABC):
45
+ """
46
+ Base class for file renderers
47
+
48
+ A renderer creates different versions (renditions) of a file based on
49
+ predefined roles. Each renderer supports specific file categories and
50
+ provides implementations for creating renditions.
51
+ """
52
+
53
+ # The file categories this renderer supports
54
+ supported_categories = []
55
+
56
+ # Default rendition definitions:
57
+ # mapping of role -> (width, height, options)
58
+ default_renditions = {}
59
+
60
+ def __init__(self, file: File):
61
+ """
62
+ Initialize renderer with a file
63
+
64
+ Args:
65
+ file: The original file to create renditions from
66
+ """
67
+ self.file = file
68
+ self.renditions = {}
69
+ self._load_existing_renditions()
70
+
71
+ def _load_existing_renditions(self):
72
+ """Load existing renditions for this file"""
73
+ for rendition in FileRendition.objects.filter(original_file=self.file):
74
+ self.renditions[rendition.role] = rendition
75
+
76
+ @classmethod
77
+ def supports_file(cls, file: File) -> bool:
78
+ """
79
+ Check if this renderer supports the given file
80
+
81
+ Args:
82
+ file: The file to check
83
+
84
+ Returns:
85
+ bool: True if this renderer supports the file, False otherwise
86
+ """
87
+ return file.category in cls.supported_categories
88
+
89
+ @abstractmethod
90
+ def create_rendition(self, role: str, options: Dict = None) -> Optional[FileRendition]:
91
+ """
92
+ Create a rendition for the specified role
93
+
94
+ Args:
95
+ role: The role of the rendition (e.g., 'thumbnail', 'preview')
96
+ options: Additional options for creating the rendition
97
+
98
+ Returns:
99
+ FileRendition: The created rendition, or None if creation failed
100
+ """
101
+ pass
102
+
103
+ def get_rendition(self, role: str, create_if_missing: bool = True) -> Optional[FileRendition]:
104
+ """
105
+ Get a rendition for the specified role
106
+
107
+ Args:
108
+ role: The role of the rendition
109
+ create_if_missing: Whether to create the rendition if it doesn't exist
110
+
111
+ Returns:
112
+ FileRendition: The rendition, or None if not found and not created
113
+ """
114
+ if role in self.renditions:
115
+ return self.renditions[role]
116
+
117
+ if create_if_missing:
118
+ options = self.default_renditions.get(role, {})
119
+ rendition = self.create_rendition(role, options)
120
+ if rendition:
121
+ self.renditions[role] = rendition
122
+ return rendition
123
+
124
+ return None
125
+
126
+ def create_all_renditions(self) -> List[FileRendition]:
127
+ """
128
+ Create all default renditions for this file
129
+
130
+ Returns:
131
+ List[FileRendition]: List of created renditions
132
+ """
133
+ results = []
134
+ for role, options in self.default_renditions.items():
135
+ rendition = self.get_rendition(role)
136
+ if rendition:
137
+ results.append(rendition)
138
+ return results
139
+
140
+ def cleanup_renditions(self):
141
+ """
142
+ Remove all renditions for this file
143
+ """
144
+ FileRendition.objects.filter(original_file=self.file).delete()
145
+ self.renditions = {}
146
+
147
+ def _create_rendition_object(self, role: str, filename: str, storage_path: str,
148
+ content_type: str, category: str, file_size: int = None) -> FileRendition:
149
+ """
150
+ Create a FileRendition object in the database
151
+
152
+ Args:
153
+ role: The role of the rendition
154
+ filename: The filename of the rendition
155
+ storage_path: The storage path of the rendition
156
+ content_type: The MIME type of the rendition
157
+ category: The category of the rendition
158
+ file_size: The size of the rendition in bytes
159
+
160
+ Returns:
161
+ FileRendition: The created rendition object
162
+ """
163
+ rendition = FileRendition(
164
+ original_file=self.file,
165
+ role=role,
166
+ filename=filename,
167
+ storage_path=storage_path,
168
+ content_type=content_type,
169
+ category=category,
170
+ file_size=file_size,
171
+ upload_status=FileRendition.COMPLETED
172
+ )
173
+ rendition.save()
174
+ return rendition
175
+
176
+ def get_temp_path(self, suffix: str = '') -> str:
177
+ """
178
+ Get a temporary file path for processing
179
+
180
+ Args:
181
+ suffix: Optional suffix for the temp file (e.g., '.jpg')
182
+
183
+ Returns:
184
+ str: Path to a temporary file
185
+ """
186
+ import tempfile
187
+ temp_dir = getattr(settings, 'MOJO_TEMP_DIR', None)
188
+ if temp_dir:
189
+ os.makedirs(temp_dir, exist_ok=True)
190
+ return os.path.join(temp_dir, f"{self.file.id}_{suffix}")
191
+ return tempfile.mktemp(suffix=suffix)
192
+
193
+ @staticmethod
194
+ def get_renderer_for_file(file: File) -> Optional['BaseRenderer']:
195
+ """
196
+ Get the appropriate renderer for a file
197
+
198
+ Args:
199
+ file: The file to get a renderer for
200
+
201
+ Returns:
202
+ BaseRenderer: The renderer instance, or None if no renderer supports the file
203
+ """
204
+ from mojo.apps.fileman.renderer import get_renderer_for_file
205
+ return get_renderer_for_file(file)