shelfy-v2/internal/controllers/downloadController.go
2025-09-28 15:44:15 +02:00

336 lines
9.3 KiB
Go
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package controllers
import (
"canguidev/shelfy/internal/client"
runner "canguidev/shelfy/internal/job"
"canguidev/shelfy/internal/models"
"context"
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"sync"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func sanitizeFileName(name string) string {
return runner.SanitizeFileName(name)
}
var seriesRegex = regexp.MustCompile(`^(.+?)\.S\d{2}E\d{2}`)
var (
jobs = make(map[string]*runner.DownloadJob)
jobsMu sync.Mutex
// --- Ajouts ---
inFlightMu sync.Mutex
inFlight = map[string]struct{}{} // set des jobs déjà en cours (par id)
parallelism = 4 // <— ajuste le nombre de DL en parallèle
sem = make(chan struct{}, parallelism)
)
func tryMarkInFlight(id string) bool {
inFlightMu.Lock()
defer inFlightMu.Unlock()
if _, ok := inFlight[id]; ok {
return false
}
inFlight[id] = struct{}{}
return true
}
func unmarkInFlight(id string) {
inFlightMu.Lock()
delete(inFlight, id)
inFlightMu.Unlock()
}
func HandleAddJobsMultiple(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
// 1. Parsing des champs du formulaire
var form struct {
Links string `json:"links" form:"links" binding:"required"`
PathID string `json:"path_id" form:"path_id" binding:"required"`
}
// On tente d'abord le bind JSON
if err := c.ShouldBindJSON(&form); err != nil {
log.Printf("[DEBUG] Reçu links=%q | path_id=%q", form.Links, form.PathID)
// Si JSON échoue, on tente avec form-urlencoded
if err := c.ShouldBind(&form); err != nil {
log.Printf("[DEBUG] Reçu links=%q | path_id=%q", form.Links, form.PathID)
c.JSON(http.StatusBadRequest, gin.H{"error": "Champs requis manquants ou invalides"})
return
}
}
lines := strings.Split(form.Links, "\n")
baseID, err := strconv.ParseInt(form.PathID, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "ID de chemin invalide"})
return
}
// 2. Vérification du dossier principal
var paths []models.PathDownload
if err := db.Find(&paths).Error; err != nil {
log.Printf("[DEBUG] Erreur lors du Find: %v", err)
} else {
log.Printf("[DEBUG] Contenu de la table PathDownload avant recherche: %+v", paths)
}
var basePath models.PathDownload
if err := db.First(&basePath, baseID).Error; err != nil {
log.Printf("[DEBUG] Pas trouvé baseID=%v dans PathDownload", baseID)
c.JSON(http.StatusBadRequest, gin.H{"error": "Dossier principal introuvable"})
return
}
log.Printf("[DEBUG] PathDownload trouvé pour baseID=%v: %+v", baseID, basePath)
// 3. Initialisation Debrid-Link
ctx := context.Background()
clt := client.NewClient(db)
account := runner.GetFirstActiveAccount(clt)
if account == nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Aucun compte Debrid-Link actif"})
return
}
clt.SetAccount(account)
// 4. Résultats à retourner
var results []map[string]interface{}
// 5. Itération des liens
for _, link := range lines {
link = strings.TrimSpace(link)
if link == "" {
continue
}
links, err := clt.AddLink(ctx, link)
if err != nil {
log.Printf("Échec débridage de %s: %v", link, err)
results = append(results, gin.H{"link": link, "status": "failed", "error": err.Error()})
continue
}
for _, l := range links {
clean := sanitizeFileName(l.Name)
series := clean
if m := seriesRegex.FindStringSubmatch(clean); len(m) == 2 {
series = m[1]
}
assignID := int(basePath.ID)
if series != "" {
dirPath := filepath.Join(basePath.Path, series)
if err := os.MkdirAll(dirPath, os.ModePerm); err != nil {
log.Printf("Erreur création dossier %s: %v", dirPath, err)
}
var sub models.PathDownload
if err := db.Where("path = ?", dirPath).First(&sub).Error; err != nil {
if err == gorm.ErrRecordNotFound {
sub = models.PathDownload{Path: dirPath, PathName: series}
if err := db.Create(&sub).Error; err != nil {
log.Printf("Erreur création PathDownload: %v", err)
}
} else {
log.Printf("Erreur lecture PathDownload: %v", err)
}
}
assignID = int(sub.ID)
}
// streamInfo, err := clt.CreateTranscode(ctx, l.ID)
// if err != nil {
// log.Printf("Erreur transcode pour %s: %v", l.ID, err)
// }
job := &runner.DownloadJob{
ID: l.ID,
Link: l.DownloadURL,
Name: l.Name,
Status: "waiting",
PathID: assignID,
Size: l.Size,
Host: l.Host,
Progress: 0,
StreamURL: "",
}
// if streamInfo != nil {
// job.StreamURL = streamInfo.StreamURL
// }
if err := runner.RegisterJobWithDB(job, db); err != nil {
log.Printf("Erreur enregistrement job: %v", err)
results = append(results, gin.H{"link": l.URL, "status": "failed", "error": err.Error()})
continue
}
results = append(results, gin.H{"link": l.URL, "status": "added", "name": job.Name})
}
}
// 6. Broadcast au frontend
runner.Broadcast()
c.JSON(http.StatusOK, gin.H{
"message": "Traitement terminé",
"results": results,
})
}
}
func HandleStartJob(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
log.Printf("[StartJob] ID = %s", id)
// Empêche un job déjà en cours d'être relancé
if !tryMarkInFlight(id) {
log.Printf("[StartJob] Job %s déjà en cours, on ignore", id)
c.Status(http.StatusNoContent)
return
}
// Récupération du job depuis la mémoire ou la DB
jobsMu.Lock()
job, exists := jobs[id]
jobsMu.Unlock()
if !exists {
var j runner.DownloadJob
if err := db.First(&j, "id = ?", id).Error; err != nil {
unmarkInFlight(id)
c.JSON(http.StatusNotFound, gin.H{"error": "Job introuvable"})
return
}
jobCopy := j
jobsMu.Lock()
jobs[id] = &jobCopy
job = &jobCopy
jobsMu.Unlock()
}
// Init client Debrid-Link
clt := client.NewClient(db)
account := runner.GetFirstActiveAccount(clt)
if account == nil {
unmarkInFlight(id)
c.JSON(http.StatusBadRequest, gin.H{"error": "Aucun compte Debrid-Link actif"})
return
}
clt.SetAccount(account)
// Lancement asynchrone avec limite de parallélisme
go func(job *runner.DownloadJob) {
sem <- struct{}{} // bloque si trop de jobs en cours
defer func() {
<-sem
unmarkInFlight(job.ID)
runner.Broadcast()
}()
runner.StartDownload(job, job.Link, clt, db)
}(job)
runner.Broadcast()
c.Status(http.StatusNoContent)
}
}
func HandlePauseJob() gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
runner.UpdateJobStatus(id, "paused", nil)
runner.Broadcast()
c.Status(http.StatusNoContent)
}
}
func HandleResumeJob(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
log.Printf("[ResumeJob] ID = %s", id)
// Empêche un job déjà en cours d'être relancé
if !tryMarkInFlight(id) {
log.Printf("[ResumeJob] Job %s déjà en cours, on ignore", id)
c.Status(http.StatusNoContent)
return
}
// Récupération du job depuis la mémoire ou la DB
jobsMu.Lock()
job, exists := jobs[id]
jobsMu.Unlock()
if !exists {
var j runner.DownloadJob
if err := db.First(&j, "id = ?", id).Error; err != nil {
unmarkInFlight(id)
c.JSON(http.StatusNotFound, gin.H{"error": "Job introuvable"})
return
}
jobCopy := j
jobsMu.Lock()
jobs[id] = &jobCopy
job = &jobCopy
jobsMu.Unlock()
}
// Init client Debrid-Link
clt := client.NewClient(db)
account := runner.GetFirstActiveAccount(clt)
if account == nil {
unmarkInFlight(id)
c.JSON(http.StatusBadRequest, gin.H{"error": "Aucun compte Debrid-Link actif"})
return
}
clt.SetAccount(account)
// Reprise asynchrone avec limite de parallélisme
go func(job *runner.DownloadJob) {
sem <- struct{}{} // bloque si trop de jobs en cours
defer func() {
<-sem
unmarkInFlight(job.ID)
runner.Broadcast()
}()
runner.StartDownload(job, job.Link, clt, db)
}(job)
runner.Broadcast()
c.Status(http.StatusNoContent)
}
}
func HandleDeleteJob(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
id := c.Param("id")
runner.DeleteJob(id, db)
go runner.Broadcast()
c.Status(http.StatusNoContent)
}
}
func HandleListJobs(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
var jobs []runner.DownloadJob
// Optionnel: filtrage (par user, status, etc.)
if err := db.Find(&jobs).Error; err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{
"jobs": jobs,
})
}
}