|
217
|
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 }
|