497 lines
14 KiB
Go
497 lines
14 KiB
Go
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
|
|
}
|