546 lines
15 KiB
Go
546 lines
15 KiB
Go
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 = utils.MaskToken(configV1.Token)
|
|
bufPassword := configV1.Password
|
|
configV1.Password = strings.Repeat("*", len(bufPassword))
|
|
|
|
// mask download tokens
|
|
bufDownloadTokens := configV1.DownloadTokens
|
|
maskedDownloadTokens := make([]string, len(configV1.DownloadTokens))
|
|
for i, token := range configV1.DownloadTokens {
|
|
maskedDownloadTokens[i] = utils.MaskToken(token)
|
|
}
|
|
configV1.DownloadTokens = maskedDownloadTokens
|
|
|
|
log.Debugf("Config dump: %+v", configV1)
|
|
|
|
// restore original values
|
|
configV1.Token = bufToken
|
|
configV1.Password = bufPassword
|
|
configV1.DownloadTokens = bufDownloadTokens
|
|
|
|
configV1.log = log
|
|
return &configV1, nil
|
|
}
|
|
|
|
func (z *ZurgConfigV1) GetVersion() string {
|
|
return "v1"
|
|
}
|
|
|
|
func (z *ZurgConfigV1) GetDirectories() []string {
|
|
rootDirectories := []string{}
|
|
for directory := range z.Directories {
|
|
rootDirectories = append(rootDirectories, directory)
|
|
}
|
|
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
|
|
}
|
|
language, err := stream.TagList.GetString("language")
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if strings.EqualFold(language, filter.MediaInfoWithAudioLanguage) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
if filter.MediaInfoWithoutAudioLanguage != "" {
|
|
for _, mediaInfo := range mediaInfos {
|
|
for _, stream := range mediaInfo.Streams {
|
|
if stream.CodecType != "audio" {
|
|
continue
|
|
}
|
|
language, err := stream.TagList.GetString("language")
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if strings.EqualFold(language, filter.MediaInfoWithoutAudioLanguage) {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
if filter.MediaInfoWithSubtitleLanguage != "" {
|
|
for _, mediaInfo := range mediaInfos {
|
|
for _, stream := range mediaInfo.Streams {
|
|
if stream.CodecType != "subtitle" {
|
|
continue
|
|
}
|
|
language, err := stream.TagList.GetString("language")
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if strings.EqualFold(language, filter.MediaInfoWithSubtitleLanguage) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
if filter.MediaInfoWithoutSubtitleLanguage != "" {
|
|
for _, mediaInfo := range mediaInfos {
|
|
for _, stream := range mediaInfo.Streams {
|
|
if stream.CodecType != "subtitle" {
|
|
continue
|
|
}
|
|
language, err := stream.TagList.GetString("language")
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if strings.EqualFold(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 := []int{}
|
|
for num := range numSet {
|
|
numList = append(numList, num)
|
|
}
|
|
sort.Ints(numList)
|
|
if hasIncreasingSequence(numList) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|