- Add support for virtual folders
- Fix minor bug fixes
This commit is contained in:
@@ -61,6 +61,6 @@ EXPOSE 8282
|
|||||||
VOLUME ["/app"]
|
VOLUME ["/app"]
|
||||||
USER nonroot:nonroot
|
USER nonroot:nonroot
|
||||||
|
|
||||||
HEALTHCHECK --interval=5s --timeout=3s --retries=10 CMD ["/usr/bin/healthcheck", "--config", "/app"]
|
HEALTHCHECK --interval=3s --retries=10 CMD ["/usr/bin/healthcheck", "--config", "/app"]
|
||||||
|
|
||||||
CMD ["/usr/bin/decypharr", "--config", "/app"]
|
CMD ["/usr/bin/decypharr", "--config", "/app"]
|
||||||
1
go.mod
1
go.mod
@@ -6,6 +6,7 @@ toolchain go1.24.3
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/torrent v1.55.0
|
github.com/anacrolix/torrent v1.55.0
|
||||||
|
github.com/beevik/etree v1.5.1
|
||||||
github.com/cavaliergopher/grab/v3 v3.0.1
|
github.com/cavaliergopher/grab/v3 v3.0.1
|
||||||
github.com/go-chi/chi/v5 v5.1.0
|
github.com/go-chi/chi/v5 v5.1.0
|
||||||
github.com/go-co-op/gocron/v2 v2.16.1
|
github.com/go-co-op/gocron/v2 v2.16.1
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -36,6 +36,8 @@ github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CM
|
|||||||
github.com/anacrolix/torrent v1.55.0 h1:s9yh/YGdPmbN9dTa+0Inh2dLdrLQRvEAj1jdFW/Hdd8=
|
github.com/anacrolix/torrent v1.55.0 h1:s9yh/YGdPmbN9dTa+0Inh2dLdrLQRvEAj1jdFW/Hdd8=
|
||||||
github.com/anacrolix/torrent v1.55.0/go.mod h1:sBdZHBSZNj4de0m+EbYg7vvs/G/STubxu/GzzNbojsE=
|
github.com/anacrolix/torrent v1.55.0/go.mod h1:sBdZHBSZNj4de0m+EbYg7vvs/G/STubxu/GzzNbojsE=
|
||||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
|
github.com/beevik/etree v1.5.1 h1:TC3zyxYp+81wAmbsi8SWUpZCurbxa6S8RITYRSkNRwo=
|
||||||
|
github.com/beevik/etree v1.5.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
|
||||||
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
|
|||||||
@@ -67,21 +67,6 @@ type Auth struct {
|
|||||||
Password string `json:"password,omitempty"`
|
Password string `json:"password,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebDav struct {
|
|
||||||
TorrentsRefreshInterval string `json:"torrents_refresh_interval,omitempty"`
|
|
||||||
DownloadLinksRefreshInterval string `json:"download_links_refresh_interval,omitempty"`
|
|
||||||
Workers int `json:"workers,omitempty"`
|
|
||||||
AutoExpireLinksAfter string `json:"auto_expire_links_after,omitempty"`
|
|
||||||
|
|
||||||
// Folder
|
|
||||||
FolderNaming string `json:"folder_naming,omitempty"`
|
|
||||||
|
|
||||||
// Rclone
|
|
||||||
RcUrl string `json:"rc_url,omitempty"`
|
|
||||||
RcUser string `json:"rc_user,omitempty"`
|
|
||||||
RcPass string `json:"rc_pass,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
// server
|
// server
|
||||||
BindAddress string `json:"bind_address,omitempty"`
|
BindAddress string `json:"bind_address,omitempty"`
|
||||||
@@ -212,7 +197,7 @@ func (c *Config) GetMinFileSize() int64 {
|
|||||||
if c.MinFileSize == "" {
|
if c.MinFileSize == "" {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
s, err := parseSize(c.MinFileSize)
|
s, err := ParseSize(c.MinFileSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -224,7 +209,7 @@ func (c *Config) GetMaxFileSize() int64 {
|
|||||||
if c.MaxFileSize == "" {
|
if c.MaxFileSize == "" {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
s, err := parseSize(c.MaxFileSize)
|
s, err := ParseSize(c.MaxFileSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -307,6 +292,19 @@ func (c *Config) updateDebrid(d Debrid) Debrid {
|
|||||||
if d.AutoExpireLinksAfter == "" {
|
if d.AutoExpireLinksAfter == "" {
|
||||||
d.AutoExpireLinksAfter = cmp.Or(c.WebDav.AutoExpireLinksAfter, "3d") // 2 days
|
d.AutoExpireLinksAfter = cmp.Or(c.WebDav.AutoExpireLinksAfter, "3d") // 2 days
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge debrid specified directories with global directories
|
||||||
|
|
||||||
|
directories := c.WebDav.Directories
|
||||||
|
if directories == nil {
|
||||||
|
directories = make(map[string]WebdavDirectories)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, dir := range d.Directories {
|
||||||
|
directories[name] = dir
|
||||||
|
}
|
||||||
|
d.Directories = directories
|
||||||
|
|
||||||
d.RcUrl = cmp.Or(d.RcUrl, c.WebDav.RcUrl)
|
d.RcUrl = cmp.Or(d.RcUrl, c.WebDav.RcUrl)
|
||||||
d.RcUser = cmp.Or(d.RcUser, c.WebDav.RcUser)
|
d.RcUser = cmp.Or(d.RcUser, c.WebDav.RcUser)
|
||||||
d.RcPass = cmp.Or(d.RcPass, c.WebDav.RcPass)
|
d.RcPass = cmp.Or(d.RcPass, c.WebDav.RcPass)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ func getDefaultExtensions() []string {
|
|||||||
return unique
|
return unique
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseSize(sizeStr string) (int64, error) {
|
func ParseSize(sizeStr string) (int64, error) {
|
||||||
sizeStr = strings.ToUpper(strings.TrimSpace(sizeStr))
|
sizeStr = strings.ToUpper(strings.TrimSpace(sizeStr))
|
||||||
|
|
||||||
// Absolute size-based cache
|
// Absolute size-based cache
|
||||||
|
|||||||
24
internal/config/webdav.go
Normal file
24
internal/config/webdav.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type WebdavDirectories struct {
|
||||||
|
Filters map[string]string `json:"filters,omitempty"`
|
||||||
|
//SaveStrms bool `json:"save_streams,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebDav struct {
|
||||||
|
TorrentsRefreshInterval string `json:"torrents_refresh_interval,omitempty"`
|
||||||
|
DownloadLinksRefreshInterval string `json:"download_links_refresh_interval,omitempty"`
|
||||||
|
Workers int `json:"workers,omitempty"`
|
||||||
|
AutoExpireLinksAfter string `json:"auto_expire_links_after,omitempty"`
|
||||||
|
|
||||||
|
// Folder
|
||||||
|
FolderNaming string `json:"folder_naming,omitempty"`
|
||||||
|
|
||||||
|
// Rclone
|
||||||
|
RcUrl string `json:"rc_url,omitempty"`
|
||||||
|
RcUser string `json:"rc_user,omitempty"`
|
||||||
|
RcPass string `json:"rc_pass,omitempty"`
|
||||||
|
|
||||||
|
// Directories
|
||||||
|
Directories map[string]WebdavDirectories `json:"directories,omitempty"`
|
||||||
|
}
|
||||||
@@ -382,29 +382,13 @@ func JSONResponse(w http.ResponseWriter, data interface{}, code int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Gzip(body []byte, pool *sync.Pool) []byte {
|
func Gzip(body []byte) []byte {
|
||||||
if len(body) == 0 {
|
if len(body) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
buf *bytes.Buffer
|
|
||||||
ok bool
|
|
||||||
)
|
|
||||||
|
|
||||||
// Check if the pool is nil
|
// Check if the pool is nil
|
||||||
if pool == nil {
|
buf := bytes.NewBuffer(make([]byte, 0, len(body)))
|
||||||
buf = bytes.NewBuffer(make([]byte, 0, len(body)))
|
|
||||||
} else {
|
|
||||||
buf, ok = pool.Get().(*bytes.Buffer)
|
|
||||||
|
|
||||||
if !ok || buf == nil {
|
|
||||||
buf = bytes.NewBuffer(make([]byte, 0, len(body)))
|
|
||||||
} else {
|
|
||||||
buf.Reset()
|
|
||||||
}
|
|
||||||
defer pool.Put(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
gz, err := gzip.NewWriterLevel(buf, gzip.BestSpeed)
|
gz, err := gzip.NewWriterLevel(buf, gzip.BestSpeed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -20,6 +20,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
hexRegex = regexp.MustCompile("^[0-9a-fA-F]{40}$")
|
||||||
|
)
|
||||||
|
|
||||||
type Magnet struct {
|
type Magnet struct {
|
||||||
Name string
|
Name string
|
||||||
InfoHash string
|
InfoHash string
|
||||||
@@ -188,7 +192,6 @@ func ExtractInfoHash(magnetDesc string) string {
|
|||||||
|
|
||||||
func processInfoHash(input string) (string, error) {
|
func processInfoHash(input string) (string, error) {
|
||||||
// Regular expression for a valid 40-character hex infohash
|
// Regular expression for a valid 40-character hex infohash
|
||||||
hexRegex := regexp.MustCompile("^[0-9a-fA-F]{40}$")
|
|
||||||
|
|
||||||
// If it's already a valid hex infohash, return it as is
|
// If it's already a valid hex infohash, return it as is
|
||||||
if hexRegex.MatchString(input) {
|
if hexRegex.MatchString(input) {
|
||||||
|
|||||||
@@ -7,14 +7,19 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
VIDEOMATCH = "(?i)(\\.)(webm|m4v|3gp|nsv|ty|strm|rm|rmvb|m3u|ifo|mov|qt|divx|xvid|bivx|nrg|pva|wmv|asf|asx|ogm|ogv|m2v|avi|bin|dat|dvr-ms|mpg|mpeg|mp4|avc|vp3|svq3|nuv|viv|dv|fli|flv|wpl|img|iso|vob|mkv|mk3d|ts|wtv|m2ts)$"
|
videoMatch = "(?i)(\\.)(webm|m4v|3gp|nsv|ty|strm|rm|rmvb|m3u|ifo|mov|qt|divx|xvid|bivx|nrg|pva|wmv|asf|asx|ogm|ogv|m2v|avi|bin|dat|dvr-ms|mpg|mpeg|mp4|avc|vp3|svq3|nuv|viv|dv|fli|flv|wpl|img|iso|vob|mkv|mk3d|ts|wtv|m2ts)$"
|
||||||
MUSICMATCH = "(?i)(\\.)(mp2|mp3|m4a|m4b|m4p|ogg|oga|opus|wma|wav|wv|flac|ape|aif|aiff|aifc)$"
|
musicMatch = "(?i)(\\.)(mp2|mp3|m4a|m4b|m4p|ogg|oga|opus|wma|wav|wv|flac|ape|aif|aiff|aifc)$"
|
||||||
|
sampleMatch = `(?i)(^|[\s/\\])(sample|trailer|thumb|special|extras?)s?[-/]|(\((sample|trailer|thumb|special|extras?)s?\))|(-\s*(sample|trailer|thumb|special|extras?)s?)`
|
||||||
)
|
)
|
||||||
|
|
||||||
var SAMPLEMATCH = `(?i)(^|[\s/\\])(sample|trailer|thumb|special|extras?)s?[-/]|(\((sample|trailer|thumb|special|extras?)s?\))|(-\s*(sample|trailer|thumb|special|extras?)s?)`
|
var (
|
||||||
|
videoRegex = regexp.MustCompile(videoMatch)
|
||||||
|
musicRegex = regexp.MustCompile(musicMatch)
|
||||||
|
mediaRegex = regexp.MustCompile(videoMatch + "|" + musicMatch)
|
||||||
|
sampleRegex = regexp.MustCompile(sampleMatch)
|
||||||
|
)
|
||||||
|
|
||||||
func RegexMatch(regex string, value string) bool {
|
func RegexMatch(re *regexp.Regexp, value string) bool {
|
||||||
re := regexp.MustCompile(regex)
|
|
||||||
return re.MatchString(value)
|
return re.MatchString(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +42,7 @@ func RemoveInvalidChars(value string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func RemoveExtension(value string) string {
|
func RemoveExtension(value string) string {
|
||||||
re := regexp.MustCompile(VIDEOMATCH + "|" + MUSICMATCH)
|
loc := mediaRegex.FindStringIndex(value)
|
||||||
|
|
||||||
// Find the last index of the matched extension
|
|
||||||
loc := re.FindStringIndex(value)
|
|
||||||
if loc != nil {
|
if loc != nil {
|
||||||
return value[:loc[0]]
|
return value[:loc[0]]
|
||||||
} else {
|
} else {
|
||||||
@@ -49,13 +51,12 @@ func RemoveExtension(value string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func IsMediaFile(path string) bool {
|
func IsMediaFile(path string) bool {
|
||||||
mediaPattern := VIDEOMATCH + "|" + MUSICMATCH
|
return RegexMatch(mediaRegex, path)
|
||||||
return RegexMatch(mediaPattern, path)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsSampleFile(path string) bool {
|
func IsSampleFile(path string) bool {
|
||||||
if strings.HasSuffix(strings.ToLower(path), "sample.mkv") {
|
if strings.HasSuffix(strings.ToLower(path), "sample.mkv") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return RegexMatch(SAMPLEMATCH, path)
|
return RegexMatch(sampleRegex, path)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package debrid
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"bytes"
|
|
||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -11,6 +10,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -75,13 +75,11 @@ type Cache struct {
|
|||||||
PropfindResp *xsync.Map[string, PropfindResponse]
|
PropfindResp *xsync.Map[string, PropfindResponse]
|
||||||
folderNaming WebDavFolderNaming
|
folderNaming WebDavFolderNaming
|
||||||
|
|
||||||
// optimizers
|
|
||||||
xmlPool sync.Pool
|
|
||||||
gzipPool sync.Pool
|
|
||||||
listingDebouncer *utils.Debouncer[bool]
|
listingDebouncer *utils.Debouncer[bool]
|
||||||
// monitors
|
// monitors
|
||||||
repairRequest sync.Map
|
repairRequest sync.Map
|
||||||
failedToReinsert sync.Map
|
failedToReinsert sync.Map
|
||||||
|
downloadLinkRequests sync.Map
|
||||||
|
|
||||||
// repair
|
// repair
|
||||||
repairChan chan RepairRequest
|
repairChan chan RepairRequest
|
||||||
@@ -101,7 +99,8 @@ type Cache struct {
|
|||||||
saveSemaphore chan struct{}
|
saveSemaphore chan struct{}
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
||||||
config config.Debrid
|
config config.Debrid
|
||||||
|
customFolders []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(dc config.Debrid, client types.Client) *Cache {
|
func New(dc config.Debrid, client types.Client) *Cache {
|
||||||
@@ -113,10 +112,28 @@ func New(dc config.Debrid, client types.Client) *Cache {
|
|||||||
if autoExpiresLinksAfter == 0 || err != nil {
|
if autoExpiresLinksAfter == 0 || err != nil {
|
||||||
autoExpiresLinksAfter = 48 * time.Hour
|
autoExpiresLinksAfter = 48 * time.Hour
|
||||||
}
|
}
|
||||||
|
var customFolders []string
|
||||||
|
dirFilters := map[string][]directoryFilter{}
|
||||||
|
for name, value := range dc.Directories {
|
||||||
|
for filterType, v := range value.Filters {
|
||||||
|
df := directoryFilter{filterType: filterType, value: v}
|
||||||
|
switch filterType {
|
||||||
|
case filterByRegex, filterByNotRegex:
|
||||||
|
df.regex = regexp.MustCompile(v)
|
||||||
|
case filterBySizeGT, filterBySizeLT:
|
||||||
|
df.sizeThreshold, _ = config.ParseSize(v)
|
||||||
|
case filterBLastAdded:
|
||||||
|
df.ageThreshold, _ = time.ParseDuration(v)
|
||||||
|
}
|
||||||
|
dirFilters[name] = append(dirFilters[name], df)
|
||||||
|
}
|
||||||
|
customFolders = append(customFolders, name)
|
||||||
|
|
||||||
|
}
|
||||||
c := &Cache{
|
c := &Cache{
|
||||||
dir: filepath.Join(cfg.Path, "cache", dc.Name), // path to save cache files
|
dir: filepath.Join(cfg.Path, "cache", dc.Name), // path to save cache files
|
||||||
|
|
||||||
torrents: newTorrentCache(),
|
torrents: newTorrentCache(dirFilters),
|
||||||
PropfindResp: xsync.NewMap[string, PropfindResponse](),
|
PropfindResp: xsync.NewMap[string, PropfindResponse](),
|
||||||
client: client,
|
client: client,
|
||||||
logger: logger.New(fmt.Sprintf("%s-webdav", client.GetName())),
|
logger: logger.New(fmt.Sprintf("%s-webdav", client.GetName())),
|
||||||
@@ -130,12 +147,8 @@ func New(dc config.Debrid, client types.Client) *Cache {
|
|||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
scheduler: s,
|
scheduler: s,
|
||||||
|
|
||||||
config: dc,
|
config: dc,
|
||||||
xmlPool: sync.Pool{
|
customFolders: customFolders,
|
||||||
New: func() interface{} {
|
|
||||||
return new(bytes.Buffer)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
c.listingDebouncer = utils.NewDebouncer[bool](250*time.Millisecond, func(refreshRclone bool) {
|
c.listingDebouncer = utils.NewDebouncer[bool](250*time.Millisecond, func(refreshRclone bool) {
|
||||||
c.RefreshListings(refreshRclone)
|
c.RefreshListings(refreshRclone)
|
||||||
@@ -464,8 +477,23 @@ func (c *Cache) setTorrents(torrents map[string]*CachedTorrent, callback func())
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetListing returns a sorted list of torrents(READ-ONLY)
|
// GetListing returns a sorted list of torrents(READ-ONLY)
|
||||||
func (c *Cache) GetListing() []os.FileInfo {
|
func (c *Cache) GetListing(folder string) []os.FileInfo {
|
||||||
return c.torrents.getListing()
|
switch folder {
|
||||||
|
case "__all__", "torrents":
|
||||||
|
return c.torrents.getListing()
|
||||||
|
default:
|
||||||
|
return c.torrents.getFolderListing(folder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) GetCustomFolders() []string {
|
||||||
|
return c.customFolders
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cache) GetDirectories() []string {
|
||||||
|
dirs := []string{"__all__", "torrents"}
|
||||||
|
dirs = append(dirs, c.customFolders...)
|
||||||
|
return dirs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) Close() error {
|
func (c *Cache) Close() error {
|
||||||
|
|||||||
@@ -15,14 +15,52 @@ type linkCache struct {
|
|||||||
expiresAt time.Time
|
expiresAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type downloadLinkRequest struct {
|
||||||
|
result string
|
||||||
|
err error
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDownloadLinkRequest() *downloadLinkRequest {
|
||||||
|
return &downloadLinkRequest{
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *downloadLinkRequest) Complete(result string, err error) {
|
||||||
|
r.result = result
|
||||||
|
r.err = err
|
||||||
|
close(r.done)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *downloadLinkRequest) Wait() (string, error) {
|
||||||
|
<-r.done
|
||||||
|
return r.result, r.err
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Cache) GetDownloadLink(torrentName, filename, fileLink string) (string, error) {
|
func (c *Cache) GetDownloadLink(torrentName, filename, fileLink string) (string, error) {
|
||||||
// Check link cache
|
// Check link cache
|
||||||
if dl := c.checkDownloadLink(fileLink); dl != "" {
|
if dl := c.checkDownloadLink(fileLink); dl != "" {
|
||||||
return dl, nil
|
return dl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if req, inFlight := c.downloadLinkRequests.Load(fileLink); inFlight {
|
||||||
|
// Wait for the other request to complete and use its result
|
||||||
|
fmt.Println("Waiting for existing request to complete")
|
||||||
|
result := req.(*downloadLinkRequest)
|
||||||
|
return result.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new request object
|
||||||
|
req := newDownloadLinkRequest()
|
||||||
|
c.downloadLinkRequests.Store(fileLink, req)
|
||||||
|
|
||||||
downloadLink, err := c.fetchDownloadLink(torrentName, filename, fileLink)
|
downloadLink, err := c.fetchDownloadLink(torrentName, filename, fileLink)
|
||||||
|
|
||||||
|
// Complete the request and remove it from the map
|
||||||
|
req.Complete(downloadLink, err)
|
||||||
|
c.downloadLinkRequests.Delete(fileLink)
|
||||||
|
|
||||||
return downloadLink, err
|
return downloadLink, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -140,8 +140,16 @@ func (c *Cache) refreshRclone() error {
|
|||||||
MaxIdleConnsPerHost: 5,
|
MaxIdleConnsPerHost: 5,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
// Create form data
|
data := ""
|
||||||
data := "dir=__all__&dir2=torrents"
|
for index, dir := range c.GetDirectories() {
|
||||||
|
if dir != "" {
|
||||||
|
if index == 0 {
|
||||||
|
data += "dir=" + dir
|
||||||
|
} else {
|
||||||
|
data += "&dir" + fmt.Sprint(index) + "=" + dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sendRequest := func(endpoint string) error {
|
sendRequest := func(endpoint string) error {
|
||||||
req, err := http.NewRequest("POST", fmt.Sprintf("%s/%s", cfg.RcUrl, endpoint), strings.NewReader(data))
|
req, err := http.NewRequest("POST", fmt.Sprintf("%s/%s", cfg.RcUrl, endpoint), strings.NewReader(data))
|
||||||
|
|||||||
@@ -2,26 +2,70 @@ package debrid
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type torrentCache struct {
|
const (
|
||||||
mu sync.RWMutex
|
filterByInclude string = "include"
|
||||||
byID map[string]string
|
filterByExclude string = "exclude"
|
||||||
byName map[string]*CachedTorrent
|
|
||||||
listing atomic.Value
|
filterByStartsWith string = "starts_with"
|
||||||
sortNeeded bool
|
filterByEndsWith string = "ends_with"
|
||||||
|
filterByNotStartsWith string = "not_starts_with"
|
||||||
|
filterByNotEndsWith string = "not_ends_with"
|
||||||
|
|
||||||
|
filterByRegex string = "regex"
|
||||||
|
filterByNotRegex string = "not_regex"
|
||||||
|
|
||||||
|
filterByExactMatch string = "exact_match"
|
||||||
|
filterByNotExactMatch string = "not_exact_match"
|
||||||
|
|
||||||
|
filterBySizeGT string = "size_gt"
|
||||||
|
filterBySizeLT string = "size_lt"
|
||||||
|
|
||||||
|
filterBLastAdded string = "last_added"
|
||||||
|
)
|
||||||
|
|
||||||
|
type directoryFilter struct {
|
||||||
|
filterType string
|
||||||
|
value string
|
||||||
|
regex *regexp.Regexp // only for regex/not_regex
|
||||||
|
sizeThreshold int64 // only for size_gt/size_lt
|
||||||
|
ageThreshold time.Duration // only for last_added
|
||||||
}
|
}
|
||||||
|
|
||||||
func newTorrentCache() *torrentCache {
|
type torrentCache struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
byID map[string]string
|
||||||
|
byName map[string]*CachedTorrent
|
||||||
|
listing atomic.Value
|
||||||
|
folderListing map[string][]os.FileInfo
|
||||||
|
folderListingMu sync.RWMutex
|
||||||
|
directoriesFilters map[string][]directoryFilter
|
||||||
|
sortNeeded bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type sortableFile struct {
|
||||||
|
name string
|
||||||
|
modTime time.Time
|
||||||
|
size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTorrentCache(dirFilters map[string][]directoryFilter) *torrentCache {
|
||||||
|
|
||||||
tc := &torrentCache{
|
tc := &torrentCache{
|
||||||
byID: make(map[string]string),
|
byID: make(map[string]string),
|
||||||
byName: make(map[string]*CachedTorrent),
|
byName: make(map[string]*CachedTorrent),
|
||||||
sortNeeded: false,
|
folderListing: make(map[string][]os.FileInfo),
|
||||||
|
sortNeeded: false,
|
||||||
|
directoriesFilters: dirFilters,
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.listing.Store(make([]os.FileInfo, 0))
|
tc.listing.Store(make([]os.FileInfo, 0))
|
||||||
return tc
|
return tc
|
||||||
}
|
}
|
||||||
@@ -66,57 +110,121 @@ func (tc *torrentCache) getListing() []os.FileInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Slow path: need to sort
|
// Slow path: need to sort
|
||||||
return tc.refreshListing()
|
tc.refreshListing()
|
||||||
|
return tc.listing.Load().([]os.FileInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *torrentCache) refreshListing() []os.FileInfo {
|
func (tc *torrentCache) getFolderListing(folderName string) []os.FileInfo {
|
||||||
tc.mu.Lock()
|
tc.folderListingMu.RLock()
|
||||||
size := len(tc.byName)
|
defer tc.folderListingMu.RUnlock()
|
||||||
tc.mu.Unlock()
|
if folderName == "" {
|
||||||
if size == 0 {
|
return tc.getListing()
|
||||||
var empty []os.FileInfo
|
|
||||||
tc.listing.Store(empty)
|
|
||||||
tc.sortNeeded = false
|
|
||||||
return empty
|
|
||||||
}
|
}
|
||||||
|
if folder, ok := tc.folderListing[folderName]; ok {
|
||||||
|
return folder
|
||||||
|
}
|
||||||
|
// If folder not found, return empty slice
|
||||||
|
return []os.FileInfo{}
|
||||||
|
}
|
||||||
|
|
||||||
// Create sortable entries
|
func (tc *torrentCache) refreshListing() {
|
||||||
type sortableFile struct {
|
|
||||||
name string
|
|
||||||
modTime time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
tc.mu.Lock()
|
tc.mu.Lock()
|
||||||
sortables := make([]sortableFile, 0, len(tc.byName))
|
all := make([]sortableFile, 0, len(tc.byName))
|
||||||
|
for name, t := range tc.byName {
|
||||||
for name, torrent := range tc.byName {
|
all = append(all, sortableFile{name, t.AddedOn, t.Size})
|
||||||
sortables = append(sortables, sortableFile{
|
|
||||||
name: name,
|
|
||||||
modTime: torrent.AddedOn,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
tc.sortNeeded = false
|
||||||
tc.mu.Unlock()
|
tc.mu.Unlock()
|
||||||
|
|
||||||
// Sort by name
|
sort.Slice(all, func(i, j int) bool {
|
||||||
sort.Slice(sortables, func(i, j int) bool {
|
if all[i].name != all[j].name {
|
||||||
return sortables[i].name < sortables[j].name
|
return all[i].name < all[j].name
|
||||||
|
}
|
||||||
|
return all[i].modTime.Before(all[j].modTime)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Create fileInfo objects
|
wg := sync.WaitGroup{}
|
||||||
files := make([]os.FileInfo, 0, len(sortables))
|
|
||||||
for _, sf := range sortables {
|
wg.Add(1) // for all listing
|
||||||
files = append(files, &fileInfo{
|
go func() {
|
||||||
name: sf.name,
|
listing := make([]os.FileInfo, len(all))
|
||||||
size: 0,
|
for i, sf := range all {
|
||||||
mode: 0755 | os.ModeDir,
|
listing[i] = &fileInfo{sf.name, sf.size, 0755 | os.ModeDir, sf.modTime, true}
|
||||||
modTime: sf.modTime,
|
}
|
||||||
isDir: true,
|
tc.listing.Store(listing)
|
||||||
})
|
}()
|
||||||
|
wg.Done()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
wg.Add(len(tc.directoriesFilters)) // for each directory filter
|
||||||
|
for dir, filters := range tc.directoriesFilters {
|
||||||
|
go func(dir string, filters []directoryFilter) {
|
||||||
|
defer wg.Done()
|
||||||
|
var matched []os.FileInfo
|
||||||
|
for _, sf := range all {
|
||||||
|
if tc.torrentMatchDirectory(filters, sf, now) {
|
||||||
|
matched = append(matched, &fileInfo{
|
||||||
|
name: sf.name, size: sf.size,
|
||||||
|
mode: 0755 | os.ModeDir, modTime: sf.modTime, isDir: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tc.folderListingMu.Lock()
|
||||||
|
if len(matched) > 0 {
|
||||||
|
tc.folderListing[dir] = matched
|
||||||
|
} else {
|
||||||
|
delete(tc.folderListing, dir)
|
||||||
|
}
|
||||||
|
tc.folderListingMu.Unlock()
|
||||||
|
}(dir, filters)
|
||||||
}
|
}
|
||||||
|
|
||||||
tc.listing.Store(files)
|
wg.Wait()
|
||||||
tc.sortNeeded = false
|
}
|
||||||
return files
|
|
||||||
|
func (tc *torrentCache) torrentMatchDirectory(filters []directoryFilter, file sortableFile, now time.Time) bool {
|
||||||
|
|
||||||
|
torrentName := strings.ToLower(file.name)
|
||||||
|
for _, filter := range filters {
|
||||||
|
matched := false
|
||||||
|
|
||||||
|
switch filter.filterType {
|
||||||
|
case filterByInclude:
|
||||||
|
matched = strings.Contains(torrentName, filter.value)
|
||||||
|
case filterByStartsWith:
|
||||||
|
matched = strings.HasPrefix(torrentName, filter.value)
|
||||||
|
case filterByEndsWith:
|
||||||
|
matched = strings.HasSuffix(torrentName, filter.value)
|
||||||
|
case filterByExactMatch:
|
||||||
|
matched = torrentName == filter.value
|
||||||
|
case filterByExclude:
|
||||||
|
matched = !strings.Contains(torrentName, filter.value)
|
||||||
|
case filterByNotStartsWith:
|
||||||
|
matched = !strings.HasPrefix(torrentName, filter.value)
|
||||||
|
case filterByNotEndsWith:
|
||||||
|
matched = !strings.HasSuffix(torrentName, filter.value)
|
||||||
|
case filterByRegex:
|
||||||
|
matched = filter.regex.MatchString(torrentName)
|
||||||
|
case filterByNotRegex:
|
||||||
|
matched = !filter.regex.MatchString(torrentName)
|
||||||
|
case filterByNotExactMatch:
|
||||||
|
matched = torrentName != filter.value
|
||||||
|
case filterBySizeGT:
|
||||||
|
matched = file.size > filter.sizeThreshold
|
||||||
|
case filterBySizeLT:
|
||||||
|
matched = file.size < filter.sizeThreshold
|
||||||
|
case filterBLastAdded:
|
||||||
|
matched = file.modTime.After(now.Add(-filter.ageThreshold))
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
return false // All filters must match
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, all filters matched
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *torrentCache) getAll() map[string]*CachedTorrent {
|
func (tc *torrentCache) getAll() map[string]*CachedTorrent {
|
||||||
|
|||||||
@@ -1,64 +1,230 @@
|
|||||||
package debrid
|
package debrid
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/beevik/etree"
|
||||||
"github.com/sirrobot01/decypharr/internal/request"
|
"github.com/sirrobot01/decypharr/internal/request"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DavNS = "DAV:"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Multistatus XML types for WebDAV response
|
||||||
|
type Multistatus struct {
|
||||||
|
XMLName xml.Name `xml:"D:multistatus"`
|
||||||
|
Namespace string `xml:"xmlns:D,attr"`
|
||||||
|
Responses []Response `xml:"D:response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Href string `xml:"D:href"`
|
||||||
|
Propstat Propstat `xml:"D:propstat"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Propstat struct {
|
||||||
|
Prop Prop `xml:"D:prop"`
|
||||||
|
Status string `xml:"D:status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Prop struct {
|
||||||
|
ResourceType ResourceType `xml:"D:resourcetype"`
|
||||||
|
DisplayName string `xml:"D:displayname"`
|
||||||
|
LastModified string `xml:"D:getlastmodified"`
|
||||||
|
ContentType string `xml:"D:getcontenttype"`
|
||||||
|
ContentLength string `xml:"D:getcontentlength"`
|
||||||
|
SupportedLock SupportedLock `xml:"D:supportedlock"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResourceType struct {
|
||||||
|
Collection *struct{} `xml:"D:collection,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SupportedLock struct {
|
||||||
|
LockEntry LockEntry `xml:"D:lockentry"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LockEntry struct {
|
||||||
|
LockScope LockScope `xml:"D:lockscope"`
|
||||||
|
LockType LockType `xml:"D:locktype"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LockScope struct {
|
||||||
|
Exclusive *struct{} `xml:"D:exclusive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LockType struct {
|
||||||
|
Write *struct{} `xml:"D:write"`
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Cache) refreshParentXml() error {
|
func (c *Cache) refreshParentXml() error {
|
||||||
|
// Refresh the defaults first
|
||||||
parents := []string{"__all__", "torrents"}
|
parents := []string{"__all__", "torrents"}
|
||||||
torrents := c.GetListing()
|
torrents := c.GetListing("__all__")
|
||||||
clientName := c.client.GetName()
|
clientName := c.client.GetName()
|
||||||
|
customFolders := c.GetCustomFolders()
|
||||||
|
wg := sync.WaitGroup{}
|
||||||
|
totalFolders := len(parents) + len(customFolders)
|
||||||
|
wg.Add(totalFolders)
|
||||||
|
errCh := make(chan error, totalFolders)
|
||||||
for _, parent := range parents {
|
for _, parent := range parents {
|
||||||
if err := c.refreshFolderXml(torrents, clientName, parent); err != nil {
|
parent := parent
|
||||||
return fmt.Errorf("failed to refresh XML for %s: %v", parent, err)
|
go func() {
|
||||||
}
|
defer wg.Done()
|
||||||
|
if err := c.refreshFolderXml(torrents, clientName, parent); err != nil {
|
||||||
|
errCh <- fmt.Errorf("failed to refresh folder %s: %v", parent, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
// refresh custom folders
|
||||||
|
for _, folder := range customFolders {
|
||||||
|
go func() {
|
||||||
|
folder := folder
|
||||||
|
defer wg.Done()
|
||||||
|
listing := c.GetListing(folder)
|
||||||
|
if err := c.refreshFolderXml(listing, clientName, folder); err != nil {
|
||||||
|
errCh <- fmt.Errorf("failed to refresh folder %s: %v", folder, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
close(errCh)
|
||||||
|
// if any errors, return the first
|
||||||
|
if err := <-errCh; err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Cache) refreshFolderXml(torrents []os.FileInfo, clientName, parent string) error {
|
func (c *Cache) refreshFolderXml(torrents []os.FileInfo, clientName, parent string) error {
|
||||||
buf := c.xmlPool.Get().(*bytes.Buffer)
|
// Get the current timestamp in RFC1123 format
|
||||||
buf.Reset()
|
currentTime := time.Now().UTC().Format(http.TimeFormat)
|
||||||
defer c.xmlPool.Put(buf)
|
|
||||||
|
|
||||||
// static prefix
|
// Create the multistatus response structure
|
||||||
buf.WriteString(`<?xml version="1.0" encoding="UTF-8"?><D:multistatus xmlns:D="DAV:">`)
|
ms := Multistatus{
|
||||||
now := time.Now().UTC().Format(http.TimeFormat)
|
Namespace: DavNS,
|
||||||
base := fmt.Sprintf("/webdav/%s/%s", clientName, parent)
|
Responses: make([]Response, 0, len(torrents)+1), // Pre-allocate for parent + torrents
|
||||||
writeResponse(buf, base+"/", parent, now)
|
|
||||||
for _, t := range torrents {
|
|
||||||
writeResponse(buf, base+"/"+t.Name()+"/", t.Name(), now)
|
|
||||||
}
|
}
|
||||||
buf.WriteString("</D:multistatus>")
|
|
||||||
|
|
||||||
data := buf.Bytes()
|
// Add the parent directory
|
||||||
gz := request.Gzip(data, &c.gzipPool)
|
baseUrl := path.Join("webdav", clientName, parent)
|
||||||
c.PropfindResp.Store(path.Clean(base), PropfindResponse{Data: data, GzippedData: gz, Ts: time.Now()})
|
|
||||||
|
// Add parent response
|
||||||
|
ms.Responses = append(ms.Responses, createDirectoryResponse(baseUrl, parent, currentTime))
|
||||||
|
|
||||||
|
// Add torrents to the response
|
||||||
|
for _, torrent := range torrents {
|
||||||
|
name := torrent.Name()
|
||||||
|
torrentPath := path.Join("/webdav", clientName, parent, name) + "/"
|
||||||
|
ms.Responses = append(ms.Responses, createDirectoryResponse(torrentPath, name, currentTime))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a buffer and encode the XML
|
||||||
|
xmlData, err := xml.MarshalIndent(ms, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to generate XML: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add XML declaration
|
||||||
|
xmlHeader := []byte(xml.Header)
|
||||||
|
xmlOutput := append(xmlHeader, xmlData...)
|
||||||
|
|
||||||
|
// Cache the result
|
||||||
|
cacheKey := fmt.Sprintf("%s:1", baseUrl)
|
||||||
|
|
||||||
|
// Assume Gzip function exists elsewhere
|
||||||
|
gzippedData := request.Gzip(xmlOutput) // Replace with your actual gzip function
|
||||||
|
|
||||||
|
c.PropfindResp.Store(cacheKey, PropfindResponse{
|
||||||
|
Data: xmlOutput,
|
||||||
|
GzippedData: gzippedData,
|
||||||
|
Ts: time.Now(),
|
||||||
|
})
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeResponse(buf *bytes.Buffer, href, name, modTime string) {
|
func createDirectoryResponse(href, displayName, modTime string) Response {
|
||||||
fmt.Fprintf(buf, `
|
return Response{
|
||||||
<D:response>
|
Href: href,
|
||||||
<D:href>%s</D:href>
|
Propstat: Propstat{
|
||||||
<D:propstat>
|
Prop: Prop{
|
||||||
<D:prop>
|
ResourceType: ResourceType{
|
||||||
<D:resourcetype><D:collection/></D:resourcetype>
|
Collection: &struct{}{},
|
||||||
<D:displayname>%s</D:displayname>
|
},
|
||||||
<D:getlastmodified>%s</D:getlastmodified>
|
DisplayName: displayName,
|
||||||
<D:getcontenttype>httpd/unix-directory</D:getcontenttype>
|
LastModified: modTime,
|
||||||
<D:getcontentlength>0</D:getcontentlength>
|
ContentType: "httpd/unix-directory",
|
||||||
<D:supportedlock>
|
ContentLength: "0",
|
||||||
<D:lockentry><D:lockscope><D:exclusive/></D:lockscope><D:locktype><D:write/></D:locktype></D:lockentry>
|
SupportedLock: SupportedLock{
|
||||||
</D:supportedlock>
|
LockEntry: LockEntry{
|
||||||
</D:prop>
|
LockScope: LockScope{
|
||||||
<D:status>HTTP/1.1 200 OK</D:status>
|
Exclusive: &struct{}{},
|
||||||
</D:propstat>
|
},
|
||||||
</D:response>`, href, name, modTime)
|
LockType: LockType{
|
||||||
|
Write: &struct{}{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Status: "HTTP/1.1 200 OK",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addDirectoryResponse(multistatus *etree.Element, href, displayName, modTime string) *etree.Element {
|
||||||
|
responseElem := multistatus.CreateElement("D:response")
|
||||||
|
|
||||||
|
// Add href - ensure it's properly formatted
|
||||||
|
hrefElem := responseElem.CreateElement("D:href")
|
||||||
|
hrefElem.SetText(href)
|
||||||
|
|
||||||
|
// Add propstat
|
||||||
|
propstatElem := responseElem.CreateElement("D:propstat")
|
||||||
|
|
||||||
|
// Add prop
|
||||||
|
propElem := propstatElem.CreateElement("D:prop")
|
||||||
|
|
||||||
|
// Add resource type (collection = directory)
|
||||||
|
resourceTypeElem := propElem.CreateElement("D:resourcetype")
|
||||||
|
resourceTypeElem.CreateElement("D:collection")
|
||||||
|
|
||||||
|
// Add display name
|
||||||
|
displayNameElem := propElem.CreateElement("D:displayname")
|
||||||
|
displayNameElem.SetText(displayName)
|
||||||
|
|
||||||
|
// Add last modified time
|
||||||
|
lastModElem := propElem.CreateElement("D:getlastmodified")
|
||||||
|
lastModElem.SetText(modTime)
|
||||||
|
|
||||||
|
// Add content type for directories
|
||||||
|
contentTypeElem := propElem.CreateElement("D:getcontenttype")
|
||||||
|
contentTypeElem.SetText("httpd/unix-directory")
|
||||||
|
|
||||||
|
// Add length (size) - directories typically have zero size
|
||||||
|
contentLengthElem := propElem.CreateElement("D:getcontentlength")
|
||||||
|
contentLengthElem.SetText("0")
|
||||||
|
|
||||||
|
// Add supported lock
|
||||||
|
lockElem := propElem.CreateElement("D:supportedlock")
|
||||||
|
lockEntryElem := lockElem.CreateElement("D:lockentry")
|
||||||
|
|
||||||
|
lockScopeElem := lockEntryElem.CreateElement("D:lockscope")
|
||||||
|
lockScopeElem.CreateElement("D:exclusive")
|
||||||
|
|
||||||
|
lockTypeElem := lockEntryElem.CreateElement("D:locktype")
|
||||||
|
lockTypeElem.CreateElement("D:write")
|
||||||
|
|
||||||
|
// Add status
|
||||||
|
statusElem := propstatElem.CreateElement("D:status")
|
||||||
|
statusElem.SetText("HTTP/1.1 200 OK")
|
||||||
|
|
||||||
|
return responseElem
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -419,9 +419,192 @@
|
|||||||
<input type="password" class="form-control webdav-field" name="debrid[${index}].rc_pass" id="debrid[${index}].rc_pass">
|
<input type="password" class="form-control webdav-field" name="debrid[${index}].rc_pass" id="debrid[${index}].rc_pass">
|
||||||
<small class="form-text text-muted">Rclone RC Password for the webdav server</small>
|
<small class="form-text text-muted">Rclone RC Password for the webdav server</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col mt-3">
|
||||||
|
<h6 class="pb-2">Custom Folders</h6>
|
||||||
|
<div class="col-12">
|
||||||
|
<p class="text-muted small">Create virtual directories with filters to organize your content</p>
|
||||||
|
<div class="directories-container" id="debrid[${index}].directories">
|
||||||
|
<!-- Dynamic directories will be added here -->
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-secondary mt-2 webdav-field" onclick="addDirectory(${index});">
|
||||||
|
<i class="bi bi-plus"></i> Add Directory
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
// Template for directory entries (with filter buttons for both positive and negative variants)
|
||||||
|
const directoryTemplate = (debridIndex, dirIndex) => `
|
||||||
|
<div class="directory-item mb-3 border rounded p-3 position-relative">
|
||||||
|
<button type="button" class="btn btn-sm btn-danger position-absolute top-0 end-0 m-2"
|
||||||
|
onclick="removeDirectory(this);" title="Remove directory">
|
||||||
|
<i class="bi bi-trash"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="col-md-4 mb-3">
|
||||||
|
<label class="form-label">Folder Name</label>
|
||||||
|
<input type="text" class="form-control webdav-field"
|
||||||
|
name="debrid[${debridIndex}].directory[${dirIndex}].name"
|
||||||
|
placeholder="e.g., Movies, TV Shows, Spiderman Collection">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<h6 class="mb-3">
|
||||||
|
Filters
|
||||||
|
<button type="button" class="btn btn-sm btn-link" onclick="showFilterHelp();">
|
||||||
|
<i class="bi bi-question-circle"></i>
|
||||||
|
</button>
|
||||||
|
</h6>
|
||||||
|
<div class="filters-container" id="debrid[${debridIndex}].directory[${dirIndex}].filters">
|
||||||
|
<!-- Filters will be added here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-2">
|
||||||
|
<div class="dropdown d-inline-block me-2 mb-2">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
Add Text Filter
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="addFilter(${debridIndex}, ${dirIndex}, 'include'); return false;">Include</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="addFilter(${debridIndex}, ${dirIndex}, 'exclude'); return false;">Exclude</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="addFilter(${debridIndex}, ${dirIndex}, 'starts_with'); return false;">Starts With</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="addFilter(${debridIndex}, ${dirIndex}, 'not_starts_with'); return false;">Not Starts With</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="addFilter(${debridIndex}, ${dirIndex}, 'ends_with'); return false;">Ends With</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="addFilter(${debridIndex}, ${dirIndex}, 'not_ends_with'); return false;">Not Ends With</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="addFilter(${debridIndex}, ${dirIndex}, 'exact_match'); return false;">Exact Match</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="addFilter(${debridIndex}, ${dirIndex}, 'not_exact_match'); return false;">Not Exact Match</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dropdown d-inline-block me-2 mb-2">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
Add Regex Filter
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="addFilter(${debridIndex}, ${dirIndex}, 'regex'); return false;">Regex Match</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="addFilter(${debridIndex}, ${dirIndex}, 'not_regex'); return false;">Regex Not Match</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dropdown d-inline-block me-2 mb-2">
|
||||||
|
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
Add Size Filter
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="addFilter(${debridIndex}, ${dirIndex}, 'size_gt'); return false;">Size Greater Than</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" onclick="addFilter(${debridIndex}, ${dirIndex}, 'size_lt'); return false;">Size Less Than</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary me-2"
|
||||||
|
onclick="addFilter(${debridIndex}, ${dirIndex}, 'last_added');">
|
||||||
|
Add Last Added Filter
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
// Enhanced filter template with support for all filter types
|
||||||
|
const filterTemplate = (debridIndex, dirIndex, filterIndex, filterType) => {
|
||||||
|
let placeholder, label;
|
||||||
|
|
||||||
|
switch(filterType) {
|
||||||
|
case 'include':
|
||||||
|
placeholder = "Text that should be included in filename";
|
||||||
|
label = "Include";
|
||||||
|
break;
|
||||||
|
case 'exclude':
|
||||||
|
placeholder = "Text that should not be in filename";
|
||||||
|
label = "Exclude";
|
||||||
|
break;
|
||||||
|
case 'regex':
|
||||||
|
placeholder = "Regular expression pattern";
|
||||||
|
label = "Regex Match";
|
||||||
|
break;
|
||||||
|
case 'not_regex':
|
||||||
|
placeholder = "Regular expression pattern that should not match";
|
||||||
|
label = "Regex Not Match";
|
||||||
|
break;
|
||||||
|
case 'exact_match':
|
||||||
|
placeholder = "Exact text to match";
|
||||||
|
label = "Exact Match";
|
||||||
|
break;
|
||||||
|
case 'not_exact_match':
|
||||||
|
placeholder = "Exact text that should not match";
|
||||||
|
label = "Not Exact Match";
|
||||||
|
break;
|
||||||
|
case 'starts_with':
|
||||||
|
placeholder = "Text that filename starts with";
|
||||||
|
label = "Starts With";
|
||||||
|
break;
|
||||||
|
case 'not_starts_with':
|
||||||
|
placeholder = "Text that filename should not start with";
|
||||||
|
label = "Not Starts With";
|
||||||
|
break;
|
||||||
|
case 'ends_with':
|
||||||
|
placeholder = "Text that filename ends with";
|
||||||
|
label = "Ends With";
|
||||||
|
break;
|
||||||
|
case 'not_ends_with':
|
||||||
|
placeholder = "Text that filename should not end with";
|
||||||
|
label = "Not Ends With";
|
||||||
|
break;
|
||||||
|
case 'size_gt':
|
||||||
|
placeholder = "Size in bytes, KB, MB, GB (e.g. 700MB)";
|
||||||
|
label = "Size Greater Than";
|
||||||
|
break;
|
||||||
|
case 'size_lt':
|
||||||
|
placeholder = "Size in bytes, KB, MB, GB (e.g. 700MB)";
|
||||||
|
label = "Size Less Than";
|
||||||
|
break;
|
||||||
|
case 'last_added':
|
||||||
|
placeholder = "Time duration (e.g. 24h, 7d, 30d)";
|
||||||
|
label = "Added in the last";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
placeholder = "Filter value";
|
||||||
|
label = filterType.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use a color coding scheme for filter types
|
||||||
|
let badgeClass = "bg-secondary";
|
||||||
|
if (filterType.startsWith('not_') || filterType === 'exclude' || filterType === 'size_lt') {
|
||||||
|
badgeClass = "bg-danger"; // Negative filters
|
||||||
|
} else if (filterType === 'last_added') {
|
||||||
|
badgeClass = "bg-info"; // Time-based filters
|
||||||
|
} else if (filterType === 'size_gt') {
|
||||||
|
badgeClass = "bg-success"; // Size filters
|
||||||
|
} else if (filterType === 'regex' || filterType === 'not_regex') {
|
||||||
|
badgeClass = "bg-warning"; // Regex filters
|
||||||
|
} else if (filterType === 'include' || filterType === 'starts_with' || filterType === 'ends_with' || filterType === 'exact_match') {
|
||||||
|
badgeClass = "bg-primary"; // Positive text filters
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="filter-item row mb-2 align-items-center">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<span class="badge ${badgeClass}">${label}</span>
|
||||||
|
<input type="hidden"
|
||||||
|
name="debrid[${debridIndex}].directory[${dirIndex}].filter[${filterIndex}].type"
|
||||||
|
value="${filterType}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<input type="text" class="form-control form-control-sm webdav-field"
|
||||||
|
name="debrid[${debridIndex}].directory[${dirIndex}].filter[${filterIndex}].value"
|
||||||
|
placeholder="${placeholder}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-1">
|
||||||
|
<button type="button" class="btn btn-sm btn-danger" onclick="removeFilter(this);">
|
||||||
|
<i class="bi bi-x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
const arrTemplate = (index) => `
|
const arrTemplate = (index) => `
|
||||||
<div class="config-item position-relative mb-3 p-3 border rounded">
|
<div class="config-item position-relative mb-3 p-3 border rounded">
|
||||||
@@ -467,6 +650,91 @@
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const debridDirectoryCounts = {};
|
||||||
|
const directoryFilterCounts = {};
|
||||||
|
|
||||||
|
// Helper function to show a tooltip explaining filter types
|
||||||
|
function showFilterHelp() {
|
||||||
|
const helpContent = `
|
||||||
|
<h5>Filter Types</h5>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Include/Exclude</strong>: Simple text inclusion/exclusion</li>
|
||||||
|
<li><strong>Starts/Ends With</strong>: Matches beginning or end of filename</li>
|
||||||
|
<li><strong>Exact Match</strong>: Match the entire filename</li>
|
||||||
|
<li><strong>Regex</strong>: Use regular expressions for complex patterns</li>
|
||||||
|
<li><strong>Size</strong>: Filter by file size</li>
|
||||||
|
<li><strong>Last Added</strong>: Show only recently added content</li>
|
||||||
|
</ul>
|
||||||
|
<p>Negative filters (Not...) will exclude matches instead of including them.</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Show a modal or tooltip with this content
|
||||||
|
// This will depend on your UI framework
|
||||||
|
// For Bootstrap:
|
||||||
|
$('#filterHelpModal .modal-body').html(helpContent);
|
||||||
|
$('#filterHelpModal').modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
function addDirectory(debridIndex, data = {}) {
|
||||||
|
if (!debridDirectoryCounts[debridIndex]) {
|
||||||
|
debridDirectoryCounts[debridIndex] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirIndex = debridDirectoryCounts[debridIndex];
|
||||||
|
const container = document.getElementById(`debrid[${debridIndex}].directories`);
|
||||||
|
container.insertAdjacentHTML('beforeend', directoryTemplate(debridIndex, dirIndex));
|
||||||
|
|
||||||
|
// Set up tracking for filters in this directory
|
||||||
|
const dirKey = `${debridIndex}-${dirIndex}`;
|
||||||
|
directoryFilterCounts[dirKey] = 0;
|
||||||
|
|
||||||
|
// Fill with directory name if provided
|
||||||
|
if (data.name) {
|
||||||
|
const nameInput = document.querySelector(`[name="debrid[${debridIndex}].directory[${dirIndex}].name"]`);
|
||||||
|
if (nameInput) nameInput.value = data.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add filters if provided
|
||||||
|
if (data.filters) {
|
||||||
|
Object.entries(data.filters).forEach(([filterType, filterValue]) => {
|
||||||
|
addFilter(debridIndex, dirIndex, filterType, filterValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
debridDirectoryCounts[debridIndex]++;
|
||||||
|
return dirIndex;
|
||||||
|
}
|
||||||
|
function addFilter(debridIndex, dirIndex, filterType, filterValue = "") {
|
||||||
|
const dirKey = `${debridIndex}-${dirIndex}`;
|
||||||
|
if (!directoryFilterCounts[dirKey]) {
|
||||||
|
directoryFilterCounts[dirKey] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterIndex = directoryFilterCounts[dirKey];
|
||||||
|
const container = document.getElementById(`debrid[${debridIndex}].directory[${dirIndex}].filters`);
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
container.insertAdjacentHTML('beforeend', filterTemplate(debridIndex, dirIndex, filterIndex, filterType));
|
||||||
|
|
||||||
|
// Set filter value if provided
|
||||||
|
if (filterValue) {
|
||||||
|
const valueInput = container.querySelector(`[name="debrid[${debridIndex}].directory[${dirIndex}].filter[${filterIndex}].value"]`);
|
||||||
|
if (valueInput) valueInput.value = filterValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
directoryFilterCounts[dirKey]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDirectory(button) {
|
||||||
|
button.closest('.directory-item').remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to remove a filter
|
||||||
|
function removeFilter(button) {
|
||||||
|
button.closest('.filter-item').remove();
|
||||||
|
}
|
||||||
|
|
||||||
// Main functionality
|
// Main functionality
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
let debridCount = 0;
|
let debridCount = 0;
|
||||||
@@ -710,6 +978,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (data.use_webdav && data.directories) {
|
||||||
|
Object.entries(data.directories).forEach(([dirName, dirData]) => {
|
||||||
|
const dirIndex = addDirectory(debridCount, { name: dirName });
|
||||||
|
|
||||||
|
// Add filters if available
|
||||||
|
if (dirData.filters) {
|
||||||
|
Object.entries(dirData.filters).forEach(([filterType, filterValue]) => {
|
||||||
|
addFilter(debridCount, dirIndex, filterType, filterValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debridCount++;
|
debridCount++;
|
||||||
@@ -809,6 +1090,33 @@
|
|||||||
debrid.rc_url = document.querySelector(`[name="debrid[${i}].rc_url"]`).value;
|
debrid.rc_url = document.querySelector(`[name="debrid[${i}].rc_url"]`).value;
|
||||||
debrid.rc_user = document.querySelector(`[name="debrid[${i}].rc_user"]`).value;
|
debrid.rc_user = document.querySelector(`[name="debrid[${i}].rc_user"]`).value;
|
||||||
debrid.rc_pass = document.querySelector(`[name="debrid[${i}].rc_pass"]`).value;
|
debrid.rc_pass = document.querySelector(`[name="debrid[${i}].rc_pass"]`).value;
|
||||||
|
|
||||||
|
//custom folders
|
||||||
|
debrid.directories = {};
|
||||||
|
const dirCount = debridDirectoryCounts[i] || 0;
|
||||||
|
|
||||||
|
for (let j = 0; j < dirCount; j++) {
|
||||||
|
const nameInput = document.querySelector(`[name="debrid[${i}].directory[${j}].name"]`);
|
||||||
|
if (nameInput && nameInput.value) {
|
||||||
|
const dirName = nameInput.value;
|
||||||
|
debrid.directories[dirName] = { filters: {} };
|
||||||
|
|
||||||
|
// Get directory key for filter counting
|
||||||
|
const dirKey = `${i}-${j}`;
|
||||||
|
const filterCount = directoryFilterCounts[dirKey] || 0;
|
||||||
|
|
||||||
|
// Collect all filters for this directory
|
||||||
|
for (let k = 0; k < filterCount; k++) {
|
||||||
|
const filterTypeInput = document.querySelector(`[name="debrid[${i}].directory[${j}].filter[${k}].type"]`);
|
||||||
|
const filterValueInput = document.querySelector(`[name="debrid[${i}].directory[${j}].filter[${k}].value"]`);
|
||||||
|
|
||||||
|
if (filterTypeInput && filterValueInput && filterValueInput.value) {
|
||||||
|
const filterType = filterTypeInput.value;
|
||||||
|
debrid.directories[dirName].filters[filterType] = filterValueInput.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (debrid.name && debrid.api_key) {
|
if (debrid.name && debrid.api_key) {
|
||||||
@@ -864,4 +1172,22 @@
|
|||||||
document.getElementById('registerMagnetLink').classList.add('bg-white', 'text-black');
|
document.getElementById('registerMagnetLink').classList.add('bg-white', 'text-black');
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- Filter Help Modal -->
|
||||||
|
<div class="modal fade" id="filterHelpModal" tabindex="-1" aria-labelledby="filterHelpModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="filterHelpModalLabel">Directory Filter Help</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- Content will be injected by the showFilterHelp function -->
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
@@ -14,7 +14,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
@@ -31,8 +30,6 @@ type Handler struct {
|
|||||||
logger zerolog.Logger
|
logger zerolog.Logger
|
||||||
cache *debrid.Cache
|
cache *debrid.Cache
|
||||||
RootPath string
|
RootPath string
|
||||||
|
|
||||||
gzipPool sync.Pool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(name string, cache *debrid.Cache, logger zerolog.Logger) *Handler {
|
func NewHandler(name string, cache *debrid.Cache, logger zerolog.Logger) *Handler {
|
||||||
@@ -83,12 +80,23 @@ func (h *Handler) getRootPath() string {
|
|||||||
return fmt.Sprintf(filepath.Join(string(os.PathSeparator), "webdav", "%s"), h.Name)
|
return fmt.Sprintf(filepath.Join(string(os.PathSeparator), "webdav", "%s"), h.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getTorrentsFolders() []os.FileInfo {
|
func (h *Handler) getTorrentsFolders(folder string) []os.FileInfo {
|
||||||
return h.cache.GetListing()
|
return h.cache.GetListing(folder)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getParentItems() []string {
|
func (h *Handler) getParentItems() []string {
|
||||||
return []string{"__all__", "torrents", "version.txt"}
|
parents := []string{"__all__", "torrents"}
|
||||||
|
|
||||||
|
// Add user-defined parent items
|
||||||
|
for _, dir := range h.cache.GetCustomFolders() {
|
||||||
|
if dir != "" {
|
||||||
|
parents = append(parents, dir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// version.txt
|
||||||
|
parents = append(parents, "version.txt")
|
||||||
|
return parents
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getParentFiles() []os.FileInfo {
|
func (h *Handler) getParentFiles() []os.FileInfo {
|
||||||
@@ -147,12 +155,12 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Single check for top-level folders
|
// Single check for top-level folders
|
||||||
if h.isParentPath(name) {
|
if parent, ok := h.isParentPath(name); ok {
|
||||||
folderName := strings.TrimPrefix(name, rootDir)
|
folderName := strings.TrimPrefix(name, rootDir)
|
||||||
folderName = strings.TrimPrefix(folderName, string(os.PathSeparator))
|
folderName = strings.TrimPrefix(folderName, string(os.PathSeparator))
|
||||||
|
|
||||||
// Only fetcher the torrent folders once
|
// Only fetcher the torrent folders once
|
||||||
children := h.getTorrentsFolders()
|
children := h.getTorrentsFolders(parent)
|
||||||
|
|
||||||
return &File{
|
return &File{
|
||||||
cache: h.cache,
|
cache: h.cache,
|
||||||
@@ -167,8 +175,9 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
|||||||
|
|
||||||
_path := strings.TrimPrefix(name, rootDir)
|
_path := strings.TrimPrefix(name, rootDir)
|
||||||
parts := strings.Split(strings.TrimPrefix(_path, string(os.PathSeparator)), string(os.PathSeparator))
|
parts := strings.Split(strings.TrimPrefix(_path, string(os.PathSeparator)), string(os.PathSeparator))
|
||||||
|
parentFolder, _ := url.QueryUnescape(parts[0])
|
||||||
|
|
||||||
if len(parts) >= 2 && (utils.Contains(h.getParentItems(), parts[0])) {
|
if len(parts) >= 2 && (utils.Contains(h.getParentItems(), parentFolder)) {
|
||||||
|
|
||||||
torrentName := parts[1]
|
torrentName := parts[1]
|
||||||
cachedTorrent := h.cache.GetTorrentByName(torrentName)
|
cachedTorrent := h.cache.GetTorrentByName(torrentName)
|
||||||
@@ -270,7 +279,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
r.Header.Set("Depth", "1")
|
r.Header.Set("Depth", "1")
|
||||||
}
|
}
|
||||||
|
|
||||||
if served := h.serveFromCacheIfValid(w, r, cleanPath); served {
|
cacheKey := fmt.Sprintf("%s:%s", cleanPath, r.Header.Get("Depth"))
|
||||||
|
|
||||||
|
if served := h.serveFromCacheIfValid(w, r, cacheKey); served {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,11 +298,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
handler.ServeHTTP(responseRecorder, r)
|
handler.ServeHTTP(responseRecorder, r)
|
||||||
responseData := responseRecorder.Body.Bytes()
|
responseData := responseRecorder.Body.Bytes()
|
||||||
gzippedData := request.Gzip(responseData, &h.gzipPool)
|
gzippedData := request.Gzip(responseData)
|
||||||
|
|
||||||
// Create compressed version
|
// Create compressed version
|
||||||
|
|
||||||
h.cache.PropfindResp.Store(cleanPath, debrid.PropfindResponse{
|
h.cache.PropfindResp.Store(cacheKey, debrid.PropfindResponse{
|
||||||
Data: responseData,
|
Data: responseData,
|
||||||
GzippedData: gzippedData,
|
GzippedData: gzippedData,
|
||||||
Ts: time.Now(),
|
Ts: time.Now(),
|
||||||
@@ -444,27 +455,27 @@ func getContentType(fileName string) string {
|
|||||||
return contentType
|
return contentType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) isParentPath(urlPath string) bool {
|
func (h *Handler) isParentPath(urlPath string) (string, bool) {
|
||||||
parents := h.getParentItems()
|
parents := h.getParentItems()
|
||||||
lastComponent := path.Base(urlPath)
|
lastComponent := path.Base(urlPath)
|
||||||
for _, p := range parents {
|
for _, p := range parents {
|
||||||
if p == lastComponent {
|
if p == lastComponent {
|
||||||
return true
|
return p, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) serveFromCacheIfValid(w http.ResponseWriter, r *http.Request, urlPath string) bool {
|
func (h *Handler) serveFromCacheIfValid(w http.ResponseWriter, r *http.Request, key string) bool {
|
||||||
respCache, ok := h.cache.PropfindResp.Load(urlPath)
|
respCache, ok := h.cache.PropfindResp.Load(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
ttl := h.getCacheTTL(urlPath)
|
ttl := h.getCacheTTL(r.URL.Path)
|
||||||
|
|
||||||
if time.Since(respCache.Ts) >= ttl {
|
if time.Since(respCache.Ts) >= ttl {
|
||||||
h.cache.PropfindResp.Delete(urlPath)
|
h.cache.PropfindResp.Delete(key)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// getName: Returns the torrent name and filename from the path
|
// getName: Returns the torrent name and filename from the path
|
||||||
// /webdav/alldebrid/__all__/TorrentName
|
|
||||||
func getName(rootDir, path string) (string, string) {
|
func getName(rootDir, path string) (string, string) {
|
||||||
path = strings.TrimPrefix(path, rootDir)
|
path = strings.TrimPrefix(path, rootDir)
|
||||||
parts := strings.Split(strings.TrimPrefix(path, string(os.PathSeparator)), string(os.PathSeparator))
|
parts := strings.Split(strings.TrimPrefix(path, string(os.PathSeparator)), string(os.PathSeparator))
|
||||||
@@ -34,7 +33,7 @@ func isValidURL(str string) bool {
|
|||||||
// use a short TTL.
|
// use a short TTL.
|
||||||
// - Otherwise, for deeper (torrent folder) paths, use a longer TTL.
|
// - Otherwise, for deeper (torrent folder) paths, use a longer TTL.
|
||||||
func (h *Handler) getCacheTTL(urlPath string) time.Duration {
|
func (h *Handler) getCacheTTL(urlPath string) time.Duration {
|
||||||
if h.isParentPath(urlPath) {
|
if _, ok := h.isParentPath(urlPath); ok {
|
||||||
return 30 * time.Second // Short TTL for parent folders
|
return 30 * time.Second // Short TTL for parent folders
|
||||||
}
|
}
|
||||||
return 2 * time.Minute // Longer TTL for other paths
|
return 2 * time.Minute // Longer TTL for other paths
|
||||||
|
|||||||
Reference in New Issue
Block a user