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{"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) } // =========================== 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 }