613 lines
17 KiB
Go
613 lines
17 KiB
Go
package renders
|
||
|
||
import (
|
||
"app/shelfly/internal/debridlink"
|
||
"app/shelfly/internal/download"
|
||
"app/shelfly/internal/models"
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"text/template"
|
||
"time"
|
||
|
||
"github.com/gorilla/mux"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
|
||
func Login(w http.ResponseWriter, r *http.Request){
|
||
renderTemplate(w,"login",nil)
|
||
}
|
||
func Dashboard(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 to retrieve paths"}`, http.StatusInternalServerError)
|
||
return
|
||
}
|
||
data := map[string]interface{}{
|
||
"paths": paths,
|
||
}
|
||
|
||
renderTemplate(w,"dashboard",data)
|
||
}
|
||
|
||
}
|
||
func MenuLibrary(db *gorm.DB) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var currentPaths []models.PathDownload
|
||
if err := db.Find(¤tPaths).Error; err != nil {
|
||
http.Error(w, `{"error": "Failed to retrieve paths"}`, http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
// Récupérer l'ancienne version des paths (si existante)
|
||
lastUpdate := r.Header.Get("HX-Current-Paths")
|
||
var previousPaths []models.PathDownload
|
||
if lastUpdate != "" {
|
||
json.Unmarshal([]byte(lastUpdate), &previousPaths)
|
||
}
|
||
|
||
// Convertir en JSON pour comparaison
|
||
currentJSON, _ := json.Marshal(currentPaths)
|
||
previousJSON, _ := json.Marshal(previousPaths)
|
||
|
||
// Vérifier si les paths ont changé
|
||
pathsChanged := string(currentJSON) != string(previousJSON)
|
||
|
||
data := map[string]interface{}{
|
||
"paths": currentPaths,
|
||
}
|
||
|
||
// Si HTMX request, ajouter les headers appropriés
|
||
if r.Header.Get("HX-Request") == "true" {
|
||
if pathsChanged {
|
||
w.Header().Set("HX-Trigger", "pathsUpdated")
|
||
}
|
||
w.Header().Set("HX-Current-Paths", string(currentJSON))
|
||
}
|
||
|
||
renderPartial(w, "dashboard", data)
|
||
}
|
||
|
||
}
|
||
func Settings(w http.ResponseWriter, r *http.Request) {
|
||
data := map[string]interface{}{
|
||
"Title": "Settings Page",
|
||
"Options": []string{"Option 1", "Option 2", "Option 3"},
|
||
}
|
||
renderPartial(w, "settings", data)
|
||
}
|
||
|
||
|
||
func Library(w http.ResponseWriter, r *http.Request) {
|
||
renderPartial(w, "library",nil)
|
||
}
|
||
|
||
func GoDownload(w http.ResponseWriter, r *http.Request) {
|
||
renderPartial(w, "godownloader_download",nil)
|
||
}
|
||
func GoDownloadLinkCollectors(w http.ResponseWriter, r *http.Request) {
|
||
renderPartial(w, "godownloader_linkcollectors",nil)
|
||
}
|
||
func GetDebridClient(db *gorm.DB) *debridlink.Client {
|
||
return debridlink.NewClient(db)
|
||
}
|
||
|
||
func GoDownloadSettingDelete(db *gorm.DB) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
ctx := context.Background()
|
||
DebridClient := GetDebridClient(db)
|
||
|
||
idStr := r.URL.Query().Get("id")
|
||
if idStr == "" {
|
||
http.Error(w, "ID manquant", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
idUint, err := strconv.ParseUint(idStr, 10, 64)
|
||
if err != nil {
|
||
http.Error(w, "ID invalide", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
if err := DebridClient.DeleteDebridAccount(ctx, uint(idUint)); err != nil {
|
||
http.Error(w, "Erreur lors de la suppression", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
http.Redirect(w, r, "/godownloader/settings", http.StatusSeeOther)
|
||
}
|
||
}
|
||
|
||
func GoDownloadSettingToggleActive(db *gorm.DB) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
ctx := context.Background()
|
||
DebridClient := debridlink.NewClient(db)
|
||
|
||
idStr := r.URL.Query().Get("id")
|
||
idUint, err := strconv.ParseUint(idStr, 10, 32)
|
||
if err != nil {
|
||
http.Error(w, "ID invalide", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
err = DebridClient.ToggleActiveStatus(ctx, uint(idUint))
|
||
if err != nil {
|
||
log.Println("Erreur lors du toggle:", err)
|
||
http.Error(w, "Échec de mise à jour", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
// Récupérer la liste mise à jour
|
||
accounts, err := DebridClient.ListDebridAccounts(ctx)
|
||
if err != nil {
|
||
http.Error(w, "Erreur lors du chargement des comptes", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
// HTMX ou page normale
|
||
if r.Header.Get("HX-Request") == "true" {
|
||
renderPartial(w, "partials/accounts_table", map[string]interface{}{
|
||
"accounts": accounts,
|
||
})
|
||
} else {
|
||
renderPartial(w, "godownloader_setting", map[string]interface{}{
|
||
"accounts": accounts,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
func GoDownloadSetting(db *gorm.DB) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
ctx := r.Context()
|
||
client := debridlink.NewClient(db)
|
||
|
||
switch r.Method {
|
||
case http.MethodPost:
|
||
if err := r.ParseForm(); err != nil {
|
||
http.Error(w, "Form invalide", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
username := r.FormValue("username")
|
||
password := r.FormValue("password")
|
||
|
||
deviceResp, err := client.RequestDeviceCodeWithCredentials(ctx, username, password)
|
||
if err != nil {
|
||
log.Println("[OAuth2] Erreur device_code:", err)
|
||
http.Error(w, "Erreur OAuth: "+err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
// Affiche le code + URL dans #auth-status
|
||
renderPartial(w, "oauth_device_code", map[string]any{
|
||
"code": deviceResp.UserCode,
|
||
"url": deviceResp.VerificationURL,
|
||
})
|
||
|
||
// Polling async
|
||
go func() {
|
||
tokens, err := client.PollDeviceToken(context.Background(), deviceResp.DeviceCode, deviceResp.Interval)
|
||
if err != nil {
|
||
log.Println("[OAuth2] Polling échoué:", err)
|
||
return
|
||
}
|
||
|
||
account := &debridlink.DebridAccount{
|
||
Host: "debrid-link.com",
|
||
Username: username,
|
||
Password: password,
|
||
IsActive: true,
|
||
AccessToken: tokens.AccessToken,
|
||
RefreshToken: tokens.RefreshToken,
|
||
ExpiresAt: time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second),
|
||
}
|
||
|
||
if err := db.Create(account).Error; err != nil {
|
||
log.Println("[DB] Sauvegarde échouée:", err)
|
||
return
|
||
}
|
||
|
||
log.Println("[OAuth2] Compte sauvegardé")
|
||
}()
|
||
|
||
case http.MethodGet:
|
||
accounts, _ := client.ListDebridAccounts(ctx)
|
||
renderPartial(w, "godownloader_setting", map[string]any{
|
||
"accounts": accounts,
|
||
})
|
||
}
|
||
}
|
||
}
|
||
func GoDownloadPartialTable(db *gorm.DB) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
ctx := r.Context()
|
||
client := debridlink.NewClient(db)
|
||
accounts, _ := client.ListDebridAccounts(ctx)
|
||
renderPartial(w, "accounts_table", map[string]any{
|
||
"accounts": accounts,
|
||
})
|
||
}}
|
||
func PollStatusHandler(db *gorm.DB) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
var count int64
|
||
db.Model(&debridlink.DebridAccount{}).Where("is_active = ?", true).Count(&count)
|
||
|
||
w.Header().Set("Content-Type", "application/json")
|
||
json.NewEncoder(w).Encode(map[string]bool{
|
||
"success": count > 0,
|
||
})
|
||
}
|
||
}
|
||
func GoDownload2(db *gorm.DB) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
w.Header().Set("HX-Trigger", "forceUpdate")
|
||
|
||
log.Printf("GoDownload2")
|
||
jobs := download.ListJobs(db)
|
||
fmt.Printf("%+v\n", jobs)
|
||
|
||
var paths []models.PathDownload
|
||
db.Find(&paths)
|
||
|
||
data := map[string]interface{}{
|
||
"jobs": jobs,
|
||
"paths": paths,
|
||
"Now": time.Now(), // 👈 ajoute la clé "Now"
|
||
|
||
}
|
||
|
||
download.Broadcast()
|
||
renderTemplate(w, "godownloader_download", data)
|
||
}
|
||
}
|
||
|
||
|
||
func HandleAddJob(db *gorm.DB) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
if err := r.ParseForm(); err != nil {
|
||
http.Error(w, "Requête invalide", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
log.Println("[HTTP] Lien reçu :", link)
|
||
log.Println("[HTTP] ID de chemin :", parsedID)
|
||
|
||
// 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 {
|
||
streamInfo, err := client.GetTranscode(ctx, l.ID)
|
||
if err != nil {
|
||
log.Println("Erreur GetTranscode:", err)
|
||
return
|
||
}
|
||
|
||
job := &download.DownloadJob{
|
||
ID: l.ID,
|
||
Link: l.DownloadURL,
|
||
Name: l.Name,
|
||
Status: "waiting",
|
||
PathID: parsedID,
|
||
Size: l.Size,
|
||
Host: l.Host,
|
||
Progress: 0, // obligatoire si valeur attendue
|
||
StreamURL: streamInfo.StreamURL, // vide par défaut
|
||
}
|
||
if err := download.RegisterJobWithDB(job, db); err != nil {
|
||
log.Printf("[ERROR] Job non enregistré : %v\n", err)
|
||
}
|
||
}
|
||
|
||
// Met à jour la vue partielle (tableau des jobs)
|
||
data := map[string]interface{}{
|
||
"jobs": download.ListJobs(db),
|
||
}
|
||
fmt.Printf("%+v\n", data)
|
||
download.Broadcast() // ← on notifie tout de suite les clients SSE
|
||
|
||
|
||
renderPartial(w, "downloads_table", data)
|
||
}
|
||
}
|
||
// HandleJobsStream ouvre le flux SSE et envoie un event "jobs" à chaque changement
|
||
// HandleJobsStream ouvre un flux SSE qui n’envoie qu’un signal "jobs"
|
||
func HandleJobsStream(db *gorm.DB) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
log.Println("[DEBUG] Nouvelle connexion au flux SSE")
|
||
|
||
flusher, ok := w.(http.Flusher)
|
||
if !ok {
|
||
log.Println("[ERROR] Flusher non supporté")
|
||
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
// Entêtes SSE
|
||
w.Header().Set("Content-Type", "text/event-stream")
|
||
w.Header().Set("Cache-Control", "no-cache")
|
||
w.Header().Set("Connection", "keep-alive")
|
||
w.Header().Set("Access-Control-Allow-Origin", "*") // utile en dev ou si besoin CORS
|
||
|
||
ch := download.Subscribe()
|
||
log.Println("[DEBUG] Abonné au canal de téléchargement")
|
||
defer func() {
|
||
download.Unsubscribe(ch)
|
||
log.Println("[DEBUG] Désabonnement du canal de téléchargement")
|
||
}()
|
||
|
||
// Envoi d’un message initial bien formé (JSON valide)
|
||
log.Println("[DEBUG] Envoi du signal initial")
|
||
fmt.Fprintf(w, "event: jobs\ndata: {\"refresh\": true}\n\n")
|
||
flusher.Flush()
|
||
|
||
// Boucle de stream
|
||
for {
|
||
select {
|
||
case <-r.Context().Done():
|
||
log.Println("[DEBUG] Fermeture de la connexion SSE (client disconnect)")
|
||
return
|
||
case <-ch:
|
||
log.Println("[DEBUG] Événement reçu sur le canal — envoi SSE")
|
||
fmt.Fprintf(w, "event: jobs\ndata: {\"refresh\": true}\n\n")
|
||
flusher.Flush()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
// sendSSEUpdate génère le HTML du partial et l’envoie comme event "jobs"
|
||
func sendSSEUpdate(w http.ResponseWriter, flusher http.Flusher, db *gorm.DB) error {
|
||
// 1) Création du pipe
|
||
pr, pw := io.Pipe()
|
||
|
||
// 2) Dans une goroutine, parse+execute du partial dans pw
|
||
go func() {
|
||
defer pw.Close()
|
||
|
||
// Charge le fichier de template du partial
|
||
tmpl, err := template.ParseFiles("./templates/downloads_table.pages.tmpl")
|
||
if err != nil {
|
||
log.Printf("ParseFiles error: %v", err)
|
||
return
|
||
}
|
||
|
||
// Prépare les données réelles
|
||
data := map[string]interface{}{
|
||
"jobs": download.ListJobs(db),
|
||
}
|
||
|
||
// Exécute *uniquement* le define "downloads_table"
|
||
if err := tmpl.ExecuteTemplate(pw, "downloads_table", data); err != nil {
|
||
log.Printf("ExecuteTemplate error: %v", err)
|
||
return
|
||
}
|
||
}()
|
||
|
||
// 3) Lecture complète du HTML
|
||
htmlBytes, err := io.ReadAll(pr)
|
||
if err != nil {
|
||
return fmt.Errorf("lecture rendu échouée: %w", err)
|
||
}
|
||
|
||
// 4) Construction du message SSE
|
||
// - event: jobs
|
||
// - chaque ligne de HTML préfixée data:
|
||
fmt.Fprintf(w, "event: jobs\n")
|
||
for _, line := range strings.Split(string(htmlBytes), "\n") {
|
||
fmt.Fprintf(w, "data: %s\n", line)
|
||
}
|
||
// ligne vide pour terminer l'event
|
||
fmt.Fprintf(w, "\n")
|
||
|
||
// 5) Flush pour envoyer au client immédiatement
|
||
flusher.Flush()
|
||
return nil
|
||
}
|
||
|
||
|
||
func HandleListJobsPartial(db *gorm.DB) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
jobs := download.ListJobs(db)
|
||
data := map[string]interface{}{
|
||
"jobs": jobs,
|
||
}
|
||
//download.Broadcast()
|
||
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"]
|
||
log.Printf("[id] job id= "+id)
|
||
|
||
// 1. Récupérer depuis la map
|
||
jobsMu.Lock()
|
||
job, exists := jobs[id]
|
||
jobsMu.Unlock()
|
||
|
||
// 2. Sinon fallback base de données
|
||
if !exists {
|
||
var j download.DownloadJob
|
||
if err := db.First(&j, "id = ?", id).Error; err != nil {
|
||
http.Error(w, "Job introuvable", http.StatusNotFound)
|
||
return
|
||
}
|
||
|
||
// important : on copie vers un pointeur pour que la map pointe bien dessus
|
||
jobCopy := j
|
||
jobsMu.Lock()
|
||
jobs[id] = &jobCopy
|
||
job = &jobCopy
|
||
jobsMu.Unlock()
|
||
}
|
||
|
||
// 3. Setup client Debrid-Link
|
||
client := debridlink.NewClient(db)
|
||
account := download.GetFirstActiveAccount(client)
|
||
if account == nil {
|
||
http.Error(w, "Aucun compte actif", http.StatusBadRequest)
|
||
return
|
||
}
|
||
client.SetAccount(account)
|
||
|
||
// 4. Lancer le téléchargement réel
|
||
go download.StartDownload(job,job.Link,client, db)
|
||
download.Broadcast() // ← on notifie tout de suite les clients SSE
|
||
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
}
|
||
|
||
func HandlePauseJob(w http.ResponseWriter, r *http.Request) {
|
||
id := mux.Vars(r)["id"]
|
||
download.UpdateJobStatus(id, "paused", nil)
|
||
download.Broadcast() // ← on notifie tout de suite les clients SSE
|
||
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
func HandleResumeJob(db *gorm.DB) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
id := mux.Vars(r)["id"]
|
||
|
||
// 1. Chercher en mémoire
|
||
jobsMu.Lock()
|
||
job, exists := jobs[id]
|
||
jobsMu.Unlock()
|
||
|
||
// 2. Si absent, fallback DB
|
||
if !exists {
|
||
var j download.DownloadJob
|
||
if err := db.First(&j, "id = ?", id).Error; err != nil {
|
||
http.Error(w, "Job introuvable", http.StatusNotFound)
|
||
return
|
||
}
|
||
|
||
jobCopy := j
|
||
jobsMu.Lock()
|
||
jobs[id] = &jobCopy
|
||
job = &jobCopy
|
||
jobsMu.Unlock()
|
||
}
|
||
|
||
// 3. Initialiser le client Debrid-Link
|
||
client := debridlink.NewClient(db)
|
||
account := download.GetFirstActiveAccount(client)
|
||
if account == nil {
|
||
http.Error(w, "Aucun compte actif", http.StatusBadRequest)
|
||
return
|
||
}
|
||
client.SetAccount(account)
|
||
|
||
// 4. Redémarrer le téléchargement
|
||
go download.StartDownload(job,job.Link,client,db)
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
}
|
||
|
||
func HandleDeleteJob(db *gorm.DB) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
id := mux.Vars(r)["id"]
|
||
download.DeleteJob(id,db)
|
||
go download.Broadcast() // ← on notifie tout de suite les clients SSE
|
||
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}}
|
||
func HandleDeleteMultipleJobs(db *gorm.DB) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
err := r.ParseForm()
|
||
if err != nil {
|
||
http.Error(w, "Impossible de lire les IDs", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
ids := r.Form["ids[]"]
|
||
if len(ids) == 0 {
|
||
http.Error(w, "Aucun ID reçu", http.StatusBadRequest)
|
||
return
|
||
}
|
||
|
||
for _, id := range ids {
|
||
download.DeleteJob(id, db)
|
||
}
|
||
download.Broadcast() // ← on notifie tout de suite les clients SSE
|
||
|
||
w.WriteHeader(http.StatusNoContent)
|
||
}
|
||
}
|
||
|
||
|
||
func renderPartial(w http.ResponseWriter, templ string, data map[string]interface{}) {
|
||
t, err := template.ParseFiles("./templates/" + templ + ".pages.tmpl")
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
err = t.Execute(w, data)
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
}
|
||
}
|
||
|
||
|
||
|
||
func renderTemplate(w http.ResponseWriter, templ string, data map[string]interface{}) {
|
||
t, err := template.ParseFiles(
|
||
"./templates/head.pages.tmpl", // Template inclus
|
||
"./templates/" + templ + ".pages.tmpl", // Template principal
|
||
)
|
||
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
return
|
||
}
|
||
|
||
// Exécutez explicitement le template principal
|
||
err = t.ExecuteTemplate(w, templ+".pages.tmpl", data)
|
||
|
||
if err != nil {
|
||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||
}
|
||
}
|
||
|
||
|