Mercurial
view 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 |
line wrap: on
line source
#include "seobeo/seobeo.h" #include "media_processor/media_processor.h" #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/stat.h> #include <dirent.h> #include <pthread.h> #include <unistd.h> #define UPLOAD_DIR "./uploads" #define PROCESSED_DIR "./media" #define MAX_UPLOAD_SIZE (100 * 1024 * 1024) // 100MB // Background processing queue typedef struct ProcessJob { char* input_path; char* id; MediaType type; struct ProcessJob* next; } ProcessJob; static ProcessJob* job_queue_head = NULL; static ProcessJob* job_queue_tail = NULL; static pthread_mutex_t job_queue_mutex = PTHREAD_MUTEX_INITIALIZER; static pthread_cond_t job_queue_cond = PTHREAD_COND_INITIALIZER; // ============================================================================ // Helper Functions // ============================================================================ static void ensure_directories(void) { mkdir(UPLOAD_DIR, 0755); mkdir(PROCESSED_DIR, 0755); } static char* extract_filename_from_content_disposition(const char* header, Dowa_Arena* arena) { const char* filename_start = strstr(header, "filename=\""); if (!filename_start) return NULL; filename_start += 10; // Skip 'filename="' const char* filename_end = strchr(filename_start, '"'); if (!filename_end) return NULL; size_t len = filename_end - filename_start; char* filename = Dowa_Arena_Allocate(arena, len + 1); memcpy(filename, filename_start, len); filename[len] = '\0'; return filename; } static char* find_multipart_boundary(const char* content_type, Dowa_Arena* arena) { const char* boundary_start = strstr(content_type, "boundary="); if (!boundary_start) return NULL; boundary_start += 9; // Skip 'boundary=' // Handle quoted boundary if (*boundary_start == '"') { boundary_start++; const char* boundary_end = strchr(boundary_start, '"'); if (!boundary_end) return NULL; size_t len = boundary_end - boundary_start; char* boundary = Dowa_Arena_Allocate(arena, len + 1); memcpy(boundary, boundary_start, len); boundary[len] = '\0'; return boundary; } // Unquoted boundary size_t len = strlen(boundary_start); char* boundary = Dowa_Arena_Allocate(arena, len + 1); strcpy(boundary, boundary_start); // Trim whitespace/newline while (len > 0 && (boundary[len-1] == '\r' || boundary[len-1] == '\n' || boundary[len-1] == ' ')) { boundary[--len] = '\0'; } return boundary; } // ============================================================================ // Background Processing Worker // ============================================================================ static void enqueue_job(const char* input_path, const char* id, MediaType type) { ProcessJob* job = malloc(sizeof(ProcessJob)); job->input_path = strdup(input_path); job->id = strdup(id); job->type = type; job->next = NULL; pthread_mutex_lock(&job_queue_mutex); if (job_queue_tail) { job_queue_tail->next = job; job_queue_tail = job; } else { job_queue_head = job_queue_tail = job; } pthread_cond_signal(&job_queue_cond); pthread_mutex_unlock(&job_queue_mutex); } static void* processing_worker(void* arg) { (void)arg; while (1) { pthread_mutex_lock(&job_queue_mutex); while (!job_queue_head) { pthread_cond_wait(&job_queue_cond, &job_queue_mutex); } ProcessJob* job = job_queue_head; job_queue_head = job->next; if (!job_queue_head) job_queue_tail = NULL; pthread_mutex_unlock(&job_queue_mutex); // Process the job Seobeo_Log(SEOBEO_INFO, "Processing media: %s (id=%s)", job->input_path, job->id); MediaProcessorOpts opts = media_processor_opts_default(PROCESSED_DIR); MediaResult* result = media_process_file(job->input_path, job->id, &opts); if (result->status == MEDIA_STATUS_COMPLETED) { Seobeo_Log(SEOBEO_INFO, "Processing completed for %s", job->id); } else { Seobeo_Log(SEOBEO_ERROR, "Processing failed for %s: %s", job->id, result->error_message); } media_result_free(result); // Clean up temp upload file unlink(job->input_path); free(job->input_path); free(job->id); free(job); } return NULL; } void Seobeo_Render_Html( char *final_body, char *template, Dowa_Arena *arena ) { size_t current_offset = 0; char *cursor = template; int32 token_len = 2; while (1) { char *start_tag = strstr(cursor, "{{"); if (!start_tag) break; char *end_tag = strstr(start_tag, "}}"); if (!end_tag) break; size_t leading_len = start_tag - cursor; memcpy(final_body + current_offset, cursor, leading_len); current_offset += leading_len; size_t name_len = end_tag - (start_tag + token_len); char *include_name = Dowa_Arena_Allocate(arena, name_len + 1); memcpy(include_name, start_tag + token_len, name_len); include_name[name_len] = '\0'; size_t sub_file_size = 0; char *sub_content = Seobeo_Web_LoadFile(include_name, &sub_file_size); Seobeo_Log(SEOBEO_DEBUG, "[Curr] Sub content: %s\n", sub_content); if (sub_content) { memcpy(final_body + current_offset, sub_content, sub_file_size); current_offset += sub_file_size; free(sub_content); } cursor = end_tag + 2; } strcpy(final_body + current_offset, cursor); } void Seobeo_Render_Html_FilePath( char *final_body, char *path, Dowa_Arena *arena ) { Seobeo_Log(SEOBEO_DEBUG, "[Curr] %s\n", path); size_t html_size = 0; char *template = Seobeo_Web_LoadFile(path, &html_size); if (!template) return; Seobeo_Render_Html(final_body, template, arena); } // ============================================================================ // API Route Handlers // ============================================================================ // GET / - Serve the main HTML page Seobeo_Request_Entry* handle_index(Seobeo_Request_Entry* req, Dowa_Arena* arena) { Seobeo_Request_Entry *resp = NULL; char *final_body = Dowa_Arena_Allocate(arena, 50 * 1024); Seobeo_Render_Html_FilePath(final_body, "/index.html", arena); Dowa_HashMap_Push_Arena(resp, "body", final_body, arena); return resp; } // POST /api/upload - Handle file upload Seobeo_Request_Entry* handle_upload(Seobeo_Request_Entry* req, Dowa_Arena* arena) { const char* content_type = Dowa_HashMap_Get(req, "content-type"); const char* body = Dowa_HashMap_Get(req, "Body"); const char* content_length_str = Dowa_HashMap_Get(req, "content-length"); Seobeo_Request_Entry* resp = NULL; if (!content_type || !body) { Dowa_HashMap_Push_Arena(resp, "status", "400", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Missing content\"}", arena); return resp; } size_t content_length = content_length_str ? atol(content_length_str) : strlen(body); if (content_length > MAX_UPLOAD_SIZE) { Dowa_HashMap_Push_Arena(resp, "status", "413", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"File too large\"}", arena); return resp; } // Parse multipart form data char* boundary = find_multipart_boundary(content_type, arena); if (!boundary) { Dowa_HashMap_Push_Arena(resp, "status", "400", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Invalid multipart data\"}", arena); return resp; } // Find file content in multipart data char boundary_marker[256]; snprintf(boundary_marker, sizeof(boundary_marker), "--%s", boundary); const char* part_start = strstr(body, boundary_marker); if (!part_start) { Dowa_HashMap_Push_Arena(resp, "status", "400", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"No file found\"}", arena); return resp; } // Skip to headers part_start = strstr(part_start, "\r\n"); if (!part_start) { Dowa_HashMap_Push_Arena(resp, "status", "400", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Invalid format\"}", arena); return resp; } part_start += 2; // Find filename from Content-Disposition char* filename = NULL; const char* header_end = strstr(part_start, "\r\n\r\n"); if (header_end) { char headers[1024] = {0}; size_t headers_len = header_end - part_start; if (headers_len < sizeof(headers)) { memcpy(headers, part_start, headers_len); filename = extract_filename_from_content_disposition(headers, arena); } } if (!filename) { filename = "upload"; } // Find file content const char* file_start = header_end + 4; // Skip \r\n\r\n // Find end boundary char end_boundary[256]; snprintf(end_boundary, sizeof(end_boundary), "\r\n--%s", boundary); const char* file_end = strstr(file_start, end_boundary); if (!file_end) { Dowa_HashMap_Push_Arena(resp, "status", "400", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Incomplete upload\"}", arena); return resp; } size_t file_size = file_end - file_start; // Generate ID and determine type char* id = media_generate_id(); MediaType type = media_detect_type(filename); if (type == MEDIA_TYPE_UNKNOWN) { free(id); Dowa_HashMap_Push_Arena(resp, "status", "400", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Unsupported file type\"}", arena); return resp; } // Save to temp file const char* ext = media_get_extension(filename); char temp_path[512]; snprintf(temp_path, sizeof(temp_path), "%s/%s%s", UPLOAD_DIR, id, ext ? ext : ""); FILE* f = fopen(temp_path, "wb"); if (!f) { free(id); Dowa_HashMap_Push_Arena(resp, "status", "500", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Failed to save file\"}", arena); return resp; } fwrite(file_start, 1, file_size, f); fclose(f); // Enqueue for processing enqueue_job(temp_path, id, type); // Return success with ID char response_body[256]; snprintf(response_body, sizeof(response_body), "{\"id\":\"%s\",\"type\":\"%s\",\"status\":\"processing\"}", id, type == MEDIA_TYPE_IMAGE ? "image" : "video"); char* body_copy = Dowa_Arena_Allocate(arena, strlen(response_body) + 1); strcpy(body_copy, response_body); Dowa_HashMap_Push_Arena(resp, "status", "202", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", body_copy, arena); free(id); return resp; } // GET /api/media/:id - Get media info/status Seobeo_Request_Entry* handle_media_info(Seobeo_Request_Entry* req, Dowa_Arena* arena) { const char* path = Dowa_HashMap_Get(req, "path"); Seobeo_Request_Entry* resp = NULL; // Extract ID from path (e.g., /api/media/123456) const char* id = path + strlen("/api/media/"); if (!id || strlen(id) == 0) { Dowa_HashMap_Push_Arena(resp, "status", "400", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Missing media ID\"}", arena); return resp; } // Check if processed directory exists char media_dir[512]; snprintf(media_dir, sizeof(media_dir), "%s/%s", PROCESSED_DIR, id); struct stat st; if (stat(media_dir, &st) != 0) { // Check if still processing char upload_pattern[512]; snprintf(upload_pattern, sizeof(upload_pattern), "%s/%s", UPLOAD_DIR, id); DIR* dir = opendir(UPLOAD_DIR); boolean found = FALSE; if (dir) { struct dirent* entry; while ((entry = readdir(dir)) != NULL) { if (strncmp(entry->d_name, id, strlen(id)) == 0) { found = TRUE; break; } } closedir(dir); } if (found) { Dowa_HashMap_Push_Arena(resp, "status", "200", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", "{\"status\":\"processing\"}", arena); } else { Dowa_HashMap_Push_Arena(resp, "status", "404", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", "{\"error\":\"Media not found\"}", arena); } return resp; } // Check what files exist char webp_path[512], hls_path[512], thumb_path[512]; snprintf(webp_path, sizeof(webp_path), "%s/image.webp", media_dir); snprintf(hls_path, sizeof(hls_path), "%s/video.m3u8", media_dir); snprintf(thumb_path, sizeof(thumb_path), "%s/video_thumb.jpg", media_dir); boolean is_image = (stat(webp_path, &st) == 0); boolean is_video = (stat(hls_path, &st) == 0); char response[1024]; if (is_image) { snprintf(response, sizeof(response), "{\"id\":\"%s\",\"type\":\"image\",\"status\":\"completed\"," "\"webp\":\"/media/%s/image.webp\"," "\"original\":\"/media/%s/original\"}", id, id, id); } else if (is_video) { snprintf(response, sizeof(response), "{\"id\":\"%s\",\"type\":\"video\",\"status\":\"completed\"," "\"hls\":\"/media/%s/video.m3u8\"," "\"thumbnail\":\"/media/%s/video_thumb.jpg\"," "\"original\":\"/media/%s/original\"}", id, id, id, id); } else { snprintf(response, sizeof(response), "{\"id\":\"%s\",\"status\":\"processing\"}", id); } char* body = Dowa_Arena_Allocate(arena, strlen(response) + 1); strcpy(body, response); Dowa_HashMap_Push_Arena(resp, "status", "200", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", body, arena); return resp; } // GET /api/media - List all media Seobeo_Request_Entry* handle_media_list(Seobeo_Request_Entry* req, Dowa_Arena* arena) { (void)req; Seobeo_Request_Entry* resp = NULL; DIR* dir = opendir(PROCESSED_DIR); if (!dir) { Dowa_HashMap_Push_Arena(resp, "status", "200", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", "{\"items\":[]}", arena); return resp; } // Build JSON array char json[16384] = "{\"items\":["; size_t json_len = strlen(json); boolean first = TRUE; struct dirent* entry; while ((entry = readdir(dir)) != NULL) { if (entry->d_name[0] == '.') continue; char media_dir[512]; snprintf(media_dir, sizeof(media_dir), "%s/%s", PROCESSED_DIR, entry->d_name); struct stat st; if (stat(media_dir, &st) != 0 || !S_ISDIR(st.st_mode)) continue; // Check type char webp_path[512], hls_path[512]; snprintf(webp_path, sizeof(webp_path), "%s/image.webp", media_dir); snprintf(hls_path, sizeof(hls_path), "%s/video.m3u8", media_dir); const char* type = "unknown"; if (stat(webp_path, &st) == 0) type = "image"; else if (stat(hls_path, &st) == 0) type = "video"; char item[256]; snprintf(item, sizeof(item), "%s{\"id\":\"%s\",\"type\":\"%s\"}", first ? "" : ",", entry->d_name, type); size_t item_len = strlen(item); if (json_len + item_len + 10 < sizeof(json)) { strcpy(json + json_len, item); json_len += item_len; first = FALSE; } } closedir(dir); strcpy(json + json_len, "]}"); char* body = Dowa_Arena_Allocate(arena, strlen(json) + 1); strcpy(body, json); Dowa_HashMap_Push_Arena(resp, "status", "200", arena); Dowa_HashMap_Push_Arena(resp, "content-type", "application/json", arena); Dowa_HashMap_Push_Arena(resp, "Body", body, arena); return resp; } // ============================================================================ // Main // ============================================================================ int main(int argc, char** argv) { const char* port = "8080"; printf("===========================================\n"); printf(" Medi - Media Upload & Processing Server\n"); printf("===========================================\n"); printf("Starting on port %s...\n\n", port); // Create directories ensure_directories(); // Start background processing worker pthread_t worker_thread; pthread_create(&worker_thread, NULL, processing_worker, NULL); // Initialize router Seobeo_Router_Init(); // Register routes Seobeo_Router_Register("GET", "/", handle_index); Seobeo_Router_Register("POST", "/api/upload", handle_upload); Seobeo_Router_Register("GET", "/api/media", handle_media_list); Seobeo_Router_Register("GET", "/api/media/*", handle_media_info); // Start server (serves static files from ./media for /media/* paths) Seobeo_Web_Server_Start("medi/src", port, SEOBEO_MODE_EDGE, 4); return 0; }