package config import ( "fmt" "regexp" "sort" "strconv" "strings" "github.com/debridmediamanager/zurg/pkg/logutil" "github.com/debridmediamanager/zurg/pkg/utils" "gopkg.in/vansante/go-ffprobe.v2" "gopkg.in/yaml.v3" ) const ( ALL_TORRENTS = "__all__" UNPLAYABLE_TORRENTS = "__unplayable__" DUMPED_TORRENTS = "__dump__" DOWNLOADS = "__downloads__" ) func loadV1Config(content []byte, log *logutil.Logger) (*ZurgConfigV1, error) { var configV1 ZurgConfigV1 if err := yaml.Unmarshal(content, &configV1); err != nil { return nil, err } // don't log token and password bufToken := configV1.Token configV1.Token = strings.Repeat("*", len(bufToken)-4) + bufToken[len(bufToken)-4:] bufPassword := configV1.Password configV1.Password = strings.Repeat("*", len(bufPassword)) log.Debugf("Config dump: %+v", configV1) configV1.Token = bufToken configV1.Password = bufPassword configV1.log = log return &configV1, nil } func (z *ZurgConfigV1) GetVersion() string { return "v1" } func (z *ZurgConfigV1) GetDirectories() []string { rootDirectories := make([]string, len(z.Directories)+3) i := 0 for directory := range z.Directories { rootDirectories[i] = directory i++ } rootDirectories[i] = ALL_TORRENTS rootDirectories[i+1] = UNPLAYABLE_TORRENTS rootDirectories[i+2] = DUMPED_TORRENTS return rootDirectories } func (z *ZurgConfigV1) GetDirectoryConfig(directory string) DirectoryV1 { if dirCfg, ok := z.Directories[directory]; ok { return *dirCfg } return DirectoryV1{} } func (z *ZurgConfigV1) GetGroupMap() map[string][]string { var groupMap = make(map[string][]string) var groupOrderMap = make(map[string]int) // To store GroupOrder for each directory // Populate the groupMap and groupOrderMap for directory, val := range z.Directories { if val.Group == "" { val.Group = directory } groupMap[val.Group] = append(groupMap[val.Group], directory) groupOrderMap[directory] = val.GroupOrder } // Sort the slice based on GroupOrder and then directory name for deterministic order for group, dirs := range groupMap { sort.Slice(dirs, func(i, j int) bool { if groupOrderMap[dirs[i]] == groupOrderMap[dirs[j]] { return dirs[i] < dirs[j] // Use directory name as secondary sort criterion } return groupOrderMap[dirs[i]] < groupOrderMap[dirs[j]] }) groupMap[group] = dirs } // Return a deep copy of the map result := make(map[string][]string) for k, v := range groupMap { temp := make([]string, len(v)) copy(temp, v) result[k] = temp // result[k] = append(result[k], UNPLAYABLE_TORRENTS) } result[ALL_TORRENTS] = []string{ALL_TORRENTS} // Add special group for all torrents return result } func (z *ZurgConfigV1) MeetsConditions(directory, torrentName string, torrentSize int64, torrentIDs, fileNames []string, fileSizes []int64, mediaInfos []*ffprobe.ProbeData) bool { if directory == ALL_TORRENTS { return true } if _, ok := z.Directories[directory]; !ok { return false } for _, filter := range z.Directories[directory].Filters { if z.matchFilter(torrentName, torrentSize, torrentIDs, fileNames, fileSizes, mediaInfos, filter) { return true } } return false } func (z *ZurgConfigV1) matchFilter(torrentName string, torrentSize int64, torrentIDs, fileNames []string, fileSizes []int64, mediaInfos []*ffprobe.ProbeData, filter *FilterConditionsV1) bool { if filter.ID != "" { for _, torrentID := range torrentIDs { if torrentID == filter.ID { return true } } } if filter.RegexStr != "" { regex, err := compilePattern(filter.RegexStr) if err != nil { z.log.Errorf("Failed to compile regex %s error: %v", filter.RegexStr, err) return false } if regex.MatchString(torrentName) { return true } } if filter.NotRegexStr != "" { regex, err := compilePattern(filter.NotRegexStr) if err != nil { z.log.Errorf("Failed to compile not_regex %s error: %v", filter.NotRegexStr, err) return false } 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 filter.SizeGreaterThanOrEqual > 0 && torrentSize >= filter.SizeGreaterThanOrEqual { return true } if filter.SizeLessThanOrEqual > 0 && torrentSize <= filter.SizeLessThanOrEqual { return true } if len(filter.And) > 0 { andResult := true for _, andFilter := range filter.And { andResult = andResult && z.matchFilter(torrentName, torrentSize, torrentIDs, fileNames, fileSizes, mediaInfos, andFilter) if !andResult { return false } } return true } if len(filter.Or) > 0 { for _, orFilter := range filter.Or { if z.matchFilter(torrentName, torrentSize, torrentIDs, fileNames, fileSizes, mediaInfos, orFilter) { return true } } } if filter.FileInsideRegexStr != "" { regex, err := compilePattern(filter.FileInsideRegexStr) if err != nil { z.log.Errorf("Failed to compile any_file_inside_regex %s error: %v", filter.FileInsideRegexStr, err) return false } for _, filename := range fileNames { if regex.MatchString(filename) { return true } } } if filter.FileInsideNotRegexStr != "" { regex, err := compilePattern(filter.FileInsideNotRegexStr) if err != nil { z.log.Errorf("Failed to compile any_file_inside_not_regex %s error: %v", filter.FileInsideNotRegexStr, err) return false } for _, filename := range fileNames { if !regex.MatchString(filename) { return true } } return false } if filter.FileInsideContains != "" { for _, filename := range fileNames { if strings.Contains(strings.ToLower(filename), strings.ToLower(filter.FileInsideContains)) { return true } } return false } if filter.FileInsideNotContains != "" { for _, filename := range fileNames { if !strings.Contains(strings.ToLower(filename), strings.ToLower(filter.FileInsideNotContains)) { return true } } return false } if filter.FileInsideContainsStrict != "" { for _, filename := range fileNames { if strings.Contains(filename, filter.FileInsideContainsStrict) { return true } } return false } if filter.FileInsideNotContainsStrict != "" { for _, filename := range fileNames { if !strings.Contains(filename, filter.FileInsideNotContainsStrict) { return true } } return false } if filter.HasEpisodes { regexes := []*regexp.Regexp{ regexp.MustCompile(`(?i)s\d\d\d?.?e\d\d\d?`), regexp.MustCompile(`(?i)seasons?\s?\d+`), regexp.MustCompile(`(?i)episodes?\s?\d+`), regexp.MustCompile(`(?i)[a-fA-F0-9]{8}`), } for _, regex := range regexes { if regex.MatchString(torrentName) { return true } for _, filename := range fileNames { if regex.MatchString(filename) { return true } } } //remove resolution from filenames regex := regexp.MustCompile(`(?i)((720|1080|2160|480|360|240|144)[pi]|\d{3,4}x\d{3,4})`) for i, filename := range fileNames { fileNames[i] = regex.ReplaceAllString(filename, "") } // remove year from filenames regex = regexp.MustCompile(`(19\d|20[0-2])\d`) for i, filename := range fileNames { fileNames[i] = regex.ReplaceAllString(filename, "") } // remove filenames with "extras" in name regex = regexp.MustCompile(`(?i)extras`) for i, filename := range fileNames { if regex.MatchString(filename) { fileNames[i] = "" } } return checkArithmeticSequenceInFilenames(fileNames) } if filter.IsMusic { musicExts := []string{".m3u", ".mp3", ".flac"} for _, filename := range fileNames { for _, ext := range musicExts { if strings.HasSuffix(strings.ToLower(filename), ext) { return true } } } return false } if filter.FileInsideSizeGreaterThanOrEqual > 0 { for _, fileSize := range fileSizes { if fileSize >= filter.FileInsideSizeGreaterThanOrEqual { return true } } } if filter.FileInsideSizeLessThanOrEqual > 0 { for _, fileSize := range fileSizes { if fileSize <= filter.FileInsideSizeLessThanOrEqual { return true } } } // media info filters if filter.MediaInfoResolution != "" { for _, mediaInfo := range mediaInfos { for _, stream := range mediaInfo.Streams { if stream.CodecType != "video" || stream.Level <= 0 { continue } else if (stream.Width >= 7680 || stream.Height >= 4320) && filter.MediaInfoResolution == "8k" { return true } else if ((stream.Width < 7680 && stream.Width >= 3840) || (stream.Height < 4320 && stream.Height >= 2160)) && filter.MediaInfoResolution == "4k" { return true } else if ((stream.Width < 3840 && stream.Width >= 1920) || (stream.Height < 2160 && stream.Height >= 1080)) && filter.MediaInfoResolution == "1080p" { return true } else if ((stream.Width < 1920 && stream.Width >= 1280) || (stream.Height < 1080 && stream.Height >= 720)) && filter.MediaInfoResolution == "720p" { return true } else if ((stream.Width < 1280 && stream.Width >= 854) || (stream.Height < 720 && stream.Height >= 480)) && filter.MediaInfoResolution == "480p" { return true } else if ((stream.Width < 854 && stream.Width >= 640) || (stream.Height < 480 && stream.Height >= 360)) && filter.MediaInfoResolution == "360p" { return true } else if ((stream.Width < 640 && stream.Width >= 426) || (stream.Height < 360 && stream.Height >= 240)) && filter.MediaInfoResolution == "240p" { return true } else if ((stream.Width < 426 && stream.Width >= 256) || (stream.Height < 240 && stream.Height >= 144)) && filter.MediaInfoResolution == "144p" { return true } } } return false } if filter.MediaInfoBitRateGreaterThanOrEqual > 0 { for _, mediaInfo := range mediaInfos { bitrate, err := strconv.ParseInt(mediaInfo.Format.BitRate, 10, 64) if err != nil { continue } if bitrate >= filter.MediaInfoBitRateGreaterThanOrEqual { return true } } return false } if filter.MediaInfoVideoBitRateGreaterThanOrEqual > 0 { for _, mediaInfo := range mediaInfos { for _, stream := range mediaInfo.Streams { if stream.CodecType != "video" || stream.Level <= 0 { continue } bitrate, err := strconv.ParseInt(stream.BitRate, 10, 64) if err != nil { continue } if bitrate >= filter.MediaInfoVideoBitRateGreaterThanOrEqual { return true } } } return false } if filter.MediaInfoAudioBitRateGreaterThanOrEqual > 0 { for _, mediaInfo := range mediaInfos { for _, stream := range mediaInfo.Streams { if stream.CodecType != "audio" { continue } bitrate, err := strconv.ParseInt(stream.BitRate, 10, 64) if err != nil { continue } if bitrate >= filter.MediaInfoAudioBitRateGreaterThanOrEqual { return true } } } return false } if filter.MediaInfoDurationGreaterThanOrEqual > 0 { for _, mediaInfo := range mediaInfos { if int64(mediaInfo.Format.DurationSeconds) >= filter.MediaInfoDurationGreaterThanOrEqual { return true } } return false } if filter.MediaInfoWithAudioLanguage != "" { for _, mediaInfo := range mediaInfos { for _, stream := range mediaInfo.Streams { if stream.CodecType != "audio" { continue } if strings.EqualFold(stream.Tags.Language, filter.MediaInfoWithAudioLanguage) { return true } } } return false } if filter.MediaInfoWithoutAudioLanguage != "" { for _, mediaInfo := range mediaInfos { for _, stream := range mediaInfo.Streams { if stream.CodecType != "audio" { continue } if strings.EqualFold(stream.Tags.Language, filter.MediaInfoWithoutAudioLanguage) { return false } } } return true } if filter.MediaInfoWithSubtitleLanguage != "" { for _, mediaInfo := range mediaInfos { for _, stream := range mediaInfo.Streams { if stream.CodecType != "subtitle" { continue } if strings.EqualFold(stream.Tags.Language, filter.MediaInfoWithSubtitleLanguage) { return true } } } return false } if filter.MediaInfoWithoutSubtitleLanguage != "" { for _, mediaInfo := range mediaInfos { for _, stream := range mediaInfo.Streams { if stream.CodecType != "subtitle" { continue } if strings.EqualFold(stream.Tags.Language, filter.MediaInfoWithoutSubtitleLanguage) { return false } } } return true } return false } func compilePattern(pattern string) (*regexp.Regexp, error) { flags := map[rune]string{ 'i': "(?i)", 'm': "(?m)", 's': "(?s)", 'x': "(?x)", } lastSlash := strings.LastIndex(pattern, "/") secondLastSlash := strings.LastIndex(pattern[:lastSlash], "/") // Extract the core pattern corePattern := pattern[secondLastSlash+1 : lastSlash] // Extract and process flags flagSection := pattern[lastSlash+1:] flagString := "" processedFlags := make(map[rune]bool) for _, flag := range flagSection { if replacement, ok := flags[flag]; ok && !processedFlags[flag] { flagString += replacement processedFlags[flag] = true } } // Combine the processed flags with the core pattern finalPattern := flagString + corePattern // Validate pattern if finalPattern == "" || finalPattern == flagString { return nil, fmt.Errorf("invalid regex pattern: %s", pattern) } return regexp.Compile(finalPattern) } func hasIncreasingSequence(arr []int) bool { if len(arr) < 3 { return false } for i := 0; i < len(arr)-2; i++ { if arr[i] < arr[i+1] && arr[i+1] < arr[i+2] { return true } } return false } func checkArithmeticSequenceInFilenames(files []string) bool { if len(files) < 3 { return false } r := regexp.MustCompile(`\d+`) for _, file := range files { if !utils.IsVideo(file) { continue } matches := r.FindAllStringIndex(file, -1) for _, match := range matches { numSet := make(map[int]struct{}) for _, file := range files { if !utils.IsVideo(file) { continue } if match[0] >= 0 && match[1] <= len(file) { num, err := strconv.Atoi(file[match[0]:match[1]]) if err == nil { numSet[num] = struct{}{} } } else { // out of bounds, ignore continue } } numList := make([]int, 0, len(numSet)) for num := range numSet { numList = append(numList, num) } sort.Ints(numList) if hasIncreasingSequence(numList) { return true } } } return false }