maj , update est teste a prevoir

This commit is contained in:
julien 2025-06-12 17:31:12 +02:00
parent 463c5b8d10
commit e0d753a927
7 changed files with 371 additions and 209 deletions

View File

@ -425,14 +425,36 @@ func (c *Client) RemoveTorrents(ctx context.Context, ids []string) error {
// =========================== Downloader ===========================
type Link struct {
ID string `json:"id" gorm:"column:id;primaryKey"`
OriginalLink string `json:"originalLink" gorm:"column:original_link"`
DownloadLink string `json:"downloadLink" gorm:"column:download_link"`
Host string `json:"host" gorm:"column:host"`
Status string `json:"status" gorm:"column:status"`
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
ID string `json:"id" gorm:"primaryKey;column:id"`
Name string `json:"name" gorm:"column:name"`
URL string `json:"url" gorm:"column:url"` // Lien d'origine
DownloadURL string `json:"downloadUrl" gorm:"column:download_url"` // Lien débridé direct
Host string `json:"host" gorm:"column:host"` // Nom de l'hébergeur
Size int64 `json:"size" gorm:"column:size"` // Taille en octets
Chunk int `json:"chunk" gorm:"column:chunk"` // Nombre de chunks
Expired bool `json:"expired" gorm:"column:expired"` // Lien expiré ou non
Created int64 `json:"created" gorm:"column:created"` // Timestamp
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
type StreamInfo struct {
ID string `json:"id" gorm:"primaryKey;column:id"`
StreamURL string `json:"streamUrl" gorm:"column:stream_url"`
DownloadURL string `json:"downloadUrl" gorm:"column:download_url"`
Type string `json:"type" gorm:"column:type"` // hls ou mp4
MimeType string `json:"mimetype" gorm:"column:mimetype"` // ex: video/mp4
Domain string `json:"domain" gorm:"column:domain"`
// Champs du fichier lié (ex : nom de la vidéo)
FileID string `json:"-" gorm:"column:file_id"` // lien avec le champ File.ID ci-dessous
FileName string `json:"-" gorm:"column:file_name"` // nom fichier
FileSize int64 `json:"-" gorm:"column:file_size"` // taille fichier
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
}
func (c *Client) ListLinks(ctx context.Context) ([]Link, error) {
var links []Link
@ -441,16 +463,41 @@ func (c *Client) ListLinks(ctx context.Context) ([]Link, error) {
}
return links, nil
}
func (c *Client) AddLink(ctx context.Context, link string) (*Link, error) {
var result Link
body := map[string]string{"url": link} // ✅ CORRECTION
if err := c.doJSON(ctx, "POST", "downloader/add", nil, body, &result); err != nil { // ✅ CORRECTION
func (c *Client) AddLink(ctx context.Context, link string) ([]Link, error) {
var envelope struct {
Success bool `json:"success"`
Value json.RawMessage `json:"value"`
}
body := map[string]string{"url": link}
// Requête brute
if err := c.doJSON(ctx, "POST", "downloader/add", nil, body, &envelope); err != nil {
return nil, err
}
return &result, nil
var links []Link
switch envelope.Value[0] {
case '{':
var single Link
if err := json.Unmarshal(envelope.Value, &single); err != nil {
return nil, err
}
links = append(links, single)
case '[':
if err := json.Unmarshal(envelope.Value, &links); err != nil {
return nil, err
}
default:
return nil, errors.New("format de réponse inattendu")
}
return links, nil
}
func (c *Client) RemoveLinks(ctx context.Context, ids []string) error {
body := map[string][]string{"ids": ids}
return c.doJSON(ctx, "DELETE", "downloader/links", nil, body, nil)
@ -467,6 +514,7 @@ type File struct {
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"`
}
func (c *Client) ListFiles(ctx context.Context, parentID string) ([]File, error) {
var files []File
path := fmt.Sprintf("files/%s", parentID)
@ -486,11 +534,39 @@ func (c *Client) CreateTranscode(ctx context.Context, fileID, preset string) (st
return resp.TranscodeID, nil
}
func (c *Client) GetTranscode(ctx context.Context, transcodeID string) (map[string]interface{}, error) {
var result map[string]interface{}
func (c *Client) GetTranscode(ctx context.Context, transcodeID string) (*StreamInfo, error) {
var raw struct {
Success bool `json:"success"`
Value struct {
ID string `json:"id"`
StreamURL string `json:"streamUrl"`
DownloadURL string `json:"downloadUrl"`
Type string `json:"type"`
MimeType string `json:"mimetype"`
Domain string `json:"domain"`
File struct {
ID string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
} `json:"file"`
} `json:"value"`
}
path := fmt.Sprintf("stream/transcode/%s", transcodeID)
if err := c.doJSON(ctx, "GET", path, nil, nil, &result); err != nil {
if err := c.doJSON(ctx, "GET", path, nil, nil, &raw); err != nil {
return nil, err
}
return result, nil
info := &StreamInfo{
ID: raw.Value.ID,
StreamURL: raw.Value.StreamURL,
DownloadURL: raw.Value.DownloadURL,
Type: raw.Value.Type,
MimeType: raw.Value.MimeType,
Domain: raw.Value.Domain,
FileID: raw.Value.File.ID,
FileName: raw.Value.File.Name,
FileSize: raw.Value.File.Size,
}
return info, nil
}

View File

@ -1,8 +1,10 @@
package download
import (
"app/shelfly/internal/debridlink"
"app/shelfly/internal/models"
"app/shelfly/query"
"context"
"encoding/json"
"errors"
"fmt"
@ -15,6 +17,23 @@ import (
"github.com/gorilla/mux"
"gorm.io/gorm"
)
func GetFirstActiveAccount(client *debridlink.Client) *debridlink.DebridAccount {
ctx := context.Background() // ✅ on remplace ici
accounts, err := client.ListDebridAccounts(ctx)
if err != nil {
log.Println("[ERROR] Impossible de récupérer les comptes :", err)
return nil
}
for _, acc := range accounts {
if acc.IsActive {
return &acc
}
}
return nil
}
func CreateSavePath(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {

View File

@ -1,232 +1,194 @@
// internal/download/jobs.go
package download
import (
"app/shelfly/internal/debridlink"
"context"
"encoding/json"
"io"
"log"
"net/http"
"strconv"
"os"
"path/filepath"
"regexp"
"sync"
"time"
"github.com/gorilla/mux"
"gorm.io/gorm"
)
type DownloadJob struct {
ID uint `gorm:"primaryKey"`
RemoteID string `gorm:"index"`
FileName string `gorm:"size:255"`
Status string `gorm:"size:50"`
Speed string `gorm:"size:50"`
StreamURL string `gorm:"-" json:"stream_url"`
ErrorMsg string `gorm:"-" json:"error_msg"`
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"` // 0100
StreamURL string `gorm:"column:stream_url"` // <- nouveau champ
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
Link string `gorm:"size:512"`
}
var (
jobs = make(map[string]*DownloadJob)
jobsMu sync.Mutex
)
func AddJob(link string, pathID uint, db *gorm.DB) *DownloadJob {
id := time.Now().UnixNano()
job := &DownloadJob{
ID: uint(id),
Link: link, // ← important !
RemoteID: "",
FileName: extractFileName(link),
Status: "added",
Speed: "",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Enregistre un job en mémoire et en base
func RegisterJobWithDB(job *DownloadJob, db *gorm.DB) error {
jobsMu.Lock()
jobs[strconv.FormatUint(uint64(id), 10)] = job
jobs[job.ID] = job
jobsMu.Unlock()
db.Create(job)
return job
log.Printf("[JOB] Enregistré : %s (%s)\n", job.Name, job.ID)
return db.Create(job).Error
}
func startDownload(job *DownloadJob, link string, client *debridlink.Client, db *gorm.DB) {
ctx := context.Background()
job.Status = "downloading"
linkResp, err := client.AddLink(ctx, link)
if err != nil {
job.Status = "error"
job.ErrorMsg = err.Error()
db.Save(job)
return
// 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
}
job.RemoteID = linkResp.ID
db.Save(job)
go syncDownloadStatus(job, client, db)
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
}
func syncDownloadStatus(job *DownloadJob, client *debridlink.Client, db *gorm.DB) {
ctx := context.Background()
var checkCount int
for {
if job.Status != "downloading" {
return
// Met à jour le status dun 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)
}
links, err := client.ListLinks(ctx)
if err != nil {
job.Status = "error"
job.ErrorMsg = err.Error()
db.Save(job)
return
}
for _, l := range links {
if l.ID == job.RemoteID {
job.Status = l.Status
checkCount++
if checkCount%2 == 0 {
job.Speed = "~2 MB/s"
} else {
job.Speed = "~1.4 MB/s"
}
if l.Status == "downloaded" {
files, err := client.ListFiles(ctx, l.ID)
if err == nil && len(files) > 0 {
transcodeID, err := client.CreateTranscode(ctx, files[0].ID, "original")
if err == nil {
job.StreamURL = "https://debrid-link.com/stream/" + transcodeID
}
}
}
db.Save(job)
if l.Status == "downloaded" || l.Status == "error" {
_ = client.RemoveLinks(ctx, []string{l.ID})
return
}
}
}
time.Sleep(2 * time.Second)
}
}
// Met à jour la progression dun 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()
var list []*DownloadJob
list := make([]*DownloadJob, 0, len(jobs))
for _, job := range jobs {
list = append(list, job)
}
return list
}
const downloadDir = "./downloads"
func PauseJob(id string) {
jobsMu.Lock()
defer jobsMu.Unlock()
if job, ok := jobs[id]; ok && job.Status == "downloading" {
job.Status = "paused"
}
}
func StartDownload(job *DownloadJob, downloadURL string, client *debridlink.Client, db *gorm.DB) {
UpdateJobStatus(job.ID, "running", db)
func ResumeJob(id string, client *debridlink.Client, db *gorm.DB) {
jobsMu.Lock()
defer jobsMu.Unlock()
if job, ok := jobs[id]; ok && job.Status == "paused" {
job.Status = "downloading"
db.Save(job)
go syncDownloadStatus(job, client, db)
}
}
func DeleteJob(id string) {
jobsMu.Lock()
defer jobsMu.Unlock()
delete(jobs, id)
}
func generateID() string {
return time.Now().Format("20060102150405.000")
}
func extractFileName(link string) string {
return "file_from_" + link
}
func HandleListJobs(w http.ResponseWriter, r *http.Request) {
jobs := ListJobs()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(jobs)
}
func HandlePauseJob(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
PauseJob(id)
w.WriteHeader(http.StatusNoContent)
}
func HandleResumeJob(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
client := debridlink.NewClient(db)
account := getFirstActiveAccount(client)
if account == nil {
http.Error(w, "Aucun compte actif", http.StatusBadRequest)
return
}
client.SetAccount(account)
ResumeJob(id, client, db)
w.WriteHeader(http.StatusNoContent)
}
}
func HandleDeleteJob(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
DeleteJob(id)
w.WriteHeader(http.StatusNoContent)
}
func getFirstActiveAccount(client *debridlink.Client) *debridlink.DebridAccount {
ctx := context.Background()
accounts, err := client.ListDebridAccounts(ctx)
resp, err := http.Get(downloadURL)
if err != nil {
return nil
log.Printf("[ERROR] Téléchargement échoué : %v\n", err)
UpdateJobStatus(job.ID, "failed", db)
return
}
for _, acc := range accounts {
if acc.IsActive {
return &acc
}
}
return nil
}
func HandleStartJob(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
defer resp.Body.Close()
jobsMu.Lock()
job, exists := jobs[id]
jobsMu.Unlock()
if !exists {
http.Error(w, "Job not found", http.StatusNotFound)
if resp.StatusCode != http.StatusOK {
log.Printf("[ERROR] Erreur HTTP : %s\n", resp.Status)
UpdateJobStatus(job.ID, "failed", db)
return
}
client := debridlink.NewClient(db)
account := getFirstActiveAccount(client)
if account == nil {
http.Error(w, "Aucun compte actif", http.StatusBadRequest)
// 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
}
client.SetAccount(account)
go startDownload(job, job.Link, client, db)
w.WriteHeader(http.StatusNoContent)
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, "_")
}
//***//

View File

@ -56,13 +56,12 @@ func RoutesProtected(r *mux.Router, bd *gorm.DB) {
r.HandleFunc("/godownloader/settings/delete", renders.GoDownloadSettingDelete(bd))
r.HandleFunc("/api/download/add", renders.HandleAddJob(bd)).Methods("POST")
r.HandleFunc("/api/download/all", renders.HandleListJobsPartial(bd)).Methods("GET")
r.HandleFunc("/api/download/pause/{id}", download.HandlePauseJob).Methods("POST")
r.HandleFunc("/api/download/resume/{id}", download.HandleResumeJob(bd)).Methods("POST")
r.HandleFunc("/api/download/delete/{id}", download.HandleDeleteJob).Methods("DELETE")
r.HandleFunc("/stream/{id}", download.HandleStreamPage()).Methods("GET")
r.HandleFunc("/downloads", renders.GoDownload2(bd))
r.HandleFunc("/api/download/start/{id}", download.HandleStartJob(bd)).Methods("POST")
r.HandleFunc("/stream/{id}", download.HandleStreamPage()).Methods("GET")
r.HandleFunc("/api/download/start/{id}", renders.HandleStartJob(bd)).Methods("POST")
r.HandleFunc("/api/download/pause/{id}", renders.HandlePauseJob).Methods("POST")
r.HandleFunc("/api/download/resume/{id}", renders.HandleResumeJob(bd)).Methods("POST")
r.HandleFunc("/api/download/delete/{id}", renders.HandleDeleteJob).Methods("DELETE")
// API user

View File

@ -9,9 +9,11 @@ import (
"log"
"net/http"
"strconv"
"sync"
"text/template"
"time"
"github.com/gorilla/mux"
"gorm.io/gorm"
)
@ -258,7 +260,11 @@ func GoDownload2(db *gorm.DB) http.HandlerFunc {
func HandleAddJob(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
if err := r.ParseForm(); err != nil {
http.Error(w, "Requête invalide", http.StatusBadRequest)
return
}
link := r.FormValue("link")
pathIDStr := r.FormValue("path_id")
@ -268,17 +274,51 @@ func HandleAddJob(db *gorm.DB) http.HandlerFunc {
return
}
_ = download.AddJob(link, uint(parsedID), db)
log.Println("[HTTP] Lien reçu :", link)
log.Println("[HTTP] ID de chemin :", parsedID)
// Mise à jour de la vue partielle du tableau
jobs := download.ListJobs()
data := map[string]interface{}{
"jobs": jobs,
// Authentification Debrid-Link
client := debridlink.NewClient(db)
account := download.GetFirstActiveAccount(client)
if account == nil {
http.Error(w, "Aucun compte Debrid-Link actif", http.StatusBadRequest)
return
}
client.SetAccount(account)
// Débride le lien
ctx := r.Context()
links, err := client.AddLink(ctx, link)
if err != nil {
log.Printf("[ERROR] Echec lors de l'ajout du lien : %v\n", err)
http.Error(w, "Erreur côté Debrid-Link", http.StatusInternalServerError)
return
}
// Enregistre chaque lien comme un job "en attente"
for _, l := range links {
job := &download.DownloadJob{
ID: l.ID,
Link: l.DownloadURL,
Name: l.Name,
Status: "waiting",
PathID: uint(parsedID),
Size: l.Size,
Host: l.Host,
CreatedAt: time.Now(),
}
download.RegisterJobWithDB(job,db) // => stocke en mémoire ou DB selon ton implémentation
}
// Met à jour la vue partielle (tableau des jobs)
data := map[string]interface{}{
"jobs": download.ListJobs(),
}
renderPartial(w, "downloads_table", data)
}
}
func HandleListJobsPartial(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
jobs := download.ListJobs()
@ -288,6 +328,72 @@ func HandleListJobsPartial(db *gorm.DB) http.HandlerFunc {
renderPartial(w, "downloads_table", data)
}
}
var (
jobs = make(map[string]*download.DownloadJob)
jobsMu sync.Mutex
)
func HandleStartJob(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
jobsMu.Lock()
job, exists := jobs[id]
jobsMu.Unlock()
if !exists {
http.Error(w, "Job introuvable", http.StatusNotFound)
return
}
client := debridlink.NewClient(db)
account := download.GetFirstActiveAccount(client)
if account == nil {
http.Error(w, "Aucun compte actif", http.StatusBadRequest)
return
}
client.SetAccount(account)
go download.StartDownload(job, job.Link, client, db)
w.WriteHeader(http.StatusNoContent)
}
}
func HandlePauseJob(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
download.UpdateJobStatus(id, "paused", nil)
w.WriteHeader(http.StatusNoContent)
}
func HandleResumeJob(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
jobsMu.Lock()
job, exists := jobs[id]
jobsMu.Unlock()
if !exists {
http.Error(w, "Job introuvable", http.StatusNotFound)
return
}
client := debridlink.NewClient(db)
account := download.GetFirstActiveAccount(client)
if account == nil {
http.Error(w, "Aucun compte actif", http.StatusBadRequest)
return
}
client.SetAccount(account)
go download.StartDownload(job, job.Link, client, db)
w.WriteHeader(http.StatusNoContent)
}
}
func HandleDeleteJob(w http.ResponseWriter, r *http.Request) {
id := mux.Vars(r)["id"]
download.DeleteJob(id)
w.WriteHeader(http.StatusNoContent)
}
// func GoDownloadSetting(db *gorm.DB) http.HandlerFunc {
// return func(w http.ResponseWriter, r *http.Request) {

Binary file not shown.

BIN
tmp/main

Binary file not shown.