django-nativemojo 0.1.10__py3-none-any.whl → 0.1.16__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.
- django_nativemojo-0.1.16.dist-info/METADATA +138 -0
- django_nativemojo-0.1.16.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/__init__.py +5 -0
- mojo/apps/account/management/commands/__init__.py +6 -0
- mojo/apps/account/management/commands/serializer_admin.py +651 -0
- mojo/apps/account/migrations/0004_user_avatar.py +20 -0
- mojo/apps/account/migrations/0005_group_last_activity.py +18 -0
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +281 -0
- mojo/apps/account/models/group.py +319 -15
- mojo/apps/account/models/member.py +29 -5
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +369 -19
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +9 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +100 -6
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +7 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/s3.py +64 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/README.md +8 -8
- mojo/apps/fileman/backends/base.py +76 -70
- mojo/apps/fileman/backends/filesystem.py +86 -86
- mojo/apps/fileman/backends/s3.py +409 -108
- mojo/apps/fileman/migrations/0001_initial.py +106 -0
- mojo/apps/fileman/migrations/0002_filemanager_parent_alter_filemanager_max_file_size.py +24 -0
- mojo/apps/fileman/migrations/0003_remove_file_fileman_fil_upload__c4bc35_idx_and_more.py +25 -0
- mojo/apps/fileman/migrations/0004_remove_file_original_filename_and_more.py +39 -0
- mojo/apps/fileman/migrations/0005_alter_file_upload_token.py +18 -0
- mojo/apps/fileman/migrations/0006_file_download_url_filemanager_forever_urls.py +23 -0
- mojo/apps/fileman/migrations/0007_remove_filemanager_forever_urls_and_more.py +22 -0
- mojo/apps/fileman/migrations/0008_file_category.py +18 -0
- mojo/apps/fileman/migrations/0009_rename_file_path_file_storage_file_path.py +18 -0
- mojo/apps/fileman/migrations/0010_filerendition.py +33 -0
- mojo/apps/fileman/migrations/0011_alter_filerendition_original_file.py +19 -0
- mojo/apps/fileman/models/__init__.py +1 -5
- mojo/apps/fileman/models/file.py +240 -58
- mojo/apps/fileman/models/manager.py +427 -31
- mojo/apps/fileman/models/rendition.py +118 -0
- mojo/apps/fileman/renderer/__init__.py +111 -0
- mojo/apps/fileman/renderer/audio.py +403 -0
- mojo/apps/fileman/renderer/base.py +205 -0
- mojo/apps/fileman/renderer/document.py +404 -0
- mojo/apps/fileman/renderer/image.py +222 -0
- mojo/apps/fileman/renderer/utils.py +297 -0
- mojo/apps/fileman/renderer/video.py +304 -0
- mojo/apps/fileman/rest/__init__.py +1 -18
- mojo/apps/fileman/rest/upload.py +22 -32
- mojo/apps/fileman/signals.py +58 -0
- mojo/apps/fileman/tasks.py +254 -0
- mojo/apps/fileman/utils/__init__.py +40 -16
- mojo/apps/incident/migrations/0005_incidenthistory.py +39 -0
- mojo/apps/incident/migrations/0006_alter_incident_state.py +18 -0
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +2 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/history.py +36 -0
- mojo/apps/incident/models/incident.py +3 -1
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -1
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/event.py +7 -1
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/migrations/0004_alter_log_level.py +18 -0
- mojo/apps/logit/models/log.py +7 -1
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +19 -2
- mojo/decorators/auth.py +6 -1
- mojo/decorators/http.py +47 -3
- mojo/helpers/aws/__init__.py +45 -0
- mojo/helpers/aws/ec2.py +804 -0
- mojo/helpers/aws/iam.py +748 -0
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/s3.py +451 -11
- mojo/helpers/aws/ses.py +483 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/aws/sns.py +461 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/dates.py +18 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +14 -2
- mojo/helpers/settings/__init__.py +2 -0
- mojo/helpers/{settings.py → settings/helper.py} +1 -37
- mojo/helpers/settings/parser.py +132 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +10 -0
- mojo/models/rest.py +494 -65
- mojo/models/secrets.py +98 -3
- mojo/serializers/__init__.py +106 -0
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/core/manager.py +550 -0
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/examples/settings.py +322 -0
- mojo/serializers/formats/csv.py +393 -0
- mojo/serializers/formats/localizers.py +509 -0
- mojo/serializers/{models.py → simple.py} +38 -15
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +35 -4
- testit/runner.py +23 -6
- django_nativemojo-0.1.10.dist-info/METADATA +0 -96
- django_nativemojo-0.1.10.dist-info/RECORD +0 -194
- mojo/apps/metrics/rest/db.py +0 -0
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/bounce.py +0 -0
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -11
- mojo/apps/tasks/manager.py +0 -489
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -62
- mojo/apps/tasks/runner.py +0 -174
- mojo/apps/tasks/tq_handlers.py +0 -14
- mojo/helpers/aws/setup_email.py +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/ws4redis/README.md +0 -174
- mojo/ws4redis/__init__.py +0 -2
- mojo/ws4redis/client.py +0 -283
- mojo/ws4redis/connection.py +0 -327
- mojo/ws4redis/exceptions.py +0 -32
- mojo/ws4redis/redis.py +0 -183
- mojo/ws4redis/servers/base.py +0 -86
- mojo/ws4redis/servers/django.py +0 -171
- mojo/ws4redis/servers/uwsgi.py +0 -63
- mojo/ws4redis/settings.py +0 -45
- mojo/ws4redis/utf8validator.py +0 -128
- mojo/ws4redis/websocket.py +0 -403
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.10.dist-info → django_nativemojo-0.1.16.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/providers → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/rest → fileman/migrations}/__init__.py +0 -0
- /mojo/{ws4redis/servers → apps/jobs/examples}/__init__.py +0 -0
- /mojo/apps/{fileman/models/render.py → jobs/migrations/__init__.py} +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/{apps/fileman/rest/__init__ → serializers/formats/__init__.py} +0 -0
@@ -0,0 +1,304 @@
|
|
1
|
+
import os
|
2
|
+
import io
|
3
|
+
import subprocess
|
4
|
+
import tempfile
|
5
|
+
import logging
|
6
|
+
import mimetypes
|
7
|
+
import shutil
|
8
|
+
from typing import Dict, Optional, Tuple, Union, BinaryIO, List
|
9
|
+
|
10
|
+
from mojo.apps.fileman.models import File, FileRendition
|
11
|
+
from mojo.apps.fileman.renderer.base import BaseRenderer, RenditionRole
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
class VideoRenderer(BaseRenderer):
|
16
|
+
"""
|
17
|
+
Renderer for video files
|
18
|
+
|
19
|
+
Creates various renditions like thumbnails, previews, and different formats using ffmpeg
|
20
|
+
"""
|
21
|
+
|
22
|
+
# Video file categories
|
23
|
+
supported_categories = ['video']
|
24
|
+
|
25
|
+
# Default rendition definitions with options
|
26
|
+
default_renditions = {
|
27
|
+
RenditionRole.VIDEO_THUMBNAIL: {
|
28
|
+
'width': 300,
|
29
|
+
'height': 169,
|
30
|
+
'time_offset': '00:00:03',
|
31
|
+
'format': 'jpg'
|
32
|
+
},
|
33
|
+
RenditionRole.THUMBNAIL: {
|
34
|
+
'width': 300,
|
35
|
+
'height': 169,
|
36
|
+
'time_offset': '00:00:03',
|
37
|
+
'format': 'jpg'
|
38
|
+
},
|
39
|
+
RenditionRole.VIDEO_PREVIEW: {
|
40
|
+
'width': 640,
|
41
|
+
'height': 360,
|
42
|
+
'bitrate': '500k',
|
43
|
+
'duration': 10,
|
44
|
+
'format': 'mp4',
|
45
|
+
'audio': True,
|
46
|
+
},
|
47
|
+
RenditionRole.VIDEO_MP4: {
|
48
|
+
'width': 1280,
|
49
|
+
'height': 720,
|
50
|
+
'bitrate': '2000k',
|
51
|
+
'format': 'mp4',
|
52
|
+
'audio': True,
|
53
|
+
},
|
54
|
+
RenditionRole.VIDEO_WEBM: {
|
55
|
+
'width': 1280,
|
56
|
+
'height': 720,
|
57
|
+
'bitrate': '2000k',
|
58
|
+
'format': 'webm',
|
59
|
+
'audio': True,
|
60
|
+
},
|
61
|
+
}
|
62
|
+
|
63
|
+
def __init__(self, file: File):
|
64
|
+
super().__init__(file)
|
65
|
+
# Check if ffmpeg is available
|
66
|
+
self._check_ffmpeg()
|
67
|
+
|
68
|
+
def _check_ffmpeg(self):
|
69
|
+
"""Check if ffmpeg is available in the system"""
|
70
|
+
try:
|
71
|
+
subprocess.run(["ffmpeg", "-version"],
|
72
|
+
stdout=subprocess.PIPE,
|
73
|
+
stderr=subprocess.PIPE,
|
74
|
+
check=True)
|
75
|
+
except (subprocess.SubprocessError, FileNotFoundError):
|
76
|
+
logger.warning("ffmpeg is not available. Video rendering may not work properly.")
|
77
|
+
|
78
|
+
def _download_original(self) -> Union[str, None]:
|
79
|
+
"""
|
80
|
+
Download the original file to a temporary location
|
81
|
+
|
82
|
+
Returns:
|
83
|
+
str: Path to the downloaded file, or None if download failed
|
84
|
+
"""
|
85
|
+
try:
|
86
|
+
file_manager = self.file.file_manager
|
87
|
+
backend = file_manager.backend
|
88
|
+
|
89
|
+
# Get file extension
|
90
|
+
_, ext = os.path.splitext(self.file.filename)
|
91
|
+
temp_path = self.get_temp_path(ext)
|
92
|
+
|
93
|
+
# Download file from storage
|
94
|
+
with open(temp_path, 'wb') as f:
|
95
|
+
backend.download(self.file.storage_file_path, f)
|
96
|
+
|
97
|
+
return temp_path
|
98
|
+
except Exception as e:
|
99
|
+
logger.error(f"Failed to download original video file: {str(e)}")
|
100
|
+
return None
|
101
|
+
|
102
|
+
def _create_thumbnail(self, source_path: str, width: int, height: int,
|
103
|
+
time_offset: str, output_format: str) -> Tuple[str, str, int]:
|
104
|
+
"""
|
105
|
+
Create a thumbnail from a video at specified time offset
|
106
|
+
|
107
|
+
Args:
|
108
|
+
source_path: Path to the source video
|
109
|
+
width: Target width
|
110
|
+
height: Target height
|
111
|
+
time_offset: Time offset for thumbnail (format: HH:MM:SS)
|
112
|
+
output_format: Output format (jpg, png)
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
Tuple[str, str, int]: (Output path, mime type, file size)
|
116
|
+
"""
|
117
|
+
temp_output = self.get_temp_path(f".{output_format}")
|
118
|
+
|
119
|
+
try:
|
120
|
+
# Use ffmpeg to extract a frame
|
121
|
+
cmd = [
|
122
|
+
"ffmpeg",
|
123
|
+
"-y", # Overwrite output files
|
124
|
+
"-ss", time_offset, # Seek to time offset
|
125
|
+
"-i", source_path, # Input file
|
126
|
+
"-vframes", "1", # Extract one frame
|
127
|
+
"-s", f"{width}x{height}", # Set size
|
128
|
+
"-f", "image2", # Force image2 format
|
129
|
+
temp_output # Output file
|
130
|
+
]
|
131
|
+
|
132
|
+
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
133
|
+
|
134
|
+
# Get file size
|
135
|
+
file_size = os.path.getsize(temp_output)
|
136
|
+
|
137
|
+
# Get mime type
|
138
|
+
mime_type = mimetypes.guess_type(f"file.{output_format}")[0]
|
139
|
+
|
140
|
+
return temp_output, mime_type, file_size
|
141
|
+
|
142
|
+
except subprocess.SubprocessError as e:
|
143
|
+
logger.error(f"Failed to create video thumbnail: {str(e)}")
|
144
|
+
if os.path.exists(temp_output):
|
145
|
+
os.unlink(temp_output)
|
146
|
+
raise
|
147
|
+
|
148
|
+
def _create_video_rendition(self, source_path: str, options: Dict) -> Tuple[str, str, int]:
|
149
|
+
"""
|
150
|
+
Create a video rendition with specified options
|
151
|
+
|
152
|
+
Args:
|
153
|
+
source_path: Path to the source video
|
154
|
+
options: Video processing options
|
155
|
+
|
156
|
+
Returns:
|
157
|
+
Tuple[str, str, int]: (Output path, mime type, file size)
|
158
|
+
"""
|
159
|
+
width = options.get('width', 1280)
|
160
|
+
height = options.get('height', 720)
|
161
|
+
bitrate = options.get('bitrate', '2000k')
|
162
|
+
output_format = options.get('format', 'mp4')
|
163
|
+
duration = options.get('duration') # Optional duration limit in seconds
|
164
|
+
audio = options.get('audio', True)
|
165
|
+
|
166
|
+
temp_output = self.get_temp_path(f".{output_format}")
|
167
|
+
|
168
|
+
try:
|
169
|
+
# Build ffmpeg command
|
170
|
+
cmd = [
|
171
|
+
"ffmpeg",
|
172
|
+
"-y", # Overwrite output files
|
173
|
+
"-i", source_path, # Input file
|
174
|
+
]
|
175
|
+
|
176
|
+
# Add duration limit if specified
|
177
|
+
if duration:
|
178
|
+
cmd.extend(["-t", str(duration)])
|
179
|
+
|
180
|
+
# Video settings
|
181
|
+
cmd.extend([
|
182
|
+
"-vf", f"scale={width}:{height}",
|
183
|
+
"-c:v", "libx264" if output_format == "mp4" else "libvpx",
|
184
|
+
"-b:v", bitrate,
|
185
|
+
])
|
186
|
+
|
187
|
+
# Audio settings
|
188
|
+
if audio:
|
189
|
+
if output_format == "mp4":
|
190
|
+
cmd.extend(["-c:a", "aac", "-b:a", "128k"])
|
191
|
+
else: # webm
|
192
|
+
cmd.extend(["-c:a", "libvorbis", "-b:a", "128k"])
|
193
|
+
else:
|
194
|
+
cmd.extend(["-an"]) # No audio
|
195
|
+
|
196
|
+
# Add output file
|
197
|
+
cmd.append(temp_output)
|
198
|
+
|
199
|
+
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
200
|
+
|
201
|
+
# Get file size
|
202
|
+
file_size = os.path.getsize(temp_output)
|
203
|
+
|
204
|
+
# Get mime type
|
205
|
+
mime_type = mimetypes.guess_type(f"file.{output_format}")[0]
|
206
|
+
|
207
|
+
return temp_output, mime_type, file_size
|
208
|
+
|
209
|
+
except subprocess.SubprocessError as e:
|
210
|
+
logger.error(f"Failed to create video rendition: {str(e)}")
|
211
|
+
if os.path.exists(temp_output):
|
212
|
+
os.unlink(temp_output)
|
213
|
+
raise
|
214
|
+
|
215
|
+
def create_rendition(self, role: str, options: Dict = None) -> Optional[FileRendition]:
|
216
|
+
"""
|
217
|
+
Create a video rendition for the specified role
|
218
|
+
|
219
|
+
Args:
|
220
|
+
role: The role of the rendition
|
221
|
+
options: Additional options for creating the rendition
|
222
|
+
|
223
|
+
Returns:
|
224
|
+
FileRendition: The created rendition, or None if creation failed
|
225
|
+
"""
|
226
|
+
try:
|
227
|
+
# Get rendition settings
|
228
|
+
settings = self.default_renditions.get(role, {})
|
229
|
+
if options:
|
230
|
+
settings.update(options)
|
231
|
+
|
232
|
+
# Download the original file
|
233
|
+
source_path = self._download_original()
|
234
|
+
if not source_path:
|
235
|
+
return None
|
236
|
+
|
237
|
+
try:
|
238
|
+
temp_output = None
|
239
|
+
mime_type = None
|
240
|
+
file_size = None
|
241
|
+
|
242
|
+
# Process based on role type
|
243
|
+
if role in [RenditionRole.THUMBNAIL, RenditionRole.VIDEO_THUMBNAIL]:
|
244
|
+
# Create thumbnail image
|
245
|
+
width = settings.get('width', 300)
|
246
|
+
height = settings.get('height', 169)
|
247
|
+
time_offset = settings.get('time_offset', '00:00:03')
|
248
|
+
output_format = settings.get('format', 'jpg')
|
249
|
+
|
250
|
+
temp_output, mime_type, file_size = self._create_thumbnail(
|
251
|
+
source_path, width, height, time_offset, output_format
|
252
|
+
)
|
253
|
+
|
254
|
+
# Set filename
|
255
|
+
name, _ = os.path.splitext(self.file.filename)
|
256
|
+
filename = f"{name}_{role}.{output_format}"
|
257
|
+
category = 'image' # Thumbnails are images
|
258
|
+
|
259
|
+
else:
|
260
|
+
# Create video rendition
|
261
|
+
temp_output, mime_type, file_size = self._create_video_rendition(
|
262
|
+
source_path, settings
|
263
|
+
)
|
264
|
+
|
265
|
+
# Set filename
|
266
|
+
name, _ = os.path.splitext(self.file.filename)
|
267
|
+
output_format = settings.get('format', 'mp4')
|
268
|
+
filename = f"{name}_{role}.{output_format}"
|
269
|
+
category = 'video'
|
270
|
+
|
271
|
+
# Save to storage
|
272
|
+
file_manager = self.file.file_manager
|
273
|
+
backend = file_manager.backend
|
274
|
+
storage_path = os.path.join(
|
275
|
+
os.path.dirname(self.file.storage_file_path),
|
276
|
+
filename
|
277
|
+
)
|
278
|
+
|
279
|
+
# Upload to storage
|
280
|
+
with open(temp_output, 'rb') as f:
|
281
|
+
backend.save(f, storage_path, mime_type)
|
282
|
+
|
283
|
+
# Create rendition record
|
284
|
+
rendition = self._create_rendition_object(
|
285
|
+
role=role,
|
286
|
+
filename=filename,
|
287
|
+
storage_path=storage_path,
|
288
|
+
content_type=mime_type,
|
289
|
+
category=category,
|
290
|
+
file_size=file_size
|
291
|
+
)
|
292
|
+
|
293
|
+
return rendition
|
294
|
+
|
295
|
+
finally:
|
296
|
+
# Clean up temporary files
|
297
|
+
if source_path and os.path.exists(source_path):
|
298
|
+
os.unlink(source_path)
|
299
|
+
if temp_output and os.path.exists(temp_output):
|
300
|
+
os.unlink(temp_output)
|
301
|
+
|
302
|
+
except Exception as e:
|
303
|
+
logger.error(f"Failed to create video rendition '{role}': {str(e)}")
|
304
|
+
return None
|
@@ -3,21 +3,4 @@ File Manager REST API endpoints
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
from .fileman import on_filemanager, on_file
|
6
|
-
from .upload import
|
7
|
-
on_upload_initiate,
|
8
|
-
on_upload_finalize,
|
9
|
-
on_direct_upload,
|
10
|
-
on_download
|
11
|
-
)
|
12
|
-
|
13
|
-
__all__ = [
|
14
|
-
# File manager model endpoints
|
15
|
-
'on_filemanager',
|
16
|
-
'on_file',
|
17
|
-
|
18
|
-
# File upload/download endpoints
|
19
|
-
'on_upload_initiate',
|
20
|
-
'on_upload_finalize',
|
21
|
-
'on_direct_upload',
|
22
|
-
'on_download'
|
23
|
-
]
|
6
|
+
from .upload import *
|
mojo/apps/fileman/rest/upload.py
CHANGED
@@ -1,12 +1,7 @@
|
|
1
1
|
from mojo import decorators as md
|
2
2
|
from mojo import JsonResponse
|
3
|
+
import mojo.errors
|
3
4
|
from mojo.apps.fileman.models import File, FileManager
|
4
|
-
from mojo.apps.fileman.utils.upload import (
|
5
|
-
initiate_upload,
|
6
|
-
finalize_upload,
|
7
|
-
direct_upload,
|
8
|
-
get_download_url
|
9
|
-
)
|
10
5
|
|
11
6
|
|
12
7
|
@md.POST('upload/initiate')
|
@@ -20,40 +15,35 @@ def on_upload_initiate(request):
|
|
20
15
|
{
|
21
16
|
"filename": "document.pdf",
|
22
17
|
"content_type": "application/pdf",
|
23
|
-
"
|
18
|
+
"file_size": 1024000
|
24
19
|
}
|
25
20
|
],
|
26
|
-
"
|
27
|
-
"
|
21
|
+
"file_manager": 123, // optional
|
22
|
+
"group": 456, // optional
|
23
|
+
"user": 789, // optional
|
28
24
|
"metadata": { // optional global metadata
|
29
25
|
"source": "web_upload",
|
30
26
|
"category": "documents"
|
31
27
|
}
|
32
28
|
}
|
33
29
|
"""
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
}
|
52
|
-
}
|
53
|
-
"""
|
54
|
-
response_data = finalize_upload(request, request.DATA)
|
55
|
-
status_code = response_data.pop('status_code', 200)
|
56
|
-
return JsonResponse(response_data, status=status_code)
|
30
|
+
# first we need to get the correct file manager
|
31
|
+
file_manager = FileManager.get_from_request(request)
|
32
|
+
if file_manager is None:
|
33
|
+
raise mojo.errors.ValueException("No file manager found")
|
34
|
+
# new lets create a new file
|
35
|
+
file = File(
|
36
|
+
filename=request.DATA['filename'],
|
37
|
+
content_type=request.DATA['content_type'],
|
38
|
+
file_size=request.DATA['file_size'],
|
39
|
+
file_manager=file_manager,
|
40
|
+
group=file_manager.group,
|
41
|
+
user=request.user)
|
42
|
+
file.on_rest_pre_save({}, True)
|
43
|
+
file.mark_as_uploading()
|
44
|
+
file.save()
|
45
|
+
file.request_upload_url()
|
46
|
+
return file.on_rest_get(request, "upload")
|
57
47
|
|
58
48
|
|
59
49
|
@md.POST('upload/<str:upload_token>')
|
@@ -0,0 +1,58 @@
|
|
1
|
+
import logging
|
2
|
+
from django.db.models.signals import post_save, post_delete
|
3
|
+
from django.dispatch import receiver
|
4
|
+
from mojo.apps.fileman.models import File
|
5
|
+
from mojo.apps.fileman.tasks import process_file_renditions, cleanup_renditions
|
6
|
+
|
7
|
+
logger = logging.getLogger(__name__)
|
8
|
+
|
9
|
+
@receiver(post_save, sender=File)
|
10
|
+
def handle_file_upload_completed(sender, instance, created, **kwargs):
|
11
|
+
"""
|
12
|
+
When a file is marked as completed, automatically create renditions
|
13
|
+
|
14
|
+
This connects to the post_save signal of the File model and checks if the
|
15
|
+
file has been marked as completed. If so, it queues a task to create renditions.
|
16
|
+
"""
|
17
|
+
# Only process if the file upload is completed
|
18
|
+
if instance.is_completed:
|
19
|
+
# Check if this is a status change to completed
|
20
|
+
if not created:
|
21
|
+
try:
|
22
|
+
# Get the previous instance from the database
|
23
|
+
old_instance = sender.objects.get(pk=instance.pk)
|
24
|
+
if old_instance.upload_status != instance.upload_status and instance.upload_status == File.COMPLETED:
|
25
|
+
logger.info(f"File {instance.id} ({instance.filename}) marked as completed, creating renditions")
|
26
|
+
# Queue task to create renditions
|
27
|
+
process_file_renditions.delay(instance.id)
|
28
|
+
except Exception as e:
|
29
|
+
logger.error(f"Error checking file status change: {str(e)}")
|
30
|
+
else:
|
31
|
+
# For new files that are already completed
|
32
|
+
if instance.upload_status == File.COMPLETED:
|
33
|
+
logger.info(f"New file {instance.id} ({instance.filename}) is completed, creating renditions")
|
34
|
+
# Queue task to create renditions
|
35
|
+
process_file_renditions.delay(instance.id)
|
36
|
+
|
37
|
+
@receiver(post_delete, sender=File)
|
38
|
+
def handle_file_deleted(sender, instance, **kwargs):
|
39
|
+
"""
|
40
|
+
When a file is deleted, clean up its renditions
|
41
|
+
|
42
|
+
This connects to the post_delete signal of the File model and ensures that
|
43
|
+
when a file is deleted, its renditions are also removed from storage.
|
44
|
+
"""
|
45
|
+
try:
|
46
|
+
# Check if we have any renditions to clean up
|
47
|
+
from mojo.apps.fileman.models import FileRendition
|
48
|
+
if FileRendition.objects.filter(original_file_id=instance.id).exists():
|
49
|
+
logger.info(f"File {instance.id} ({instance.filename}) deleted, cleaning up renditions")
|
50
|
+
# Queue task to clean up renditions
|
51
|
+
# Note: We can't use instance.id directly in the task since the object is being deleted
|
52
|
+
# So we pass the ID value
|
53
|
+
cleanup_renditions.delay(instance.id)
|
54
|
+
except Exception as e:
|
55
|
+
logger.error(f"Error queueing rendition cleanup for deleted file: {str(e)}")
|
56
|
+
|
57
|
+
# Note: No need to manually connect signals as Django will automatically
|
58
|
+
# discover and connect properly decorated signal handlers
|