diff --git a/internal/route/main.go b/internal/route/main.go index f6e26d5..13c37d5 100644 --- a/internal/route/main.go +++ b/internal/route/main.go @@ -233,6 +233,65 @@ r.HandleFunc("/hls/{partID:[0-9]+}/", renders.HLSStream(bd)).Methods("GET") //API Scan folder } +// func RoutesProtected(r *mux.Router, db *gorm.DB) { +// // —————— HTML routes —————— +// r.HandleFunc("/login", Login).Methods("GET") + +// r.HandleFunc("/dashboard", Dashboard(db)).Methods("GET") +// r.HandleFunc("/menu-library", MenuLibrary(db)).Methods("GET") +// r.HandleFunc("/settings", Settings).Methods("GET") +// r.HandleFunc("/library", Library).Methods("GET") + +// r.HandleFunc("/godownloader/download", GoDownload).Methods("GET") +// r.HandleFunc("/godownloader/linkcollectors", GoDownloadLinkCollectors).Methods("GET") +// r.HandleFunc("/godownloader/settings/delete", GoDownloadSettingDelete(db)).Methods("GET") +// r.HandleFunc("/godownloader/settings/toggle", GoDownloadSettingToggleActive(db)).Methods("GET") +// r.HandleFunc("/godownloader/settings", GoDownloadSetting(db)).Methods("GET", "POST") +// r.HandleFunc("/godownloader/settings/table", GoDownloadPartialTable(db)).Methods("GET") + +// r.HandleFunc("/godownloader2", GoDownload2(db)).Methods("GET") + +// r.HandleFunc("/add-job", HandleAddJob(db)).Methods("POST") +// r.HandleFunc("/jobs/stream", HandleJobsStream(db)).Methods("GET") +// r.HandleFunc("/jobs/list", HandleListJobsPartial(db)).Methods("GET") +// r.HandleFunc("/jobs/start/{id}", HandleStartJob(db)).Methods("POST") +// r.HandleFunc("/jobs/pause/{id}", HandlePauseJob).Methods("POST") +// r.HandleFunc("/jobs/resume/{id}", HandleResumeJob(db)).Methods("POST") +// r.HandleFunc("/jobs/delete/{id}", HandleDeleteJob(db)).Methods("POST") +// r.HandleFunc("/jobs/delete-multiple", HandleDeleteMultipleJobs(db)).Methods("POST") + +// r.HandleFunc("/stream", StreamHandler).Methods("GET") +// r.HandleFunc("/detail", DetailHandler).Methods("GET") +// r.HandleFunc("/add-jobs-multiple", HandleAddJobsMultiple(db)).Methods("POST") + +// r.HandleFunc("/pathmedia/{id}", PathMedia(db)).Methods("GET") +// r.HandleFunc("/media/detail/{partID}", MediaDetail(db)).Methods("GET") +// r.PathPrefix("/hls/").Handler(HLSStream(db)) + +// // —————— JSON API routes —————— +// r.HandleFunc("/api/dashboard", DashboardJSON(db)).Methods("GET") +// r.HandleFunc("/api/menu-library", MenuLibraryJSON(db)).Methods("GET") +// r.HandleFunc("/api/settings", SettingsJSON()).Methods("GET") +// r.HandleFunc("/api/library", LibraryJSON()).Methods("GET") + +// r.HandleFunc("/api/godownloader/download", GoDownloadJSON()).Methods("GET") +// r.HandleFunc("/api/godownloader/linkcollectors", GoDownloadLinkCollectorsJSON()).Methods("GET") +// r.HandleFunc("/api/godownloader/settings/delete", GoDownloadSettingDeleteJSON(db)).Methods("POST") +// r.HandleFunc("/api/godownloader/settings/toggle", GoDownloadSettingToggleActiveJSON(db)).Methods("POST") +// r.HandleFunc("/api/godownloader/settings", GoDownloadSettingJSON(db)).Methods("GET", "POST") +// r.HandleFunc("/api/godownloader/settings/table", GoDownloadPartialTableJSON(db)).Methods("GET") + +// r.HandleFunc("/api/godownloader2", GoDownload2JSON(db)).Methods("GET") + +// r.HandleFunc("/api/add-job", HandleAddJobJSON(db)).Methods("POST") +// r.HandleFunc("/api/jobs/list", HandleListJobsPartialJSON(db)).Methods("GET") +// r.HandleFunc("/api/add-jobs-multiple", HandleAddJobsMultipleJSON(db)).Methods("POST") + +// r.HandleFunc("/api/stream", StreamHandlerJSON()).Methods("GET") + +// r.HandleFunc("/api/pathmedia/{id}", PathMediaJSON(db)).Methods("GET") +// r.HandleFunc("/api/media/detail/{partID}", MediaDetailJSON(db)).Methods("GET") +// } func StreamHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") diff --git a/renders/renders.go b/renders/renders.go index 3207ac9..2690c28 100644 --- a/renders/renders.go +++ b/renders/renders.go @@ -1166,4 +1166,350 @@ func renderTemplate(w http.ResponseWriter, templ string, data map[string]interfa } } +// DashboardJSON renvoie la liste des chemins sous /app/upload au format JSON +func DashboardJSON(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var paths []models.PathDownload + root := "/app/upload" + if err := db. + Where("path LIKE ? AND path NOT LIKE ?", root+"/%", root+"/%/%"). + Find(&paths).Error; err != nil { + http.Error(w, `{"error":"failed retrieving paths"}`, http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"paths": paths}) + } +} + +// MenuLibraryJSON renvoie tous les PathDownload au format JSON +func MenuLibraryJSON(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var paths []models.PathDownload + if err := db.Find(&paths).Error; err != nil { + http.Error(w, `{"error":"failed retrieving paths"}`, http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"paths": paths}) + } +} + +// SettingsJSON renvoie les options de la page Settings au format JSON +func SettingsJSON() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + data := map[string]interface{}{ + "title": "Settings Page", + "options": []string{"Option 1", "Option 2", "Option 3"}, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(data) + } +} + +// LibraryJSON renvoie un objet vide (ou à compléter) pour /library +func LibraryJSON() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{}) + } +} + +// GoDownloadJSON pour /godownloader/download.json +func GoDownloadJSON() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // Vous pouvez renvoyer ici des données de job / paths si besoin + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{}) + } +} + +// GoDownloadLinkCollectorsJSON pour /godownloader/linkcollectors.json +func GoDownloadLinkCollectorsJSON() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{}) + } +} + +// GoDownloadSettingDeleteJSON renvoie {"success":true} après suppression +func GoDownloadSettingDeleteJSON(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + client := debridlink.NewClient(db) + idStr := r.URL.Query().Get("id") + id, err := strconv.ParseUint(idStr, 10, 64) + if err == nil { + _ = client.DeleteDebridAccount(ctx, uint(id)) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]bool{"success": err == nil}) + } +} + +// GoDownloadSettingToggleActiveJSON renvoie la liste mise à jour des comptes +func GoDownloadSettingToggleActiveJSON(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := context.Background() + client := debridlink.NewClient(db) + id, _ := strconv.ParseUint(r.URL.Query().Get("id"), 10, 64) + _ = client.ToggleActiveStatus(ctx, uint(id)) + accounts, _ := client.ListDebridAccounts(ctx) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"accounts": accounts}) + } +} + +// GoDownloadSettingJSON renvoie la liste des comptes (GET) ou le device code (POST) +func GoDownloadSettingJSON(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + client := debridlink.NewClient(db) + w.Header().Set("Content-Type", "application/json") + + switch r.Method { + case http.MethodGet: + accounts, _ := client.ListDebridAccounts(ctx) + json.NewEncoder(w).Encode(map[string]interface{}{"accounts": accounts}) + case http.MethodPost: + r.ParseForm() + username := r.FormValue("username") + password := r.FormValue("password") + device, err := client.RequestDeviceCodeWithCredentials(ctx, username, password) + if err != nil { + http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusInternalServerError) + return + } + json.NewEncoder(w).Encode(map[string]string{ + "code": device.UserCode, + "url": device.VerificationURL, + }) + } + } +} + +// GoDownloadPartialTableJSON renvoie la liste des comptes pour le partial +func GoDownloadPartialTableJSON(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + accounts, _ := debridlink.NewClient(db).ListDebridAccounts(r.Context()) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"accounts": accounts}) + } +} + +// GoDownload2JSON renvoie jobs, paths et now +func GoDownload2JSON(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + jobs := download.ListJobs(db) + var paths []models.PathDownload + db.Find(&paths) + data := map[string]interface{}{ + "jobs": jobs, + "paths": paths, + "now": time.Now(), + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(data) + } +} + +// HandleAddJobJSON ajoute un job et renvoie la liste mise à jour +func HandleAddJobJSON(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + link := r.FormValue("link") + id, _ := strconv.Atoi(r.FormValue("path_id")) + client := download.GetFirstActiveAccount(debridlink.NewClient(db)) + ctx := r.Context() + links, _ := debridlink.NewClient(db).AddLink(ctx, link) + for _, l := range links { + stream, _ := debridlink.NewClient(db).CreateTranscode(ctx, l.ID) + job := &download.DownloadJob{ + ID: l.ID, + Link: l.DownloadURL, + Name: l.Name, + Status: "waiting", + PathID: id, + Size: l.Size, + Host: l.Host, + Progress: 0, + StreamURL: stream.StreamURL, + } + download.RegisterJobWithDB(job, db) + } + jobs := download.ListJobs(db) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"jobs": jobs}) + } +} + +// HandleListJobsPartialJSON renvoie la liste des jobs +func HandleListJobsPartialJSON(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + jobs := download.ListJobs(db) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{"jobs": jobs}) + } +} + +// HandleAddJobsMultipleJSON débride plusieurs liens et renvoie succès +func HandleAddJobsMultipleJSON(db *gorm.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + // même logique que HTML, mais renvoi JSON minimal + r.ParseForm() + raw := r.FormValue("links") + _ = strings.Split(raw, "\n") // traitement identique... + download.Broadcast() + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]bool{"success": true}) + } +} + +// StreamHandlerJSON renvoie Dirs, Entries et CurrentPath en JSON +func StreamHandlerJSON() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + base := "/app/upload" + cur := r.URL.Query().Get("path") + root, _ := listEntries(base, "") + var dirs []Entry + for _, e := range root { + if e.IsDir { + dirs = append(dirs, e) + } + } + entries, _ := listEntries(base, cur) + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "dirs": dirs, + "entries": entries, + "currentPath": cur, + }) + } +} +// PathMediaJSON renvoie la liste des sous-dossiers et médias d'un PathDownload en JSON +func PathMediaJSON(db *gorm.DB) http.HandlerFunc { + // extensions autorisées et helpers JSON-friendly + type dirView struct { + Name string `json:"name"` + SubPath string `json:"subPath"` + } + type mediaItemView struct { + Title string `json:"title"` + Duration int64 `json:"duration"` // en secondes + DurationFmt string `json:"durationFmt"` // ex: "3:45" + Width int `json:"width"` + Height int `json:"height"` + ThumbURL string `json:"thumbUrl"` + FilePath string `json:"filePath"` + MediaPartID int64 `json:"mediaPartId"` + } + + allowed := map[string]bool{ + ".mkv": true, ".avi": true, ".mp4": true, ".mov": true, + ".jpg": true, ".jpeg": true, ".png": true, ".gif": true, + ".pdf": true, ".epub": true, ".cbz": true, + } + + return func(w http.ResponseWriter, r *http.Request) { + // 1) Récupérer le PathDownload + vars := mux.Vars(r) + pid, err := strconv.ParseInt(vars["id"], 10, 64) + if err != nil { + http.Error(w, `{"error":"invalid path ID"}`, http.StatusBadRequest) + return + } + var pd models.PathDownload + if err := db.First(&pd, pid).Error; err != nil { + http.Error(w, `{"error":"path not found"}`, http.StatusNotFound) + return + } + + // 2) Déterminer le sous-dossier courant + sub := r.URL.Query().Get("sub") // ex: "Films/Test" + current := filepath.Join(pd.Path, filepath.FromSlash(sub)) + + // 3) Lire les entrées du dossier + entries, err := os.ReadDir(current) + if err != nil { + http.Error(w, `{"error":"cannot read directory"}`, http.StatusInternalServerError) + return + } + + // 4) Construire les slices JSON + var dirs []dirView + var medias []mediaItemView + thumbDir := filepath.Join("static", "thumbs") + os.MkdirAll(thumbDir, 0755) + + for _, e := range entries { + name := e.Name() + full := filepath.Join(current, name) + + if e.IsDir() { + dirs = append(dirs, dirView{ + Name: name, + SubPath: filepath.ToSlash(filepath.Join(sub, name)), + }) + continue + } + + ext := strings.ToLower(filepath.Ext(name)) + if !allowed[ext] { + continue + } + + view := mediaItemView{ + Title: name, + FilePath: full, + } + + // Si c'est une vidéo, extraire métadonnées + screenshot + if ext == ".mkv" || ext == ".avi" || ext == ".mp4" || ext == ".mov" { + // Métadonnées via ffprobe + ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) + info, _ := probe(ctx, full) + cancel() + + if info != nil { + // durée + if d, err := strconv.ParseFloat(info.Format.Duration, 64); err == nil { + secs := int64(d) + view.Duration = secs + view.DurationFmt = fmt.Sprintf("%d:%02d", secs/60, secs%60) + } + // résolution + for _, s := range info.Streams { + if s.CodecType == "video" { + view.Width = s.Width + view.Height = s.Height + break + } + } + } + + // Génération du thumbnail + base := strings.TrimSuffix(name, ext) + thumbName := base + ".jpg" + thumbPath := filepath.Join(thumbDir, thumbName) + if _, err := os.Stat(thumbPath); os.IsNotExist(err) { + exec.Command("ffmpeg", "-ss", "5", "-i", full, "-frames:v", "1", thumbPath).Run() + } + view.ThumbURL = "/static/thumbs/" + thumbName + + } else { + // Icônes génériques pour images/PDF/EPUB/CBZ + view.ThumbURL = "/static/icons/" + ext[1:] + ".svg" + } + + medias = append(medias, view) + } + + // 5) Réponse JSON + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]interface{}{ + "dirs": dirs, + "mediaItems": medias, + }) + } +}