Mercurial
diff 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 diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/medi/main.c Sun Jan 25 10:44:04 2026 -0800 @@ -0,0 +1,531 @@ +#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; +}