package http import ( "fmt" "io" "net/http" "os" "path/filepath" "time" "git.vakhrushev.me/av/transcriber/internal/entity" "git.vakhrushev.me/av/transcriber/internal/repo" "git.vakhrushev.me/av/transcriber/internal/repo/ffmpeg" "git.vakhrushev.me/av/transcriber/internal/service/s3" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type TranscribeHandler struct { jobRepo repo.TranscriptJobRepository fileRepo repo.FileRepository } func NewTranscribeHandler(jobRepo repo.TranscriptJobRepository, fileRepo repo.FileRepository) *TranscribeHandler { return &TranscribeHandler{jobRepo: jobRepo, fileRepo: fileRepo} } type CreateTranscribeJobResponse struct { JobID string `json:"job_id"` State string `json:"status"` } type GetTranscribeJobResponse struct { JobID string `json:"job_id"` State string `json:"status"` CreatedAt time.Time `json:"created_at"` } func (h *TranscribeHandler) CreateTranscribeJob(c *gin.Context) { // Получаем файл из формы file, header, err := c.Request.FormFile("audio") if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "No audio file provided"}) return } defer file.Close() // Генерируем UUID для файла fileId := uuid.New().String() // Определяем расширение файла ext := filepath.Ext(header.Filename) if ext == "" { ext = ".audio" // fallback если расширение не определено } // Создаем путь для сохранения файла fileName := fmt.Sprintf("%s%s", fileId, ext) filePath := filepath.Join("data", "files", fileName) // Создаем файл на диске dst, err := os.Create(filePath) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create file"}) return } defer dst.Close() // Копируем содержимое загруженного файла size, err := io.Copy(dst, file) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file"}) return } // Создаем запись в таблице files fileRecord := &entity.File{ Id: fileId, Storage: entity.StorageLocal, FileName: fileName, Size: size, CreatedAt: time.Now(), } if err := h.fileRepo.Create(fileRecord); err != nil { // Удаляем файл если не удалось создать запись в БД os.Remove(filePath) c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save file record"}) return } // Создаем запись в таблице transcribe_jobs jobId := uuid.New().String() job := &entity.TranscribeJob{ Id: jobId, State: entity.StateCreated, FileID: &fileId, IsError: false, CreatedAt: time.Now(), } if err := h.jobRepo.Create(job); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create transcribe job"}) return } // Возвращаем успешный ответ response := CreateTranscribeJobResponse{ JobID: job.Id, State: job.State, } c.JSON(http.StatusCreated, response) } func (h *TranscribeHandler) GetTranscribeJobStatus(c *gin.Context) { jobID := c.Param("id") job, err := h.jobRepo.GetByID(jobID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "Job not found"}) return } c.JSON(http.StatusOK, GetTranscribeJobResponse{ JobID: job.Id, State: job.State, CreatedAt: job.CreatedAt, }) } func (h *TranscribeHandler) RunConversionJob(c *gin.Context) { acquisitionId := uuid.NewString() rottingTime := time.Now().Add(-1 * time.Hour) job, err := h.jobRepo.FindAndAcquire(entity.StateCreated, acquisitionId, rottingTime) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } srcFile, err := h.fileRepo.GetByID(*job.FileID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } srcFilePath := filepath.Join("data", "files", srcFile.FileName) destFileId := uuid.New().String() destFileName := fmt.Sprintf("%s%s", destFileId, ".ogg") destFilePath := filepath.Join("data", "files", destFileName) conv := ffmpeg.NewFileConverter() err = conv.Convert(srcFilePath, destFilePath) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } stat, err := os.Stat(destFilePath) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Создаем запись в таблице files destFileRecord := &entity.File{ Id: destFileId, Storage: entity.StorageLocal, FileName: destFileName, Size: stat.Size(), CreatedAt: time.Now(), } job.FileID = &destFileId job.MoveToState(entity.StateConverted) err = h.fileRepo.Create(destFileRecord) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } err = h.jobRepo.Save(job) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } c.Status(http.StatusOK) } func (h *TranscribeHandler) RunUploadJob(c *gin.Context) { acquisitionId := uuid.NewString() rottingTime := time.Now().Add(-1 * time.Hour) job, err := h.jobRepo.FindAndAcquire(entity.StateConverted, acquisitionId, rottingTime) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } fileRecord, err := h.fileRepo.GetByID(*job.FileID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } filePath := filepath.Join("data", "files", fileRecord.FileName) destFileId := uuid.New().String() destFileRecord := &entity.File{ Id: destFileId, Storage: entity.StorageS3, FileName: fileRecord.FileName, Size: fileRecord.Size, CreatedAt: time.Now(), } // Создаем S3 сервис s3Service, err := s3.NewS3Service() if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to initialize S3 service: " + err.Error()}) return } // Загружаем файл на S3 err = s3Service.UploadFile(filePath, destFileRecord.FileName) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to upload file to S3: " + err.Error()}) return } job.FileID = &destFileId job.MoveToState(entity.StateTranscribeReady) // Сохраняем информацию о загрузке файла на S3 err = h.fileRepo.Create(destFileRecord) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update file record: " + err.Error()}) return } // Обновляем состояние задачи err = h.jobRepo.Save(job) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update job state: " + err.Error()}) return } c.Status(http.StatusOK) }