Rewrote account switching, fix some minor bugs here and there

This commit is contained in:
Mukhtar Akere
2025-09-16 21:15:24 +01:00
parent 76f5b85313
commit 30b2db06e7
34 changed files with 945 additions and 866 deletions

View File

@@ -1,282 +0,0 @@
package types
import (
"slices"
"sync"
"sync/atomic"
"github.com/sirrobot01/decypharr/internal/config"
)
type Accounts struct {
current atomic.Value
accounts sync.Map // map[string]*Account // key is token
}
func NewAccounts(debridConf config.Debrid) *Accounts {
a := &Accounts{
accounts: sync.Map{},
}
var current *Account
for idx, token := range debridConf.DownloadAPIKeys {
if token == "" {
continue
}
account := newAccount(debridConf.Name, token, idx)
a.accounts.Store(token, account)
if current == nil {
current = account
}
}
a.setCurrent(current)
return a
}
type Account struct {
Debrid string // e.g., "realdebrid", "torbox", etc.
Order int
Disabled bool
Token string `json:"token"`
links map[string]*DownloadLink
mu sync.RWMutex
TrafficUsed int64 `json:"traffic_used"` // Traffic used in bytes
Username string `json:"username"` // Username for the account
}
func (a *Accounts) Active() []*Account {
activeAccounts := make([]*Account, 0)
a.accounts.Range(func(key, value interface{}) bool {
acc, ok := value.(*Account)
if ok && !acc.Disabled {
activeAccounts = append(activeAccounts, acc)
}
return true
})
// Sort active accounts by their Order field
slices.SortFunc(activeAccounts, func(i, j *Account) int {
return i.Order - j.Order
})
return activeAccounts
}
func (a *Accounts) All() []*Account {
allAccounts := make([]*Account, 0)
a.accounts.Range(func(key, value interface{}) bool {
acc, ok := value.(*Account)
if ok {
allAccounts = append(allAccounts, acc)
}
return true
})
// Sort all accounts by their Order field
slices.SortFunc(allAccounts, func(i, j *Account) int {
return i.Order - j.Order
})
return allAccounts
}
func (a *Accounts) getCurrent() *Account {
if acc := a.current.Load(); acc != nil {
if current, ok := acc.(*Account); ok {
return current
}
}
return nil
}
func (a *Accounts) Current() *Account {
current := a.getCurrent()
if current != nil && !current.Disabled {
return current
}
activeAccounts := a.Active()
if len(activeAccounts) == 0 {
return current
}
current = activeAccounts[0]
a.setCurrent(current)
return current
}
func (a *Accounts) setCurrent(account *Account) {
if account == nil {
return
}
a.current.Store(account)
}
func (a *Accounts) Disable(account *Account) {
account.Disabled = true
a.accounts.Store(account.Token, account)
current := a.getCurrent()
if current.Equals(account) {
var newCurrent *Account
a.accounts.Range(func(key, value interface{}) bool {
acc, ok := value.(*Account)
if ok && !acc.Disabled {
newCurrent = acc
return false // Break the loop
}
return true // Continue the loop
})
a.setCurrent(newCurrent)
}
}
func (a *Accounts) Reset() {
var current *Account
a.accounts.Range(func(key, value interface{}) bool {
acc, ok := value.(*Account)
if ok {
acc.resetDownloadLinks()
acc.Disabled = false
a.accounts.Store(key, acc)
if current == nil {
current = acc
}
}
return true
})
a.setCurrent(current)
}
func (a *Accounts) GetDownloadLink(fileLink string) (*DownloadLink, *Account, error) {
current := a.Current()
if current == nil {
return nil, nil, NoActiveAccountsError
}
dl, ok := current.getLink(fileLink)
if !ok {
return nil, current, NoDownloadLinkError
}
if err := dl.Valid(); err != nil {
return nil, current, err
}
return dl, current, nil
}
func (a *Accounts) GetAccountFromLink(fileLink string) (*Account, error) {
currentAccount := a.Current()
if currentAccount == nil {
return nil, NoActiveAccountsError
}
dl, ok := currentAccount.getLink(fileLink)
if !ok {
return nil, NoDownloadLinkError
}
if dl.DownloadLink == "" {
return currentAccount, EmptyDownloadLinkError
}
return currentAccount, nil
}
// SetDownloadLink sets the download link for the current account
func (a *Accounts) SetDownloadLink(account *Account, dl *DownloadLink) {
if dl == nil {
return
}
if account == nil {
account = a.getCurrent()
}
account.setLink(dl.Link, dl)
}
func (a *Accounts) DeleteDownloadLink(fileLink string) {
if a.Current() == nil {
return
}
a.Current().deleteLink(fileLink)
}
func (a *Accounts) GetLinksCount() int {
if a.Current() == nil {
return 0
}
return a.Current().LinksCount()
}
func (a *Accounts) SetDownloadLinks(account *Account, links map[string]*DownloadLink) {
if account == nil {
account = a.Current()
}
account.setLinks(links)
a.accounts.Store(account.Token, account)
}
func (a *Accounts) Update(account *Account) {
if account == nil {
return
}
a.accounts.Store(account.Token, account)
}
func newAccount(debridName, token string, index int) *Account {
return &Account{
Debrid: debridName,
Token: token,
Order: index,
links: make(map[string]*DownloadLink),
}
}
func (a *Account) Equals(other *Account) bool {
if other == nil {
return false
}
return a.Token == other.Token && a.Debrid == other.Debrid
}
func (a *Account) getLink(fileLink string) (*DownloadLink, bool) {
a.mu.RLock()
defer a.mu.RUnlock()
dl, ok := a.links[a.sliceFileLink(fileLink)]
return dl, ok
}
func (a *Account) setLink(fileLink string, dl *DownloadLink) {
a.mu.Lock()
defer a.mu.Unlock()
a.links[a.sliceFileLink(fileLink)] = dl
}
func (a *Account) deleteLink(fileLink string) {
a.mu.Lock()
defer a.mu.Unlock()
delete(a.links, a.sliceFileLink(fileLink))
}
func (a *Account) resetDownloadLinks() {
a.mu.Lock()
defer a.mu.Unlock()
a.links = make(map[string]*DownloadLink)
}
func (a *Account) LinksCount() int {
a.mu.RLock()
defer a.mu.RUnlock()
return len(a.links)
}
func (a *Account) setLinks(links map[string]*DownloadLink) {
a.mu.Lock()
defer a.mu.Unlock()
for _, dl := range links {
if err := dl.Valid(); err != nil {
continue
}
a.links[a.sliceFileLink(dl.Link)] = dl
}
}
// slice download link
func (a *Account) sliceFileLink(fileLink string) string {
if a.Debrid != "realdebrid" {
return fileLink
}
if len(fileLink) < 39 {
return fileLink
}
return fileLink[0:39]
}

View File

@@ -1,29 +0,0 @@
package types
import (
"github.com/rs/zerolog"
)
type Client interface {
SubmitMagnet(tr *Torrent) (*Torrent, error)
CheckStatus(tr *Torrent) (*Torrent, error)
GetFileDownloadLinks(tr *Torrent) error
GetDownloadLink(tr *Torrent, file *File) (*DownloadLink, *Account, error)
DeleteTorrent(torrentId string) error
IsAvailable(infohashes []string) map[string]bool
GetDownloadUncached() bool
UpdateTorrent(torrent *Torrent) error
GetTorrent(torrentId string) (*Torrent, error)
GetTorrents() ([]*Torrent, error)
Name() string
Logger() zerolog.Logger
GetDownloadingStatus() []string
RefreshDownloadLinks() error
CheckLink(link string) error
GetMountPath() string
Accounts() *Accounts // Returns the active download account/token
DeleteDownloadLink(linkId string) error
GetProfile() (*Profile, error)
GetAvailableSlots() (int, error)
SyncAccounts() error // Updates each accounts details(like traffic, username, etc.)
}

View File

@@ -14,7 +14,7 @@ var NoActiveAccountsError = &Error{
Code: "no_active_accounts",
}
var NoDownloadLinkError = &Error{
var ErrDownloadLinkNotFound = &Error{
Message: "No download link found",
Code: "no_download_link",
}

View File

@@ -2,6 +2,7 @@ package types
import (
"fmt"
"net/url"
"os"
"path/filepath"
"sync"
@@ -116,18 +117,18 @@ func (t *Torrent) GetFiles() []File {
}
type File struct {
TorrentId string `json:"torrent_id"`
Id string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
IsRar bool `json:"is_rar"`
ByteRange *[2]int64 `json:"byte_range,omitempty"`
Path string `json:"path"`
Link string `json:"link"`
AccountId string `json:"account_id"`
Generated time.Time `json:"generated"`
Deleted bool `json:"deleted"`
DownloadLink *DownloadLink `json:"-"`
TorrentId string `json:"torrent_id"`
Id string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
IsRar bool `json:"is_rar"`
ByteRange *[2]int64 `json:"byte_range,omitempty"`
Path string `json:"path"`
Link string `json:"link"`
AccountId string `json:"account_id"`
Generated time.Time `json:"generated"`
Deleted bool `json:"deleted"`
DownloadLink DownloadLink `json:"-"`
}
func (t *Torrent) Cleanup(remove bool) {
@@ -170,6 +171,8 @@ type Profile struct {
}
type DownloadLink struct {
Debrid string `json:"debrid"`
Token string `json:"token"`
Filename string `json:"filename"`
Link string `json:"link"`
DownloadLink string `json:"download_link"`
@@ -179,16 +182,27 @@ type DownloadLink struct {
ExpiresAt time.Time
}
func isValidURL(str string) bool {
u, err := url.Parse(str)
// A valid URL should parse without error, and have a non-empty scheme and host.
return err == nil && u.Scheme != "" && u.Host != ""
}
func (dl *DownloadLink) Valid() error {
if dl.DownloadLink == "" {
if dl.Empty() {
return EmptyDownloadLinkError
}
if dl.ExpiresAt.IsZero() || dl.ExpiresAt.Before(time.Now()) {
return DownloadLinkExpiredError
// Check if the link is actually a valid URL
if !isValidURL(dl.DownloadLink) {
return ErrDownloadLinkNotFound
}
return nil
}
func (dl *DownloadLink) Empty() bool {
return dl.DownloadLink == ""
}
func (dl *DownloadLink) String() string {
return dl.DownloadLink
}