diff --git a/.env b/.env new file mode 100644 index 0000000..2076b42 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +DEBRIDLINK_CLIENT_ID=bMs32shaby43qVGVKnkRqw +DEBRIDLINK_CLIENT_SECRET=XRMJ8JNJtJZBsRP8gzBnYc6huLK2cqXay3ihKId9mt4 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 56f6855..acc0b87 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -4,3 +4,5 @@ services: context: . dockerfile: Dockerfile volumes: [] # pas de montage source + env_file: + - .env diff --git a/docker-compose.yml b/docker-compose.yml index 8b2ae06..a203711 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,4 +8,6 @@ services: - "4000:4000" volumes: - .:/app - - ./shelfly_db.db:/app/shelfly_db.db \ No newline at end of file + - ./shelfly_db.db:/app/shelfly_db.db + env_file: + - .env \ No newline at end of file diff --git a/go.mod b/go.mod index 32678cd..60c684d 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/golang-jwt/jwt v3.2.2+incompatible github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect + github.com/joho/godotenv v1.5.1 golang.org/x/crypto v0.31.0 golang.org/x/text v0.21.0 // indirect gorm.io/driver/mysql v1.5.7 // indirect diff --git a/go.sum b/go.sum index bfda59f..7232964 100644 --- a/go.sum +++ b/go.sum @@ -29,6 +29,8 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..ee4cad3 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,8 @@ +package config + +import "os" + +var ( + DebridClientID = os.Getenv("DEBRIDLINK_CLIENT_ID") + DebridClientSecret = os.Getenv("DEBRIDLINK_CLIENT_SECRET") +) diff --git a/internal/db/db.go b/internal/db/db.go index 6964bb7..01cfb4a 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -1,8 +1,10 @@ package db import ( - "fmt" + "app/shelfly/internal/debridlink" "app/shelfly/internal/models" + "fmt" + "gorm.io/driver/sqlite" "gorm.io/gorm" ) @@ -17,7 +19,26 @@ func InitDB()*gorm.DB { } // Migrate the schema - db.AutoMigrate(&models.User{},&models.Files{},&models.LibrarySection{},&models.MediaItem{},&models.MediaPart{},&models.MetadataItem{},&models.SectionLocation{},&models.Tag{},&models.Tagging{},&models.PathDownload{}) + db.AutoMigrate( + &models.User{}, + &models.Files{}, + &models.LibrarySection{}, + &models.MediaItem{}, + &models.MediaPart{}, + &models.MetadataItem{}, + &models.SectionLocation{}, + &models.Tag{}, + &models.Tagging{}, + &models.PathDownload{}, + &debridlink.File{}, + &debridlink.Link{}, + &debridlink.RSSFeed{}, + &debridlink.RSSItem{}, + &debridlink.Torrent{}, + &debridlink.DebridAccount{}, + + + ) fmt.Println("Connexion réussie à MySQL !") fmt.Println("Auto migration completed") diff --git a/internal/debridlink/client.go b/internal/debridlink/client.go new file mode 100644 index 0000000..93735ce --- /dev/null +++ b/internal/debridlink/client.go @@ -0,0 +1,495 @@ +package debridlink + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log" + "net/http" + "net/url" + "strings" + "time" + + "gorm.io/gorm" +) + +const baseURL = "https://api.debrid-link.com/" + +type Client struct { + http *http.Client + db *gorm.DB + clientID string + clientSecret string + account *DebridAccount +} + +type DebridAccount struct { + ID uint `gorm:"primaryKey"` + Host string `gorm:"column:host" json:"host"` + Username string `gorm:"column:username" json:"username"` + Password string `gorm:"column:password" json:"password"` + IsActive bool `gorm:"column:is_active" json:"is_active"` + AccessToken string `gorm:"column:access_token"` + RefreshToken string `gorm:"column:refresh_token"` + ExpiresAt time.Time `gorm:"column:expires_at"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` +} + +type TokenResponse struct { + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + ExpiresIn int64 `json:"expires_in"` +} + +func NewClient(db *gorm.DB) *Client { + return &Client{ + http: &http.Client{Timeout: 15 * time.Second}, + db: db, + clientID: "bMs32shaby43qVGVKnkRqw", + clientSecret: "XRMJ8JNJtJZBsRP8gzBnYc6huLK2cqXay3ihKId9mt4", + } +} + +func (c *Client) SetAccount(account *DebridAccount) { + c.account = account +} + +func (c *Client) refreshAccessToken(ctx context.Context) error { + form := url.Values{} + form.Set("client_id", c.clientID) + form.Set("client_secret", c.clientSecret) + form.Set("grant_type", "refresh_token") + form.Set("refresh_token", c.account.RefreshToken) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"oauth/token", strings.NewReader(form.Encode())) + if err != nil { + return err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := c.http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + b, _ := io.ReadAll(resp.Body) + return fmt.Errorf("refresh failed: %s", string(b)) + } + + var tokenResp TokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + return err + } + + c.account.AccessToken = tokenResp.AccessToken + c.account.RefreshToken = tokenResp.RefreshToken + c.account.ExpiresAt = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second) + + return c.db.Save(c.account).Error +} +func (c *Client) ToggleActiveStatus(ctx context.Context, id uint) error { + if c.db == nil { + return errors.New("la connexion à la base de données est manquante") + } + + var account DebridAccount + if err := c.db.First(&account, id).Error; err != nil { + return fmt.Errorf("compte introuvable : %w", err) + } + + account.IsActive = !account.IsActive + account.UpdatedAt = time.Now() + + if err := c.db.Save(&account).Error; err != nil { + return fmt.Errorf("échec de la mise à jour : %w", err) + } + + return nil +} +// =========================== +// CRUD pour DebridAccount +// =========================== + +func (c *Client) CreateDebridAccount(ctx context.Context, acc *DebridAccount) error { + return c.db.Create(acc).Error +} + +func (c *Client) GetDebridAccount(ctx context.Context, id uint) (*DebridAccount, error) { + var acc DebridAccount + if err := c.db.First(&acc, id).Error; err != nil { + return nil, err + } + return &acc, nil +} + +func (c *Client) ListDebridAccounts(ctx context.Context) ([]DebridAccount, error) { + var accounts []DebridAccount + if err := c.db.Order("id desc").Find(&accounts).Error; err != nil { + return nil, err + } + return accounts, nil +} + +func (c *Client) UpdateDebridAccount(ctx context.Context, acc *DebridAccount) error { + return c.db.Save(acc).Error +} + +func (c *Client) DeleteDebridAccount(ctx context.Context, id uint) error { + return c.db.Delete(&DebridAccount{}, id).Error +} +func (c *Client) RequestDeviceCodeWithCredentials(ctx context.Context, username, password string) (*DeviceCodeResponse, error) { + + form := url.Values{} + log.Println("[DEBUG] Envoi device_code avec :") + log.Println("client_id:", c.clientID) + log.Println("username:", username) + log.Println("password (len):", len(password)) + log.Println("scope:", form.Get("scope")) + log.Println("URL:", "https://debrid-link.com/api/oauth/device/code") + form.Set("client_id", c.clientID) + form.Set("grant_type", "password") // possible variation + form.Set("username", username) + form.Set("password", password) + form.Set("scope", "get.post.downloader get.post.seedbox get.account") + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://debrid-link.com/api/oauth/device/code", strings.NewReader(form.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := c.http.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("device code request failed: %s", string(body)) + } + + var result DeviceCodeResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return &result, nil +} + + + +func (c *Client) RequestDeviceCode(ctx context.Context) (*DeviceCodeResponse, error) { + form := url.Values{} + form.Set("client_id", c.clientID) + form.Set("scope", "get.post.downloader get.post.seedbox get.account") + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://debrid-link.com/api/oauth/device/code", strings.NewReader(form.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := c.http.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("device code request failed: %s", string(body)) + } + + var result DeviceCodeResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return &result, nil +} + + +type DeviceCodeResponse struct { + DeviceCode string `json:"device_code"` + UserCode string `json:"user_code"` + VerificationURL string `json:"verification_url"` + ExpiresIn int `json:"expires_in"` + Interval int `json:"interval"` +} +func (c *Client) PasswordGrant(ctx context.Context, username, password string) (*TokenResponse, error) { + form := url.Values{} + form.Set("client_id", c.clientID) + form.Set("client_secret", c.clientSecret) + form.Set("grant_type", "password") + form.Set("username", username) + form.Set("password", password) + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://debrid-link.com/api/oauth/token", strings.NewReader(form.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := c.http.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("auth error: %s", string(body)) + } + + var tokens TokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tokens); err != nil { + return nil, err + } + + return &tokens, nil +} + +func (c *Client) PollDeviceToken(ctx context.Context, deviceCode string, interval int) (*TokenResponse, error) { + form := url.Values{} + form.Set("client_id", c.clientID) + form.Set("grant_type", "urn:ietf:params:oauth:grant-type:device_code") + form.Set("code", deviceCode) + + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + case <-time.After(time.Duration(interval) * time.Second): + req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://debrid-link.com/api/oauth/token", strings.NewReader(form.Encode())) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := c.http.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode == 200 { + var tokens TokenResponse + if err := json.NewDecoder(resp.Body).Decode(&tokens); err != nil { + return nil, err + } + return &tokens, nil + } + + body, _ := io.ReadAll(resp.Body) + if strings.Contains(string(body), "authorization_pending") { + continue // Attente de validation utilisateur + } + return nil, fmt.Errorf("device auth failed: %s", string(body)) + } + } +} + + + +func (c *Client) doJSON(ctx context.Context, method, path string, params url.Values, body, out interface{}) error { + if c.account == nil { + return errors.New("no active Debrid account") + } + if time.Now().After(c.account.ExpiresAt) { + if err := c.refreshAccessToken(ctx); err != nil { + return err + } + } + + urlStr := baseURL + path + if params != nil { + urlStr += "?" + params.Encode() + } + + var reqBody io.Reader + if body != nil { + b, err := json.Marshal(body) + if err != nil { + return err + } + reqBody = bytes.NewReader(b) + } + + req, err := http.NewRequestWithContext(ctx, method, urlStr, reqBody) + if err != nil { + return err + } + + req.Header.Set("Authorization", "Bearer "+c.account.AccessToken) + req.Header.Set("Content-Type", "application/json") + + resp, err := c.http.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + data, _ := io.ReadAll(resp.Body) + return fmt.Errorf("HTTP error %d: %s", resp.StatusCode, string(data)) + } + + if out != nil { + return json.NewDecoder(resp.Body).Decode(out) + } + return nil +} + +// =========================== RSS =========================== +type RSSFeed struct { + ID string `json:"id" gorm:"column:id;primaryKey"` + URL string `json:"url" gorm:"column:url"` + Enabled bool `json:"enabled" gorm:"column:enabled"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` +} + +type RSSItem struct { + Title string `json:"title" gorm:"column:title"` + Link string `json:"link" gorm:"column:link"` + PubDate time.Time `json:"pubDate" gorm:"column:pub_date"` +} +func (c *Client) ListRSS(ctx context.Context) ([]RSSFeed, error) { + var feeds []RSSFeed + if err := c.doJSON(ctx, "GET", "rss", nil, nil, &feeds); err != nil { + return nil, err + } + return feeds, nil +} + +func (c *Client) AddRSS(ctx context.Context, url string) (*RSSFeed, error) { + var feed RSSFeed + body := map[string]string{"url": url} + if err := c.doJSON(ctx, "POST", "rss", nil, body, &feed); err != nil { + return nil, err + } + return &feed, nil +} + +func (c *Client) TestRSS(ctx context.Context, url string) ([]RSSItem, error) { + var items []RSSItem + body := map[string]string{"url": url} + if err := c.doJSON(ctx, "POST", "rss/test", nil, body, &items); err != nil { + return nil, err + } + return items, nil +} + +func (c *Client) DeleteRSS(ctx context.Context, id string) error { + return c.doJSON(ctx, "DELETE", fmt.Sprintf("rss/%s", id), nil, nil, nil) +} + +// =========================== Seedbox =========================== +type Torrent struct { + ID string `json:"id" gorm:"column:id;primaryKey"` + Name string `json:"name" gorm:"column:name"` + Status string `json:"status" gorm:"column:status"` + Progress int `json:"progress" gorm:"column:progress"` + Added time.Time `json:"added" gorm:"column:added"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` +} + +func (c *Client) ListTorrents(ctx context.Context) ([]Torrent, error) { + var torrents []Torrent + if err := c.doJSON(ctx, "GET", "seedbox/torrents", nil, nil, &torrents); err != nil { + return nil, err + } + return torrents, nil +} + +func (c *Client) AddTorrent(ctx context.Context, link string) (*Torrent, error) { + var t Torrent + body := map[string]string{"link": link} + if err := c.doJSON(ctx, "POST", "seedbox/torrents", nil, body, &t); err != nil { + return nil, err + } + return &t, nil +} + +func (c *Client) RemoveTorrents(ctx context.Context, ids []string) error { + body := map[string][]string{"ids": ids} + return c.doJSON(ctx, "DELETE", "seedbox/torrents", nil, body, nil) +} + +// =========================== 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"` +} + +func (c *Client) ListLinks(ctx context.Context) ([]Link, error) { + var links []Link + if err := c.doJSON(ctx, "GET", "downloader/links", nil, nil, &links); err != nil { + return nil, err + } + 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 { + 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) +} + +// =========================== Files =========================== +type File struct { + ID string `json:"id" gorm:"column:id;primaryKey"` + Name string `json:"name" gorm:"column:name"` + Size int64 `json:"size" gorm:"column:size"` + Link string `json:"link" gorm:"column:link"` + ParentID string `json:"parent_id" gorm:"column:parent_id"` + CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"` + 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) + if err := c.doJSON(ctx, "GET", path, nil, nil, &files); err != nil { + return nil, err + } + return files, nil +} + +// =========================== Stream =========================== +func (c *Client) CreateTranscode(ctx context.Context, fileID, preset string) (string, error) { + var resp struct{ TranscodeID string `json:"transcodeId"` } + body := map[string]string{"fileId": fileID, "preset": preset} + if err := c.doJSON(ctx, "POST", "stream/transcode", nil, body, &resp); err != nil { + return "", err + } + return resp.TranscodeID, nil +} + +func (c *Client) GetTranscode(ctx context.Context, transcodeID string) (map[string]interface{}, error) { + var result map[string]interface{} + path := fmt.Sprintf("stream/transcode/%s", transcodeID) + if err := c.doJSON(ctx, "GET", path, nil, nil, &result); err != nil { + return nil, err + } + return result, nil +} diff --git a/internal/route/main.go b/internal/route/main.go index c87f8ae..fa33fbd 100644 --- a/internal/route/main.go +++ b/internal/route/main.go @@ -50,7 +50,12 @@ func RoutesProtected(r *mux.Router, bd *gorm.DB) { r.HandleFunc("/menuLibary", renders.Library) r.HandleFunc("/godownloader/downloads", renders.GoDownload) r.HandleFunc("/godownloader/linkcollectors", renders.GoDownloadLinkCollectors) - r.HandleFunc("/godownloader/settings", renders.GoDownloadSetting) + r.HandleFunc("/godownloader/settings", renders.GoDownloadSetting(bd)) + r.HandleFunc("/godownloader/poll-status", renders.PollStatusHandler(bd)) + r.HandleFunc("/godownloader/table-refresh", renders.GoDownloadPartialTable(bd)) + r.HandleFunc("/godownloader/settings/delete", renders.GoDownloadSettingDelete(bd)) + + // API user r.HandleFunc("/api/user/create", users.CreateUser(bd)).Methods("POST") r.HandleFunc("/api/user/update/{id}", users.UpdateUser(bd)).Methods("PUT") diff --git a/main.go b/main.go index 961d5b2..24aff3f 100644 --- a/main.go +++ b/main.go @@ -9,9 +9,14 @@ 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 e39fe44..44d27ea 100644 --- a/renders/renders.go +++ b/renders/renders.go @@ -1,10 +1,15 @@ package renders import ( + "app/shelfly/internal/debridlink" "app/shelfly/internal/models" + "context" "encoding/json" + "log" "net/http" + "strconv" "text/template" + "time" "gorm.io/gorm" ) @@ -85,10 +90,244 @@ func GoDownload(w http.ResponseWriter, r *http.Request) { func GoDownloadLinkCollectors(w http.ResponseWriter, r *http.Request) { renderPartial(w, "godownloader_linkcollectors",nil) } -func GoDownloadSetting(w http.ResponseWriter, r *http.Request) { - renderPartial(w, "godownloader_setting",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 GoDownloadSetting(db *gorm.DB) http.HandlerFunc { +// return func(w http.ResponseWriter, r *http.Request) { +// ctx := r.Context() + +// // Initialise le client avec .env (client_id, secret) +// DebridClient := debridlink.NewClient(db) + +// switch r.Method { +// case http.MethodPost: +// if err := r.ParseForm(); err != nil { +// http.Error(w, "Formulaire invalide", http.StatusBadRequest) +// return +// } + +// host := r.FormValue("host") +// username := r.FormValue("username") +// password := r.FormValue("password") +// isActive := r.FormValue("is_active") == "on" + +// // Authentification via Password Grant +// tokens, err := DebridClient.PasswordGrant(ctx, username, password) +// if err != nil { +// log.Println("[OAuth2] Erreur:", err) +// http.Error(w, "Authentification échouée", http.StatusUnauthorized) +// return +// } + +// // Création du compte à enregistrer +// account := &debridlink.DebridAccount{ +// Host: host, +// Username: username, +// Password: password, +// IsActive: isActive, +// AccessToken: tokens.AccessToken, +// RefreshToken: tokens.RefreshToken, +// ExpiresAt: time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second), +// } + +// if err := db.Save(account).Error; err != nil { +// log.Println("[DB] Sauvegarde échouée:", err) +// http.Error(w, "Erreur DB", http.StatusInternalServerError) +// return +// } + +// var accounts []debridlink.DebridAccount +// db.Order("id desc").Find(&accounts) + +// if r.Header.Get("HX-Request") == "true" { +// renderPartial(w, "partials/accounts_table", map[string]interface{}{ +// "accounts": accounts, +// }) +// return +// } + +// renderPartial(w, "godownloader_setting", map[string]interface{}{ +// "accounts": accounts, +// }) + +// case http.MethodGet: +// var accounts []debridlink.DebridAccount +// db.Order("id desc").Find(&accounts) + +// renderPartial(w, "godownloader_setting", map[string]interface{}{ +// "accounts": accounts, +// }) +// } +// } +// } + + + + + +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( @@ -109,16 +348,4 @@ func renderTemplate(w http.ResponseWriter, templ string, data map[string]interfa } } -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) - } -} diff --git a/shelfly_db.db b/shelfly_db.db index 7d27d0e..f1024b6 100644 Binary files a/shelfly_db.db and b/shelfly_db.db differ diff --git a/templates/accounts_table.pages.tmpl b/templates/accounts_table.pages.tmpl new file mode 100644 index 0000000..8f9aed6 --- /dev/null +++ b/templates/accounts_table.pages.tmpl @@ -0,0 +1,42 @@ +
| Host | +Utilisateur | +Actif | +Token | +Expire | +Actions | +
|---|---|---|---|---|---|
| {{ .Host }} | +{{ .Username }} | +{{ if .IsActive }}✅{{ else }}❌{{ end }} | +{{ .AccessToken | printf "%.12s..." }} |
+ {{ .ExpiresAt.Format "02/01/2006 15:04" }} | ++ + | +
Aucun compte enregistré.
+ {{ end }} +