From 4650213218a976eb6536aad85ba6b7109ba83946 Mon Sep 17 00:00:00 2001 From: Ben Sarmiento Date: Wed, 18 Oct 2023 21:09:25 +0200 Subject: [PATCH] Add support for configs --- .gitignore | 3 ++ config.yml | 38 ++++++++++++++ config.yml.example | 35 +++++++++++++ go.mod | 2 + go.sum | 3 ++ internal/config/load.go | 34 ++++++++++++ internal/config/types.go | 5 ++ internal/config/v1.go | 100 ++++++++++++++++++++++++++++++++++++ internal/config/v1types.go | 22 ++++++++ internal/dav/getfile.go | 3 +- internal/dav/propfind.go | 78 ++++++++++++++++++++-------- internal/dav/response.go | 14 ++--- internal/dav/router.go | 13 +++-- internal/dav/util.go | 6 +-- internal/torrent/manager.go | 45 +++++++++++++--- internal/torrent/types.go | 1 + pkg/dav/response.go | 1 - pkg/dav/types.go | 2 - pkg/realdebrid/types.go | 2 +- 19 files changed, 359 insertions(+), 48 deletions(-) create mode 100644 config.yml create mode 100644 config.yml.example create mode 100644 internal/config/load.go create mode 100644 internal/config/types.go create mode 100644 internal/config/v1.go create mode 100644 internal/config/v1types.go diff --git a/.gitignore b/.gitignore index cb3100c..dd3533c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,7 @@ go.work data/ + +# Executables rclone +main diff --git a/config.yml b/config.yml new file mode 100644 index 0000000..33515d4 --- /dev/null +++ b/config.yml @@ -0,0 +1,38 @@ +zurg: v1 + +directories: + torrents: + group: asd + filters: + - not_contains: xxx + - not_contains_strict: trailer + + shows: + group: sdf + filters: + - regex: /season/i + - regex: /Season/i + - regex: /Saison/i + + remuxes: + group: gdfg + filters: + - contains: remux + + "dolby vision": + group: zxc + filters: + - and: + - regex: /\bdovi\b/i + - contains: 4k + kids: + group: wqe + filters: + - id: XFPQ5UCMUVAEG + - id: VDRPYNRPQHEXC + - id: YELNX3XR5XJQM + + default: + group: xcv + filters: + - regex: /.*/ diff --git a/config.yml.example b/config.yml.example new file mode 100644 index 0000000..446db2f --- /dev/null +++ b/config.yml.example @@ -0,0 +1,35 @@ +zurg: v1 + +directories: + torrents: + or: + - regex: /.*/ # you can specify multiple conditions and it works as an OR + - contains: season # use 'contains' for string, case insensitive + - contains_strict: season # case sensitive + - and: + - not_contains: xxx + - not_contains_strict: trailer + shows: + or: + - regex: /season/i # you can specify multiple regex conditions and it works as an OR + - regex: /Season/i + - regex: /Saison/i + "4k content": + regex: /\b4k\b/i + "dolby vision": + and: + regex: /\bdovi\b/i + contains: 4k + kids: + or: + - id: XFPQ5UCMUVAEG # you can specify the torrent ID as well + - id: VDRPYNRPQHEXC + - id: YELNX3XR5XJQM + default: + regex: /.*/ # if duplicates=false, best practice to add a catch-all bucket + +duplicates: true +# if true, it means as long as a torrent satisfies the conditions set, then it will appear on that directory +# if false, it follows the order of directories and only appears once in one of them (or none so be careful) +# it processes each movie to see if it meets DIR1 conditions +# if not, it checks for DIR2 conditions, and so on... diff --git a/go.mod b/go.mod index acd30ad..41a3792 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/debridmediamanager.com/zurg go 1.21.3 + +require gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index e69de29..4bc0337 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,3 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/load.go b/internal/config/load.go new file mode 100644 index 0000000..9b2cf15 --- /dev/null +++ b/internal/config/load.go @@ -0,0 +1,34 @@ +package config + +import ( + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +type ConfigInterface interface { + GetVersion() string + GetDirectories() []string + MeetsConditions(directory, fileID, fileName string) bool +} + +func LoadZurgConfig(filename string) (ConfigInterface, error) { + content, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + var initialConfig ZurgConfig + if err := yaml.Unmarshal(content, &initialConfig); err != nil { + return nil, err + } + + switch initialConfig.Version { + case "v1": + return loadV1Config(content) + + default: + return nil, fmt.Errorf("invalid config version: %s", initialConfig.Version) + } +} diff --git a/internal/config/types.go b/internal/config/types.go new file mode 100644 index 0000000..77afb9a --- /dev/null +++ b/internal/config/types.go @@ -0,0 +1,5 @@ +package config + +type ZurgConfig struct { + Version string `yaml:"zurg"` +} diff --git a/internal/config/v1.go b/internal/config/v1.go new file mode 100644 index 0000000..9b8fc57 --- /dev/null +++ b/internal/config/v1.go @@ -0,0 +1,100 @@ +package config + +import ( + "regexp" + "strings" + + "gopkg.in/yaml.v3" +) + +func loadV1Config(content []byte) (*ZurgConfigV1, error) { + var configV1 ZurgConfigV1 + if err := yaml.Unmarshal(content, &configV1); err != nil { + return nil, err + } + return &configV1, nil +} + +func (z *ZurgConfigV1) GetVersion() string { + return z.Version +} + +func (z *ZurgConfigV1) GetDirectories() []string { + var rootDirectories []string + for directory := range z.Directories { + rootDirectories = append(rootDirectories, directory) + } + return rootDirectories +} + +func (z *ZurgConfigV1) GetGroupMap() map[string][]string { + var groupMap = make(map[string][]string) + for directory, val := range z.Directories { + groupMap[val.Group] = append(groupMap[val.Group], directory) + } + return groupMap +} + +func (z *ZurgConfigV1) GetDirectoriesByGroup(group string) []string { + var groupDirs []string + for directory := range z.Directories { + if z.Directories[directory].Group == group { + groupDirs = append(groupDirs, directory) + } + } + return groupDirs +} + +func (z *ZurgConfigV1) MeetsConditions(directory, fileID, torrentName string) bool { + if _, ok := z.Directories[directory]; !ok { + return false + } + for _, filter := range z.Directories[directory].Filters { + if z.matchFilter(fileID, torrentName, filter) { + return true + } + } + return false +} + +func (z *ZurgConfigV1) matchFilter(fileID, torrentName string, filter *FilterConditionsV1) bool { + if filter.ID != "" && fileID == filter.ID { + return true + } + if filter.RegexStr != "" { + regex := regexp.MustCompile(filter.RegexStr) + if regex.MatchString(torrentName) { + return true + } + } + if filter.ContainsStrict != "" && strings.Contains(torrentName, filter.ContainsStrict) { + return true + } + if filter.Contains != "" && strings.Contains(strings.ToLower(torrentName), strings.ToLower(filter.Contains)) { + return true + } + if filter.NotContainsStrict != "" && !strings.Contains(torrentName, filter.NotContainsStrict) { + return true + } + if filter.NotContains != "" && !strings.Contains(strings.ToLower(torrentName), strings.ToLower(filter.NotContains)) { + return true + } + if len(filter.And) > 0 { + andResult := true + for _, andFilter := range filter.And { + andResult = andResult && z.matchFilter(fileID, torrentName, andFilter) + if !andResult { + return false + } + } + return true + } + if len(filter.Or) > 0 { + for _, orFilter := range filter.Or { + if z.matchFilter(fileID, torrentName, orFilter) { + return true + } + } + } + return false +} diff --git a/internal/config/v1types.go b/internal/config/v1types.go new file mode 100644 index 0000000..0d3e43b --- /dev/null +++ b/internal/config/v1types.go @@ -0,0 +1,22 @@ +package config + +type ZurgConfigV1 struct { + ZurgConfig + Directories map[string]*DirectoryFilterConditionsV1 `yaml:"directories"` + Duplicates bool `yaml:"duplicates"` +} +type DirectoryFilterConditionsV1 struct { + Group string `yaml:"group"` + Filters []*FilterConditionsV1 `yaml:"filters"` +} + +type FilterConditionsV1 struct { + RegexStr string `yaml:"regex"` + Contains string `yaml:"contains"` + ContainsStrict string `yaml:"contains_strict"` + NotContains string `yaml:"not_contains"` + NotContainsStrict string `yaml:"not_contains_strict"` + ID string `yaml:"id"` + And []*FilterConditionsV1 `yaml:"and"` + Or []*FilterConditionsV1 `yaml:"or"` +} diff --git a/internal/dav/getfile.go b/internal/dav/getfile.go index 87f4f09..6c7f2e5 100644 --- a/internal/dav/getfile.go +++ b/internal/dav/getfile.go @@ -27,10 +27,11 @@ func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torrent } // Get the last two segments + baseDirectory := segments[len(segments)-3] torrentName := segments[len(segments)-2] filename := segments[len(segments)-1] - torrents := findAllTorrentsWithName(t, torrentName) + torrents := findAllTorrentsWithName(t, baseDirectory, torrentName) if torrents == nil { log.Println("Cannot find torrent", torrentName) http.Error(w, "Cannot find file", http.StatusNotFound) diff --git a/internal/dav/propfind.go b/internal/dav/propfind.go index 7ef3701..db50625 100644 --- a/internal/dav/propfind.go +++ b/internal/dav/propfind.go @@ -6,23 +6,40 @@ import ( "log" "net/http" "path" + "strings" + "github.com/debridmediamanager.com/zurg/internal/config" "github.com/debridmediamanager.com/zurg/internal/torrent" "github.com/debridmediamanager.com/zurg/pkg/dav" ) // HandlePropfindRequest handles a PROPFIND request -func HandlePropfindRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager) { +func HandlePropfindRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface) { var output []byte var err error requestPath := path.Clean(r.URL.Path) - if requestPath == "/" { - output, err = handleRoot(w, r) - } else if requestPath == "/torrents" { - output, err = handleListOfTorrents(w, r, t) - } else { - output, err = handleSingleTorrent(w, r, t) + pathSegments := strings.Split(requestPath, "/") + + // Remove empty segments caused by leading or trailing slashes + filteredSegments := make([]string, 0, len(pathSegments)) + for _, segment := range pathSegments { + if segment != "" { + filteredSegments = append(filteredSegments, segment) + } + } + + switch len(filteredSegments) { + case 0: // Just the root "/" + output, err = handleRoot(w, r, c) + case 1: // It's just the basedir e.g. "/basedir" + output, err = handleListOfTorrents(requestPath, w, r, t, c) + case 2: // It's a specific torrent under a basedir e.g. "/basedir/torrentname/" + output, err = handleSingleTorrent(requestPath, w, r, t) + default: + // Handle any other paths, e.g., send a 404 Not Found response + http.Error(w, "Not Found", http.StatusNotFound) + return } if err != nil { log.Printf("Cannot marshal xml: %v\n", err.Error()) @@ -38,23 +55,39 @@ func HandlePropfindRequest(w http.ResponseWriter, r *http.Request, t *torrent.To } // handleRoot handles a PROPFIND request to the root directory -func handleRoot(w http.ResponseWriter, r *http.Request) ([]byte, error) { +func handleRoot(w http.ResponseWriter, r *http.Request, c config.ConfigInterface) ([]byte, error) { + var responses []dav.Response + responses = append(responses, dav.Directory("/")) + for _, directory := range c.GetDirectories() { + responses = append(responses, dav.Directory(fmt.Sprintf("/%s", directory))) + } rootResponse := dav.MultiStatus{ - XMLNS: "DAV:", - Response: []dav.Response{ - dav.Directory("/"), - dav.Directory("/torrents"), - }, + XMLNS: "DAV:", + Response: responses, } return xml.MarshalIndent(rootResponse, "", " ") } // handleListOfTorrents handles a PROPFIND request to the /torrents directory -func handleListOfTorrents(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager) ([]byte, error) { - torrents := t.GetAll() - resp, err := createMultiTorrentResponse(torrents) +func handleListOfTorrents(requestPath string, w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface) ([]byte, error) { + basePath := path.Base(requestPath) + + found := false + for _, directory := range c.GetDirectories() { + if basePath == directory { + found = true + } + } + if !found { + log.Println("Cannot find directory", requestPath) + http.Error(w, "Cannot find directory", http.StatusNotFound) + return nil, nil + } + + torrents := t.GetByDirectory(basePath) + resp, err := createMultiTorrentResponse(fmt.Sprintf("/%s", basePath), torrents) if err != nil { - log.Printf("Cannot read directory (/torrents): %v\n", err.Error()) + log.Printf("Cannot read directory (%s): %v\n", basePath, err.Error()) http.Error(w, "Cannot read directory", http.StatusInternalServerError) return nil, nil } @@ -62,17 +95,18 @@ func handleListOfTorrents(w http.ResponseWriter, r *http.Request, t *torrent.Tor } // handleSingleTorrent handles a PROPFIND request to a single torrent directory -func handleSingleTorrent(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager) ([]byte, error) { - requestPath := path.Clean(r.URL.Path) +func handleSingleTorrent(requestPath string, w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager) ([]byte, error) { + basePath := path.Dir(requestPath) + torrentName := path.Base(requestPath) - torrents := findAllTorrentsWithName(t, torrentName) - if len(torrents) == 0 { + sameNameTorrents := findAllTorrentsWithName(t, basePath, torrentName) + if len(sameNameTorrents) == 0 { log.Println("Cannot find directory", requestPath) http.Error(w, "Cannot find directory", http.StatusNotFound) return nil, nil } var resp *dav.MultiStatus - resp, err := createCombinedTorrentResponse(torrents, t) + resp, err := createSingleTorrentResponse(fmt.Sprintf("/%s", basePath), sameNameTorrents, t) if err != nil { log.Printf("Cannot read directory (%s): %v\n", requestPath, err.Error()) http.Error(w, "Cannot read directory", http.StatusInternalServerError) diff --git a/internal/dav/response.go b/internal/dav/response.go index 65254ff..d026863 100644 --- a/internal/dav/response.go +++ b/internal/dav/response.go @@ -9,9 +9,9 @@ import ( ) // createMultiTorrentResponse creates a WebDAV response for a list of torrents -func createMultiTorrentResponse(torrents []torrent.Torrent) (*dav.MultiStatus, error) { +func createMultiTorrentResponse(basePath string, torrents []torrent.Torrent) (*dav.MultiStatus, error) { var responses []dav.Response - responses = append(responses, dav.Directory("/torrents")) + responses = append(responses, dav.Directory(basePath)) seen := make(map[string]bool) @@ -19,12 +19,12 @@ func createMultiTorrentResponse(torrents []torrent.Torrent) (*dav.MultiStatus, e if item.Progress != 100 { continue } - if _, exists := seen[item.Filename]; exists { + if _, exists := seen[item.Name]; exists { continue } - seen[item.Filename] = true + seen[item.Name] = true - path := filepath.Join("/torrents", item.Filename) + path := filepath.Join(basePath, item.Name) responses = append(responses, dav.Directory(path)) } @@ -36,10 +36,10 @@ func createMultiTorrentResponse(torrents []torrent.Torrent) (*dav.MultiStatus, e // createTorrentResponse creates a WebDAV response for a single torrent // but it also handles the case where there are many torrents with the same name -func createCombinedTorrentResponse(torrents []torrent.Torrent, t *torrent.TorrentManager) (*dav.MultiStatus, error) { +func createSingleTorrentResponse(basePath string, torrents []torrent.Torrent, t *torrent.TorrentManager) (*dav.MultiStatus, error) { var responses []dav.Response // initial response is the directory itself - currentPath := filepath.Join("/torrents", torrents[0].Filename) + currentPath := filepath.Join(basePath, torrents[0].Name) responses = append(responses, dav.Directory(currentPath)) seen := make(map[string]bool) diff --git a/internal/dav/router.go b/internal/dav/router.go index c569c7a..79f8f22 100644 --- a/internal/dav/router.go +++ b/internal/dav/router.go @@ -5,20 +5,23 @@ import ( "net/http" "os" + "github.com/debridmediamanager.com/zurg/internal/config" "github.com/debridmediamanager.com/zurg/internal/torrent" ) // Router creates a WebDAV router func Router(mux *http.ServeMux) { - t := torrent.NewTorrentManager(os.Getenv("RD_TOKEN")) + c, err := config.LoadZurgConfig("./config.yml") + if err != nil { + log.Panicf("Config failed to load: %v", err) + } + + t := torrent.NewTorrentManager(os.Getenv("RD_TOKEN"), c) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - // requestPath := path.Clean(r.URL.Path) - // log.Println(r.Method, requestPath) - switch r.Method { case "PROPFIND": - HandlePropfindRequest(w, r, t) + HandlePropfindRequest(w, r, t, c) case http.MethodGet: HandleGetRequest(w, r, t) diff --git a/internal/dav/util.go b/internal/dav/util.go index 2d645ff..c50bc5f 100644 --- a/internal/dav/util.go +++ b/internal/dav/util.go @@ -19,12 +19,12 @@ func convertDate(input string) string { } // findAllTorrentsWithName finds all torrents with a given name -func findAllTorrentsWithName(t *torrent.TorrentManager, filename string) []torrent.Torrent { +func findAllTorrentsWithName(t *torrent.TorrentManager, directory, torrentName string) []torrent.Torrent { var matchingTorrents []torrent.Torrent - torrents := t.GetAll() + torrents := t.GetByDirectory(directory) for _, torrent := range torrents { - if torrent.Filename == filename || strings.HasPrefix(torrent.Filename, filename) { + if torrent.Name == torrentName || strings.HasPrefix(torrent.Name, torrentName) { matchingTorrents = append(matchingTorrents, torrent) } } diff --git a/internal/torrent/manager.go b/internal/torrent/manager.go index b17b6cb..bb57cc0 100644 --- a/internal/torrent/manager.go +++ b/internal/torrent/manager.go @@ -7,6 +7,7 @@ import ( "strings" "sync" + "github.com/debridmediamanager.com/zurg/internal/config" "github.com/debridmediamanager.com/zurg/pkg/realdebrid" ) @@ -14,15 +15,17 @@ type TorrentManager struct { token string torrents []Torrent workerPool chan bool + config config.ConfigInterface } // NewTorrentManager creates a new torrent manager // it will fetch all torrents and their info in the background // and store them in-memory -func NewTorrentManager(token string) *TorrentManager { +func NewTorrentManager(token string, config config.ConfigInterface) *TorrentManager { handler := &TorrentManager{ token: token, workerPool: make(chan bool, 10), + config: config, } handler.torrents = handler.getAll() @@ -40,18 +43,36 @@ func NewTorrentManager(token string) *TorrentManager { } func (t *TorrentManager) getAll() []Torrent { + log.Println("Getting all torrents") torrents, err := realdebrid.GetTorrents(t.token) if err != nil { log.Printf("Cannot get torrents: %v\n", err.Error()) return nil } var torrentsV2 []Torrent - for _, torrent := range torrents { - torrentsV2 = append(torrentsV2, Torrent{ - Torrent: torrent, - SelectedFiles: nil, - }) + + version := t.config.GetVersion() + if version == "v1" { + configV1 := t.config.(*config.ZurgConfigV1) + groupMap := configV1.GetGroupMap() + for _, directories := range groupMap { + for _, torrent := range torrents { + // process grouping + torrentV2 := Torrent{ + Torrent: torrent, + SelectedFiles: nil, + } + for _, directory := range directories { + if configV1.MeetsConditions(directory, torrent.ID, torrent.Name) { + torrentV2.Directories = append(torrentV2.Directories, directory) + break + } + } + torrentsV2 = append(torrentsV2, torrentV2) + } + } } + log.Printf("Fetched %d torrents", len(torrentsV2)) return torrentsV2 } @@ -59,6 +80,18 @@ func (t *TorrentManager) GetAll() []Torrent { return t.torrents } +func (t *TorrentManager) GetByDirectory(directory string) []Torrent { + var torrents []Torrent + for _, torrent := range t.torrents { + for _, dir := range torrent.Directories { + if dir == directory { + torrents = append(torrents, torrent) + } + } + } + return torrents +} + func (t *TorrentManager) getInfo(torrentID string) *Torrent { torrentFromFile := t.readFromFile(torrentID) if torrentFromFile != nil { diff --git a/internal/torrent/types.go b/internal/torrent/types.go index a56dd6a..50b3946 100644 --- a/internal/torrent/types.go +++ b/internal/torrent/types.go @@ -4,6 +4,7 @@ import "github.com/debridmediamanager.com/zurg/pkg/realdebrid" type Torrent struct { realdebrid.Torrent + Directories []string SelectedFiles []File } diff --git a/pkg/dav/response.go b/pkg/dav/response.go index 60d65f9..58a5891 100644 --- a/pkg/dav/response.go +++ b/pkg/dav/response.go @@ -21,7 +21,6 @@ func File(path string, fileSize int64, added string, link string) Response { IsHidden: 0, CreationDate: added, LastModified: added, - Link: link, }, Status: "HTTP/1.1 200 OK", }, diff --git a/pkg/dav/types.go b/pkg/dav/types.go index 453960d..5daee06 100644 --- a/pkg/dav/types.go +++ b/pkg/dav/types.go @@ -24,8 +24,6 @@ type Prop struct { CreationDate string `xml:"d:creationdate"` LastModified string `xml:"d:getlastmodified"` IsHidden int `xml:"d:ishidden"` - Filename string `xml:"-"` - Link string `xml:"-"` } type ResourceType struct { diff --git a/pkg/realdebrid/types.go b/pkg/realdebrid/types.go index eef4dff..94c2228 100644 --- a/pkg/realdebrid/types.go +++ b/pkg/realdebrid/types.go @@ -15,7 +15,7 @@ type UnrestrictResponse struct { type Torrent struct { ID string `json:"id"` - Filename string `json:"filename"` + Name string `json:"filename"` Hash string `json:"hash"` Progress int `json:"progress"` Added string `json:"added"`