comparison medi/main.c @ 217:7ef4c9d2a72d hg-web

[DO NOT PUSH] Random values.
author MrJuneJune <me@mrjunejune.com>
date Sun, 25 Jan 2026 10:44:04 -0800
parents
children
comparison
equal deleted inserted replaced
192:b818a4561a3c 217:7ef4c9d2a72d
1 #include "seobeo/seobeo.h"
2 #include "media_processor/media_processor.h"
3
4 #include <stdio.h>
5 #include <stdlib.h>
6 #include <string.h>
7 #include <sys/stat.h>
8 #include <dirent.h>
9 #include <pthread.h>
10 #include <unistd.h>
11
12 #define UPLOAD_DIR "./uploads"
13 #define PROCESSED_DIR "./media"
14 #define MAX_UPLOAD_SIZE (100 * 1024 * 1024) // 100MB
15
16 // Background processing queue
17 typedef struct ProcessJob {
18 char* input_path;
19 char* id;
20 MediaType type;
21 struct ProcessJob* next;
22 } ProcessJob;
23
24 static ProcessJob* job_queue_head = NULL;
25 static ProcessJob* job_queue_tail = NULL;
26 static pthread_mutex_t job_queue_mutex = PTHREAD_MUTEX_INITIALIZER;
27 static pthread_cond_t job_queue_cond = PTHREAD_COND_INITIALIZER;
28
29 // ============================================================================
30 // Helper Functions
31 // ============================================================================
32
33 static void ensure_directories(void) {
34 mkdir(UPLOAD_DIR, 0755);
35 mkdir(PROCESSED_DIR, 0755);
36 }
37
38 static char* extract_filename_from_content_disposition(const char* header, Dowa_Arena* arena) {
39 const char* filename_start = strstr(header, "filename=\"");
40 if (!filename_start) return NULL;
41
42 filename_start += 10; // Skip 'filename="'
43 const char* filename_end = strchr(filename_start, '"');
44 if (!filename_end) return NULL;
45
46 size_t len = filename_end - filename_start;
47 char* filename = Dowa_Arena_Allocate(arena, len + 1);
48 memcpy(filename, filename_start, len);
49 filename[len] = '\0';
50 return filename;
51 }
52
53 static char* find_multipart_boundary(const char* content_type, Dowa_Arena* arena) {
54 const char* boundary_start = strstr(content_type, "boundary=");
55 if (!boundary_start) return NULL;
56
57 boundary_start += 9; // Skip 'boundary='
58
59 // Handle quoted boundary
60 if (*boundary_start == '"') {
61 boundary_start++;
62 const char* boundary_end = strchr(boundary_start, '"');
63 if (!boundary_end) return NULL;
64 size_t len = boundary_end - boundary_start;
65 char* boundary = Dowa_Arena_Allocate(arena, len + 1);
66 memcpy(boundary, boundary_start, len);
67 boundary[len] = '\0';
68 return boundary;
69 }
70
71 // Unquoted boundary
72 size_t len = strlen(boundary_start);
73 char* boundary = Dowa_Arena_Allocate(arena, len + 1);
74 strcpy(boundary, boundary_start);
75 // Trim whitespace/newline
76 while (len > 0 && (boundary[len-1] == '\r' || boundary[len-1] == '\n' || boundary[len-1] == ' ')) {
77 boundary[--len] = '\0';
78 }
79 return boundary;
80 }
81
82 // ============================================================================
83 // Background Processing Worker
84 // ============================================================================
85
86 static void enqueue_job(const char* input_path, const char* id, MediaType type) {
87 ProcessJob* job = malloc(sizeof(ProcessJob));
88 job->input_path = strdup(input_path);
89 job->id = strdup(id);
90 job->type = type;
91 job->next = NULL;
92
93 pthread_mutex_lock(&job_queue_mutex);
94 if (job_queue_tail) {
95 job_queue_tail->next = job;
96 job_queue_tail = job;
97 } else {
98 job_queue_head = job_queue_tail = job;
99 }
100 pthread_cond_signal(&job_queue_cond);
101 pthread_mutex_unlock(&job_queue_mutex);
102 }
103
104 static void* processing_worker(void* arg) {
105 (void)arg;
106
107 while (1) {
108 pthread_mutex_lock(&job_queue_mutex);
109 while (!job_queue_head) {
110 pthread_cond_wait(&job_queue_cond, &job_queue_mutex);
111 }
112 ProcessJob* job = job_queue_head;
113 job_queue_head = job->next;
114 if (!job_queue_head) job_queue_tail = NULL;
115 pthread_mutex_unlock(&job_queue_mutex);
116
117 // Process the job
118 Seobeo_Log(SEOBEO_INFO, "Processing media: %s (id=%s)", job->input_path, job->id);
119
120 MediaProcessorOpts opts = media_processor_opts_default(PROCESSED_DIR);
121 MediaResult* result = media_process_file(job->input_path, job->id, &opts);
122
123 if (result->status == MEDIA_STATUS_COMPLETED) {
124 Seobeo_Log(SEOBEO_INFO, "Processing completed for %s", job->id);
125 } else {
126 Seobeo_Log(SEOBEO_ERROR, "Processing failed for %s: %s", job->id, result->error_message);
127 }
128
129 media_result_free(result);
130
131 // Clean up temp upload file
132 unlink(job->input_path);
133
134 free(job->input_path);
135 free(job->id);
136 free(job);
137 }
138
139 return NULL;
140 }
141
142 void Seobeo_Render_Html(
143 char *final_body,
144 char *template,
145 Dowa_Arena *arena
146 )
147 {
148 size_t current_offset = 0;
149 char *cursor = template;
150
151 int32 token_len = 2;
152
153 while (1)
154 {
155 char *start_tag = strstr(cursor, "{{");
156 if (!start_tag) break;
157
158 char *end_tag = strstr(start_tag, "}}");
159 if (!end_tag) break;
160
161 size_t leading_len = start_tag - cursor;
162 memcpy(final_body + current_offset, cursor, leading_len);
163 current_offset += leading_len;
164
165 size_t name_len = end_tag - (start_tag + token_len);
166 char *include_name = Dowa_Arena_Allocate(arena, name_len + 1);
167 memcpy(include_name, start_tag + token_len, name_len);
168 include_name[name_len] = '\0';
169
170 size_t sub_file_size = 0;
171 char *sub_content = Seobeo_Web_LoadFile(include_name, &sub_file_size);
172 Seobeo_Log(SEOBEO_DEBUG, "[Curr] Sub content: %s\n", sub_content);
173 if (sub_content)
174 {
175 memcpy(final_body + current_offset, sub_content, sub_file_size);
176 current_offset += sub_file_size;
177 free(sub_content);
178 }
179
180 cursor = end_tag + 2;
181 }
182 strcpy(final_body + current_offset, cursor);
183 }
184
185
186 void Seobeo_Render_Html_FilePath(
187 char *final_body,
188 char *path,
189 Dowa_Arena *arena
190 ) {
191 Seobeo_Log(SEOBEO_DEBUG, "[Curr] %s\n", path);
192 size_t html_size = 0;
193 char *template = Seobeo_Web_LoadFile(path, &html_size);
194 if (!template) return;
195 Seobeo_Render_Html(final_body, template, arena);
196 }
197
198 // ============================================================================
199 // API Route Handlers
200 // ============================================================================
201
202 // GET / - Serve the main HTML page
203 Seobeo_Request_Entry* handle_index(Seobeo_Request_Entry* req, Dowa_Arena* arena) {
204 Seobeo_Request_Entry *resp = NULL;
205 char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024);
206 Seobeo_Render_Html_FilePath(final_body, "/index.html", arena);
207 Dowa_HashMap_Push_Arena(resp, "body", final_body, arena);
208 return resp;
209 }
210
211 // POST /api/upload - Handle file upload
212 Seobeo_Request_Entry* handle_upload(Seobeo_Request_Entry* req, Dowa_Arena* arena) {
213 const char* content_type = Dowa_HashMap_Get(req, "content-type");
214 const char* body = Dowa_HashMap_Get(req, "Body");
215 const char* content_length_str = Dowa_HashMap_Get(req, "content-length");
216
217 Seobeo_Request_Entry* resp = NULL;
218
219 if (!content_type || !body) {
220 Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
221 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
222 Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Missing content\"}", arena);
223 return resp;
224 }
225
226 size_t content_length = content_length_str ? atol(content_length_str) : strlen(body);
227
228 if (content_length > MAX_UPLOAD_SIZE) {
229 Dowa_HashMap_Push_Arena(resp, "status", "413", arena);
230 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
231 Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"File too large\"}", arena);
232 return resp;
233 }
234
235 // Parse multipart form data
236 char* boundary = find_multipart_boundary(content_type, arena);
237 if (!boundary) {
238 Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
239 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
240 Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Invalid multipart data\"}", arena);
241 return resp;
242 }
243
244 // Find file content in multipart data
245 char boundary_marker[256];
246 snprintf(boundary_marker, sizeof(boundary_marker), "--%s", boundary);
247
248 const char* part_start = strstr(body, boundary_marker);
249 if (!part_start) {
250 Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
251 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
252 Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"No file found\"}", arena);
253 return resp;
254 }
255
256 // Skip to headers
257 part_start = strstr(part_start, "\r\n");
258 if (!part_start) {
259 Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
260 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
261 Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Invalid format\"}", arena);
262 return resp;
263 }
264 part_start += 2;
265
266 // Find filename from Content-Disposition
267 char* filename = NULL;
268 const char* header_end = strstr(part_start, "\r\n\r\n");
269 if (header_end) {
270 char headers[1024] = {0};
271 size_t headers_len = header_end - part_start;
272 if (headers_len < sizeof(headers)) {
273 memcpy(headers, part_start, headers_len);
274 filename = extract_filename_from_content_disposition(headers, arena);
275 }
276 }
277
278 if (!filename) {
279 filename = "upload";
280 }
281
282 // Find file content
283 const char* file_start = header_end + 4; // Skip \r\n\r\n
284
285 // Find end boundary
286 char end_boundary[256];
287 snprintf(end_boundary, sizeof(end_boundary), "\r\n--%s", boundary);
288 const char* file_end = strstr(file_start, end_boundary);
289
290 if (!file_end) {
291 Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
292 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
293 Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Incomplete upload\"}", arena);
294 return resp;
295 }
296
297 size_t file_size = file_end - file_start;
298
299 // Generate ID and determine type
300 char* id = media_generate_id();
301 MediaType type = media_detect_type(filename);
302
303 if (type == MEDIA_TYPE_UNKNOWN) {
304 free(id);
305 Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
306 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
307 Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Unsupported file type\"}", arena);
308 return resp;
309 }
310
311 // Save to temp file
312 const char* ext = media_get_extension(filename);
313 char temp_path[512];
314 snprintf(temp_path, sizeof(temp_path), "%s/%s%s", UPLOAD_DIR, id, ext ? ext : "");
315
316 FILE* f = fopen(temp_path, "wb");
317 if (!f) {
318 free(id);
319 Dowa_HashMap_Push_Arena(resp, "status", "500", arena);
320 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
321 Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Failed to save file\"}", arena);
322 return resp;
323 }
324
325 fwrite(file_start, 1, file_size, f);
326 fclose(f);
327
328 // Enqueue for processing
329 enqueue_job(temp_path, id, type);
330
331 // Return success with ID
332 char response_body[256];
333 snprintf(response_body, sizeof(response_body),
334 "{\"id\":\"%s\",\"type\":\"%s\",\"status\":\"processing\"}",
335 id, type == MEDIA_TYPE_IMAGE ? "image" : "video");
336
337 char* body_copy = Dowa_Arena_Allocate(arena, strlen(response_body) + 1);
338 strcpy(body_copy, response_body);
339
340 Dowa_HashMap_Push_Arena(resp, "status", "202", arena);
341 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
342 Dowa_HashMap_Push_Arena(resp, "Body", body_copy, arena);
343
344 free(id);
345 return resp;
346 }
347
348 // GET /api/media/:id - Get media info/status
349 Seobeo_Request_Entry* handle_media_info(Seobeo_Request_Entry* req, Dowa_Arena* arena) {
350 const char* path = Dowa_HashMap_Get(req, "path");
351
352 Seobeo_Request_Entry* resp = NULL;
353
354 // Extract ID from path (e.g., /api/media/123456)
355 const char* id = path + strlen("/api/media/");
356 if (!id || strlen(id) == 0) {
357 Dowa_HashMap_Push_Arena(resp, "status", "400", arena);
358 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
359 Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Missing media ID\"}", arena);
360 return resp;
361 }
362
363 // Check if processed directory exists
364 char media_dir[512];
365 snprintf(media_dir, sizeof(media_dir), "%s/%s", PROCESSED_DIR, id);
366
367 struct stat st;
368 if (stat(media_dir, &st) != 0) {
369 // Check if still processing
370 char upload_pattern[512];
371 snprintf(upload_pattern, sizeof(upload_pattern), "%s/%s", UPLOAD_DIR, id);
372
373 DIR* dir = opendir(UPLOAD_DIR);
374 boolean found = FALSE;
375 if (dir) {
376 struct dirent* entry;
377 while ((entry = readdir(dir)) != NULL) {
378 if (strncmp(entry->d_name, id, strlen(id)) == 0) {
379 found = TRUE;
380 break;
381 }
382 }
383 closedir(dir);
384 }
385
386 if (found) {
387 Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
388 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
389 Dowa_HashMap_Push_Arena(resp, "Body", "{\"status\":\"processing\"}", arena);
390 } else {
391 Dowa_HashMap_Push_Arena(resp, "status", "404", arena);
392 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
393 Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Media not found\"}", arena);
394 }
395 return resp;
396 }
397
398 // Check what files exist
399 char webp_path[512], hls_path[512], thumb_path[512];
400 snprintf(webp_path, sizeof(webp_path), "%s/image.webp", media_dir);
401 snprintf(hls_path, sizeof(hls_path), "%s/video.m3u8", media_dir);
402 snprintf(thumb_path, sizeof(thumb_path), "%s/video_thumb.jpg", media_dir);
403
404 boolean is_image = (stat(webp_path, &st) == 0);
405 boolean is_video = (stat(hls_path, &st) == 0);
406
407 char response[1024];
408 if (is_image) {
409 snprintf(response, sizeof(response),
410 "{\"id\":\"%s\",\"type\":\"image\",\"status\":\"completed\","
411 "\"webp\":\"/media/%s/image.webp\","
412 "\"original\":\"/media/%s/original\"}",
413 id, id, id);
414 } else if (is_video) {
415 snprintf(response, sizeof(response),
416 "{\"id\":\"%s\",\"type\":\"video\",\"status\":\"completed\","
417 "\"hls\":\"/media/%s/video.m3u8\","
418 "\"thumbnail\":\"/media/%s/video_thumb.jpg\","
419 "\"original\":\"/media/%s/original\"}",
420 id, id, id, id);
421 } else {
422 snprintf(response, sizeof(response), "{\"id\":\"%s\",\"status\":\"processing\"}", id);
423 }
424
425 char* body = Dowa_Arena_Allocate(arena, strlen(response) + 1);
426 strcpy(body, response);
427
428 Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
429 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
430 Dowa_HashMap_Push_Arena(resp, "Body", body, arena);
431
432 return resp;
433 }
434
435 // GET /api/media - List all media
436 Seobeo_Request_Entry* handle_media_list(Seobeo_Request_Entry* req, Dowa_Arena* arena) {
437 (void)req;
438
439 Seobeo_Request_Entry* resp = NULL;
440
441 DIR* dir = opendir(PROCESSED_DIR);
442 if (!dir) {
443 Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
444 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
445 Dowa_HashMap_Push_Arena(resp, "Body", "{\"items\":[]}", arena);
446 return resp;
447 }
448
449 // Build JSON array
450 char json[16384] = "{\"items\":[";
451 size_t json_len = strlen(json);
452 boolean first = TRUE;
453
454 struct dirent* entry;
455 while ((entry = readdir(dir)) != NULL) {
456 if (entry->d_name[0] == '.') continue;
457
458 char media_dir[512];
459 snprintf(media_dir, sizeof(media_dir), "%s/%s", PROCESSED_DIR, entry->d_name);
460
461 struct stat st;
462 if (stat(media_dir, &st) != 0 || !S_ISDIR(st.st_mode)) continue;
463
464 // Check type
465 char webp_path[512], hls_path[512];
466 snprintf(webp_path, sizeof(webp_path), "%s/image.webp", media_dir);
467 snprintf(hls_path, sizeof(hls_path), "%s/video.m3u8", media_dir);
468
469 const char* type = "unknown";
470 if (stat(webp_path, &st) == 0) type = "image";
471 else if (stat(hls_path, &st) == 0) type = "video";
472
473 char item[256];
474 snprintf(item, sizeof(item), "%s{\"id\":\"%s\",\"type\":\"%s\"}",
475 first ? "" : ",", entry->d_name, type);
476
477 size_t item_len = strlen(item);
478 if (json_len + item_len + 10 < sizeof(json)) {
479 strcpy(json + json_len, item);
480 json_len += item_len;
481 first = FALSE;
482 }
483 }
484
485 closedir(dir);
486
487 strcpy(json + json_len, "]}");
488
489 char* body = Dowa_Arena_Allocate(arena, strlen(json) + 1);
490 strcpy(body, json);
491
492 Dowa_HashMap_Push_Arena(resp, "status", "200", arena);
493 Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena);
494 Dowa_HashMap_Push_Arena(resp, "Body", body, arena);
495
496 return resp;
497 }
498
499 // ============================================================================
500 // Main
501 // ============================================================================
502
503 int main(int argc, char** argv) {
504 const char* port = "8080";
505
506 printf("===========================================\n");
507 printf(" Medi - Media Upload & Processing Server\n");
508 printf("===========================================\n");
509 printf("Starting on port %s...\n\n", port);
510
511 // Create directories
512 ensure_directories();
513
514 // Start background processing worker
515 pthread_t worker_thread;
516 pthread_create(&worker_thread, NULL, processing_worker, NULL);
517
518 // Initialize router
519 Seobeo_Router_Init();
520
521 // Register routes
522 Seobeo_Router_Register("GET", "/", handle_index);
523 Seobeo_Router_Register("POST", "/api/upload", handle_upload);
524 Seobeo_Router_Register("GET", "/api/media", handle_media_list);
525 Seobeo_Router_Register("GET", "/api/media/*", handle_media_info);
526
527 // Start server (serves static files from ./media for /media/* paths)
528 Seobeo_Web_Server_Start("medi/src", port, SEOBEO_MODE_EDGE, 4);
529
530 return 0;
531 }