diff --git a/.DS_Store b/.DS_Store index 5008ddf..7b0af80 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/docker-compose.yml b/docker-compose.yml index a203711..b5ec9dc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,5 @@ version: '3.8' + services: shelfly: build: @@ -9,5 +10,6 @@ services: volumes: - .:/app - ./shelfly_db.db:/app/shelfly_db.db - env_file: - - .env \ No newline at end of file + dns: + - 8.8.8.8 + - 1.1.1.1 diff --git a/internal/.DS_Store b/internal/.DS_Store index b9905d2..99f6b1b 100644 Binary files a/internal/.DS_Store and b/internal/.DS_Store differ diff --git a/internal/db/db.go b/internal/db/db.go index 01cfb4a..9fb22e2 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -2,6 +2,7 @@ package db import ( "app/shelfly/internal/debridlink" + "app/shelfly/internal/download" "app/shelfly/internal/models" "fmt" @@ -36,6 +37,7 @@ func InitDB()*gorm.DB { &debridlink.RSSItem{}, &debridlink.Torrent{}, &debridlink.DebridAccount{}, + &download.DownloadJob{}, ) diff --git a/internal/debridlink/client.go b/internal/debridlink/client.go index 93735ce..ed7d419 100644 --- a/internal/debridlink/client.go +++ b/internal/debridlink/client.go @@ -54,6 +54,7 @@ func NewClient(db *gorm.DB) *Client { } } + func (c *Client) SetAccount(account *DebridAccount) { c.account = account } @@ -440,16 +441,16 @@ 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{"link": link} - if err := c.doJSON(ctx, "POST", "downloader/links", nil, body, &result); err != nil { + body := map[string]string{"url": link} // ✅ CORRECTION + if err := c.doJSON(ctx, "POST", "downloader/add", nil, body, &result); err != nil { // ✅ CORRECTION return nil, err } return &result, 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) diff --git a/internal/download/download.go b/internal/download/download.go index dd4022b..a05fe65 100644 --- a/internal/download/download.go +++ b/internal/download/download.go @@ -6,10 +6,12 @@ import ( "encoding/json" "errors" "fmt" + "html/template" "log" "net/http" "os" "strings" + "github.com/gorilla/mux" "gorm.io/gorm" ) @@ -282,3 +284,30 @@ func PathValidationHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, "Failed to encode response", http.StatusInternalServerError) } } + +type StreamPageData struct { + StreamURL string +} + +func HandleStreamPage() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + id := mux.Vars(r)["id"] + job := jobs[id] + if job == nil || job.StreamURL == "" { + http.Error(w, "Stream non disponible", http.StatusNotFound) + return + } + + tmpl := ` +Streaming + + + +` + t := template.Must(template.New("stream").Parse(tmpl)) + t.Execute(w, StreamPageData{StreamURL: job.StreamURL}) + } +} diff --git a/internal/download/jobs.go b/internal/download/jobs.go new file mode 100644 index 0000000..6b6138a --- /dev/null +++ b/internal/download/jobs.go @@ -0,0 +1,232 @@ +// internal/download/jobs.go +package download + +import ( + "app/shelfly/internal/debridlink" + "context" + "encoding/json" + "net/http" + "strconv" + "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"` + 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(), + } + jobsMu.Lock() + jobs[strconv.FormatUint(uint64(id), 10)] = job + jobsMu.Unlock() + + db.Create(job) + return job +} + + + +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 + } + job.RemoteID = linkResp.ID + db.Save(job) + go syncDownloadStatus(job, client, db) +} + +func syncDownloadStatus(job *DownloadJob, client *debridlink.Client, db *gorm.DB) { + ctx := context.Background() + var checkCount int + for { + if job.Status != "downloading" { + return + } + 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) + } +} + +func ListJobs() []*DownloadJob { + jobsMu.Lock() + defer jobsMu.Unlock() + var list []*DownloadJob + for _, job := range jobs { + list = append(list, job) + } + return list +} + +func PauseJob(id string) { + jobsMu.Lock() + defer jobsMu.Unlock() + if job, ok := jobs[id]; ok && job.Status == "downloading" { + job.Status = "paused" + } +} + +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) + if err != nil { + return nil + } + 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"] + + jobsMu.Lock() + job, exists := jobs[id] + jobsMu.Unlock() + + if !exists { + http.Error(w, "Job not found", http.StatusNotFound) + return + } + + client := debridlink.NewClient(db) + account := getFirstActiveAccount(client) + if account == nil { + http.Error(w, "Aucun compte actif", http.StatusBadRequest) + return + } + client.SetAccount(account) + + go startDownload(job, job.Link, client, db) + + w.WriteHeader(http.StatusNoContent) + } +} + diff --git a/internal/models/models.go b/internal/models/models.go index 07b7c94..cea2907 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -2,6 +2,7 @@ package models + type User struct { ID uint `json:"id" gorm:"primaryKey"` Username string `json:"username" gorm:"size:255"` diff --git a/internal/route/main.go b/internal/route/main.go index fa33fbd..04a3f6e 100644 --- a/internal/route/main.go +++ b/internal/route/main.go @@ -54,6 +54,15 @@ func RoutesProtected(r *mux.Router, bd *gorm.DB) { r.HandleFunc("/godownloader/poll-status", renders.PollStatusHandler(bd)) r.HandleFunc("/godownloader/table-refresh", renders.GoDownloadPartialTable(bd)) 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") + // API user diff --git a/main.go b/main.go index 24aff3f..de47522 100644 --- a/main.go +++ b/main.go @@ -9,14 +9,10 @@ import ( "net/http" "github.com/gorilla/mux" - "github.com/joho/godotenv" ) func main() { - err := godotenv.Load() - if err != nil { - log.Fatal("Erreur de chargement du fichier .env") - } + // 1. Démarrer le routeur principal r := mux.NewRouter() diff --git a/renders/renders.go b/renders/renders.go index 44d27ea..84278c8 100644 --- a/renders/renders.go +++ b/renders/renders.go @@ -2,6 +2,7 @@ package renders import ( "app/shelfly/internal/debridlink" + "app/shelfly/internal/download" "app/shelfly/internal/models" "context" "encoding/json" @@ -240,6 +241,53 @@ func PollStatusHandler(db *gorm.DB) http.HandlerFunc { }) } } +func GoDownload2(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + jobs := download.ListJobs() + var paths []models.PathDownload + db.Find(&paths) + + data := map[string]interface{}{ + "jobs": jobs, + "paths": paths, + } + + renderTemplate(w, "godownloader_download", data) + } +} + +func HandleAddJob(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + link := r.FormValue("link") + pathIDStr := r.FormValue("path_id") + + parsedID, err := strconv.Atoi(pathIDStr) + if err != nil { + http.Error(w, "Chemin invalide", http.StatusBadRequest) + return + } + + _ = download.AddJob(link, uint(parsedID), db) + + // Mise à jour de la vue partielle du tableau + jobs := download.ListJobs() + data := map[string]interface{}{ + "jobs": jobs, + } + + renderPartial(w, "downloads_table", data) + } +} +func HandleListJobsPartial(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + jobs := download.ListJobs() + data := map[string]interface{}{ + "jobs": jobs, + } + renderPartial(w, "downloads_table", data) + } +} // func GoDownloadSetting(db *gorm.DB) http.HandlerFunc { // return func(w http.ResponseWriter, r *http.Request) { diff --git a/shelfly_db.db b/shelfly_db.db index f1024b6..73b0e53 100644 Binary files a/shelfly_db.db and b/shelfly_db.db differ diff --git a/templates/.DS_Store b/templates/.DS_Store new file mode 100644 index 0000000..d617ffe Binary files /dev/null and b/templates/.DS_Store differ diff --git a/templates/dashboard.pages.tmpl b/templates/dashboard.pages.tmpl index f929fc2..289dde8 100644 --- a/templates/dashboard.pages.tmpl +++ b/templates/dashboard.pages.tmpl @@ -11,9 +11,15 @@
diff --git a/templates/godownloader_download.pages.tmpl b/templates/godownloader_download.pages.tmpl index 916e6a3..dd78e5b 100644 --- a/templates/godownloader_download.pages.tmpl +++ b/templates/godownloader_download.pages.tmpl @@ -1 +1,75 @@ -

Download

\ No newline at end of file +

Download

+
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+ + + + + + + + + + +{{ range .jobs }} + + + + + + +{{ if eq .Status "error" }} + + + +{{ end }} +{{ end }} + +
FichierStatutVitesseActions
{{ .FileName }}{{ .Status }}{{ .Speed }} + {{ if eq .Status "added" }} + + {{ end }} + {{ if eq .Status "paused" }} + + {{ else if eq .Status "downloading" }} + + {{ end }} + {{ if and (eq .Status "downloaded") .StreamURL }} + 🎬 Stream + + + {{ end }} + +
Erreur : {{ .ErrorMsg }}
+
+
\ No newline at end of file diff --git a/templates/open ./.DS_Store b/templates/open ./.DS_Store new file mode 100644 index 0000000..a119e23 Binary files /dev/null and b/templates/open ./.DS_Store differ diff --git a/tmp/main b/tmp/main index 04cb652..3b63b0d 100755 Binary files a/tmp/main and b/tmp/main differ diff --git a/tmp/stdout b/tmp/stdout index 1049595..4c65af2 100644 --- a/tmp/stdout +++ b/tmp/stdout @@ -1 +1 @@ -exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file +exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1 \ No newline at end of file