195 lines
4.5 KiB
Go
195 lines
4.5 KiB
Go
package download
|
||
|
||
import (
|
||
"app/shelfly/internal/debridlink"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"sync"
|
||
"time"
|
||
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type DownloadJob struct {
|
||
ID string `gorm:"primaryKey;column:id"`
|
||
Link string `gorm:"column:link"`
|
||
Name string `gorm:"column:name"`
|
||
Status string `gorm:"column:status"` // waiting, running, done, failed, paused
|
||
PathID uint `gorm:"column:path_id"`
|
||
Size int64 `gorm:"column:size"`
|
||
Host string `gorm:"column:host"`
|
||
Progress int `gorm:"column:progress"` // 0–100
|
||
StreamURL string `gorm:"column:stream_url"` // <- nouveau champ
|
||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||
UpdatedAt time.Time `gorm:"autoUpdateTime"`
|
||
}
|
||
|
||
|
||
var (
|
||
jobs = make(map[string]*DownloadJob)
|
||
jobsMu sync.Mutex
|
||
)
|
||
|
||
// Enregistre un job en mémoire et en base
|
||
func RegisterJobWithDB(job *DownloadJob, db *gorm.DB) error {
|
||
jobsMu.Lock()
|
||
jobs[job.ID] = job
|
||
jobsMu.Unlock()
|
||
|
||
log.Printf("[JOB] Enregistré : %s (%s)\n", job.Name, job.ID)
|
||
return db.Create(job).Error
|
||
}
|
||
|
||
// Charge tous les jobs depuis la base en mémoire (au démarrage)
|
||
func InitJobsFromDB(db *gorm.DB) error {
|
||
var jobList []DownloadJob
|
||
if err := db.Find(&jobList).Error; err != nil {
|
||
return err
|
||
}
|
||
|
||
jobsMu.Lock()
|
||
defer jobsMu.Unlock()
|
||
|
||
for _, j := range jobList {
|
||
jobCopy := j
|
||
jobs[j.ID] = &jobCopy
|
||
}
|
||
log.Printf("[JOB] %d jobs rechargés depuis la base\n", len(jobs))
|
||
return nil
|
||
}
|
||
|
||
// Met à jour le status d’un job et le persiste
|
||
func UpdateJobStatus(id string, status string, db *gorm.DB) {
|
||
jobsMu.Lock()
|
||
defer jobsMu.Unlock()
|
||
|
||
if job, ok := jobs[id]; ok {
|
||
job.Status = status
|
||
job.UpdatedAt = time.Now()
|
||
if db != nil {
|
||
_ = db.Save(job)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Met à jour la progression d’un job et le persiste
|
||
func UpdateJobProgress(id string, progress int, db *gorm.DB) {
|
||
jobsMu.Lock()
|
||
defer jobsMu.Unlock()
|
||
|
||
if job, ok := jobs[id]; ok {
|
||
job.Progress = progress
|
||
job.UpdatedAt = time.Now()
|
||
if db != nil {
|
||
_ = db.Save(job)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Supprime un job (mémoire uniquement)
|
||
func DeleteJob(id string) {
|
||
jobsMu.Lock()
|
||
defer jobsMu.Unlock()
|
||
|
||
delete(jobs, id)
|
||
}
|
||
|
||
// Liste tous les jobs
|
||
func ListJobs() []*DownloadJob {
|
||
jobsMu.Lock()
|
||
defer jobsMu.Unlock()
|
||
|
||
list := make([]*DownloadJob, 0, len(jobs))
|
||
for _, job := range jobs {
|
||
list = append(list, job)
|
||
}
|
||
return list
|
||
}
|
||
const downloadDir = "./downloads"
|
||
|
||
func StartDownload(job *DownloadJob, downloadURL string, client *debridlink.Client, db *gorm.DB) {
|
||
UpdateJobStatus(job.ID, "running", db)
|
||
|
||
resp, err := http.Get(downloadURL)
|
||
if err != nil {
|
||
log.Printf("[ERROR] Téléchargement échoué : %v\n", err)
|
||
UpdateJobStatus(job.ID, "failed", db)
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
log.Printf("[ERROR] Erreur HTTP : %s\n", resp.Status)
|
||
UpdateJobStatus(job.ID, "failed", db)
|
||
return
|
||
}
|
||
|
||
// Créer le fichier de destination
|
||
if err := os.MkdirAll(downloadDir, os.ModePerm); err != nil {
|
||
log.Printf("[ERROR] Création du dossier %s échouée : %v\n", downloadDir, err)
|
||
UpdateJobStatus(job.ID, "failed", db)
|
||
return
|
||
}
|
||
destPath := filepath.Join(downloadDir, sanitizeFileName(job.Name))
|
||
outFile, err := os.Create(destPath)
|
||
if err != nil {
|
||
log.Printf("[ERROR] Impossible de créer le fichier : %v\n", err)
|
||
UpdateJobStatus(job.ID, "failed", db)
|
||
return
|
||
}
|
||
defer outFile.Close()
|
||
|
||
// Taille totale
|
||
totalSize := resp.ContentLength
|
||
if totalSize <= 0 && job.Size > 0 {
|
||
totalSize = job.Size
|
||
}
|
||
|
||
// Téléchargement avec suivi de progression
|
||
buf := make([]byte, 32*1024) // 32KB
|
||
var downloaded int64
|
||
lastUpdate := time.Now()
|
||
|
||
for {
|
||
n, err := resp.Body.Read(buf)
|
||
if n > 0 {
|
||
if _, writeErr := outFile.Write(buf[:n]); writeErr != nil {
|
||
log.Printf("[ERROR] Écriture échouée : %v\n", writeErr)
|
||
UpdateJobStatus(job.ID, "failed", db)
|
||
return
|
||
}
|
||
downloaded += int64(n)
|
||
}
|
||
if err != nil {
|
||
if err == io.EOF {
|
||
break
|
||
}
|
||
log.Printf("[ERROR] Erreur de lecture : %v\n", err)
|
||
UpdateJobStatus(job.ID, "failed", db)
|
||
return
|
||
}
|
||
|
||
// Mise à jour de la progression toutes les 500ms
|
||
if time.Since(lastUpdate) > 500*time.Millisecond && totalSize > 0 {
|
||
progress := int((downloaded * 100) / totalSize)
|
||
UpdateJobProgress(job.ID, progress, db)
|
||
lastUpdate = time.Now()
|
||
}
|
||
}
|
||
|
||
// 100% si on arrive ici
|
||
UpdateJobProgress(job.ID, 100, db)
|
||
UpdateJobStatus(job.ID, "done", db)
|
||
log.Printf("[OK] Fichier téléchargé : %s\n", destPath)
|
||
}
|
||
func sanitizeFileName(name string) string {
|
||
re := regexp.MustCompile(`[^\w\-.]`)
|
||
return re.ReplaceAllString(name, "_")
|
||
}
|
||
//***//
|
||
|