2025-07-27 14:26:30 +00:00
|
|
|
|
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
|
2025-09-28 13:44:15 +00:00
|
|
|
|
// --- 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)
|
2025-07-27 14:26:30 +00:00
|
|
|
|
)
|
2025-09-28 13:44:15 +00:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
2025-07-27 14:26:30 +00:00
|
|
|
|
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
|
2025-08-18 19:26:33 +00:00
|
|
|
|
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)
|
|
|
|
|
|
|
2025-07-27 14:26:30 +00:00
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2025-09-28 13:44:15 +00:00
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
|
|
id := c.Param("id")
|
|
|
|
|
|
log.Printf("[StartJob] ID = %s", id)
|
2025-07-27 14:26:30 +00:00
|
|
|
|
|
2025-09-28 13:44:15 +00:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2025-07-27 14:26:30 +00:00
|
|
|
|
|
2025-09-28 13:44:15 +00:00
|
|
|
|
// 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()
|
|
|
|
|
|
}
|
2025-07-27 14:26:30 +00:00
|
|
|
|
|
2025-09-28 13:44:15 +00:00
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
2025-07-27 14:26:30 +00:00
|
|
|
|
}
|
2025-09-28 13:44:15 +00:00
|
|
|
|
|
2025-07-27 14:26:30 +00:00
|
|
|
|
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 {
|
2025-09-28 13:44:15 +00:00
|
|
|
|
return func(c *gin.Context) {
|
|
|
|
|
|
id := c.Param("id")
|
|
|
|
|
|
log.Printf("[ResumeJob] ID = %s", id)
|
2025-07-27 14:26:30 +00:00
|
|
|
|
|
2025-09-28 13:44:15 +00:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|
2025-07-27 14:26:30 +00:00
|
|
|
|
|
2025-09-28 13:44:15 +00:00
|
|
|
|
// 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()
|
|
|
|
|
|
}
|
2025-07-27 14:26:30 +00:00
|
|
|
|
|
2025-09-28 13:44:15 +00:00
|
|
|
|
// 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)
|
|
|
|
|
|
}
|
2025-07-27 14:26:30 +00:00
|
|
|
|
}
|
|
|
|
|
|
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,
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|