mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 00:39:13 +00:00
feat: add backend
This commit is contained in:
parent
bc418ff248
commit
a838d994a6
68 changed files with 9561 additions and 0 deletions
109
cmd/autobrr/main.go
Normal file
109
cmd/autobrr/main.go
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
_ "modernc.org/sqlite"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/action"
|
||||||
|
"github.com/autobrr/autobrr/internal/announce"
|
||||||
|
"github.com/autobrr/autobrr/internal/config"
|
||||||
|
"github.com/autobrr/autobrr/internal/database"
|
||||||
|
"github.com/autobrr/autobrr/internal/download_client"
|
||||||
|
"github.com/autobrr/autobrr/internal/filter"
|
||||||
|
"github.com/autobrr/autobrr/internal/http"
|
||||||
|
"github.com/autobrr/autobrr/internal/indexer"
|
||||||
|
"github.com/autobrr/autobrr/internal/irc"
|
||||||
|
"github.com/autobrr/autobrr/internal/logger"
|
||||||
|
"github.com/autobrr/autobrr/internal/release"
|
||||||
|
"github.com/autobrr/autobrr/internal/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
cfg config.Cfg
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var configPath string
|
||||||
|
pflag.StringVar(&configPath, "config", "", "path to configuration file")
|
||||||
|
pflag.Parse()
|
||||||
|
|
||||||
|
// read config
|
||||||
|
cfg = config.Read(configPath)
|
||||||
|
|
||||||
|
// setup logger
|
||||||
|
logger.Setup(cfg)
|
||||||
|
|
||||||
|
// if configPath is set then put database inside that path, otherwise create wherever it's run
|
||||||
|
var dataSource = database.DataSourceName(configPath, "autobrr.db")
|
||||||
|
|
||||||
|
// open database connection
|
||||||
|
db, err := sql.Open("sqlite", dataSource)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("could not open db connection")
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
if err = database.Migrate(db); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("could not migrate db")
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup repos
|
||||||
|
// var announceRepo = database.NewAnnounceRepo(db)
|
||||||
|
var (
|
||||||
|
actionRepo = database.NewActionRepo(db)
|
||||||
|
downloadClientRepo = database.NewDownloadClientRepo(db)
|
||||||
|
filterRepo = database.NewFilterRepo(db)
|
||||||
|
indexerRepo = database.NewIndexerRepo(db)
|
||||||
|
ircRepo = database.NewIrcRepo(db)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
downloadClientService = download_client.NewService(downloadClientRepo)
|
||||||
|
actionService = action.NewService(actionRepo, downloadClientService)
|
||||||
|
indexerService = indexer.NewService(indexerRepo)
|
||||||
|
filterService = filter.NewService(filterRepo, actionRepo, indexerService)
|
||||||
|
releaseService = release.NewService(actionService)
|
||||||
|
announceService = announce.NewService(filterService, indexerService, releaseService)
|
||||||
|
ircService = irc.NewService(ircRepo, announceService)
|
||||||
|
)
|
||||||
|
|
||||||
|
addr := fmt.Sprintf("%v:%v", cfg.Host, cfg.Port)
|
||||||
|
|
||||||
|
errorChannel := make(chan error)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
httpServer := http.NewServer(addr, cfg.BaseURL, actionService, downloadClientService, filterService, indexerService, ircService)
|
||||||
|
errorChannel <- httpServer.Open()
|
||||||
|
}()
|
||||||
|
|
||||||
|
srv := server.NewServer(ircService, indexerService)
|
||||||
|
srv.Hostname = cfg.Host
|
||||||
|
srv.Port = cfg.Port
|
||||||
|
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||||
|
|
||||||
|
if err := srv.Start(); err != nil {
|
||||||
|
log.Fatal().Err(err).Msg("could not start server")
|
||||||
|
}
|
||||||
|
|
||||||
|
for sig := range sigCh {
|
||||||
|
switch sig {
|
||||||
|
case syscall.SIGHUP:
|
||||||
|
log.Print("shutting down server")
|
||||||
|
os.Exit(1)
|
||||||
|
case syscall.SIGINT, syscall.SIGTERM:
|
||||||
|
log.Print("shutting down server")
|
||||||
|
//srv.Shutdown()
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
config.toml
Normal file
36
config.toml
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
# config.toml
|
||||||
|
|
||||||
|
# Hostname / IP
|
||||||
|
#
|
||||||
|
# Default: "localhost"
|
||||||
|
#
|
||||||
|
host = "127.0.0.1"
|
||||||
|
|
||||||
|
# Port
|
||||||
|
#
|
||||||
|
# Default: 8989
|
||||||
|
#
|
||||||
|
port = 8989
|
||||||
|
|
||||||
|
# Base url
|
||||||
|
# Set custom baseUrl eg /autobrr/ to serve in subdirectory.
|
||||||
|
# Not needed for subdomain, or by accessing with the :port directly.
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
#baseUrl = "/autobrr/"
|
||||||
|
|
||||||
|
# autobrr logs file
|
||||||
|
# If not defined, logs to stdout
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
#logPath = "log/autobrr.log"
|
||||||
|
|
||||||
|
# Log level
|
||||||
|
#
|
||||||
|
# Default: "DEBUG"
|
||||||
|
#
|
||||||
|
# Options: "ERROR", "DEBUG", "INFO", "WARN"
|
||||||
|
#
|
||||||
|
logLevel = "DEBUG"
|
23
go.mod
Normal file
23
go.mod
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
module github.com/autobrr/autobrr
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/anacrolix/torrent v1.29.1
|
||||||
|
github.com/fluffle/goirc v1.0.3
|
||||||
|
github.com/go-chi/chi v1.5.4
|
||||||
|
github.com/lib/pq v1.10.2
|
||||||
|
github.com/pelletier/go-toml v1.6.0 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1
|
||||||
|
github.com/rs/zerolog v1.20.0
|
||||||
|
github.com/smartystreets/assertions v1.0.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.3
|
||||||
|
github.com/spf13/viper v1.7.1
|
||||||
|
github.com/stretchr/testify v1.7.0
|
||||||
|
golang.org/x/net v0.0.0-20210427231257-85d9c07bbe3a
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||||
|
gopkg.in/irc.v3 v3.1.1
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||||
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
modernc.org/sqlite v1.12.0
|
||||||
|
)
|
278
internal/action/service.go
Normal file
278
internal/action/service.go
Normal file
|
@ -0,0 +1,278 @@
|
||||||
|
package action
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
"github.com/autobrr/autobrr/internal/download_client"
|
||||||
|
"github.com/autobrr/autobrr/pkg/qbittorrent"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const REANNOUNCE_MAX_ATTEMPTS = 30
|
||||||
|
const REANNOUNCE_INTERVAL = 7000
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
RunActions(torrentFile string, hash string, filter domain.Filter) error
|
||||||
|
Store(action domain.Action) (*domain.Action, error)
|
||||||
|
Fetch() ([]domain.Action, error)
|
||||||
|
Delete(actionID int) error
|
||||||
|
ToggleEnabled(actionID int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo domain.ActionRepo
|
||||||
|
clientSvc download_client.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo domain.ActionRepo, clientSvc download_client.Service) Service {
|
||||||
|
return &service{repo: repo, clientSvc: clientSvc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) RunActions(torrentFile string, hash string, filter domain.Filter) error {
|
||||||
|
for _, action := range filter.Actions {
|
||||||
|
if !action.Enabled {
|
||||||
|
// only run active actions
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("process action: %v", action.Name)
|
||||||
|
|
||||||
|
switch action.Type {
|
||||||
|
case domain.ActionTypeTest:
|
||||||
|
go s.test(torrentFile)
|
||||||
|
|
||||||
|
case domain.ActionTypeWatchFolder:
|
||||||
|
go s.watchFolder(action.WatchFolder, torrentFile)
|
||||||
|
|
||||||
|
case domain.ActionTypeQbittorrent:
|
||||||
|
go func() {
|
||||||
|
err := s.qbittorrent(action, hash, torrentFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("error sending torrent to client")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// deluge
|
||||||
|
// pvr *arr
|
||||||
|
// exec
|
||||||
|
default:
|
||||||
|
panic("implement me")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Store(action domain.Action) (*domain.Action, error) {
|
||||||
|
// validate data
|
||||||
|
|
||||||
|
a, err := s.repo.Store(action)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Delete(actionID int) error {
|
||||||
|
if err := s.repo.Delete(actionID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Fetch() ([]domain.Action, error) {
|
||||||
|
actions, err := s.repo.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) ToggleEnabled(actionID int) error {
|
||||||
|
if err := s.repo.ToggleEnabled(actionID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) test(torrentFile string) {
|
||||||
|
log.Info().Msgf("action TEST: %v", torrentFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) watchFolder(dir string, torrentFile string) {
|
||||||
|
log.Debug().Msgf("action WATCH_FOLDER: %v file: %v", dir, torrentFile)
|
||||||
|
|
||||||
|
// Open original file
|
||||||
|
original, err := os.Open(torrentFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
defer original.Close()
|
||||||
|
|
||||||
|
tmpFileName := strings.Split(torrentFile, "/")
|
||||||
|
fullFileName := fmt.Sprintf("%v/%v", dir, tmpFileName[1])
|
||||||
|
|
||||||
|
// Create new file
|
||||||
|
newFile, err := os.Create(fullFileName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
defer newFile.Close()
|
||||||
|
|
||||||
|
// Copy file
|
||||||
|
_, err = io.Copy(newFile, original)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msgf("action WATCH_FOLDER: wrote file: %v", fullFileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) qbittorrent(action domain.Action, hash string, torrentFile string) error {
|
||||||
|
log.Debug().Msgf("action QBITTORRENT: %v", torrentFile)
|
||||||
|
|
||||||
|
// get client for action
|
||||||
|
client, err := s.clientSvc.FindByID(action.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error finding client: %v", action.ClientID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if client == nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
qbtSettings := qbittorrent.Settings{
|
||||||
|
Hostname: client.Host,
|
||||||
|
Port: uint(client.Port),
|
||||||
|
Username: client.Username,
|
||||||
|
Password: client.Password,
|
||||||
|
SSL: client.SSL,
|
||||||
|
}
|
||||||
|
|
||||||
|
qbt := qbittorrent.NewClient(qbtSettings)
|
||||||
|
// save cookies?
|
||||||
|
err = qbt.Login()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error logging into client: %v", action.ClientID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO check for active downloads and other rules
|
||||||
|
|
||||||
|
options := map[string]string{}
|
||||||
|
|
||||||
|
if action.Paused {
|
||||||
|
options["paused"] = "true"
|
||||||
|
}
|
||||||
|
if action.SavePath != "" {
|
||||||
|
options["savepath"] = action.SavePath
|
||||||
|
options["autoTMM"] = "false"
|
||||||
|
}
|
||||||
|
if action.Category != "" {
|
||||||
|
options["category"] = action.Category
|
||||||
|
}
|
||||||
|
if action.Tags != "" {
|
||||||
|
options["tags"] = action.Tags
|
||||||
|
}
|
||||||
|
if action.LimitUploadSpeed > 0 {
|
||||||
|
options["upLimit"] = strconv.FormatInt(action.LimitUploadSpeed, 10)
|
||||||
|
}
|
||||||
|
if action.LimitDownloadSpeed > 0 {
|
||||||
|
options["dlLimit"] = strconv.FormatInt(action.LimitDownloadSpeed, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = qbt.AddTorrentFromFile(torrentFile, options)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error sending to client: %v", action.ClientID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !action.Paused && hash != "" {
|
||||||
|
err = checkTrackerStatus(*qbt, hash)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not get tracker status for torrent: %v", hash)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("torrent %v successfully added to: %v", hash, client.Name)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkTrackerStatus(qb qbittorrent.Client, hash string) error {
|
||||||
|
announceOK := false
|
||||||
|
attempts := 0
|
||||||
|
|
||||||
|
for attempts < REANNOUNCE_MAX_ATTEMPTS {
|
||||||
|
log.Debug().Msgf("RE-ANNOUNCE %v attempt: %v", hash, attempts)
|
||||||
|
|
||||||
|
// initial sleep to give tracker a head start
|
||||||
|
time.Sleep(REANNOUNCE_INTERVAL * time.Millisecond)
|
||||||
|
|
||||||
|
trackers, err := qb.GetTorrentTrackers(hash)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not get trackers of torrent: %v", hash)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if status not working or something else
|
||||||
|
_, working := findTrackerStatus(trackers, qbittorrent.TrackerStatusOK)
|
||||||
|
|
||||||
|
if !working {
|
||||||
|
err = qb.ReAnnounceTorrents([]string{hash})
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not get re-announce torrent: %v", hash)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
attempts++
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
log.Debug().Msgf("RE-ANNOUNCE %v OK", hash)
|
||||||
|
|
||||||
|
announceOK = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !announceOK {
|
||||||
|
log.Debug().Msgf("RE-ANNOUNCE %v took too long, deleting torrent", hash)
|
||||||
|
|
||||||
|
err := qb.DeleteTorrents([]string{hash}, false)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not delete torrent: %v", hash)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if status not working or something else
|
||||||
|
// https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-trackers
|
||||||
|
// 0 Tracker is disabled (used for DHT, PeX, and LSD)
|
||||||
|
// 1 Tracker has not been contacted yet
|
||||||
|
// 2 Tracker has been contacted and is working
|
||||||
|
// 3 Tracker is updating
|
||||||
|
// 4 Tracker has been contacted, but it is not working (or doesn't send proper replies)
|
||||||
|
func findTrackerStatus(slice []qbittorrent.TorrentTracker, status qbittorrent.TrackerStatus) (int, bool) {
|
||||||
|
for i, item := range slice {
|
||||||
|
if item.Status == status {
|
||||||
|
return i, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1, false
|
||||||
|
}
|
588
internal/announce/parse.go
Normal file
588
internal/announce/parse.go
Normal file
|
@ -0,0 +1,588 @@
|
||||||
|
package announce
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"net/url"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
"github.com/autobrr/autobrr/pkg/releaseinfo"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *service) parseLineSingle(def *domain.IndexerDefinition, announce *domain.Announce, line string) error {
|
||||||
|
for _, extract := range def.Parse.Lines {
|
||||||
|
tmpVars := map[string]string{}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
err = s.parseExtract(extract.Pattern, extract.Vars, tmpVars, line)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Msgf("error parsing extract: %v", line)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// on lines matched
|
||||||
|
err = s.onLinesMatched(def, tmpVars, announce)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Msgf("error match line: %v", line)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) parseMultiLine() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) parseExtract(pattern string, vars []string, tmpVars map[string]string, line string) error {
|
||||||
|
|
||||||
|
rxp, err := regExMatch(pattern, line)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Msgf("did not match expected line: %v", line)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rxp == nil {
|
||||||
|
//return nil, nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract matched
|
||||||
|
for i, v := range vars {
|
||||||
|
value := ""
|
||||||
|
|
||||||
|
if rxp[i] != "" {
|
||||||
|
value = rxp[i]
|
||||||
|
// tmpVars[v] = rxp[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpVars[v] = value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) onLinesMatched(def *domain.IndexerDefinition, vars map[string]string, announce *domain.Announce) error {
|
||||||
|
// TODO implement set tracker.lastAnnounce = now
|
||||||
|
|
||||||
|
announce.TorrentName = vars["torrentName"]
|
||||||
|
|
||||||
|
//err := s.postProcess(ti, vars, *announce)
|
||||||
|
//if err != nil {
|
||||||
|
// return err
|
||||||
|
//}
|
||||||
|
|
||||||
|
// TODO extractReleaseInfo
|
||||||
|
err := s.extractReleaseInfo(vars, announce.TorrentName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolution
|
||||||
|
// source
|
||||||
|
// encoder
|
||||||
|
// canonicalize name
|
||||||
|
|
||||||
|
err = s.mapToAnnounce(vars, announce)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// torrent url
|
||||||
|
torrentUrl, err := s.processTorrentUrl(def.Parse.Match.TorrentURL, vars, def.SettingsMap, def.Parse.Match.Encode)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Msgf("error torrent url: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if torrentUrl != "" {
|
||||||
|
announce.TorrentUrl = torrentUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) processTorrentUrl(match string, vars map[string]string, extraVars map[string]string, encode []string) (string, error) {
|
||||||
|
tmpVars := map[string]string{}
|
||||||
|
|
||||||
|
// copy vars to new tmp map
|
||||||
|
for k, v := range vars {
|
||||||
|
tmpVars[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge extra vars with vars
|
||||||
|
if extraVars != nil {
|
||||||
|
for k, v := range extraVars {
|
||||||
|
tmpVars[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle url encode of values
|
||||||
|
if encode != nil {
|
||||||
|
for _, e := range encode {
|
||||||
|
if v, ok := tmpVars[e]; ok {
|
||||||
|
// url encode value
|
||||||
|
t := url.QueryEscape(v)
|
||||||
|
tmpVars[e] = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup text template to inject variables into
|
||||||
|
tmpl, err := template.New("torrenturl").Parse(match)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("could not create torrent url template")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
err = tmpl.Execute(&b, &tmpVars)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("could not write torrent url template output")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func split(r rune) bool {
|
||||||
|
return r == ' ' || r == '.'
|
||||||
|
}
|
||||||
|
|
||||||
|
func Splitter(s string, splits string) []string {
|
||||||
|
m := make(map[rune]int)
|
||||||
|
for _, r := range splits {
|
||||||
|
m[r] = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
splitter := func(r rune) bool {
|
||||||
|
return m[r] == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.FieldsFunc(s, splitter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalizeString(s string) []string {
|
||||||
|
//a := strings.FieldsFunc(s, split)
|
||||||
|
a := Splitter(s, " .")
|
||||||
|
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanReleaseName(input string) string {
|
||||||
|
// Make a Regex to say we only want letters and numbers
|
||||||
|
reg, err := regexp.Compile("[^a-zA-Z0-9]+")
|
||||||
|
if err != nil {
|
||||||
|
//log.Fatal(err)
|
||||||
|
}
|
||||||
|
processedString := reg.ReplaceAllString(input, " ")
|
||||||
|
|
||||||
|
return processedString
|
||||||
|
}
|
||||||
|
|
||||||
|
func findLast(input string, pattern string) (string, error) {
|
||||||
|
matched := make([]string, 0)
|
||||||
|
//for _, s := range arr {
|
||||||
|
|
||||||
|
rxp, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
//return errors.Wrapf(err, "invalid regex: %s", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := rxp.FindStringSubmatch(input)
|
||||||
|
if matches != nil {
|
||||||
|
log.Trace().Msgf("matches: %v", matches)
|
||||||
|
// first value is the match, second value is the text
|
||||||
|
if len(matches) >= 1 {
|
||||||
|
last := matches[len(matches)-1]
|
||||||
|
|
||||||
|
// add to temp slice
|
||||||
|
matched = append(matched, last)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//}
|
||||||
|
|
||||||
|
// check if multiple values in temp slice, if so get the last one
|
||||||
|
if len(matched) >= 1 {
|
||||||
|
last := matched[len(matched)-1]
|
||||||
|
|
||||||
|
return last, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractYear(releaseName string) (string, bool) {
|
||||||
|
yearMatch, err := findLast(releaseName, "(?:^|\\D)(19[3-9]\\d|20[012]\\d)(?:\\D|$)")
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
log.Trace().Msgf("year matches: %v", yearMatch)
|
||||||
|
return yearMatch, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractSeason(releaseName string) (string, bool) {
|
||||||
|
seasonMatch, err := findLast(releaseName, "\\sS(\\d+)\\s?[ED]\\d+/i")
|
||||||
|
sm2, err := findLast(releaseName, "\\s(?:S|Season\\s*)(\\d+)/i")
|
||||||
|
//sm3, err := findLast(releaseName, "\\s((?<!\\d)\\d{1,2})x\\d+/i")
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("season matches: %v", seasonMatch)
|
||||||
|
log.Trace().Msgf("season matches: %v", sm2)
|
||||||
|
return seasonMatch, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractEpisode(releaseName string) (string, bool) {
|
||||||
|
epMatch, err := findLast(releaseName, "\\sS\\d+\\s?E(\\d+)/i")
|
||||||
|
ep2, err := findLast(releaseName, "\\s(?:E|Episode\\s*)(\\d+)/i")
|
||||||
|
//ep3, err := findLast(releaseName, "\\s(?<!\\d)\\d{1,2}x(\\d+)/i")
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Trace().Msgf("ep matches: %v", epMatch)
|
||||||
|
log.Trace().Msgf("ep matches: %v", ep2)
|
||||||
|
return epMatch, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) extractReleaseInfo(varMap map[string]string, releaseName string) error {
|
||||||
|
// https://github.com/middelink/go-parse-torrent-name
|
||||||
|
|
||||||
|
canonReleaseName := cleanReleaseName(releaseName)
|
||||||
|
log.Trace().Msgf("canonicalize release name: %v", canonReleaseName)
|
||||||
|
|
||||||
|
release, err := releaseinfo.Parse(releaseName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("release: %+v", release)
|
||||||
|
|
||||||
|
// https://github.com/autodl-community/autodl-irssi/pull/194/files
|
||||||
|
// year
|
||||||
|
//year, yearMatch := extractYear(canonReleaseName)
|
||||||
|
//if yearMatch {
|
||||||
|
// setVariable("year", year, varMap, nil)
|
||||||
|
//}
|
||||||
|
//log.Trace().Msgf("year matches: %v", year)
|
||||||
|
|
||||||
|
// season
|
||||||
|
//season, seasonMatch := extractSeason(canonReleaseName)
|
||||||
|
//if seasonMatch {
|
||||||
|
// // set var
|
||||||
|
// log.Trace().Msgf("season matches: %v", season)
|
||||||
|
//}
|
||||||
|
|
||||||
|
// episode
|
||||||
|
//episode, episodeMatch := extractEpisode(canonReleaseName)
|
||||||
|
//if episodeMatch {
|
||||||
|
// // set var
|
||||||
|
// log.Trace().Msgf("episode matches: %v", episode)
|
||||||
|
//}
|
||||||
|
|
||||||
|
// resolution
|
||||||
|
|
||||||
|
// source
|
||||||
|
|
||||||
|
// encoder
|
||||||
|
|
||||||
|
// ignore
|
||||||
|
|
||||||
|
// tv or movie
|
||||||
|
|
||||||
|
// music stuff
|
||||||
|
|
||||||
|
// game stuff
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) mapToAnnounce(varMap map[string]string, ann *domain.Announce) error {
|
||||||
|
|
||||||
|
if torrentName, err := getFirstStringMapValue(varMap, []string{"torrentName"}); err != nil {
|
||||||
|
return errors.Wrap(err, "failed parsing required field")
|
||||||
|
} else {
|
||||||
|
ann.TorrentName = html.UnescapeString(torrentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if category, err := getFirstStringMapValue(varMap, []string{"category"}); err == nil {
|
||||||
|
ann.Category = category
|
||||||
|
}
|
||||||
|
|
||||||
|
if freeleech, err := getFirstStringMapValue(varMap, []string{"freeleech"}); err == nil {
|
||||||
|
ann.Freeleech = strings.EqualFold(freeleech, "freeleech") || strings.EqualFold(freeleech, "yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if freeleechPercent, err := getFirstStringMapValue(varMap, []string{"freeleechPercent"}); err == nil {
|
||||||
|
ann.FreeleechPercent = freeleechPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
if uploader, err := getFirstStringMapValue(varMap, []string{"uploader"}); err == nil {
|
||||||
|
ann.Uploader = uploader
|
||||||
|
}
|
||||||
|
|
||||||
|
if scene, err := getFirstStringMapValue(varMap, []string{"scene"}); err == nil {
|
||||||
|
ann.Scene = strings.EqualFold(scene, "true") || strings.EqualFold(scene, "yes")
|
||||||
|
}
|
||||||
|
|
||||||
|
if year, err := getFirstStringMapValue(varMap, []string{"year"}); err == nil {
|
||||||
|
yearI, err := strconv.Atoi(year)
|
||||||
|
if err != nil {
|
||||||
|
//log.Debug().Msgf("bad year var: %v", year)
|
||||||
|
}
|
||||||
|
ann.Year = yearI
|
||||||
|
}
|
||||||
|
|
||||||
|
if tags, err := getFirstStringMapValue(varMap, []string{"releaseTags", "tags"}); err == nil {
|
||||||
|
ann.Tags = tags
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) mapToAnnounceObj(varMap map[string]string, ann *domain.Announce) error {
|
||||||
|
|
||||||
|
if torrentName, err := getFirstStringMapValue(varMap, []string{"torrentName", "$torrentName"}); err != nil {
|
||||||
|
return errors.Wrap(err, "failed parsing required field")
|
||||||
|
} else {
|
||||||
|
ann.TorrentName = html.UnescapeString(torrentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if torrentUrl, err := getFirstStringMapValue(varMap, []string{"torrentUrl", "$torrentUrl"}); err != nil {
|
||||||
|
return errors.Wrap(err, "failed parsing required field")
|
||||||
|
} else {
|
||||||
|
ann.TorrentUrl = torrentUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
if releaseType, err := getFirstStringMapValue(varMap, []string{"releaseType", "$releaseType"}); err == nil {
|
||||||
|
ann.ReleaseType = releaseType
|
||||||
|
}
|
||||||
|
|
||||||
|
if name1, err := getFirstStringMapValue(varMap, []string{"name1", "$name1"}); err == nil {
|
||||||
|
ann.Name1 = name1
|
||||||
|
}
|
||||||
|
|
||||||
|
if name2, err := getFirstStringMapValue(varMap, []string{"name2", "$name2"}); err == nil {
|
||||||
|
ann.Name2 = name2
|
||||||
|
}
|
||||||
|
|
||||||
|
if category, err := getFirstStringMapValue(varMap, []string{"category", "$category"}); err == nil {
|
||||||
|
ann.Category = category
|
||||||
|
}
|
||||||
|
if freeleech, err := getFirstStringMapValue(varMap, []string{"freeleech", "$freeleech"}); err == nil {
|
||||||
|
ann.Freeleech = strings.EqualFold(freeleech, "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if uploader, err := getFirstStringMapValue(varMap, []string{"uploader", "$uploader"}); err == nil {
|
||||||
|
ann.Uploader = uploader
|
||||||
|
}
|
||||||
|
|
||||||
|
if tags, err := getFirstStringMapValue(varMap, []string{"$releaseTags", "$tags", "releaseTags", "tags"}); err == nil {
|
||||||
|
ann.Tags = tags
|
||||||
|
}
|
||||||
|
|
||||||
|
if cue, err := getFirstStringMapValue(varMap, []string{"cue", "$cue"}); err == nil {
|
||||||
|
ann.Cue = strings.EqualFold(cue, "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if logVar, err := getFirstStringMapValue(varMap, []string{"log", "$log"}); err == nil {
|
||||||
|
ann.Log = logVar
|
||||||
|
}
|
||||||
|
|
||||||
|
if media, err := getFirstStringMapValue(varMap, []string{"media", "$media"}); err == nil {
|
||||||
|
ann.Media = media
|
||||||
|
}
|
||||||
|
|
||||||
|
if format, err := getFirstStringMapValue(varMap, []string{"format", "$format"}); err == nil {
|
||||||
|
ann.Format = format
|
||||||
|
}
|
||||||
|
|
||||||
|
if bitRate, err := getFirstStringMapValue(varMap, []string{"bitrate", "$bitrate"}); err == nil {
|
||||||
|
ann.Bitrate = bitRate
|
||||||
|
}
|
||||||
|
|
||||||
|
if resolution, err := getFirstStringMapValue(varMap, []string{"resolution"}); err == nil {
|
||||||
|
ann.Resolution = resolution
|
||||||
|
}
|
||||||
|
|
||||||
|
if source, err := getFirstStringMapValue(varMap, []string{"source"}); err == nil {
|
||||||
|
ann.Source = source
|
||||||
|
}
|
||||||
|
|
||||||
|
if encoder, err := getFirstStringMapValue(varMap, []string{"encoder"}); err == nil {
|
||||||
|
ann.Encoder = encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
if container, err := getFirstStringMapValue(varMap, []string{"container"}); err == nil {
|
||||||
|
ann.Container = container
|
||||||
|
}
|
||||||
|
|
||||||
|
if scene, err := getFirstStringMapValue(varMap, []string{"scene", "$scene"}); err == nil {
|
||||||
|
ann.Scene = strings.EqualFold(scene, "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
if year, err := getFirstStringMapValue(varMap, []string{"year", "$year"}); err == nil {
|
||||||
|
yearI, err := strconv.Atoi(year)
|
||||||
|
if err != nil {
|
||||||
|
//log.Debug().Msgf("bad year var: %v", year)
|
||||||
|
}
|
||||||
|
ann.Year = yearI
|
||||||
|
}
|
||||||
|
|
||||||
|
//return &ann, nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setVariable(varName string, value string, varMap map[string]string, settings map[string]string) bool {
|
||||||
|
|
||||||
|
// check in instance options (auth)
|
||||||
|
//optVal, ok := settings[name]
|
||||||
|
//if !ok {
|
||||||
|
// //return ""
|
||||||
|
//}
|
||||||
|
////ret = optVal
|
||||||
|
//if optVal != "" {
|
||||||
|
// return false
|
||||||
|
//}
|
||||||
|
|
||||||
|
// else in varMap
|
||||||
|
val, ok := varMap[varName]
|
||||||
|
if !ok {
|
||||||
|
//return ""
|
||||||
|
varMap[varName] = value
|
||||||
|
} else {
|
||||||
|
// do something else?
|
||||||
|
}
|
||||||
|
log.Trace().Msgf("setVariable: %v", val)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVariable(name string, varMap map[string]string, obj domain.Announce, settings map[string]string) string {
|
||||||
|
var ret string
|
||||||
|
|
||||||
|
// check in announce obj
|
||||||
|
// TODO reflect struct
|
||||||
|
|
||||||
|
// check in instance options (auth)
|
||||||
|
optVal, ok := settings[name]
|
||||||
|
if !ok {
|
||||||
|
//return ""
|
||||||
|
}
|
||||||
|
//ret = optVal
|
||||||
|
if optVal != "" {
|
||||||
|
return optVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// else in varMap
|
||||||
|
val, ok := varMap[name]
|
||||||
|
if !ok {
|
||||||
|
//return ""
|
||||||
|
}
|
||||||
|
ret = val
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
//func contains(s []string, str string) bool {
|
||||||
|
// for _, v := range s {
|
||||||
|
// if v == str {
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return false
|
||||||
|
//}
|
||||||
|
|
||||||
|
func listContains(list []string, key string) bool {
|
||||||
|
for _, lKey := range list {
|
||||||
|
if strings.EqualFold(lKey, key) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStringMapValue(stringMap map[string]string, key string) (string, error) {
|
||||||
|
lowerKey := strings.ToLower(key)
|
||||||
|
|
||||||
|
// case sensitive match
|
||||||
|
//if caseSensitive {
|
||||||
|
// v, ok := stringMap[key]
|
||||||
|
// if !ok {
|
||||||
|
// return "", fmt.Errorf("key was not found in map: %q", key)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return v, nil
|
||||||
|
//}
|
||||||
|
|
||||||
|
// case insensitive match
|
||||||
|
for k, v := range stringMap {
|
||||||
|
if strings.ToLower(k) == lowerKey {
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("key was not found in map: %q", lowerKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFirstStringMapValue(stringMap map[string]string, keys []string) (string, error) {
|
||||||
|
for _, k := range keys {
|
||||||
|
if val, err := getStringMapValue(stringMap, k); err == nil {
|
||||||
|
return val, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", fmt.Errorf("key were not found in map: %q", strings.Join(keys, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeElement(s []string, i int) ([]string, error) {
|
||||||
|
// s is [1,2,3,4,5,6], i is 2
|
||||||
|
|
||||||
|
// perform bounds checking first to prevent a panic!
|
||||||
|
if i >= len(s) || i < 0 {
|
||||||
|
return nil, fmt.Errorf("Index is out of range. Index is %d with slice length %d", i, len(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// This creates a new slice by creating 2 slices from the original:
|
||||||
|
// s[:i] -> [1, 2]
|
||||||
|
// s[i+1:] -> [4, 5, 6]
|
||||||
|
// and joining them together using `append`
|
||||||
|
return append(s[:i], s[i+1:]...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func regExMatch(pattern string, value string) ([]string, error) {
|
||||||
|
|
||||||
|
rxp, err := regexp.Compile(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
//return errors.Wrapf(err, "invalid regex: %s", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := rxp.FindStringSubmatch(value)
|
||||||
|
if matches == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
res := make([]string, 0)
|
||||||
|
if matches != nil {
|
||||||
|
res, err = removeElement(matches, 0)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
585
internal/announce/parse_test.go
Normal file
585
internal/announce/parse_test.go
Normal file
|
@ -0,0 +1,585 @@
|
||||||
|
package announce
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
//func Test_service_OnNewLine(t *testing.T) {
|
||||||
|
// tfiles := tracker.NewService()
|
||||||
|
// tfiles.ReadFiles()
|
||||||
|
//
|
||||||
|
// type fields struct {
|
||||||
|
// trackerSvc tracker.Service
|
||||||
|
// }
|
||||||
|
// type args struct {
|
||||||
|
// msg string
|
||||||
|
// }
|
||||||
|
// tests := []struct {
|
||||||
|
// name string
|
||||||
|
// fields fields
|
||||||
|
// args args
|
||||||
|
// wantErr bool
|
||||||
|
// }{
|
||||||
|
// // TODO: Add test cases.
|
||||||
|
// {
|
||||||
|
// name: "parse announce",
|
||||||
|
// fields: fields{
|
||||||
|
// trackerSvc: tfiles,
|
||||||
|
// },
|
||||||
|
// args: args{
|
||||||
|
// msg: "New Torrent Announcement: <PC :: Iso> Name:'debian live 10 6 0 amd64 standard iso' uploaded by 'Anonymous' - http://www.tracker01.test/torrent/263302",
|
||||||
|
// },
|
||||||
|
// // expect struct: category, torrentName uploader freeleech baseurl torrentId
|
||||||
|
// wantErr: false,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// for _, tt := range tests {
|
||||||
|
// t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// s := &service{
|
||||||
|
// trackerSvc: tt.fields.trackerSvc,
|
||||||
|
// }
|
||||||
|
// if err := s.OnNewLine(tt.args.msg); (err != nil) != tt.wantErr {
|
||||||
|
// t.Errorf("OnNewLine() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
//func Test_service_parse(t *testing.T) {
|
||||||
|
// type fields struct {
|
||||||
|
// trackerSvc tracker.Service
|
||||||
|
// }
|
||||||
|
// type args struct {
|
||||||
|
// serverName string
|
||||||
|
// channelName string
|
||||||
|
// announcer string
|
||||||
|
// line string
|
||||||
|
// }
|
||||||
|
// tests := []struct {
|
||||||
|
// name string
|
||||||
|
// fields fields
|
||||||
|
// args args
|
||||||
|
// wantErr bool
|
||||||
|
// }{
|
||||||
|
// // TODO: Add test cases.
|
||||||
|
// }
|
||||||
|
// for _, tt := range tests {
|
||||||
|
// t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// s := &service{
|
||||||
|
// trackerSvc: tt.fields.trackerSvc,
|
||||||
|
// }
|
||||||
|
// if err := s.parse(tt.args.serverName, tt.args.channelName, tt.args.announcer, tt.args.line); (err != nil) != tt.wantErr {
|
||||||
|
// t.Errorf("parse() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
/*
|
||||||
|
var (
|
||||||
|
tracker01 = domain.TrackerInstance{
|
||||||
|
Name: "T01",
|
||||||
|
Enabled: true,
|
||||||
|
Settings: nil,
|
||||||
|
Auth: map[string]string{"rsskey": "000aaa111bbb222ccc333ddd"},
|
||||||
|
//IRC: nil,
|
||||||
|
Info: &domain.TrackerInfo{
|
||||||
|
Type: "t01",
|
||||||
|
ShortName: "T01",
|
||||||
|
LongName: "Tracker01",
|
||||||
|
SiteName: "www.tracker01.test",
|
||||||
|
IRC: domain.TrackerIRCServer{
|
||||||
|
Network: "Tracker01.test",
|
||||||
|
ServerNames: []string{"irc.tracker01.test"},
|
||||||
|
ChannelNames: []string{"#tracker01", "#t01announces"},
|
||||||
|
AnnouncerNames: []string{"_AnnounceBot_"},
|
||||||
|
},
|
||||||
|
ParseInfo: domain.ParseInfo{
|
||||||
|
LinePatterns: []domain.TrackerExtractPattern{
|
||||||
|
|
||||||
|
{
|
||||||
|
PatternType: "linepattern",
|
||||||
|
Optional: false,
|
||||||
|
Regex: regexp.MustCompile("New Torrent Announcement:\\s*<([^>]*)>\\s*Name:'(.*)' uploaded by '([^']*)'\\s*(freeleech)*\\s*-\\s*https?\\:\\/\\/([^\\/]+\\/)torrent\\/(\\d+)"),
|
||||||
|
Vars: []string{"category", "torrentName", "uploader", "$freeleech", "$baseUrl", "$torrentId"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MultiLinePatterns: nil,
|
||||||
|
LineMatched: domain.LineMatched{
|
||||||
|
Vars: []domain.LineMatchVars{
|
||||||
|
{
|
||||||
|
Name: "freeleech",
|
||||||
|
Vars: []domain.LineMatchVarElem{
|
||||||
|
{Type: "string", Value: "false"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "torrentUrl",
|
||||||
|
Vars: []domain.LineMatchVarElem{
|
||||||
|
{Type: "string", Value: "https://"},
|
||||||
|
{Type: "var", Value: "$baseUrl"},
|
||||||
|
{Type: "string", Value: "rss/download/"},
|
||||||
|
{Type: "var", Value: "$torrentId"},
|
||||||
|
{Type: "string", Value: "/"},
|
||||||
|
{Type: "var", Value: "rsskey"},
|
||||||
|
{Type: "string", Value: "/"},
|
||||||
|
{Type: "varenc", Value: "torrentName"},
|
||||||
|
{Type: "string", Value: ".torrent"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extract: nil,
|
||||||
|
LineMatchIf: nil,
|
||||||
|
VarReplace: nil,
|
||||||
|
SetRegex: &domain.SetRegex{
|
||||||
|
SrcVar: "$freeleech",
|
||||||
|
Regex: regexp.MustCompile("freeleech"),
|
||||||
|
VarName: "freeleech",
|
||||||
|
NewValue: "true",
|
||||||
|
},
|
||||||
|
ExtractOne: domain.ExtractOne{Extract: nil},
|
||||||
|
ExtractTags: domain.ExtractTags{
|
||||||
|
Name: "",
|
||||||
|
SrcVar: "",
|
||||||
|
Split: "",
|
||||||
|
Regex: nil,
|
||||||
|
SetVarIf: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Ignore: []domain.TrackerIgnore{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tracker05 = domain.TrackerInstance{
|
||||||
|
Name: "T05",
|
||||||
|
Enabled: true,
|
||||||
|
Settings: nil,
|
||||||
|
Auth: map[string]string{"authkey": "000aaa111bbb222ccc333ddd", "torrent_pass": "eee444fff555ggg666hhh777"},
|
||||||
|
//IRC: nil,
|
||||||
|
Info: &domain.TrackerInfo{
|
||||||
|
Type: "t05",
|
||||||
|
ShortName: "T05",
|
||||||
|
LongName: "Tracker05",
|
||||||
|
SiteName: "tracker05.test",
|
||||||
|
IRC: domain.TrackerIRCServer{
|
||||||
|
Network: "Tracker05.test",
|
||||||
|
ServerNames: []string{"irc.tracker05.test"},
|
||||||
|
ChannelNames: []string{"#t05-announce"},
|
||||||
|
AnnouncerNames: []string{"Drone"},
|
||||||
|
},
|
||||||
|
ParseInfo: domain.ParseInfo{
|
||||||
|
LinePatterns: []domain.TrackerExtractPattern{
|
||||||
|
|
||||||
|
{
|
||||||
|
PatternType: "linepattern",
|
||||||
|
Optional: false,
|
||||||
|
Regex: regexp.MustCompile("^(.*)\\s+-\\s+https?:.*[&\\?]id=.*https?\\:\\/\\/([^\\/]+\\/).*[&\\?]id=(\\d+)\\s*-\\s*(.*)"),
|
||||||
|
Vars: []string{"torrentName", "$baseUrl", "$torrentId", "tags"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MultiLinePatterns: nil,
|
||||||
|
LineMatched: domain.LineMatched{
|
||||||
|
Vars: []domain.LineMatchVars{
|
||||||
|
{
|
||||||
|
Name: "scene",
|
||||||
|
Vars: []domain.LineMatchVarElem{
|
||||||
|
{Type: "string", Value: "false"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "log",
|
||||||
|
Vars: []domain.LineMatchVarElem{
|
||||||
|
{Type: "string", Value: "false"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "cue",
|
||||||
|
Vars: []domain.LineMatchVarElem{
|
||||||
|
{Type: "string", Value: "false"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "freeleech",
|
||||||
|
Vars: []domain.LineMatchVarElem{
|
||||||
|
{Type: "string", Value: "false"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "torrentUrl",
|
||||||
|
Vars: []domain.LineMatchVarElem{
|
||||||
|
{Type: "string", Value: "https://"},
|
||||||
|
{Type: "var", Value: "$baseUrl"},
|
||||||
|
{Type: "string", Value: "torrents.php?action=download&id="},
|
||||||
|
{Type: "var", Value: "$torrentId"},
|
||||||
|
{Type: "string", Value: "&authkey="},
|
||||||
|
{Type: "var", Value: "authkey"},
|
||||||
|
{Type: "string", Value: "&torrent_pass="},
|
||||||
|
{Type: "var", Value: "torrent_pass"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Extract: []domain.Extract{
|
||||||
|
{SrcVar: "torrentName", Optional: true, Regex: regexp.MustCompile("[(\\[]((?:19|20)\\d\\d)[)\\]]"), Vars: []string{"year"}},
|
||||||
|
{SrcVar: "$releaseTags", Optional: true, Regex: regexp.MustCompile("([\\d.]+)%"), Vars: []string{"logScore"}},
|
||||||
|
},
|
||||||
|
LineMatchIf: nil,
|
||||||
|
VarReplace: []domain.ParseVarReplace{
|
||||||
|
{Name: "tags", SrcVar: "tags", Regex: regexp.MustCompile("[._]"), Replace: " "},
|
||||||
|
},
|
||||||
|
SetRegex: nil,
|
||||||
|
ExtractOne: domain.ExtractOne{Extract: []domain.Extract{
|
||||||
|
{SrcVar: "torrentName", Optional: false, Regex: regexp.MustCompile("^(.+?) - ([^\\[]+).*\\[(\\d{4})\\] \\[([^\\[]+)\\] - ([^\\-\\[\\]]+)"), Vars: []string{"name1", "name2", "year", "releaseType", "$releaseTags"}},
|
||||||
|
{SrcVar: "torrentName", Optional: false, Regex: regexp.MustCompile("^([^\\-]+)\\s+-\\s+(.+)"), Vars: []string{"name1", "name2"}},
|
||||||
|
{SrcVar: "torrentName", Optional: false, Regex: regexp.MustCompile("(.*)"), Vars: []string{"name1"}},
|
||||||
|
}},
|
||||||
|
ExtractTags: domain.ExtractTags{
|
||||||
|
Name: "",
|
||||||
|
SrcVar: "$releaseTags",
|
||||||
|
Split: "/",
|
||||||
|
Regex: []*regexp.Regexp{regexp.MustCompile("^(?:5\\.1 Audio|\\.m4a|Various.*|~.*|>.*)$")},
|
||||||
|
SetVarIf: []domain.SetVarIf{
|
||||||
|
{VarName: "format", Value: "", NewValue: "", Regex: regexp.MustCompile("^(?:MP3|FLAC|Ogg Vorbis|AAC|AC3|DTS)$")},
|
||||||
|
{VarName: "bitrate", Value: "", NewValue: "", Regex: regexp.MustCompile("Lossless$")},
|
||||||
|
{VarName: "bitrate", Value: "", NewValue: "", Regex: regexp.MustCompile("^(?:vbr|aps|apx|v\\d|\\d{2,4}|\\d+\\.\\d+|q\\d+\\.[\\dx]+|Other)?(?:\\s*kbps|\\s*kbits?|\\s*k)?(?:\\s*\\(?(?:vbr|cbr)\\)?)?$")},
|
||||||
|
{VarName: "media", Value: "", NewValue: "", Regex: regexp.MustCompile("^(?:CD|DVD|Vinyl|Soundboard|SACD|DAT|Cassette|WEB|Blu-ray|Other)$")},
|
||||||
|
{VarName: "scene", Value: "Scene", NewValue: "true", Regex: nil},
|
||||||
|
{VarName: "log", Value: "Log", NewValue: "true", Regex: nil},
|
||||||
|
{VarName: "cue", Value: "Cue", NewValue: "true", Regex: nil},
|
||||||
|
{VarName: "freeleech", Value: "Freeleech!", NewValue: "true", Regex: nil},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Ignore: []domain.TrackerIgnore{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
*/
|
||||||
|
|
||||||
|
//func Test_service_parse(t *testing.T) {
|
||||||
|
// type fields struct {
|
||||||
|
// name string
|
||||||
|
// trackerSvc tracker.Service
|
||||||
|
// queues map[string]chan string
|
||||||
|
// }
|
||||||
|
// type args struct {
|
||||||
|
// ti *domain.TrackerInstance
|
||||||
|
// message string
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// tests := []struct {
|
||||||
|
// name string
|
||||||
|
// fields fields
|
||||||
|
// args args
|
||||||
|
// want *domain.Announce
|
||||||
|
// wantErr bool
|
||||||
|
// }{
|
||||||
|
// {
|
||||||
|
// name: "tracker01_no_freeleech",
|
||||||
|
// fields: fields{
|
||||||
|
// name: "T01",
|
||||||
|
// trackerSvc: nil,
|
||||||
|
// queues: make(map[string]chan string),
|
||||||
|
// }, args: args{
|
||||||
|
// ti: &tracker01,
|
||||||
|
// message: "New Torrent Announcement: <PC :: Iso> Name:'debian live 10 6 0 amd64 standard iso' uploaded by 'Anonymous' - http://www.tracker01.test/torrent/263302",
|
||||||
|
// },
|
||||||
|
// want: &domain.Announce{
|
||||||
|
// Freeleech: false,
|
||||||
|
// Category: "PC :: Iso",
|
||||||
|
// TorrentName: "debian live 10 6 0 amd64 standard iso",
|
||||||
|
// Uploader: "Anonymous",
|
||||||
|
// TorrentUrl: "https://www.tracker01.test/rss/download/263302/000aaa111bbb222ccc333ddd/debian+live+10+6+0+amd64+standard+iso.torrent",
|
||||||
|
// Site: "T01",
|
||||||
|
// },
|
||||||
|
// wantErr: false,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: "tracker01_freeleech",
|
||||||
|
// fields: fields{
|
||||||
|
// name: "T01",
|
||||||
|
// trackerSvc: nil,
|
||||||
|
// queues: make(map[string]chan string),
|
||||||
|
// }, args: args{
|
||||||
|
// ti: &tracker01,
|
||||||
|
// message: "New Torrent Announcement: <PC :: Iso> Name:'debian live 10 6 0 amd64 standard iso' uploaded by 'Anonymous' freeleech - http://www.tracker01.test/torrent/263302",
|
||||||
|
// },
|
||||||
|
// want: &domain.Announce{
|
||||||
|
// Freeleech: true,
|
||||||
|
// Category: "PC :: Iso",
|
||||||
|
// TorrentName: "debian live 10 6 0 amd64 standard iso",
|
||||||
|
// Uploader: "Anonymous",
|
||||||
|
// TorrentUrl: "https://www.tracker01.test/rss/download/263302/000aaa111bbb222ccc333ddd/debian+live+10+6+0+amd64+standard+iso.torrent",
|
||||||
|
// Site: "T01",
|
||||||
|
// },
|
||||||
|
// wantErr: false,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: "tracker05_01",
|
||||||
|
// fields: fields{
|
||||||
|
// name: "T05",
|
||||||
|
// trackerSvc: nil,
|
||||||
|
// queues: make(map[string]chan string),
|
||||||
|
// }, args: args{
|
||||||
|
// ti: &tracker05,
|
||||||
|
// message: "Roy Buchanan - Loading Zone [1977] - FLAC / Lossless / Log / 100% / Cue / CD - http://passtheheadphones.me/torrents.php?id=97614 / http://tracker05.test/torrents.php?action=download&id=1382972 - blues, rock, classic.rock,jazz,blues.rock,electric.blues",
|
||||||
|
// },
|
||||||
|
// want: &domain.Announce{
|
||||||
|
// Name1: "Roy Buchanan - Loading Zone [1977] - FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
// Name2: "Loading Zone [1977] - FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
// Freeleech: false,
|
||||||
|
// TorrentName: "Roy Buchanan - Loading Zone [1977] - FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
// TorrentUrl: "https://tracker05.test/torrents.php?action=download&id=1382972&authkey=000aaa111bbb222ccc333ddd&torrent_pass=eee444fff555ggg666hhh777",
|
||||||
|
// Site: "T05",
|
||||||
|
// Tags: "blues, rock, classic rock,jazz,blues rock,electric blues",
|
||||||
|
// Log: "true",
|
||||||
|
// Cue: true,
|
||||||
|
// Format: "FLAC",
|
||||||
|
// Bitrate: "Lossless",
|
||||||
|
// Media: "CD",
|
||||||
|
// Scene: false,
|
||||||
|
// Year: 1977,
|
||||||
|
// },
|
||||||
|
// wantErr: false,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: "tracker05_02",
|
||||||
|
// fields: fields{
|
||||||
|
// name: "T05",
|
||||||
|
// trackerSvc: nil,
|
||||||
|
// queues: make(map[string]chan string),
|
||||||
|
// }, args: args{
|
||||||
|
// ti: &tracker05,
|
||||||
|
// message: "Heirloom - Road to the Isles [1998] [Album] - FLAC / Lossless / Log / 100% / Cue / CD - http://tracker05.test/torrents.php?id=72158898 / http://tracker05.test/torrents.php?action=download&id=29910415 - 1990s, folk, world_music, celtic",
|
||||||
|
// },
|
||||||
|
// want: &domain.Announce{
|
||||||
|
// ReleaseType: "Album",
|
||||||
|
// Name1: "Heirloom - Road to the Isles [1998] [Album] - FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
// Name2: "Road to the Isles [1998] [Album] - FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
// Freeleech: false,
|
||||||
|
// TorrentName: "Heirloom - Road to the Isles [1998] [Album] - FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
// TorrentUrl: "https://tracker05.test/torrents.php?action=download&id=29910415&authkey=000aaa111bbb222ccc333ddd&torrent_pass=eee444fff555ggg666hhh777",
|
||||||
|
// Site: "T05",
|
||||||
|
// Tags: "1990s, folk, world music, celtic",
|
||||||
|
// Log: "true",
|
||||||
|
// Cue: true,
|
||||||
|
// Format: "FLAC",
|
||||||
|
// Bitrate: "Lossless",
|
||||||
|
// Media: "CD",
|
||||||
|
// Scene: false,
|
||||||
|
// Year: 1998,
|
||||||
|
// },
|
||||||
|
// wantErr: false,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// for _, tt := range tests {
|
||||||
|
// t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// s := &service{
|
||||||
|
// name: tt.fields.name,
|
||||||
|
// trackerSvc: tt.fields.trackerSvc,
|
||||||
|
// queues: tt.fields.queues,
|
||||||
|
// }
|
||||||
|
// got, err := s.parse(tt.args.ti, tt.args.message)
|
||||||
|
//
|
||||||
|
// if (err != nil) != tt.wantErr {
|
||||||
|
// t.Errorf("parse() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// assert.Equal(t, tt.want, got)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
//func Test_service_parseSingleLine(t *testing.T) {
|
||||||
|
// type fields struct {
|
||||||
|
// name string
|
||||||
|
// ts tracker.Service
|
||||||
|
// queues map[string]chan string
|
||||||
|
// }
|
||||||
|
// type args struct {
|
||||||
|
// ti *domain.TrackerInstance
|
||||||
|
// line string
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// tests := []struct {
|
||||||
|
// name string
|
||||||
|
// fields fields
|
||||||
|
// args args
|
||||||
|
// want *domain.Announce
|
||||||
|
// wantErr bool
|
||||||
|
// }{
|
||||||
|
// {
|
||||||
|
// name: "tracker01_no_freeleech",
|
||||||
|
// fields: fields{
|
||||||
|
// name: "T01",
|
||||||
|
// ts: nil,
|
||||||
|
// queues: make(map[string]chan string),
|
||||||
|
// }, args: args{
|
||||||
|
// ti: &tracker01,
|
||||||
|
// line: "New Torrent Announcement: <PC :: Iso> Name:'debian live 10 6 0 amd64 standard iso' uploaded by 'Anonymous' - http://www.tracker01.test/torrent/263302",
|
||||||
|
// },
|
||||||
|
// want: &domain.Announce{
|
||||||
|
// Freeleech: false,
|
||||||
|
// Category: "PC :: Iso",
|
||||||
|
// TorrentName: "debian live 10 6 0 amd64 standard iso",
|
||||||
|
// Uploader: "Anonymous",
|
||||||
|
// TorrentUrl: "https://www.tracker01.test/rss/download/263302/000aaa111bbb222ccc333ddd/debian+live+10+6+0+amd64+standard+iso.torrent",
|
||||||
|
// Site: "T01",
|
||||||
|
// },
|
||||||
|
// wantErr: false,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: "tracker01_freeleech",
|
||||||
|
// fields: fields{
|
||||||
|
// name: "T01",
|
||||||
|
// ts: nil,
|
||||||
|
// queues: make(map[string]chan string),
|
||||||
|
// }, args: args{
|
||||||
|
// ti: &tracker01,
|
||||||
|
// line: "New Torrent Announcement: <PC :: Iso> Name:'debian live 10 6 0 amd64 standard iso' uploaded by 'Anonymous' freeleech - http://www.tracker01.test/torrent/263302",
|
||||||
|
// },
|
||||||
|
// want: &domain.Announce{
|
||||||
|
// Freeleech: true,
|
||||||
|
// Category: "PC :: Iso",
|
||||||
|
// TorrentName: "debian live 10 6 0 amd64 standard iso",
|
||||||
|
// Uploader: "Anonymous",
|
||||||
|
// TorrentUrl: "https://www.tracker01.test/rss/download/263302/000aaa111bbb222ccc333ddd/debian+live+10+6+0+amd64+standard+iso.torrent",
|
||||||
|
// Site: "T01",
|
||||||
|
// },
|
||||||
|
// wantErr: false,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: "tracker05_01",
|
||||||
|
// fields: fields{
|
||||||
|
// name: "T05",
|
||||||
|
// ts: nil,
|
||||||
|
// queues: make(map[string]chan string),
|
||||||
|
// }, args: args{
|
||||||
|
// ti: &tracker05,
|
||||||
|
// line: "Roy Buchanan - Loading Zone [1977] - FLAC / Lossless / Log / 100% / Cue / CD - http://passtheheadphones.me/torrents.php?id=97614 / http://tracker05.test/torrents.php?action=download&id=1382972 - blues, rock, classic.rock,jazz,blues.rock,electric.blues",
|
||||||
|
// },
|
||||||
|
// want: &domain.Announce{
|
||||||
|
// Name1: "Roy Buchanan - Loading Zone [1977] - FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
// Name2: "Loading Zone [1977] - FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
// Freeleech: false,
|
||||||
|
// TorrentName: "Roy Buchanan - Loading Zone [1977] - FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
// TorrentUrl: "https://tracker05.test/torrents.php?action=download&id=1382972&authkey=000aaa111bbb222ccc333ddd&torrent_pass=eee444fff555ggg666hhh777",
|
||||||
|
// Site: "T05",
|
||||||
|
// Tags: "blues, rock, classic rock,jazz,blues rock,electric blues",
|
||||||
|
// //Log: "true",
|
||||||
|
// //Cue: true,
|
||||||
|
// //Format: "FLAC",
|
||||||
|
// //Bitrate: "Lossless",
|
||||||
|
// //Media: "CD",
|
||||||
|
// Log: "false",
|
||||||
|
// Cue: false,
|
||||||
|
// Format: "",
|
||||||
|
// Bitrate: "",
|
||||||
|
// Media: "",
|
||||||
|
// Scene: false,
|
||||||
|
// Year: 1977,
|
||||||
|
// },
|
||||||
|
// wantErr: false,
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: "tracker05_02",
|
||||||
|
// fields: fields{
|
||||||
|
// name: "T05",
|
||||||
|
// ts: nil,
|
||||||
|
// queues: make(map[string]chan string),
|
||||||
|
// }, args: args{
|
||||||
|
// ti: &tracker05,
|
||||||
|
// line: "Heirloom - Road to the Isles [1998] [Album] - FLAC / Lossless / Log / 100% / Cue / CD - http://tracker05.test/torrents.php?id=72158898 / http://tracker05.test/torrents.php?action=download&id=29910415 - 1990s, folk, world_music, celtic",
|
||||||
|
// },
|
||||||
|
// want: &domain.Announce{
|
||||||
|
// ReleaseType: "Album",
|
||||||
|
// Name1: "Heirloom - Road to the Isles [1998] [Album] - FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
// Name2: "Road to the Isles [1998] [Album] - FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
// Freeleech: false,
|
||||||
|
// TorrentName: "Heirloom - Road to the Isles [1998] [Album] - FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
// TorrentUrl: "https://tracker05.test/torrents.php?action=download&id=29910415&authkey=000aaa111bbb222ccc333ddd&torrent_pass=eee444fff555ggg666hhh777",
|
||||||
|
// Site: "T05",
|
||||||
|
// Tags: "1990s, folk, world music, celtic",
|
||||||
|
// Log: "true",
|
||||||
|
// Cue: true,
|
||||||
|
// Format: "FLAC",
|
||||||
|
// Bitrate: "Lossless",
|
||||||
|
// Media: "CD",
|
||||||
|
// Scene: false,
|
||||||
|
// Year: 1998,
|
||||||
|
// },
|
||||||
|
// wantErr: false,
|
||||||
|
// },
|
||||||
|
// }
|
||||||
|
// for _, tt := range tests {
|
||||||
|
// t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// s := &service{
|
||||||
|
// name: tt.fields.name,
|
||||||
|
// trackerSvc: tt.fields.ts,
|
||||||
|
// queues: tt.fields.queues,
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// announce := domain.Announce{
|
||||||
|
// Site: tt.fields.name,
|
||||||
|
// //Line: msg,
|
||||||
|
// }
|
||||||
|
// got, err := s.parseSingleLine(tt.args.ti, tt.args.line, &announce)
|
||||||
|
// if (err != nil) != tt.wantErr {
|
||||||
|
// t.Errorf("parseSingleLine() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// assert.Equal(t, tt.want, got)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
func Test_service_extractReleaseInfo(t *testing.T) {
|
||||||
|
type fields struct {
|
||||||
|
name string
|
||||||
|
queues map[string]chan string
|
||||||
|
}
|
||||||
|
type args struct {
|
||||||
|
varMap map[string]string
|
||||||
|
releaseName string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
fields fields
|
||||||
|
args args
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test_01",
|
||||||
|
fields: fields{
|
||||||
|
name: "", queues: nil,
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
varMap: map[string]string{},
|
||||||
|
releaseName: "Heirloom - Road to the Isles [1998] [Album] - FLAC / Lossless / Log / 100% / Cue / CD",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test_02",
|
||||||
|
fields: fields{
|
||||||
|
name: "", queues: nil,
|
||||||
|
},
|
||||||
|
args: args{
|
||||||
|
varMap: map[string]string{},
|
||||||
|
releaseName: "Lost S06E07 720p WEB-DL DD 5.1 H.264 - LP",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := &service{
|
||||||
|
queues: tt.fields.queues,
|
||||||
|
}
|
||||||
|
if err := s.extractReleaseInfo(tt.args.varMap, tt.args.releaseName); (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("extractReleaseInfo() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
91
internal/announce/service.go
Normal file
91
internal/announce/service.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
package announce
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
"github.com/autobrr/autobrr/internal/filter"
|
||||||
|
"github.com/autobrr/autobrr/internal/indexer"
|
||||||
|
"github.com/autobrr/autobrr/internal/release"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Parse(announceID string, msg string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
filterSvc filter.Service
|
||||||
|
indexerSvc indexer.Service
|
||||||
|
releaseSvc release.Service
|
||||||
|
queues map[string]chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(filterService filter.Service, indexerSvc indexer.Service, releaseService release.Service) Service {
|
||||||
|
|
||||||
|
//queues := make(map[string]chan string)
|
||||||
|
//for _, channel := range tinfo {
|
||||||
|
//
|
||||||
|
//}
|
||||||
|
|
||||||
|
return &service{
|
||||||
|
filterSvc: filterService,
|
||||||
|
indexerSvc: indexerSvc,
|
||||||
|
releaseSvc: releaseService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse announce line
|
||||||
|
func (s *service) Parse(announceID string, msg string) error {
|
||||||
|
// announceID (server:channel:announcer)
|
||||||
|
def := s.indexerSvc.GetIndexerByAnnounce(announceID)
|
||||||
|
if def == nil {
|
||||||
|
log.Debug().Msgf("could not find indexer definition: %v", announceID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
announce := domain.Announce{
|
||||||
|
Site: def.Identifier,
|
||||||
|
Line: msg,
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse lines
|
||||||
|
if def.Parse.Type == "single" {
|
||||||
|
err := s.parseLineSingle(def, &announce, msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Msgf("could not parse single line: %v", msg)
|
||||||
|
log.Error().Err(err).Msgf("could not parse single line: %v", msg)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// implement multiline parsing
|
||||||
|
|
||||||
|
// find filter
|
||||||
|
foundFilter, err := s.filterSvc.FindByIndexerIdentifier(announce)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("could not find filter")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// no filter found, lets return
|
||||||
|
if foundFilter == nil {
|
||||||
|
log.Debug().Msg("no matching filter found")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
announce.Filter = foundFilter
|
||||||
|
|
||||||
|
log.Trace().Msgf("announce: %+v", announce)
|
||||||
|
|
||||||
|
log.Info().Msgf("Matched %v (%v) for %v", announce.TorrentName, announce.Filter.Name, announce.Site)
|
||||||
|
|
||||||
|
// match release
|
||||||
|
|
||||||
|
// process release
|
||||||
|
go func() {
|
||||||
|
err = s.releaseSvc.Process(announce)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not process release: %+v", announce)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
81
internal/client/http.go
Normal file
81
internal/client/http.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DownloadFileResponse struct {
|
||||||
|
Body *io.ReadCloser
|
||||||
|
FileName string
|
||||||
|
}
|
||||||
|
|
||||||
|
type HttpClient struct {
|
||||||
|
http *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHttpClient() *HttpClient {
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: time.Second * 10,
|
||||||
|
}
|
||||||
|
return &HttpClient{
|
||||||
|
http: httpClient,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *HttpClient) DownloadFile(url string, opts map[string]string) (*DownloadFileResponse, error) {
|
||||||
|
if url == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// create md5 hash of url for tmp file
|
||||||
|
hash := md5.Sum([]byte(url))
|
||||||
|
hashString := hex.EncodeToString(hash[:])
|
||||||
|
tmpFileName := fmt.Sprintf("/tmp/%v", hashString)
|
||||||
|
|
||||||
|
log.Debug().Msgf("tmpFileName: %v", tmpFileName)
|
||||||
|
|
||||||
|
// Create the file
|
||||||
|
out, err := os.Create(tmpFileName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
// Get the data
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
// TODO better error message
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// retry logic
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the body to file
|
||||||
|
_, err = io.Copy(out, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove file if fail
|
||||||
|
|
||||||
|
res := DownloadFileResponse{
|
||||||
|
Body: &resp.Body,
|
||||||
|
FileName: tmpFileName,
|
||||||
|
}
|
||||||
|
|
||||||
|
return &res, nil
|
||||||
|
}
|
154
internal/config/config.go
Normal file
154
internal/config/config.go
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cfg struct {
|
||||||
|
Host string `toml:"host"`
|
||||||
|
Port int `toml:"port"`
|
||||||
|
LogLevel string `toml:"logLevel"`
|
||||||
|
LogPath string `toml:"logPath"`
|
||||||
|
BaseURL string `toml:"baseUrl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var Config Cfg
|
||||||
|
|
||||||
|
func Defaults() Cfg {
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
hostname = "localhost"
|
||||||
|
}
|
||||||
|
return Cfg{
|
||||||
|
Host: hostname,
|
||||||
|
Port: 8989,
|
||||||
|
LogLevel: "DEBUG",
|
||||||
|
LogPath: "",
|
||||||
|
BaseURL: "/",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeConfig(configPath string, configFile string) error {
|
||||||
|
path := filepath.Join(configPath, configFile)
|
||||||
|
|
||||||
|
// check if configPath exists, if not create it
|
||||||
|
if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) {
|
||||||
|
err := os.MkdirAll(configPath, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if config exists, if not create it
|
||||||
|
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
|
||||||
|
f, err := os.Create(path)
|
||||||
|
if err != nil { // perm 0666
|
||||||
|
// handle failed create
|
||||||
|
log.Printf("error creating file: %q", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
_, err = f.WriteString(`# config.toml
|
||||||
|
|
||||||
|
# Hostname / IP
|
||||||
|
#
|
||||||
|
# Default: "localhost"
|
||||||
|
#
|
||||||
|
host = "127.0.0.1"
|
||||||
|
|
||||||
|
# Port
|
||||||
|
#
|
||||||
|
# Default: 8989
|
||||||
|
#
|
||||||
|
port = 8989
|
||||||
|
|
||||||
|
# Base url
|
||||||
|
# Set custom baseUrl eg /autobrr/ to serve in subdirectory.
|
||||||
|
# Not needed for subdomain, or by accessing with the :port directly.
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
#baseUrl = "/autobrr/"
|
||||||
|
|
||||||
|
# autobrr logs file
|
||||||
|
# If not defined, logs to stdout
|
||||||
|
#
|
||||||
|
# Optional
|
||||||
|
#
|
||||||
|
#logPath = "log/autobrr.log"
|
||||||
|
|
||||||
|
# Log level
|
||||||
|
#
|
||||||
|
# Default: "DEBUG"
|
||||||
|
#
|
||||||
|
# Options: "ERROR", "DEBUG", "INFO", "WARN"
|
||||||
|
#
|
||||||
|
logLevel = "DEBUG"`)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error writing contents to file: %v %q", configPath, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.Sync()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Read(configPath string) Cfg {
|
||||||
|
config := Defaults()
|
||||||
|
|
||||||
|
// or use viper.SetDefault(val, def)
|
||||||
|
//viper.SetDefault("host", config.Host)
|
||||||
|
//viper.SetDefault("port", config.Port)
|
||||||
|
//viper.SetDefault("logLevel", config.LogLevel)
|
||||||
|
//viper.SetDefault("logPath", config.LogPath)
|
||||||
|
|
||||||
|
viper.SetConfigType("toml")
|
||||||
|
|
||||||
|
// clean trailing slash from configPath
|
||||||
|
configPath = path.Clean(configPath)
|
||||||
|
|
||||||
|
if configPath != "" {
|
||||||
|
//viper.SetConfigName("config")
|
||||||
|
|
||||||
|
// check if path and file exists
|
||||||
|
// if not, create path and file
|
||||||
|
err := writeConfig(configPath, "config.toml")
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("write error: %q", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
viper.SetConfigFile(path.Join(configPath, "config.toml"))
|
||||||
|
} else {
|
||||||
|
viper.SetConfigName("config")
|
||||||
|
|
||||||
|
// Search config in directories
|
||||||
|
viper.AddConfigPath(".")
|
||||||
|
viper.AddConfigPath("$HOME/.config/autobrr")
|
||||||
|
viper.AddConfigPath("$HOME/.autobrr")
|
||||||
|
}
|
||||||
|
|
||||||
|
// read config
|
||||||
|
if err := viper.ReadInConfig(); err != nil {
|
||||||
|
log.Printf("config read error: %q", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := viper.Unmarshal(&config); err != nil {
|
||||||
|
log.Fatalf("Could not unmarshal config file: %v", viper.ConfigFileUsed())
|
||||||
|
}
|
||||||
|
|
||||||
|
Config = config
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
197
internal/database/action.go
Normal file
197
internal/database/action.go
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActionRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewActionRepo(db *sql.DB) domain.ActionRepo {
|
||||||
|
return &ActionRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ActionRepo) FindByFilterID(filterID int) ([]domain.Action, error) {
|
||||||
|
|
||||||
|
rows, err := r.db.Query("SELECT id, name, type, enabled, exec_cmd, exec_args, watch_folder, category, tags, label, save_path, paused, ignore_rules, limit_download_speed, limit_upload_speed, client_id FROM action WHERE action.filter_id = ?", filterID)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var actions []domain.Action
|
||||||
|
for rows.Next() {
|
||||||
|
var a domain.Action
|
||||||
|
|
||||||
|
var execCmd, execArgs, watchFolder, category, tags, label, savePath sql.NullString
|
||||||
|
var limitUl, limitDl sql.NullInt64
|
||||||
|
var clientID sql.NullInt32
|
||||||
|
// filterID
|
||||||
|
var paused, ignoreRules sql.NullBool
|
||||||
|
|
||||||
|
if err := rows.Scan(&a.ID, &a.Name, &a.Type, &a.Enabled, &execCmd, &execArgs, &watchFolder, &category, &tags, &label, &savePath, &paused, &ignoreRules, &limitDl, &limitUl, &clientID); err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.ExecCmd = execCmd.String
|
||||||
|
a.ExecArgs = execArgs.String
|
||||||
|
a.WatchFolder = watchFolder.String
|
||||||
|
a.Category = category.String
|
||||||
|
a.Tags = tags.String
|
||||||
|
a.Label = label.String
|
||||||
|
a.SavePath = savePath.String
|
||||||
|
a.Paused = paused.Bool
|
||||||
|
a.IgnoreRules = ignoreRules.Bool
|
||||||
|
a.LimitUploadSpeed = limitUl.Int64
|
||||||
|
a.LimitDownloadSpeed = limitDl.Int64
|
||||||
|
a.ClientID = clientID.Int32
|
||||||
|
|
||||||
|
actions = append(actions, a)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ActionRepo) List() ([]domain.Action, error) {
|
||||||
|
|
||||||
|
rows, err := r.db.Query("SELECT id, name, type, enabled, exec_cmd, exec_args, watch_folder, category, tags, label, save_path, paused, ignore_rules, limit_download_speed, limit_upload_speed, client_id FROM action")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var actions []domain.Action
|
||||||
|
for rows.Next() {
|
||||||
|
var a domain.Action
|
||||||
|
|
||||||
|
var execCmd, execArgs, watchFolder, category, tags, label, savePath sql.NullString
|
||||||
|
var limitUl, limitDl sql.NullInt64
|
||||||
|
var clientID sql.NullInt32
|
||||||
|
var paused, ignoreRules sql.NullBool
|
||||||
|
|
||||||
|
if err := rows.Scan(&a.ID, &a.Name, &a.Type, &a.Enabled, &execCmd, &execArgs, &watchFolder, &category, &tags, &label, &savePath, &paused, &ignoreRules, &limitDl, &limitUl, &clientID); err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Category = category.String
|
||||||
|
a.Tags = tags.String
|
||||||
|
a.Label = label.String
|
||||||
|
a.SavePath = savePath.String
|
||||||
|
a.Paused = paused.Bool
|
||||||
|
a.IgnoreRules = ignoreRules.Bool
|
||||||
|
a.LimitUploadSpeed = limitUl.Int64
|
||||||
|
a.LimitDownloadSpeed = limitDl.Int64
|
||||||
|
a.ClientID = clientID.Int32
|
||||||
|
|
||||||
|
actions = append(actions, a)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ActionRepo) Delete(actionID int) error {
|
||||||
|
res, err := r.db.Exec(`DELETE FROM action WHERE action.id = ?`, actionID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, _ := res.RowsAffected()
|
||||||
|
|
||||||
|
log.Info().Msgf("rows affected %v", rows)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ActionRepo) Store(action domain.Action) (*domain.Action, error) {
|
||||||
|
|
||||||
|
execCmd := toNullString(action.ExecCmd)
|
||||||
|
execArgs := toNullString(action.ExecArgs)
|
||||||
|
watchFolder := toNullString(action.WatchFolder)
|
||||||
|
category := toNullString(action.Category)
|
||||||
|
tags := toNullString(action.Tags)
|
||||||
|
label := toNullString(action.Label)
|
||||||
|
savePath := toNullString(action.SavePath)
|
||||||
|
|
||||||
|
limitDL := toNullInt64(action.LimitDownloadSpeed)
|
||||||
|
limitUL := toNullInt64(action.LimitUploadSpeed)
|
||||||
|
clientID := toNullInt32(action.ClientID)
|
||||||
|
filterID := toNullInt32(int32(action.FilterID))
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if action.ID != 0 {
|
||||||
|
log.Info().Msg("UPDATE existing record")
|
||||||
|
_, err = r.db.Exec(`UPDATE action SET name = ?, type = ?, enabled = ?, exec_cmd = ?, exec_args = ?, watch_folder = ? , category =? , tags = ?, label = ?, save_path = ?, paused = ?, ignore_rules = ?, limit_upload_speed = ?, limit_download_speed = ?, client_id = ?
|
||||||
|
WHERE id = ?`, action.Name, action.Type, action.Enabled, execCmd, execArgs, watchFolder, category, tags, label, savePath, action.Paused, action.IgnoreRules, limitUL, limitDL, clientID, action.ID)
|
||||||
|
} else {
|
||||||
|
var res sql.Result
|
||||||
|
|
||||||
|
res, err = r.db.Exec(`INSERT INTO action(name, type, enabled, exec_cmd, exec_args, watch_folder, category, tags, label, save_path, paused, ignore_rules, limit_upload_speed, limit_download_speed, client_id, filter_id)
|
||||||
|
VALUES (?, ?, ?, ?, ?,? ,?, ?,?,?,?,?,?,?,?,?) ON CONFLICT DO NOTHING`, action.Name, action.Type, action.Enabled, execCmd, execArgs, watchFolder, category, tags, label, savePath, action.Paused, action.IgnoreRules, limitUL, limitDL, clientID, filterID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resId, _ := res.LastInsertId()
|
||||||
|
log.Info().Msgf("LAST INSERT ID %v", resId)
|
||||||
|
action.ID = int(resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &action, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ActionRepo) ToggleEnabled(actionID int) error {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
var res sql.Result
|
||||||
|
|
||||||
|
res, err = r.db.Exec(`UPDATE action SET enabled = NOT enabled WHERE id = ?`, actionID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resId, _ := res.LastInsertId()
|
||||||
|
log.Info().Msgf("LAST INSERT ID %v", resId)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toNullString(s string) sql.NullString {
|
||||||
|
return sql.NullString{
|
||||||
|
String: s,
|
||||||
|
Valid: s != "",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toNullInt32(s int32) sql.NullInt32 {
|
||||||
|
return sql.NullInt32{
|
||||||
|
Int32: s,
|
||||||
|
Valid: s != 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func toNullInt64(s int64) sql.NullInt64 {
|
||||||
|
return sql.NullInt64{
|
||||||
|
Int64: s,
|
||||||
|
Valid: s != 0,
|
||||||
|
}
|
||||||
|
}
|
19
internal/database/announce.go
Normal file
19
internal/database/announce.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AnnounceRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAnnounceRepo(db *sql.DB) domain.AnnounceRepo {
|
||||||
|
return &AnnounceRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AnnounceRepo) Store(announce domain.Announce) error {
|
||||||
|
return nil
|
||||||
|
}
|
134
internal/database/download_client.go
Normal file
134
internal/database/download_client.go
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DownloadClientRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDownloadClientRepo(db *sql.DB) domain.DownloadClientRepo {
|
||||||
|
return &DownloadClientRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DownloadClientRepo) List() ([]domain.DownloadClient, error) {
|
||||||
|
|
||||||
|
rows, err := r.db.Query("SELECT id, name, type, enabled, host, port, ssl, username, password FROM client")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
clients := make([]domain.DownloadClient, 0)
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var f domain.DownloadClient
|
||||||
|
|
||||||
|
if err := rows.Scan(&f.ID, &f.Name, &f.Type, &f.Enabled, &f.Host, &f.Port, &f.SSL, &f.Username, &f.Password); err != nil {
|
||||||
|
log.Error().Err(err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
clients = append(clients, f)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return clients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DownloadClientRepo) FindByID(id int32) (*domain.DownloadClient, error) {
|
||||||
|
|
||||||
|
query := `
|
||||||
|
SELECT id, name, type, enabled, host, port, ssl, username, password FROM client WHERE id = ?
|
||||||
|
`
|
||||||
|
|
||||||
|
row := r.db.QueryRow(query, id)
|
||||||
|
if err := row.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var client domain.DownloadClient
|
||||||
|
|
||||||
|
if err := row.Scan(&client.ID, &client.Name, &client.Type, &client.Enabled, &client.Host, &client.Port, &client.SSL, &client.Username, &client.Password); err != nil {
|
||||||
|
log.Error().Err(err).Msg("could not scan download client to struct")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DownloadClientRepo) FindByActionID(actionID int) ([]domain.DownloadClient, error) {
|
||||||
|
|
||||||
|
rows, err := r.db.Query("SELECT id, name, type, enabled, host, port, ssl, username, password FROM client, action_client WHERE client.id = action_client.client_id AND action_client.action_id = ?", actionID)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var clients []domain.DownloadClient
|
||||||
|
for rows.Next() {
|
||||||
|
var f domain.DownloadClient
|
||||||
|
|
||||||
|
if err := rows.Scan(&f.ID, &f.Name, &f.Type, &f.Enabled, &f.Host, &f.Port, &f.SSL, &f.Username, &f.Password); err != nil {
|
||||||
|
log.Error().Err(err)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
clients = append(clients, f)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return clients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DownloadClientRepo) Store(client domain.DownloadClient) (*domain.DownloadClient, error) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if client.ID != 0 {
|
||||||
|
log.Info().Msg("UPDATE existing record")
|
||||||
|
_, err = r.db.Exec(`UPDATE client SET name = ?, type = ?, enabled = ?, host = ?, port = ?, ssl = ?, username = ?, password = ? WHERE id = ?`, client.Name, client.Type, client.Enabled, client.Host, client.Port, client.SSL, client.Username, client.Password, client.ID)
|
||||||
|
} else {
|
||||||
|
var res sql.Result
|
||||||
|
|
||||||
|
res, err = r.db.Exec(`INSERT INTO client(name, type, enabled, host, port, ssl, username, password)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ? , ?, ?) ON CONFLICT DO NOTHING`, client.Name, client.Type, client.Enabled, client.Host, client.Port, client.SSL, client.Username, client.Password)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resId, _ := res.LastInsertId()
|
||||||
|
log.Info().Msgf("LAST INSERT ID %v", resId)
|
||||||
|
client.ID = int(resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DownloadClientRepo) Delete(clientID int) error {
|
||||||
|
res, err := r.db.Exec(`DELETE FROM client WHERE client.id = ?`, clientID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, _ := res.RowsAffected()
|
||||||
|
|
||||||
|
log.Info().Msgf("rows affected %v", rows)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
441
internal/database/filter.go
Normal file
441
internal/database/filter.go
Normal file
|
@ -0,0 +1,441 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FilterRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFilterRepo(db *sql.DB) domain.FilterRepo {
|
||||||
|
return &FilterRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FilterRepo) ListFilters() ([]domain.Filter, error) {
|
||||||
|
|
||||||
|
rows, err := r.db.Query("SELECT id, enabled, name, match_releases, except_releases, created_at, updated_at FROM filter")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var filters []domain.Filter
|
||||||
|
for rows.Next() {
|
||||||
|
var f domain.Filter
|
||||||
|
|
||||||
|
var matchReleases, exceptReleases sql.NullString
|
||||||
|
var createdAt, updatedAt string
|
||||||
|
|
||||||
|
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &matchReleases, &exceptReleases, &createdAt, &updatedAt); err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("filters_list: error scanning data to struct")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.MatchReleases = matchReleases.String
|
||||||
|
f.ExceptReleases = exceptReleases.String
|
||||||
|
|
||||||
|
ua, _ := time.Parse(time.RFC3339, updatedAt)
|
||||||
|
ca, _ := time.Parse(time.RFC3339, createdAt)
|
||||||
|
|
||||||
|
f.UpdatedAt = ua
|
||||||
|
f.CreatedAt = ca
|
||||||
|
|
||||||
|
filters = append(filters, f)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FilterRepo) FindByID(filterID int) (*domain.Filter, error) {
|
||||||
|
|
||||||
|
row := r.db.QueryRow("SELECT id, enabled, name, min_size, max_size, delay, match_releases, except_releases, use_regex, match_release_groups, except_release_groups, scene, freeleech, freeleech_percent, shows, seasons, episodes, resolutions, codecs, sources, containers, years, match_categories, except_categories, match_uploaders, except_uploaders, tags, except_tags, created_at, updated_at FROM filter WHERE id = ?", filterID)
|
||||||
|
|
||||||
|
var f domain.Filter
|
||||||
|
|
||||||
|
if err := row.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var minSize, maxSize, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, freeleechPercent, shows, seasons, episodes, years, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags sql.NullString
|
||||||
|
var useRegex, scene, freeleech sql.NullBool
|
||||||
|
var delay sql.NullInt32
|
||||||
|
var createdAt, updatedAt string
|
||||||
|
|
||||||
|
if err := row.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &scene, &freeleech, &freeleechPercent, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), &years, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, &createdAt, &updatedAt); err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("filter: %v : error scanning data to struct", filterID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.MinSize = minSize.String
|
||||||
|
f.MaxSize = maxSize.String
|
||||||
|
f.Delay = int(delay.Int32)
|
||||||
|
f.MatchReleases = matchReleases.String
|
||||||
|
f.ExceptReleases = exceptReleases.String
|
||||||
|
f.MatchReleaseGroups = matchReleaseGroups.String
|
||||||
|
f.ExceptReleaseGroups = exceptReleaseGroups.String
|
||||||
|
f.FreeleechPercent = freeleechPercent.String
|
||||||
|
f.Shows = shows.String
|
||||||
|
f.Seasons = seasons.String
|
||||||
|
f.Episodes = minSize.String
|
||||||
|
f.Years = years.String
|
||||||
|
f.MatchCategories = matchCategories.String
|
||||||
|
f.ExceptCategories = exceptCategories.String
|
||||||
|
f.MatchUploaders = matchUploaders.String
|
||||||
|
f.ExceptUploaders = exceptUploaders.String
|
||||||
|
f.Tags = tags.String
|
||||||
|
f.ExceptTags = exceptTags.String
|
||||||
|
f.UseRegex = useRegex.Bool
|
||||||
|
f.Scene = scene.Bool
|
||||||
|
f.Freeleech = freeleech.Bool
|
||||||
|
|
||||||
|
updatedTime, _ := time.Parse(time.RFC3339, updatedAt)
|
||||||
|
createdTime, _ := time.Parse(time.RFC3339, createdAt)
|
||||||
|
|
||||||
|
f.UpdatedAt = updatedTime
|
||||||
|
f.CreatedAt = createdTime
|
||||||
|
|
||||||
|
return &f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO remove
|
||||||
|
func (r *FilterRepo) FindFiltersForSite(site string) ([]domain.Filter, error) {
|
||||||
|
|
||||||
|
rows, err := r.db.Query("SELECT id, enabled, name, match_releases, except_releases, created_at, updated_at FROM filter WHERE match_sites LIKE ?", site)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var filters []domain.Filter
|
||||||
|
for rows.Next() {
|
||||||
|
var f domain.Filter
|
||||||
|
|
||||||
|
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, pq.Array(&f.MatchReleases), pq.Array(&f.ExceptReleases), &f.CreatedAt, &f.UpdatedAt); err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error scanning data to struct")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
filters = append(filters, f)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FilterRepo) FindByIndexerIdentifier(indexer string) ([]domain.Filter, error) {
|
||||||
|
|
||||||
|
rows, err := r.db.Query(`
|
||||||
|
SELECT
|
||||||
|
f.id,
|
||||||
|
f.enabled,
|
||||||
|
f.name,
|
||||||
|
f.min_size,
|
||||||
|
f.max_size,
|
||||||
|
f.delay,
|
||||||
|
f.match_releases,
|
||||||
|
f.except_releases,
|
||||||
|
f.use_regex,
|
||||||
|
f.match_release_groups,
|
||||||
|
f.except_release_groups,
|
||||||
|
f.scene,
|
||||||
|
f.freeleech,
|
||||||
|
f.freeleech_percent,
|
||||||
|
f.shows,
|
||||||
|
f.seasons,
|
||||||
|
f.episodes,
|
||||||
|
f.resolutions,
|
||||||
|
f.codecs,
|
||||||
|
f.sources,
|
||||||
|
f.containers,
|
||||||
|
f.years,
|
||||||
|
f.match_categories,
|
||||||
|
f.except_categories,
|
||||||
|
f.match_uploaders,
|
||||||
|
f.except_uploaders,
|
||||||
|
f.tags,
|
||||||
|
f.except_tags,
|
||||||
|
f.created_at,
|
||||||
|
f.updated_at
|
||||||
|
FROM filter f
|
||||||
|
JOIN filter_indexer fi on f.id = fi.filter_id
|
||||||
|
JOIN indexer i on i.id = fi.indexer_id
|
||||||
|
WHERE i.identifier = ?`, indexer)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var filters []domain.Filter
|
||||||
|
for rows.Next() {
|
||||||
|
var f domain.Filter
|
||||||
|
|
||||||
|
var minSize, maxSize, matchReleases, exceptReleases, matchReleaseGroups, exceptReleaseGroups, freeleechPercent, shows, seasons, episodes, years, matchCategories, exceptCategories, matchUploaders, exceptUploaders, tags, exceptTags sql.NullString
|
||||||
|
var useRegex, scene, freeleech sql.NullBool
|
||||||
|
var delay sql.NullInt32
|
||||||
|
var createdAt, updatedAt string
|
||||||
|
|
||||||
|
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &minSize, &maxSize, &delay, &matchReleases, &exceptReleases, &useRegex, &matchReleaseGroups, &exceptReleaseGroups, &scene, &freeleech, &freeleechPercent, &shows, &seasons, &episodes, pq.Array(&f.Resolutions), pq.Array(&f.Codecs), pq.Array(&f.Sources), pq.Array(&f.Containers), &years, &matchCategories, &exceptCategories, &matchUploaders, &exceptUploaders, &tags, &exceptTags, &createdAt, &updatedAt); err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error scanning data to struct")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.MinSize = minSize.String
|
||||||
|
f.MaxSize = maxSize.String
|
||||||
|
f.Delay = int(delay.Int32)
|
||||||
|
f.MatchReleases = matchReleases.String
|
||||||
|
f.ExceptReleases = exceptReleases.String
|
||||||
|
f.MatchReleaseGroups = matchReleaseGroups.String
|
||||||
|
f.ExceptReleaseGroups = exceptReleaseGroups.String
|
||||||
|
f.FreeleechPercent = freeleechPercent.String
|
||||||
|
f.Shows = shows.String
|
||||||
|
f.Seasons = seasons.String
|
||||||
|
f.Episodes = minSize.String
|
||||||
|
f.Years = years.String
|
||||||
|
f.MatchCategories = matchCategories.String
|
||||||
|
f.ExceptCategories = exceptCategories.String
|
||||||
|
f.MatchUploaders = matchUploaders.String
|
||||||
|
f.ExceptUploaders = exceptUploaders.String
|
||||||
|
f.Tags = tags.String
|
||||||
|
f.ExceptTags = exceptTags.String
|
||||||
|
f.UseRegex = useRegex.Bool
|
||||||
|
f.Scene = scene.Bool
|
||||||
|
f.Freeleech = freeleech.Bool
|
||||||
|
|
||||||
|
updatedTime, _ := time.Parse(time.RFC3339, updatedAt)
|
||||||
|
createdTime, _ := time.Parse(time.RFC3339, createdAt)
|
||||||
|
|
||||||
|
f.UpdatedAt = updatedTime
|
||||||
|
f.CreatedAt = createdTime
|
||||||
|
|
||||||
|
filters = append(filters, f)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FilterRepo) Store(filter domain.Filter) (*domain.Filter, error) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if filter.ID != 0 {
|
||||||
|
log.Debug().Msg("update existing record")
|
||||||
|
} else {
|
||||||
|
var res sql.Result
|
||||||
|
|
||||||
|
res, err = r.db.Exec(`INSERT INTO filter (
|
||||||
|
name,
|
||||||
|
enabled,
|
||||||
|
min_size,
|
||||||
|
max_size,
|
||||||
|
delay,
|
||||||
|
match_releases,
|
||||||
|
except_releases,
|
||||||
|
use_regex,
|
||||||
|
match_release_groups,
|
||||||
|
except_release_groups,
|
||||||
|
scene,
|
||||||
|
freeleech,
|
||||||
|
freeleech_percent,
|
||||||
|
shows,
|
||||||
|
seasons,
|
||||||
|
episodes,
|
||||||
|
resolutions,
|
||||||
|
codecs,
|
||||||
|
sources,
|
||||||
|
containers,
|
||||||
|
years,
|
||||||
|
match_categories,
|
||||||
|
except_categories,
|
||||||
|
match_uploaders,
|
||||||
|
except_uploaders,
|
||||||
|
tags,
|
||||||
|
except_tags
|
||||||
|
)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27) ON CONFLICT DO NOTHING`,
|
||||||
|
filter.Name,
|
||||||
|
filter.Enabled,
|
||||||
|
filter.MinSize,
|
||||||
|
filter.MaxSize,
|
||||||
|
filter.Delay,
|
||||||
|
filter.MatchReleases,
|
||||||
|
filter.ExceptReleases,
|
||||||
|
filter.UseRegex,
|
||||||
|
filter.MatchReleaseGroups,
|
||||||
|
filter.ExceptReleaseGroups,
|
||||||
|
filter.Scene,
|
||||||
|
filter.Freeleech,
|
||||||
|
filter.FreeleechPercent,
|
||||||
|
filter.Shows,
|
||||||
|
filter.Seasons,
|
||||||
|
filter.Episodes,
|
||||||
|
pq.Array(filter.Resolutions),
|
||||||
|
pq.Array(filter.Codecs),
|
||||||
|
pq.Array(filter.Sources),
|
||||||
|
pq.Array(filter.Containers),
|
||||||
|
filter.Years,
|
||||||
|
filter.MatchCategories,
|
||||||
|
filter.ExceptCategories,
|
||||||
|
filter.MatchUploaders,
|
||||||
|
filter.ExceptUploaders,
|
||||||
|
filter.Tags,
|
||||||
|
filter.ExceptTags,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error executing query")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resId, _ := res.LastInsertId()
|
||||||
|
filter.ID = int(resId)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FilterRepo) Update(filter domain.Filter) (*domain.Filter, error) {
|
||||||
|
|
||||||
|
//var res sql.Result
|
||||||
|
|
||||||
|
var err error
|
||||||
|
_, err = r.db.Exec(`
|
||||||
|
UPDATE filter SET
|
||||||
|
name = ?,
|
||||||
|
enabled = ?,
|
||||||
|
min_size = ?,
|
||||||
|
max_size = ?,
|
||||||
|
delay = ?,
|
||||||
|
match_releases = ?,
|
||||||
|
except_releases = ?,
|
||||||
|
use_regex = ?,
|
||||||
|
match_release_groups = ?,
|
||||||
|
except_release_groups = ?,
|
||||||
|
scene = ?,
|
||||||
|
freeleech = ?,
|
||||||
|
freeleech_percent = ?,
|
||||||
|
shows = ?,
|
||||||
|
seasons = ?,
|
||||||
|
episodes = ?,
|
||||||
|
resolutions = ?,
|
||||||
|
codecs = ?,
|
||||||
|
sources = ?,
|
||||||
|
containers = ?,
|
||||||
|
years = ?,
|
||||||
|
match_categories = ?,
|
||||||
|
except_categories = ?,
|
||||||
|
match_uploaders = ?,
|
||||||
|
except_uploaders = ?,
|
||||||
|
tags = ?,
|
||||||
|
except_tags = ?,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?`,
|
||||||
|
filter.Name,
|
||||||
|
filter.Enabled,
|
||||||
|
filter.MinSize,
|
||||||
|
filter.MaxSize,
|
||||||
|
filter.Delay,
|
||||||
|
filter.MatchReleases,
|
||||||
|
filter.ExceptReleases,
|
||||||
|
filter.UseRegex,
|
||||||
|
filter.MatchReleaseGroups,
|
||||||
|
filter.ExceptReleaseGroups,
|
||||||
|
filter.Scene,
|
||||||
|
filter.Freeleech,
|
||||||
|
filter.FreeleechPercent,
|
||||||
|
filter.Shows,
|
||||||
|
filter.Seasons,
|
||||||
|
filter.Episodes,
|
||||||
|
pq.Array(filter.Resolutions),
|
||||||
|
pq.Array(filter.Codecs),
|
||||||
|
pq.Array(filter.Sources),
|
||||||
|
pq.Array(filter.Containers),
|
||||||
|
filter.Years,
|
||||||
|
filter.MatchCategories,
|
||||||
|
filter.ExceptCategories,
|
||||||
|
filter.MatchUploaders,
|
||||||
|
filter.ExceptUploaders,
|
||||||
|
filter.Tags,
|
||||||
|
filter.ExceptTags,
|
||||||
|
filter.ID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error executing query")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FilterRepo) StoreIndexerConnection(filterID int, indexerID int) error {
|
||||||
|
query := `INSERT INTO filter_indexer (filter_id, indexer_id) VALUES ($1, $2)`
|
||||||
|
_, err := r.db.Exec(query, filterID, indexerID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error executing query")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FilterRepo) DeleteIndexerConnections(filterID int) error {
|
||||||
|
|
||||||
|
query := `DELETE FROM filter_indexer WHERE filter_id = ?`
|
||||||
|
_, err := r.db.Exec(query, filterID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error executing query")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FilterRepo) Delete(filterID int) error {
|
||||||
|
|
||||||
|
res, err := r.db.Exec(`DELETE FROM filter WHERE id = ?`, filterID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error executing query")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, _ := res.RowsAffected()
|
||||||
|
|
||||||
|
log.Info().Msgf("rows affected %v", rows)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split string to slice. We store comma separated strings and convert to slice
|
||||||
|
func stringToSlice(str string) []string {
|
||||||
|
if str == "" {
|
||||||
|
return []string{}
|
||||||
|
} else if !strings.Contains(str, ",") {
|
||||||
|
return []string{str}
|
||||||
|
}
|
||||||
|
|
||||||
|
split := strings.Split(str, ",")
|
||||||
|
|
||||||
|
return split
|
||||||
|
}
|
152
internal/database/indexer.go
Normal file
152
internal/database/indexer.go
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IndexerRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIndexerRepo(db *sql.DB) domain.IndexerRepo {
|
||||||
|
return &IndexerRepo{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IndexerRepo) Store(indexer domain.Indexer) (*domain.Indexer, error) {
|
||||||
|
|
||||||
|
settings, err := json.Marshal(indexer.Settings)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error marshaling json data")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.db.Exec(`INSERT INTO indexer (enabled, name, identifier, settings) VALUES (?, ?, ?, ?)`, indexer.Enabled, indexer.Name, indexer.Identifier, settings)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error executing query")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &indexer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IndexerRepo) Update(indexer domain.Indexer) (*domain.Indexer, error) {
|
||||||
|
|
||||||
|
sett, err := json.Marshal(indexer.Settings)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error marshaling json data")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = r.db.Exec(`UPDATE indexer SET enabled = ?, name = ?, settings = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?`, indexer.Enabled, indexer.Name, sett, indexer.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error executing query")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &indexer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IndexerRepo) List() ([]domain.Indexer, error) {
|
||||||
|
|
||||||
|
rows, err := r.db.Query("SELECT id, enabled, name, identifier, settings FROM indexer")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var indexers []domain.Indexer
|
||||||
|
for rows.Next() {
|
||||||
|
var f domain.Indexer
|
||||||
|
|
||||||
|
var settings string
|
||||||
|
var settingsMap map[string]string
|
||||||
|
|
||||||
|
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &f.Identifier, &settings); err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("indexer.list: error scanning data to struct")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(settings), &settingsMap)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("indexer.list: error unmarshal settings")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.Settings = settingsMap
|
||||||
|
|
||||||
|
indexers = append(indexers, f)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IndexerRepo) FindByFilterID(id int) ([]domain.Indexer, error) {
|
||||||
|
rows, err := r.db.Query(`
|
||||||
|
SELECT i.id, i.enabled, i.name, i.identifier
|
||||||
|
FROM indexer i
|
||||||
|
JOIN filter_indexer fi on i.id = fi.indexer_id
|
||||||
|
WHERE fi.filter_id = ?`, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var indexers []domain.Indexer
|
||||||
|
for rows.Next() {
|
||||||
|
var f domain.Indexer
|
||||||
|
|
||||||
|
//var settings string
|
||||||
|
//var settingsMap map[string]string
|
||||||
|
|
||||||
|
if err := rows.Scan(&f.ID, &f.Enabled, &f.Name, &f.Identifier); err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("indexer.list: error scanning data to struct")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//err = json.Unmarshal([]byte(settings), &settingsMap)
|
||||||
|
//if err != nil {
|
||||||
|
// log.Error().Stack().Err(err).Msg("indexer.list: error unmarshal settings")
|
||||||
|
// return nil, err
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//f.Settings = settingsMap
|
||||||
|
|
||||||
|
indexers = append(indexers, f)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return indexers, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *IndexerRepo) Delete(id int) error {
|
||||||
|
|
||||||
|
res, err := r.db.Exec(`DELETE FROM indexer WHERE id = ?`, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error executing query")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, _ := res.RowsAffected()
|
||||||
|
|
||||||
|
log.Info().Msgf("rows affected %v", rows)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
277
internal/database/irc.go
Normal file
277
internal/database/irc.go
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IrcRepo struct {
|
||||||
|
db *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIrcRepo(db *sql.DB) domain.IrcRepo {
|
||||||
|
return &IrcRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ir *IrcRepo) Store(announce domain.Announce) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ir *IrcRepo) GetNetworkByID(id int64) (*domain.IrcNetwork, error) {
|
||||||
|
|
||||||
|
row := ir.db.QueryRow("SELECT id, enabled, name, addr, tls, nick, pass, connect_commands, sasl_mechanism, sasl_plain_username, sasl_plain_password FROM irc_network WHERE id = ?", id)
|
||||||
|
if err := row.Err(); err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var n domain.IrcNetwork
|
||||||
|
|
||||||
|
var pass, connectCommands sql.NullString
|
||||||
|
var saslMechanism, saslPlainUsername, saslPlainPassword sql.NullString
|
||||||
|
var tls sql.NullBool
|
||||||
|
|
||||||
|
if err := row.Scan(&n.ID, &n.Enabled, &n.Name, &n.Addr, &tls, &n.Nick, &pass, &connectCommands, &saslMechanism, &saslPlainUsername, &saslPlainPassword); err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
n.TLS = tls.Bool
|
||||||
|
n.Pass = pass.String
|
||||||
|
if connectCommands.Valid {
|
||||||
|
n.ConnectCommands = strings.Split(connectCommands.String, "\r\n")
|
||||||
|
}
|
||||||
|
n.SASL.Mechanism = saslMechanism.String
|
||||||
|
n.SASL.Plain.Username = saslPlainUsername.String
|
||||||
|
n.SASL.Plain.Password = saslPlainPassword.String
|
||||||
|
|
||||||
|
return &n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ir *IrcRepo) DeleteNetwork(ctx context.Context, id int64) error {
|
||||||
|
tx, err := ir.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, `DELETE FROM irc_network WHERE id = ?`, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("error deleting network: %v", id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.ExecContext(ctx, `DELETE FROM irc_channel WHERE network_id = ?`, id)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("error deleting channels for network: %v", id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msgf("error deleting network: %v", id)
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ir *IrcRepo) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error) {
|
||||||
|
|
||||||
|
rows, err := ir.db.QueryContext(ctx, "SELECT id, enabled, name, addr, tls, nick, pass, connect_commands FROM irc_network")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var networks []domain.IrcNetwork
|
||||||
|
for rows.Next() {
|
||||||
|
var net domain.IrcNetwork
|
||||||
|
|
||||||
|
//var username, realname, pass, connectCommands sql.NullString
|
||||||
|
var pass, connectCommands sql.NullString
|
||||||
|
var tls sql.NullBool
|
||||||
|
|
||||||
|
if err := rows.Scan(&net.ID, &net.Enabled, &net.Name, &net.Addr, &tls, &net.Nick, &pass, &connectCommands); err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
net.TLS = tls.Bool
|
||||||
|
net.Pass = pass.String
|
||||||
|
|
||||||
|
if connectCommands.Valid {
|
||||||
|
net.ConnectCommands = strings.Split(connectCommands.String, "\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
networks = append(networks, net)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return networks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ir *IrcRepo) ListChannels(networkID int64) ([]domain.IrcChannel, error) {
|
||||||
|
|
||||||
|
rows, err := ir.db.Query("SELECT id, name, enabled FROM irc_channel WHERE network_id = ?", networkID)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var channels []domain.IrcChannel
|
||||||
|
for rows.Next() {
|
||||||
|
var ch domain.IrcChannel
|
||||||
|
|
||||||
|
//if err := rows.Scan(&ch.ID, &ch.Name, &ch.Enabled, &ch.Pass, &ch.InviteCommand, &ch.InviteHTTPURL, &ch.InviteHTTPHeader, &ch.InviteHTTPData); err != nil {
|
||||||
|
if err := rows.Scan(&ch.ID, &ch.Name, &ch.Enabled); err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
channels = append(channels, ch)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return channels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ir *IrcRepo) StoreNetwork(network *domain.IrcNetwork) error {
|
||||||
|
|
||||||
|
netName := toNullString(network.Name)
|
||||||
|
pass := toNullString(network.Pass)
|
||||||
|
connectCommands := toNullString(strings.Join(network.ConnectCommands, "\r\n"))
|
||||||
|
|
||||||
|
var saslMechanism, saslPlainUsername, saslPlainPassword sql.NullString
|
||||||
|
if network.SASL.Mechanism != "" {
|
||||||
|
saslMechanism = toNullString(network.SASL.Mechanism)
|
||||||
|
switch network.SASL.Mechanism {
|
||||||
|
case "PLAIN":
|
||||||
|
saslPlainUsername = toNullString(network.SASL.Plain.Username)
|
||||||
|
saslPlainPassword = toNullString(network.SASL.Plain.Password)
|
||||||
|
default:
|
||||||
|
log.Warn().Msgf("unsupported SASL mechanism: %q", network.SASL.Mechanism)
|
||||||
|
//return fmt.Errorf("cannot store network: unsupported SASL mechanism %q", network.SASL.Mechanism)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if network.ID != 0 {
|
||||||
|
// update record
|
||||||
|
_, err = ir.db.Exec(`UPDATE irc_network
|
||||||
|
SET enabled = ?,
|
||||||
|
name = ?,
|
||||||
|
addr = ?,
|
||||||
|
tls = ?,
|
||||||
|
nick = ?,
|
||||||
|
pass = ?,
|
||||||
|
connect_commands = ?,
|
||||||
|
sasl_mechanism = ?,
|
||||||
|
sasl_plain_username = ?,
|
||||||
|
sasl_plain_password = ?,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?`,
|
||||||
|
network.Enabled,
|
||||||
|
netName,
|
||||||
|
network.Addr,
|
||||||
|
network.TLS,
|
||||||
|
network.Nick,
|
||||||
|
pass,
|
||||||
|
connectCommands,
|
||||||
|
saslMechanism,
|
||||||
|
saslPlainUsername,
|
||||||
|
saslPlainPassword,
|
||||||
|
network.ID,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
var res sql.Result
|
||||||
|
|
||||||
|
res, err = ir.db.Exec(`INSERT INTO irc_network (
|
||||||
|
enabled,
|
||||||
|
name,
|
||||||
|
addr,
|
||||||
|
tls,
|
||||||
|
nick,
|
||||||
|
pass,
|
||||||
|
connect_commands,
|
||||||
|
sasl_mechanism,
|
||||||
|
sasl_plain_username,
|
||||||
|
sasl_plain_password
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
network.Enabled,
|
||||||
|
netName,
|
||||||
|
network.Addr,
|
||||||
|
network.TLS,
|
||||||
|
network.Nick,
|
||||||
|
pass,
|
||||||
|
connectCommands,
|
||||||
|
saslMechanism,
|
||||||
|
saslPlainUsername,
|
||||||
|
saslPlainPassword,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error executing query")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
network.ID, err = res.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ir *IrcRepo) StoreChannel(networkID int64, channel *domain.IrcChannel) error {
|
||||||
|
pass := toNullString(channel.Password)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if channel.ID != 0 {
|
||||||
|
// update record
|
||||||
|
_, err = ir.db.Exec(`UPDATE irc_channel
|
||||||
|
SET
|
||||||
|
enabled = ?,
|
||||||
|
detached = ?,
|
||||||
|
name = ?,
|
||||||
|
password = ?
|
||||||
|
WHERE
|
||||||
|
id = ?`,
|
||||||
|
channel.Enabled,
|
||||||
|
channel.Detached,
|
||||||
|
channel.Name,
|
||||||
|
pass,
|
||||||
|
channel.ID,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
var res sql.Result
|
||||||
|
|
||||||
|
res, err = ir.db.Exec(`INSERT INTO irc_channel (
|
||||||
|
enabled,
|
||||||
|
detached,
|
||||||
|
name,
|
||||||
|
password,
|
||||||
|
network_id
|
||||||
|
) VALUES (?, ?, ?, ?, ?)`,
|
||||||
|
channel.Enabled,
|
||||||
|
true,
|
||||||
|
channel.Name,
|
||||||
|
pass,
|
||||||
|
networkID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Stack().Err(err).Msg("error executing query")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.ID, err = res.LastInsertId()
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
175
internal/database/migrate.go
Normal file
175
internal/database/migrate.go
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
const schema = `
|
||||||
|
CREATE TABLE indexer
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
identifier TEXT,
|
||||||
|
enabled BOOLEAN,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
settings TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE irc_network
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
enabled BOOLEAN,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
addr TEXT NOT NULL,
|
||||||
|
nick TEXT NOT NULL,
|
||||||
|
tls BOOLEAN,
|
||||||
|
pass TEXT,
|
||||||
|
connect_commands TEXT,
|
||||||
|
sasl_mechanism TEXT,
|
||||||
|
sasl_plain_username TEXT,
|
||||||
|
sasl_plain_password TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
unique (addr, nick)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE irc_channel
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
enabled BOOLEAN,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
password TEXT,
|
||||||
|
detached BOOLEAN,
|
||||||
|
network_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (network_id) REFERENCES irc_network(id),
|
||||||
|
unique (network_id, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE filter
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
enabled BOOLEAN,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
min_size TEXT,
|
||||||
|
max_size TEXT,
|
||||||
|
delay INTEGER,
|
||||||
|
match_releases TEXT,
|
||||||
|
except_releases TEXT,
|
||||||
|
use_regex BOOLEAN,
|
||||||
|
match_release_groups TEXT,
|
||||||
|
except_release_groups TEXT,
|
||||||
|
scene BOOLEAN,
|
||||||
|
freeleech BOOLEAN,
|
||||||
|
freeleech_percent TEXT,
|
||||||
|
shows TEXT,
|
||||||
|
seasons TEXT,
|
||||||
|
episodes TEXT,
|
||||||
|
resolutions TEXT [] DEFAULT '{}' NOT NULL,
|
||||||
|
codecs TEXT [] DEFAULT '{}' NOT NULL,
|
||||||
|
sources TEXT [] DEFAULT '{}' NOT NULL,
|
||||||
|
containers TEXT [] DEFAULT '{}' NOT NULL,
|
||||||
|
years TEXT,
|
||||||
|
match_categories TEXT,
|
||||||
|
except_categories TEXT,
|
||||||
|
match_uploaders TEXT,
|
||||||
|
except_uploaders TEXT,
|
||||||
|
tags TEXT,
|
||||||
|
except_tags TEXT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE filter_indexer
|
||||||
|
(
|
||||||
|
filter_id INTEGER,
|
||||||
|
indexer_id INTEGER,
|
||||||
|
FOREIGN KEY (filter_id) REFERENCES filter(id),
|
||||||
|
FOREIGN KEY (indexer_id) REFERENCES indexer(id),
|
||||||
|
PRIMARY KEY (filter_id, indexer_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE client
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
enabled BOOLEAN,
|
||||||
|
type TEXT,
|
||||||
|
host TEXT NOT NULL,
|
||||||
|
port INTEGER,
|
||||||
|
ssl BOOLEAN,
|
||||||
|
username TEXT,
|
||||||
|
password TEXT,
|
||||||
|
settings TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE action
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
type TEXT,
|
||||||
|
enabled BOOLEAN,
|
||||||
|
exec_cmd TEXT,
|
||||||
|
exec_args TEXT,
|
||||||
|
watch_folder TEXT,
|
||||||
|
category TEXT,
|
||||||
|
tags TEXT,
|
||||||
|
label TEXT,
|
||||||
|
save_path TEXT,
|
||||||
|
paused BOOLEAN,
|
||||||
|
ignore_rules BOOLEAN,
|
||||||
|
limit_upload_speed INT,
|
||||||
|
limit_download_speed INT,
|
||||||
|
client_id INTEGER,
|
||||||
|
filter_id INTEGER,
|
||||||
|
FOREIGN KEY (client_id) REFERENCES client(id),
|
||||||
|
FOREIGN KEY (filter_id) REFERENCES filter(id)
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
var migrations = []string{
|
||||||
|
"",
|
||||||
|
}
|
||||||
|
|
||||||
|
func Migrate(db *sql.DB) error {
|
||||||
|
log.Info().Msg("Migrating database...")
|
||||||
|
|
||||||
|
var version int
|
||||||
|
if err := db.QueryRow("PRAGMA user_version").Scan(&version); err != nil {
|
||||||
|
return fmt.Errorf("failed to query schema version: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if version == len(migrations) {
|
||||||
|
return nil
|
||||||
|
} else if version > len(migrations) {
|
||||||
|
return fmt.Errorf("autobrr (version %d) older than schema (version: %d)", len(migrations), version)
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if version == 0 {
|
||||||
|
if _, err := tx.Exec(schema); err != nil {
|
||||||
|
return fmt.Errorf("failed to initialize schema: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for i := version; i < len(migrations); i++ {
|
||||||
|
if _, err := tx.Exec(migrations[i]); err != nil {
|
||||||
|
return fmt.Errorf("failed to execute migration #%v: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", len(migrations)))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to bump schema version: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
13
internal/database/utils.go
Normal file
13
internal/database/utils.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DataSourceName(configPath string, name string) string {
|
||||||
|
if configPath != "" {
|
||||||
|
return path.Join(configPath, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return name
|
||||||
|
}
|
58
internal/database/utils_test.go
Normal file
58
internal/database/utils_test.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDataSourceName(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
configPath string
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default",
|
||||||
|
args: args{
|
||||||
|
configPath: "",
|
||||||
|
name: "autobrr.db",
|
||||||
|
},
|
||||||
|
want: "autobrr.db",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path_1",
|
||||||
|
args: args{
|
||||||
|
configPath: "/config",
|
||||||
|
name: "autobrr.db",
|
||||||
|
},
|
||||||
|
want: "/config/autobrr.db",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path_2",
|
||||||
|
args: args{
|
||||||
|
configPath: "/config/",
|
||||||
|
name: "autobrr.db",
|
||||||
|
},
|
||||||
|
want: "/config/autobrr.db",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path_3",
|
||||||
|
args: args{
|
||||||
|
configPath: "/config//",
|
||||||
|
name: "autobrr.db",
|
||||||
|
},
|
||||||
|
want: "/config/autobrr.db",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := DataSourceName(tt.args.configPath, tt.args.name)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
39
internal/domain/action.go
Normal file
39
internal/domain/action.go
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
type ActionRepo interface {
|
||||||
|
Store(action Action) (*Action, error)
|
||||||
|
FindByFilterID(filterID int) ([]Action, error)
|
||||||
|
List() ([]Action, error)
|
||||||
|
Delete(actionID int) error
|
||||||
|
ToggleEnabled(actionID int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Action struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type ActionType `json:"type"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
ExecCmd string `json:"exec_cmd,omitempty"`
|
||||||
|
ExecArgs string `json:"exec_args,omitempty"`
|
||||||
|
WatchFolder string `json:"watch_folder,omitempty"`
|
||||||
|
Category string `json:"category,omitempty"`
|
||||||
|
Tags string `json:"tags,omitempty"`
|
||||||
|
Label string `json:"label,omitempty"`
|
||||||
|
SavePath string `json:"save_path,omitempty"`
|
||||||
|
Paused bool `json:"paused,omitempty"`
|
||||||
|
IgnoreRules bool `json:"ignore_rules,omitempty"`
|
||||||
|
LimitUploadSpeed int64 `json:"limit_upload_speed,omitempty"`
|
||||||
|
LimitDownloadSpeed int64 `json:"limit_download_speed,omitempty"`
|
||||||
|
FilterID int `json:"filter_id,omitempty"`
|
||||||
|
ClientID int32 `json:"client_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActionType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ActionTypeTest ActionType = "TEST"
|
||||||
|
ActionTypeExec ActionType = "EXEC"
|
||||||
|
ActionTypeQbittorrent ActionType = "QBITTORRENT"
|
||||||
|
ActionTypeDeluge ActionType = "DELUGE"
|
||||||
|
ActionTypeWatchFolder ActionType = "WATCH_FOLDER"
|
||||||
|
)
|
51
internal/domain/announce.go
Normal file
51
internal/domain/announce.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
type Announce struct {
|
||||||
|
ReleaseType string
|
||||||
|
Freeleech bool
|
||||||
|
FreeleechPercent string
|
||||||
|
Origin string
|
||||||
|
ReleaseGroup string
|
||||||
|
Category string
|
||||||
|
TorrentName string
|
||||||
|
Uploader string
|
||||||
|
TorrentSize string
|
||||||
|
PreTime string
|
||||||
|
TorrentUrl string
|
||||||
|
TorrentUrlSSL string
|
||||||
|
Year int
|
||||||
|
Name1 string // artist, show, movie
|
||||||
|
Name2 string // album
|
||||||
|
Season int
|
||||||
|
Episode int
|
||||||
|
Resolution string
|
||||||
|
Source string
|
||||||
|
Encoder string
|
||||||
|
Container string
|
||||||
|
Format string
|
||||||
|
Bitrate string
|
||||||
|
Media string
|
||||||
|
Tags string
|
||||||
|
Scene bool
|
||||||
|
Log string
|
||||||
|
LogScore string
|
||||||
|
Cue bool
|
||||||
|
|
||||||
|
Line string
|
||||||
|
OrigLine string
|
||||||
|
Site string
|
||||||
|
HttpHeaders string
|
||||||
|
Filter *Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
//type Announce struct {
|
||||||
|
// Channel string
|
||||||
|
// Announcer string
|
||||||
|
// Message string
|
||||||
|
// CreatedAt time.Time
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
|
||||||
|
type AnnounceRepo interface {
|
||||||
|
Store(announce Announce) error
|
||||||
|
}
|
28
internal/domain/client.go
Normal file
28
internal/domain/client.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
type DownloadClientRepo interface {
|
||||||
|
//FindByActionID(actionID int) ([]DownloadClient, error)
|
||||||
|
List() ([]DownloadClient, error)
|
||||||
|
FindByID(id int32) (*DownloadClient, error)
|
||||||
|
Store(client DownloadClient) (*DownloadClient, error)
|
||||||
|
Delete(clientID int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type DownloadClient struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type DownloadClientType `json:"type"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
SSL bool `json:"ssl"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DownloadClientType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
DownloadClientTypeQbittorrent DownloadClientType = "QBITTORRENT"
|
||||||
|
DownloadClientTypeDeluge DownloadClientType = "DELUGE"
|
||||||
|
)
|
11
internal/domain/config.go
Normal file
11
internal/domain/config.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
Host string `toml:"host"`
|
||||||
|
Debug bool
|
||||||
|
}
|
||||||
|
|
||||||
|
//type AppConfig struct {
|
||||||
|
// Settings `toml:"settings"`
|
||||||
|
// Trackers []Tracker `mapstructure:"tracker"`
|
||||||
|
//}
|
90
internal/domain/filter.go
Normal file
90
internal/domain/filter.go
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
/*
|
||||||
|
Works the same way as for autodl-irssi
|
||||||
|
https://autodl-community.github.io/autodl-irssi/configuration/filter/
|
||||||
|
*/
|
||||||
|
|
||||||
|
type FilterRepo interface {
|
||||||
|
FindByID(filterID int) (*Filter, error)
|
||||||
|
FindFiltersForSite(site string) ([]Filter, error)
|
||||||
|
FindByIndexerIdentifier(indexer string) ([]Filter, error)
|
||||||
|
ListFilters() ([]Filter, error)
|
||||||
|
Store(filter Filter) (*Filter, error)
|
||||||
|
Update(filter Filter) (*Filter, error)
|
||||||
|
Delete(filterID int) error
|
||||||
|
StoreIndexerConnection(filterID int, indexerID int) error
|
||||||
|
DeleteIndexerConnections(filterID int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Filter struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
|
|
||||||
|
FilterGeneral
|
||||||
|
FilterP2P
|
||||||
|
FilterTVMovies
|
||||||
|
FilterMusic
|
||||||
|
FilterAdvanced
|
||||||
|
|
||||||
|
Actions []Action `json:"actions"`
|
||||||
|
Indexers []Indexer `json:"indexers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterGeneral struct {
|
||||||
|
MinSize string `json:"min_size"`
|
||||||
|
MaxSize string `json:"max_size"`
|
||||||
|
Delay int `json:"delay"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterP2P struct {
|
||||||
|
MatchReleases string `json:"match_releases"`
|
||||||
|
ExceptReleases string `json:"except_releases"`
|
||||||
|
UseRegex bool `json:"use_regex"`
|
||||||
|
MatchReleaseGroups string `json:"match_release_groups"`
|
||||||
|
ExceptReleaseGroups string `json:"except_release_groups"`
|
||||||
|
Scene bool `json:"scene"`
|
||||||
|
Origins string `json:"origins"`
|
||||||
|
Freeleech bool `json:"freeleech"`
|
||||||
|
FreeleechPercent string `json:"freeleech_percent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterTVMovies struct {
|
||||||
|
Shows string `json:"shows"`
|
||||||
|
Seasons string `json:"seasons"`
|
||||||
|
Episodes string `json:"episodes"`
|
||||||
|
Resolutions []string `json:"resolutions"` // SD, 480i, 480p, 576p, 720p, 810p, 1080i, 1080p.
|
||||||
|
Codecs []string `json:"codecs"` // XviD, DivX, x264, h.264 (or h264), mpeg2 (or mpeg-2), VC-1 (or VC1), WMV, Remux, h.264 Remux (or h264 Remux), VC-1 Remux (or VC1 Remux).
|
||||||
|
Sources []string `json:"sources"` // DSR, PDTV, HDTV, HR.PDTV, HR.HDTV, DVDRip, DVDScr, BDr, BD5, BD9, BDRip, BRRip, DVDR, MDVDR, HDDVD, HDDVDRip, BluRay, WEB-DL, TVRip, CAM, R5, TELESYNC, TS, TELECINE, TC. TELESYNC and TS are synonyms (you don't need both). Same for TELECINE and TC
|
||||||
|
Containers []string `json:"containers"`
|
||||||
|
Years string `json:"years"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterMusic struct {
|
||||||
|
Artists string `json:"artists"`
|
||||||
|
Albums string `json:"albums"`
|
||||||
|
MatchReleaseTypes string `json:"match_release_types"` // Album,Single,EP
|
||||||
|
ExceptReleaseTypes string `json:"except_release_types"`
|
||||||
|
Formats []string `json:"formats"` // MP3, FLAC, Ogg, AAC, AC3, DTS
|
||||||
|
Bitrates []string `json:"bitrates"` // 192, 320, APS (VBR), V2 (VBR), V1 (VBR), APX (VBR), V0 (VBR), q8.x (VBR), Lossless, 24bit Lossless, Other
|
||||||
|
Media []string `json:"media"` // CD, DVD, Vinyl, Soundboard, SACD, DAT, Cassette, WEB, Other
|
||||||
|
Cue bool `json:"cue"`
|
||||||
|
Log bool `json:"log"`
|
||||||
|
LogScores string `json:"log_scores"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilterAdvanced struct {
|
||||||
|
MatchCategories string `json:"match_categories"`
|
||||||
|
ExceptCategories string `json:"except_categories"`
|
||||||
|
MatchUploaders string `json:"match_uploaders"`
|
||||||
|
ExceptUploaders string `json:"except_uploaders"`
|
||||||
|
Tags string `json:"tags"`
|
||||||
|
ExceptTags string `json:"except_tags"`
|
||||||
|
TagsAny string `json:"tags_any"`
|
||||||
|
ExceptTagsAny string `json:"except_tags_any"`
|
||||||
|
}
|
68
internal/domain/indexer.go
Normal file
68
internal/domain/indexer.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
type IndexerRepo interface {
|
||||||
|
Store(indexer Indexer) (*Indexer, error)
|
||||||
|
Update(indexer Indexer) (*Indexer, error)
|
||||||
|
List() ([]Indexer, error)
|
||||||
|
Delete(id int) error
|
||||||
|
FindByFilterID(id int) ([]Indexer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Indexer struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Settings map[string]string `json:"settings,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexerDefinition struct {
|
||||||
|
ID int `json:"id,omitempty"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Language string `json:"language"`
|
||||||
|
Privacy string `json:"privacy"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
URLS []string `json:"urls"`
|
||||||
|
Settings []IndexerSetting `json:"settings"`
|
||||||
|
SettingsMap map[string]string `json:"-"`
|
||||||
|
IRC *IndexerIRC `json:"irc"`
|
||||||
|
Parse IndexerParse `json:"parse"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexerSetting struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Required bool `json:"required,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Value string `json:"value,omitempty"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Regex string `json:"regex,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexerIRC struct {
|
||||||
|
Network string
|
||||||
|
Server string
|
||||||
|
Channels []string
|
||||||
|
Announcers []string
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexerParse struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Lines []IndexerParseExtract `json:"lines"`
|
||||||
|
Match IndexerParseMatch `json:"match"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexerParseExtract struct {
|
||||||
|
Test []string `json:"test"`
|
||||||
|
Pattern string `json:"pattern"`
|
||||||
|
Vars []string `json:"vars"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IndexerParseMatch struct {
|
||||||
|
TorrentURL string `json:"torrenturl"`
|
||||||
|
Encode []string `json:"encode"`
|
||||||
|
}
|
43
internal/domain/irc.go
Normal file
43
internal/domain/irc.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type IrcChannel struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Detached bool `json:"detached"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SASL struct {
|
||||||
|
Mechanism string `json:"mechanism,omitempty"`
|
||||||
|
|
||||||
|
Plain struct {
|
||||||
|
Username string `json:"username,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
} `json:"plain,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IrcNetwork struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Addr string `json:"addr"`
|
||||||
|
TLS bool `json:"tls"`
|
||||||
|
Nick string `json:"nick"`
|
||||||
|
Pass string `json:"pass"`
|
||||||
|
ConnectCommands []string `json:"connect_commands"`
|
||||||
|
SASL SASL `json:"sasl,omitempty"`
|
||||||
|
Channels []IrcChannel `json:"channels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type IrcRepo interface {
|
||||||
|
Store(announce Announce) error
|
||||||
|
StoreNetwork(network *IrcNetwork) error
|
||||||
|
StoreChannel(networkID int64, channel *IrcChannel) error
|
||||||
|
ListNetworks(ctx context.Context) ([]IrcNetwork, error)
|
||||||
|
ListChannels(networkID int64) ([]IrcChannel, error)
|
||||||
|
GetNetworkByID(id int64) (*IrcNetwork, error)
|
||||||
|
DeleteNetwork(ctx context.Context, id int64) error
|
||||||
|
}
|
95
internal/download_client/service.go
Normal file
95
internal/download_client/service.go
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
package download_client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
"github.com/autobrr/autobrr/pkg/qbittorrent"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
List() ([]domain.DownloadClient, error)
|
||||||
|
FindByID(id int32) (*domain.DownloadClient, error)
|
||||||
|
Store(client domain.DownloadClient) (*domain.DownloadClient, error)
|
||||||
|
Delete(clientID int) error
|
||||||
|
Test(client domain.DownloadClient) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo domain.DownloadClientRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo domain.DownloadClientRepo) Service {
|
||||||
|
return &service{repo: repo}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) List() ([]domain.DownloadClient, error) {
|
||||||
|
clients, err := s.repo.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return clients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) FindByID(id int32) (*domain.DownloadClient, error) {
|
||||||
|
client, err := s.repo.FindByID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Store(client domain.DownloadClient) (*domain.DownloadClient, error) {
|
||||||
|
// validate data
|
||||||
|
|
||||||
|
// store
|
||||||
|
c, err := s.repo.Store(client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Delete(clientID int) error {
|
||||||
|
if err := s.repo.Delete(clientID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("delete client: %v", clientID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Test(client domain.DownloadClient) error {
|
||||||
|
// test
|
||||||
|
err := s.testConnection(client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) testConnection(client domain.DownloadClient) error {
|
||||||
|
if client.Type == "QBITTORRENT" {
|
||||||
|
qbtSettings := qbittorrent.Settings{
|
||||||
|
Hostname: client.Host,
|
||||||
|
Port: uint(client.Port),
|
||||||
|
Username: client.Username,
|
||||||
|
Password: client.Password,
|
||||||
|
SSL: client.SSL,
|
||||||
|
}
|
||||||
|
|
||||||
|
qbt := qbittorrent.NewClient(qbtSettings)
|
||||||
|
err := qbt.Login()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error logging into client: %v", client.Host)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
342
internal/filter/service.go
Normal file
342
internal/filter/service.go
Normal file
|
@ -0,0 +1,342 @@
|
||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
"github.com/autobrr/autobrr/internal/indexer"
|
||||||
|
"github.com/autobrr/autobrr/pkg/wildcard"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
//FindFilter(announce domain.Announce) (*domain.Filter, error)
|
||||||
|
|
||||||
|
FindByID(filterID int) (*domain.Filter, error)
|
||||||
|
FindByIndexerIdentifier(announce domain.Announce) (*domain.Filter, error)
|
||||||
|
ListFilters() ([]domain.Filter, error)
|
||||||
|
Store(filter domain.Filter) (*domain.Filter, error)
|
||||||
|
Update(filter domain.Filter) (*domain.Filter, error)
|
||||||
|
Delete(filterID int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo domain.FilterRepo
|
||||||
|
actionRepo domain.ActionRepo
|
||||||
|
indexerSvc indexer.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo domain.FilterRepo, actionRepo domain.ActionRepo, indexerSvc indexer.Service) Service {
|
||||||
|
return &service{
|
||||||
|
repo: repo,
|
||||||
|
actionRepo: actionRepo,
|
||||||
|
indexerSvc: indexerSvc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) ListFilters() ([]domain.Filter, error) {
|
||||||
|
// get filters
|
||||||
|
filters, err := s.repo.ListFilters()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret []domain.Filter
|
||||||
|
|
||||||
|
for _, filter := range filters {
|
||||||
|
indexers, err := s.indexerSvc.FindByFilterID(filter.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
filter.Indexers = indexers
|
||||||
|
|
||||||
|
ret = append(ret, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) FindByID(filterID int) (*domain.Filter, error) {
|
||||||
|
// find filter
|
||||||
|
filter, err := s.repo.FindByID(filterID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// find actions and attach
|
||||||
|
//actions, err := s.actionRepo.FindFilterActions(filter.ID)
|
||||||
|
actions, err := s.actionRepo.FindByFilterID(filter.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("could not find filter actions: %+v", &filter.ID)
|
||||||
|
}
|
||||||
|
filter.Actions = actions
|
||||||
|
|
||||||
|
// find indexers and attach
|
||||||
|
indexers, err := s.indexerSvc.FindByFilterID(filter.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not find indexers for filter: %+v", &filter.Name)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
filter.Indexers = indexers
|
||||||
|
|
||||||
|
//log.Debug().Msgf("found filter: %+v", filter)
|
||||||
|
|
||||||
|
return filter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) FindByIndexerIdentifier(announce domain.Announce) (*domain.Filter, error) {
|
||||||
|
// get filter for tracker
|
||||||
|
filters, err := s.repo.FindByIndexerIdentifier(announce.Site)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not find filters for indexer: %v", announce.Site)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// match against announce/releaseInfo
|
||||||
|
for _, filter := range filters {
|
||||||
|
// if match, return the filter
|
||||||
|
matchedFilter := s.checkFilter(filter, announce)
|
||||||
|
if matchedFilter {
|
||||||
|
log.Trace().Msgf("found filter: %+v", &filter)
|
||||||
|
log.Debug().Msgf("found filter: %+v", &filter.Name)
|
||||||
|
|
||||||
|
// find actions and attach
|
||||||
|
actions, err := s.actionRepo.FindByFilterID(filter.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not find filter actions: %+v", &filter.ID)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no actions found, check next filter
|
||||||
|
if actions == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.Actions = actions
|
||||||
|
|
||||||
|
return &filter, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if no match, return nil
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
//func (s *service) FindFilter(announce domain.Announce) (*domain.Filter, error) {
|
||||||
|
// // get filter for tracker
|
||||||
|
// filters, err := s.repo.FindFiltersForSite(announce.Site)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // match against announce/releaseInfo
|
||||||
|
// for _, filter := range filters {
|
||||||
|
// // if match, return the filter
|
||||||
|
// matchedFilter := s.checkFilter(filter, announce)
|
||||||
|
// if matchedFilter {
|
||||||
|
//
|
||||||
|
// log.Debug().Msgf("found filter: %+v", &filter)
|
||||||
|
//
|
||||||
|
// // find actions and attach
|
||||||
|
// actions, err := s.actionRepo.FindByFilterID(filter.ID)
|
||||||
|
// if err != nil {
|
||||||
|
// log.Error().Msgf("could not find filter actions: %+v", &filter.ID)
|
||||||
|
// }
|
||||||
|
// filter.Actions = actions
|
||||||
|
//
|
||||||
|
// return &filter, nil
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // if no match, return nil
|
||||||
|
// return nil, nil
|
||||||
|
//}
|
||||||
|
|
||||||
|
func (s *service) Store(filter domain.Filter) (*domain.Filter, error) {
|
||||||
|
// validate data
|
||||||
|
|
||||||
|
// store
|
||||||
|
f, err := s.repo.Store(filter)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not store filter: %v", filter)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Update(filter domain.Filter) (*domain.Filter, error) {
|
||||||
|
// validate data
|
||||||
|
|
||||||
|
// store
|
||||||
|
f, err := s.repo.Update(filter)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not update filter: %v", filter.Name)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// take care of connected indexers
|
||||||
|
if err = s.repo.DeleteIndexerConnections(f.ID); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not delete filter indexer connections: %v", filter.Name)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, i := range filter.Indexers {
|
||||||
|
if err = s.repo.StoreIndexerConnection(f.ID, i.ID); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not store filter indexer connections: %v", filter.Name)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Delete(filterID int) error {
|
||||||
|
if filterID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete
|
||||||
|
if err := s.repo.Delete(filterID); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not delete filter: %v", filterID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkFilter tries to match filter against announce
|
||||||
|
func (s *service) checkFilter(filter domain.Filter, announce domain.Announce) bool {
|
||||||
|
|
||||||
|
if !filter.Enabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Scene && announce.Scene != filter.Scene {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Freeleech && announce.Freeleech != filter.Freeleech {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Shows != "" && !checkFilterStrings(announce.TorrentName, filter.Shows) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
//if filter.Seasons != "" && !checkFilterStrings(announce.TorrentName, filter.Seasons) {
|
||||||
|
// return false
|
||||||
|
//}
|
||||||
|
//
|
||||||
|
//if filter.Episodes != "" && !checkFilterStrings(announce.TorrentName, filter.Episodes) {
|
||||||
|
// return false
|
||||||
|
//}
|
||||||
|
|
||||||
|
// matchRelease
|
||||||
|
if filter.MatchReleases != "" && !checkFilterStrings(announce.TorrentName, filter.MatchReleases) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.MatchReleaseGroups != "" && !checkFilterStrings(announce.TorrentName, filter.MatchReleaseGroups) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.ExceptReleaseGroups != "" && checkFilterStrings(announce.TorrentName, filter.ExceptReleaseGroups) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.MatchUploaders != "" && !checkFilterStrings(announce.Uploader, filter.MatchUploaders) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.ExceptUploaders != "" && checkFilterStrings(announce.Uploader, filter.ExceptUploaders) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.Resolutions) > 0 && !checkFilterSlice(announce.TorrentName, filter.Resolutions) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.Codecs) > 0 && !checkFilterSlice(announce.TorrentName, filter.Codecs) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.Sources) > 0 && !checkFilterSlice(announce.TorrentName, filter.Sources) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(filter.Containers) > 0 && !checkFilterSlice(announce.TorrentName, filter.Containers) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Years != "" && !checkFilterStrings(announce.TorrentName, filter.Years) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.MatchCategories != "" && !checkFilterStrings(announce.Category, filter.MatchCategories) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.ExceptCategories != "" && checkFilterStrings(announce.Category, filter.ExceptCategories) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Tags != "" && !checkFilterStrings(announce.Tags, filter.Tags) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.ExceptTags != "" && checkFilterStrings(announce.Tags, filter.ExceptTags) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkFilterSlice(name string, filterList []string) bool {
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
|
||||||
|
for _, filter := range filterList {
|
||||||
|
filter = strings.ToLower(filter)
|
||||||
|
// check if line contains * or ?, if so try wildcard match, otherwise try substring match
|
||||||
|
a := strings.ContainsAny(filter, "?|*")
|
||||||
|
if a {
|
||||||
|
match := wildcard.Match(filter, name)
|
||||||
|
if match {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b := strings.Contains(name, filter)
|
||||||
|
if b {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkFilterStrings(name string, filterList string) bool {
|
||||||
|
filterSplit := strings.Split(filterList, ",")
|
||||||
|
name = strings.ToLower(name)
|
||||||
|
|
||||||
|
for _, s := range filterSplit {
|
||||||
|
s = strings.ToLower(s)
|
||||||
|
// check if line contains * or ?, if so try wildcard match, otherwise try substring match
|
||||||
|
a := strings.ContainsAny(s, "?|*")
|
||||||
|
if a {
|
||||||
|
match := wildcard.Match(s, name)
|
||||||
|
if match {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
b := strings.Contains(name, s)
|
||||||
|
if b {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
651
internal/filter/service_test.go
Normal file
651
internal/filter/service_test.go
Normal file
|
@ -0,0 +1,651 @@
|
||||||
|
package filter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_checkFilterStrings(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
name string
|
||||||
|
filterList string
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test_01",
|
||||||
|
args: args{
|
||||||
|
name: "The End",
|
||||||
|
filterList: "The End, Other movie",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test_02",
|
||||||
|
args: args{
|
||||||
|
name: "The Simpsons S12",
|
||||||
|
filterList: "The End, Other movie",
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test_03",
|
||||||
|
args: args{
|
||||||
|
name: "The.Simpsons.S12",
|
||||||
|
filterList: "The?Simpsons*, Other movie",
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := checkFilterStrings(tt.args.name, tt.args.filterList)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_service_checkFilter(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
filter domain.Filter
|
||||||
|
announce domain.Announce
|
||||||
|
}
|
||||||
|
|
||||||
|
svcMock := &service{
|
||||||
|
repo: nil,
|
||||||
|
actionRepo: nil,
|
||||||
|
indexerSvc: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "freeleech",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
Freeleech: true,
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterP2P: domain.FilterP2P{
|
||||||
|
Freeleech: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "scene",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
Scene: true,
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterP2P: domain.FilterP2P{
|
||||||
|
Scene: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not_scene",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
Scene: false,
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterP2P: domain.FilterP2P{
|
||||||
|
Scene: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "shows_1",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 1080p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Shows: "That show",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "shows_2",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 1080p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Shows: "That show, The Other show",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "shows_3",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 1080p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Shows: "That?show*, The?Other?show",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "shows_4",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 1080p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Shows: "The Other show",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "shows_5",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 1080p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Shows: "*show*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "shows_6",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That.Show.S06.1080p.BluRay.DD5.1.x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Shows: "*show*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "shows_7",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That.Show.S06.1080p.BluRay.DD5.1.x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Shows: "That?show*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_releases_single",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 1080p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterP2P: domain.FilterP2P{
|
||||||
|
MatchReleases: "That show",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_releases_single_wildcard",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 1080p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterP2P: domain.FilterP2P{
|
||||||
|
MatchReleases: "That show*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_releases_multiple",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 1080p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterP2P: domain.FilterP2P{
|
||||||
|
MatchReleases: "That show*, Other one",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_release_groups",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 1080p BluRay DD5.1 x264-GROUP1",
|
||||||
|
ReleaseGroup: "GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterP2P: domain.FilterP2P{
|
||||||
|
MatchReleaseGroups: "GROUP1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_release_groups_multiple",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 1080p BluRay DD5.1 x264-GROUP1",
|
||||||
|
ReleaseGroup: "GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterP2P: domain.FilterP2P{
|
||||||
|
MatchReleaseGroups: "GROUP1,GROUP2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_release_groups_dont_match",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 1080p BluRay DD5.1 x264-GROUP1",
|
||||||
|
ReleaseGroup: "GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterP2P: domain.FilterP2P{
|
||||||
|
MatchReleaseGroups: "GROUP2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "except_release_groups",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 1080p BluRay DD5.1 x264-GROUP1",
|
||||||
|
ReleaseGroup: "GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterP2P: domain.FilterP2P{
|
||||||
|
ExceptReleaseGroups: "GROUP1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_uploaders",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
Uploader: "Uploader1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterAdvanced: domain.FilterAdvanced{
|
||||||
|
MatchUploaders: "Uploader1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non_match_uploaders",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
Uploader: "Uploader2",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterAdvanced: domain.FilterAdvanced{
|
||||||
|
MatchUploaders: "Uploader1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "except_uploaders",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
Uploader: "Uploader1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterAdvanced: domain.FilterAdvanced{
|
||||||
|
ExceptUploaders: "Uploader1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resolutions_1080p",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 1080p BluRay DD5.1 x264-GROUP1",
|
||||||
|
Resolution: "1080p",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Resolutions: []string{"1080p"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resolutions_2160p",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 2160p BluRay DD5.1 x264-GROUP1",
|
||||||
|
Resolution: "2160p",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Resolutions: []string{"2160p"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resolutions_no_match",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 2160p BluRay DD5.1 x264-GROUP1",
|
||||||
|
Resolution: "2160p",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Resolutions: []string{"1080p"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "codecs_1_match",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 2160p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Codecs: []string{"x264"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "codecs_2_no_match",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 2160p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Codecs: []string{"h264"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sources_1_match",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 2160p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Sources: []string{"BluRay"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sources_2_no_match",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 2160p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Sources: []string{"WEB"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "years_1",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Movie 2020 2160p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Years: "2020",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "years_2",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Movie 2020 2160p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Years: "2020,1990",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "years_3_no_match",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Movie 2020 2160p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Years: "1990",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "years_4_no_match",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Show S06 2160p BluRay DD5.1 x264-GROUP1",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Years: "2020",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_categories_1",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
Category: "TV",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterAdvanced: domain.FilterAdvanced{
|
||||||
|
MatchCategories: "TV",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_categories_2",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
Category: "TV :: HD",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterAdvanced: domain.FilterAdvanced{
|
||||||
|
MatchCategories: "*TV*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_categories_3",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
Category: "TV :: HD",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterAdvanced: domain.FilterAdvanced{
|
||||||
|
MatchCategories: "*TV*, *HD*",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_categories_4_no_match",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
Category: "TV :: HD",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterAdvanced: domain.FilterAdvanced{
|
||||||
|
MatchCategories: "Movies",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "except_categories_1",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
Category: "Movies",
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterAdvanced: domain.FilterAdvanced{
|
||||||
|
ExceptCategories: "Movies",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "match_multiple_fields_1",
|
||||||
|
args: args{
|
||||||
|
announce: domain.Announce{
|
||||||
|
TorrentName: "That Movie 2020 2160p BluRay DD5.1 x264-GROUP1",
|
||||||
|
Category: "Movies",
|
||||||
|
Freeleech: true,
|
||||||
|
},
|
||||||
|
filter: domain.Filter{
|
||||||
|
Enabled: true,
|
||||||
|
FilterAdvanced: domain.FilterAdvanced{
|
||||||
|
MatchCategories: "Movies",
|
||||||
|
},
|
||||||
|
FilterTVMovies: domain.FilterTVMovies{
|
||||||
|
Resolutions: []string{"2160p"},
|
||||||
|
Sources: []string{"BluRay"},
|
||||||
|
Years: "2020",
|
||||||
|
},
|
||||||
|
FilterP2P: domain.FilterP2P{
|
||||||
|
MatchReleaseGroups: "GROUP1",
|
||||||
|
MatchReleases: "That movie",
|
||||||
|
Freeleech: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := svcMock.checkFilter(tt.args.filter, tt.args.announce)
|
||||||
|
assert.Equal(t, tt.expected, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
113
internal/http/action.go
Normal file
113
internal/http/action.go
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
type actionService interface {
|
||||||
|
Fetch() ([]domain.Action, error)
|
||||||
|
Store(action domain.Action) (*domain.Action, error)
|
||||||
|
Delete(actionID int) error
|
||||||
|
ToggleEnabled(actionID int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type actionHandler struct {
|
||||||
|
encoder encoder
|
||||||
|
actionService actionService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h actionHandler) Routes(r chi.Router) {
|
||||||
|
r.Get("/", h.getActions)
|
||||||
|
r.Post("/", h.storeAction)
|
||||||
|
r.Delete("/{actionID}", h.deleteAction)
|
||||||
|
r.Put("/{actionID}", h.updateAction)
|
||||||
|
r.Patch("/{actionID}/toggleEnabled", h.toggleActionEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h actionHandler) getActions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
actions, err := h.actionService.Fetch()
|
||||||
|
if err != nil {
|
||||||
|
// encode error
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, actions, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h actionHandler) storeAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
data domain.Action
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
// encode error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action, err := h.actionService.Store(data)
|
||||||
|
if err != nil {
|
||||||
|
// encode error
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, action, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h actionHandler) updateAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
data domain.Action
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
// encode error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action, err := h.actionService.Store(data)
|
||||||
|
if err != nil {
|
||||||
|
// encode error
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, action, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h actionHandler) deleteAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
actionID = chi.URLParam(r, "actionID")
|
||||||
|
)
|
||||||
|
|
||||||
|
// if !actionID return error
|
||||||
|
|
||||||
|
id, _ := strconv.Atoi(actionID)
|
||||||
|
|
||||||
|
if err := h.actionService.Delete(id); err != nil {
|
||||||
|
// encode error
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h actionHandler) toggleActionEnabled(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
actionID = chi.URLParam(r, "actionID")
|
||||||
|
)
|
||||||
|
|
||||||
|
// if !actionID return error
|
||||||
|
|
||||||
|
id, _ := strconv.Atoi(actionID)
|
||||||
|
|
||||||
|
if err := h.actionService.ToggleEnabled(id); err != nil {
|
||||||
|
// encode error
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, nil, http.StatusCreated)
|
||||||
|
}
|
41
internal/http/config.go
Normal file
41
internal/http/config.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/config"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
type configJson struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
LogLevel string `json:"log_level"`
|
||||||
|
LogPath string `json:"log_path"`
|
||||||
|
BaseURL string `json:"base_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type configHandler struct {
|
||||||
|
encoder encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h configHandler) Routes(r chi.Router) {
|
||||||
|
r.Get("/", h.getConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h configHandler) getConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
c := config.Config
|
||||||
|
|
||||||
|
conf := configJson{
|
||||||
|
Host: c.Host,
|
||||||
|
Port: c.Port,
|
||||||
|
LogLevel: c.LogLevel,
|
||||||
|
LogPath: c.LogPath,
|
||||||
|
BaseURL: c.BaseURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, conf, http.StatusOK)
|
||||||
|
}
|
119
internal/http/download_client.go
Normal file
119
internal/http/download_client.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type downloadClientService interface {
|
||||||
|
List() ([]domain.DownloadClient, error)
|
||||||
|
Store(client domain.DownloadClient) (*domain.DownloadClient, error)
|
||||||
|
Delete(clientID int) error
|
||||||
|
Test(client domain.DownloadClient) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type downloadClientHandler struct {
|
||||||
|
encoder encoder
|
||||||
|
downloadClientService downloadClientService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h downloadClientHandler) Routes(r chi.Router) {
|
||||||
|
r.Get("/", h.listDownloadClients)
|
||||||
|
r.Post("/", h.store)
|
||||||
|
r.Put("/", h.update)
|
||||||
|
r.Post("/test", h.test)
|
||||||
|
r.Delete("/{clientID}", h.delete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h downloadClientHandler) listDownloadClients(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
clients, err := h.downloadClientService.List()
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, clients, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h downloadClientHandler) store(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
data domain.DownloadClient
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
// encode error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := h.downloadClientService.Store(data)
|
||||||
|
if err != nil {
|
||||||
|
// encode error
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, client, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h downloadClientHandler) test(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
data domain.DownloadClient
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
// encode error
|
||||||
|
h.encoder.StatusResponse(ctx, w, nil, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.downloadClientService.Test(data)
|
||||||
|
if err != nil {
|
||||||
|
// encode error
|
||||||
|
h.encoder.StatusResponse(ctx, w, nil, http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h downloadClientHandler) update(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
data domain.DownloadClient
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
// encode error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := h.downloadClientService.Store(data)
|
||||||
|
if err != nil {
|
||||||
|
// encode error
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, client, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h downloadClientHandler) delete(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
clientID = chi.URLParam(r, "clientID")
|
||||||
|
)
|
||||||
|
|
||||||
|
// if !clientID return error
|
||||||
|
|
||||||
|
id, _ := strconv.Atoi(clientID)
|
||||||
|
|
||||||
|
if err := h.downloadClientService.Delete(id); err != nil {
|
||||||
|
// encode error
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
26
internal/http/encoder.go
Normal file
26
internal/http/encoder.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type encoder struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoder) StatusResponse(ctx context.Context, w http.ResponseWriter, response interface{}, status int) {
|
||||||
|
if response != nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf=8")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
if err := json.NewEncoder(w).Encode(response); err != nil {
|
||||||
|
// log err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.WriteHeader(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e encoder) StatusNotFound(ctx context.Context, w http.ResponseWriter) {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
}
|
132
internal/http/filter.go
Normal file
132
internal/http/filter.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type filterService interface {
|
||||||
|
ListFilters() ([]domain.Filter, error)
|
||||||
|
FindByID(filterID int) (*domain.Filter, error)
|
||||||
|
Store(filter domain.Filter) (*domain.Filter, error)
|
||||||
|
Delete(filterID int) error
|
||||||
|
Update(filter domain.Filter) (*domain.Filter, error)
|
||||||
|
//StoreFilterAction(action domain.Action) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type filterHandler struct {
|
||||||
|
encoder encoder
|
||||||
|
filterService filterService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h filterHandler) Routes(r chi.Router) {
|
||||||
|
r.Get("/", h.getFilters)
|
||||||
|
r.Get("/{filterID}", h.getByID)
|
||||||
|
r.Post("/", h.store)
|
||||||
|
r.Put("/{filterID}", h.update)
|
||||||
|
r.Delete("/{filterID}", h.delete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h filterHandler) getFilters(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
trackers, err := h.filterService.ListFilters()
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, trackers, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h filterHandler) getByID(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
filterID = chi.URLParam(r, "filterID")
|
||||||
|
)
|
||||||
|
|
||||||
|
id, _ := strconv.Atoi(filterID)
|
||||||
|
|
||||||
|
filter, err := h.filterService.FindByID(id)
|
||||||
|
if err != nil {
|
||||||
|
h.encoder.StatusNotFound(ctx, w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, filter, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h filterHandler) storeFilterAction(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
filterID = chi.URLParam(r, "filterID")
|
||||||
|
)
|
||||||
|
|
||||||
|
id, _ := strconv.Atoi(filterID)
|
||||||
|
|
||||||
|
filter, err := h.filterService.FindByID(id)
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, filter, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h filterHandler) store(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
data domain.Filter
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
// encode error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filter, err := h.filterService.Store(data)
|
||||||
|
if err != nil {
|
||||||
|
// encode error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, filter, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h filterHandler) update(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
data domain.Filter
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
// encode error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filter, err := h.filterService.Update(data)
|
||||||
|
if err != nil {
|
||||||
|
// encode error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, filter, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h filterHandler) delete(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
filterID = chi.URLParam(r, "filterID")
|
||||||
|
)
|
||||||
|
|
||||||
|
id, _ := strconv.Atoi(filterID)
|
||||||
|
|
||||||
|
if err := h.filterService.Delete(id); err != nil {
|
||||||
|
// return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
118
internal/http/indexer.go
Normal file
118
internal/http/indexer.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
type indexerService interface {
|
||||||
|
Store(indexer domain.Indexer) (*domain.Indexer, error)
|
||||||
|
Update(indexer domain.Indexer) (*domain.Indexer, error)
|
||||||
|
List() ([]domain.Indexer, error)
|
||||||
|
GetAll() ([]*domain.IndexerDefinition, error)
|
||||||
|
GetTemplates() ([]domain.IndexerDefinition, error)
|
||||||
|
Delete(id int) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type indexerHandler struct {
|
||||||
|
encoder encoder
|
||||||
|
indexerService indexerService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h indexerHandler) Routes(r chi.Router) {
|
||||||
|
r.Get("/schema", h.getSchema)
|
||||||
|
r.Post("/", h.store)
|
||||||
|
r.Put("/", h.update)
|
||||||
|
r.Get("/", h.getAll)
|
||||||
|
r.Get("/options", h.list)
|
||||||
|
r.Delete("/{indexerID}", h.delete)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h indexerHandler) getSchema(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
indexers, err := h.indexerService.GetTemplates()
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, indexers, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h indexerHandler) store(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
data domain.Indexer
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
indexer, err := h.indexerService.Store(data)
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, indexer, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h indexerHandler) update(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
data domain.Indexer
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
indexer, err := h.indexerService.Update(data)
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, indexer, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h indexerHandler) delete(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
idParam = chi.URLParam(r, "indexerID")
|
||||||
|
)
|
||||||
|
|
||||||
|
id, _ := strconv.Atoi(idParam)
|
||||||
|
|
||||||
|
if err := h.indexerService.Delete(id); err != nil {
|
||||||
|
// return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h indexerHandler) getAll(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
indexers, err := h.indexerService.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, indexers, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h indexerHandler) list(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
indexers, err := h.indexerService.List()
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, indexers, http.StatusOK)
|
||||||
|
}
|
132
internal/http/irc.go
Normal file
132
internal/http/irc.go
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ircService interface {
|
||||||
|
ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error)
|
||||||
|
DeleteNetwork(ctx context.Context, id int64) error
|
||||||
|
GetNetworkByID(id int64) (*domain.IrcNetwork, error)
|
||||||
|
StoreNetwork(network *domain.IrcNetwork) error
|
||||||
|
StoreChannel(networkID int64, channel *domain.IrcChannel) error
|
||||||
|
StopNetwork(name string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ircHandler struct {
|
||||||
|
encoder encoder
|
||||||
|
ircService ircService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h ircHandler) Routes(r chi.Router) {
|
||||||
|
r.Get("/", h.listNetworks)
|
||||||
|
r.Post("/", h.storeNetwork)
|
||||||
|
r.Put("/network/{networkID}", h.storeNetwork)
|
||||||
|
r.Post("/network/{networkID}/channel", h.storeChannel)
|
||||||
|
r.Get("/network/{networkID}/stop", h.stopNetwork)
|
||||||
|
r.Get("/network/{networkID}", h.getNetworkByID)
|
||||||
|
r.Delete("/network/{networkID}", h.deleteNetwork)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h ircHandler) listNetworks(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
networks, err := h.ircService.ListNetworks(ctx)
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, networks, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h ircHandler) getNetworkByID(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
networkID = chi.URLParam(r, "networkID")
|
||||||
|
)
|
||||||
|
|
||||||
|
id, _ := strconv.Atoi(networkID)
|
||||||
|
|
||||||
|
network, err := h.ircService.GetNetworkByID(int64(id))
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, network, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h ircHandler) storeNetwork(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
data domain.IrcNetwork
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.ircService.StoreNetwork(&data)
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, nil, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h ircHandler) storeChannel(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
data domain.IrcChannel
|
||||||
|
networkID = chi.URLParam(r, "networkID")
|
||||||
|
)
|
||||||
|
|
||||||
|
id, _ := strconv.Atoi(networkID)
|
||||||
|
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := h.ircService.StoreChannel(int64(id), &data)
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, nil, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h ircHandler) stopNetwork(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
networkID = chi.URLParam(r, "networkID")
|
||||||
|
)
|
||||||
|
|
||||||
|
err := h.ircService.StopNetwork(networkID)
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, nil, http.StatusCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h ircHandler) deleteNetwork(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var (
|
||||||
|
ctx = r.Context()
|
||||||
|
networkID = chi.URLParam(r, "networkID")
|
||||||
|
)
|
||||||
|
|
||||||
|
id, _ := strconv.Atoi(networkID)
|
||||||
|
|
||||||
|
err := h.ircService.DeleteNetwork(ctx, int64(id))
|
||||||
|
if err != nil {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
h.encoder.StatusResponse(ctx, w, nil, http.StatusNoContent)
|
||||||
|
}
|
123
internal/http/service.go
Normal file
123
internal/http/service.go
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/config"
|
||||||
|
"github.com/autobrr/autobrr/web"
|
||||||
|
|
||||||
|
"github.com/go-chi/chi"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
address string
|
||||||
|
baseUrl string
|
||||||
|
actionService actionService
|
||||||
|
downloadClientService downloadClientService
|
||||||
|
filterService filterService
|
||||||
|
indexerService indexerService
|
||||||
|
ircService ircService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(address string, baseUrl string, actionService actionService, downloadClientSvc downloadClientService, filterSvc filterService, indexerSvc indexerService, ircSvc ircService) Server {
|
||||||
|
return Server{
|
||||||
|
address: address,
|
||||||
|
baseUrl: baseUrl,
|
||||||
|
actionService: actionService,
|
||||||
|
downloadClientService: downloadClientSvc,
|
||||||
|
filterService: filterSvc,
|
||||||
|
indexerService: indexerSvc,
|
||||||
|
ircService: ircSvc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Server) Open() error {
|
||||||
|
listener, err := net.Listen("tcp", s.address)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
server := http.Server{
|
||||||
|
Handler: s.Handler(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return server.Serve(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Server) Handler() http.Handler {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
//r.Get("/", index)
|
||||||
|
//r.Get("/dashboard", dashboard)
|
||||||
|
|
||||||
|
//handler := web.AssetHandler("/", "build")
|
||||||
|
|
||||||
|
encoder := encoder{}
|
||||||
|
|
||||||
|
assets, _ := fs.Sub(web.Assets, "build/static")
|
||||||
|
r.HandleFunc("/static/*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fileSystem := http.StripPrefix("/static/", http.FileServer(http.FS(assets)))
|
||||||
|
fileSystem.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
|
||||||
|
actionHandler := actionHandler{
|
||||||
|
encoder: encoder,
|
||||||
|
actionService: s.actionService,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Route("/api/actions", actionHandler.Routes)
|
||||||
|
|
||||||
|
downloadClientHandler := downloadClientHandler{
|
||||||
|
encoder: encoder,
|
||||||
|
downloadClientService: s.downloadClientService,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Route("/api/download_clients", downloadClientHandler.Routes)
|
||||||
|
|
||||||
|
filterHandler := filterHandler{
|
||||||
|
encoder: encoder,
|
||||||
|
filterService: s.filterService,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Route("/api/filters", filterHandler.Routes)
|
||||||
|
|
||||||
|
ircHandler := ircHandler{
|
||||||
|
encoder: encoder,
|
||||||
|
ircService: s.ircService,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Route("/api/irc", ircHandler.Routes)
|
||||||
|
|
||||||
|
indexerHandler := indexerHandler{
|
||||||
|
encoder: encoder,
|
||||||
|
indexerService: s.indexerService,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Route("/api/indexer", indexerHandler.Routes)
|
||||||
|
|
||||||
|
configHandler := configHandler{
|
||||||
|
encoder: encoder,
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Route("/api/config", configHandler.Routes)
|
||||||
|
})
|
||||||
|
|
||||||
|
//r.HandleFunc("/*", handler.ServeHTTP)
|
||||||
|
r.Get("/", index)
|
||||||
|
r.Get("/*", index)
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func index(w http.ResponseWriter, r *http.Request) {
|
||||||
|
p := web.IndexParams{
|
||||||
|
Title: "Dashboard",
|
||||||
|
Version: "thisistheversion",
|
||||||
|
BaseUrl: config.Config.BaseURL,
|
||||||
|
}
|
||||||
|
web.Index(w, p)
|
||||||
|
}
|
6
internal/indexer/definitions.go
Normal file
6
internal/indexer/definitions.go
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
package indexer
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed definitions
|
||||||
|
var Definitions embed.FS
|
60
internal/indexer/definitions/alpharatio.yaml
Normal file
60
internal/indexer/definitions/alpharatio.yaml
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
#id: alpharatio
|
||||||
|
name: AlphaRatio
|
||||||
|
identifier: alpharatio
|
||||||
|
description: AlphaRatio (AR) is a private torrent tracker for 0DAY / GENERAL
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://alpharatio.cc/
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: gazelle
|
||||||
|
settings:
|
||||||
|
- name: authkey
|
||||||
|
type: text
|
||||||
|
label: Auth key
|
||||||
|
tooltip: Right click DL on a torrent and get the authkey.
|
||||||
|
description: Right click DL on a torrent and get the authkey.
|
||||||
|
- name: torrent_pass
|
||||||
|
type: text
|
||||||
|
label: Torrent pass
|
||||||
|
tooltip: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
description: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: AlphaRatio
|
||||||
|
server: irc.alpharatio.cc:6697
|
||||||
|
port: 6697
|
||||||
|
channels:
|
||||||
|
- "#Announce"
|
||||||
|
announcers:
|
||||||
|
- Voyager
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: multi
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "[New Release]-[MovieHD]-[War.For.The.Planet.Of.The.Apes.2017.INTERNAL.1080p.BluRay.CRF.x264-SAPHiRE]-[URL]-[ https://alpharatio.cc/torrents.php?id=699463 ]-[ 699434 ]-[ Uploaded 2 Mins, 59 Secs after pre. ]"
|
||||||
|
pattern: \[New Release\]-\[(.*)\]-\[(.*)\]-\[URL\]-\[ (https?://.*)id=\d+ \]-\[ (\d+) \](?:-\[ Uploaded (.*) after pre. ])?
|
||||||
|
vars:
|
||||||
|
- category
|
||||||
|
- torrentName
|
||||||
|
- baseUrl
|
||||||
|
- torrentId
|
||||||
|
- preTime
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "[AutoDL]-[MovieHD]-[699434]-[ 1 | 10659 | 1 | 1 ]-[War.For.The.Planet.Of.The.Apes.2017.INTERNAL.1080p.BluRay.CRF.x264-SAPHiRE]"
|
||||||
|
pattern: \[AutoDL\]-\[.*\]-\[.*\]-\[ ([01]) \| (\d+) \| ([01]) \| ([01]) \]-\[.+\]
|
||||||
|
vars:
|
||||||
|
- scene
|
||||||
|
- torrentSize
|
||||||
|
- freeleech
|
||||||
|
- auto
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "{{ .baseUrl }}action=download&id={{ .torrentId }}&authkey={{ .authkey }}&torrent_pass={{ .torrent_pass }}"
|
48
internal/indexer/definitions/beyondhd.yaml
Normal file
48
internal/indexer/definitions/beyondhd.yaml
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
---
|
||||||
|
#id: beyondhd
|
||||||
|
name: BeyondHD
|
||||||
|
identifier: beyondhd
|
||||||
|
description: BeyondHD (BHD) is a private torrent tracker for HD MOVIES / TV
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://beyond-hd.me/
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: UNIT3D (F3NIX)
|
||||||
|
settings:
|
||||||
|
- name: passkey
|
||||||
|
type: text
|
||||||
|
label: Passkey
|
||||||
|
tooltip: The passkey in your BeyondHD RSS feed.
|
||||||
|
description: "Go to your profile and copy and paste your RSS link to extract the rsskey."
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: BeyondHD-IRC
|
||||||
|
server: irc.beyond-hd.me:6697
|
||||||
|
port: 6697
|
||||||
|
channels:
|
||||||
|
- "#bhd_announce"
|
||||||
|
announcers:
|
||||||
|
- Willie
|
||||||
|
- Millie
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: single
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "New Torrent: Orange.Is.the.New.Black.S01.1080p.Blu-ray.AVC.DTS-HD.MA.5.1-Test Category: TV By: Uploader Size: 137.73 GB Link: https://beyond-hd.me/details.php?id=25918"
|
||||||
|
pattern: 'New Torrent:(.*)Category:(.*)By:(.*)Size:(.*)Link: https?\:\/\/([^\/]+\/).*[&\?]id=(\d+)'
|
||||||
|
vars:
|
||||||
|
- torrentName
|
||||||
|
- category
|
||||||
|
- uploader
|
||||||
|
- torrentSize
|
||||||
|
- baseUrl
|
||||||
|
- torrentId
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "https://{{ .baseUrl }}torrent/download/auto.{{ .torrentId }}.{{ .passkey }}"
|
68
internal/indexer/definitions/btn.yaml
Normal file
68
internal/indexer/definitions/btn.yaml
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
---
|
||||||
|
#id: btn
|
||||||
|
name: BroadcasTheNet
|
||||||
|
identifier: btn
|
||||||
|
description: BroadcasTheNet (BTN) is a private torrent tracker focused on TV shows
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://broadcasthe.net/
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: gazelle
|
||||||
|
settings:
|
||||||
|
- name: authkey
|
||||||
|
type: text
|
||||||
|
label: Auth key
|
||||||
|
description: Right click DL on a torrent and get the authkey.
|
||||||
|
- name: torrent_pass
|
||||||
|
type: text
|
||||||
|
label: Torrent pass
|
||||||
|
description: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: BroadcasTheNet
|
||||||
|
server: irc.broadcasthenet.net:6697
|
||||||
|
port: 6697
|
||||||
|
channels:
|
||||||
|
- "#BTN-Announce"
|
||||||
|
announcers:
|
||||||
|
- Barney
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: multi
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "NOW BROADCASTING! [ Lost S06E07 720p WEB-DL DD 5.1 H.264 - LP ]"
|
||||||
|
pattern: ^NOW BROADCASTING! \[(.*)\]
|
||||||
|
vars:
|
||||||
|
- torrentName
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "[ Title: S06E07 ] [ Series: Lost ]"
|
||||||
|
pattern: '^\[ Title: (.*) \] \[ Series: (.*) \]'
|
||||||
|
vars:
|
||||||
|
- title
|
||||||
|
- name1
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "[ 2010 ] [ Episode ] [ MKV | x264 | WEB ] [ Uploader: Uploader1 ]"
|
||||||
|
pattern: '^(?:\[ (\d+) \] )?\[ (.*) \] \[ (.*) \] \[ Uploader: (.*?) \](?: \[ Pretime: (.*) \])?'
|
||||||
|
vars:
|
||||||
|
- year
|
||||||
|
- category
|
||||||
|
- tags
|
||||||
|
- uploader
|
||||||
|
- preTime
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "[ https://XXXXXXXXX/torrents.php?id=7338 / https://XXXXXXXXX/torrents.php?action=download&id=9116 ]"
|
||||||
|
pattern: ^\[ .* / (https?://.*id=\d+) \]
|
||||||
|
vars:
|
||||||
|
- baseUrl
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "{{ .baseUrl }}&authkey={{ .authkey }}&torrent_pass={{ .torrent_pass }}"
|
48
internal/indexer/definitions/emp.yaml
Normal file
48
internal/indexer/definitions/emp.yaml
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
---
|
||||||
|
#id: emp
|
||||||
|
name: Empornium
|
||||||
|
identifier: emp
|
||||||
|
description: Empornium (EMP) is a private torrent tracker for XXX
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://www.empornium.is
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: gazelle
|
||||||
|
settings:
|
||||||
|
- name: authkey
|
||||||
|
type: text
|
||||||
|
label: Auth key
|
||||||
|
description: Right click DL on a torrent and get the authkey.
|
||||||
|
- name: torrent_pass
|
||||||
|
type: text
|
||||||
|
label: Torrent pass
|
||||||
|
description: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: DigitalIRC
|
||||||
|
server: irc.empornium.is:6697
|
||||||
|
port: 6697
|
||||||
|
channels:
|
||||||
|
- "#empornium-announce"
|
||||||
|
announcers:
|
||||||
|
- "^Wizard^"
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: single
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
pattern: '^(.*?) - Size: ([0-9]+?.*?) - Uploader: (.*?) - Tags: (.*?) - (https://.*torrents.php\?)id=(.*)$'
|
||||||
|
vars:
|
||||||
|
- torrentName
|
||||||
|
- torrentSize
|
||||||
|
- uploader
|
||||||
|
- tags
|
||||||
|
- baseUrl
|
||||||
|
- torrentId
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "{{ .baseUrl }}action=download&id={{ .torrentId }}&authkey={{ .authkey }}&torrent_pass={{ .torrent_pass }}"
|
53
internal/indexer/definitions/filelist.yaml
Normal file
53
internal/indexer/definitions/filelist.yaml
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
---
|
||||||
|
#id: filelist
|
||||||
|
name: FileList
|
||||||
|
identifier: fl
|
||||||
|
description: FileList (FL) is a ROMANIAN private torrent tracker for MOVIES / TV / GENERAL
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://filelist.io
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: custom
|
||||||
|
settings:
|
||||||
|
- name: passkey
|
||||||
|
type: text
|
||||||
|
label: Passkey
|
||||||
|
tooltip: The passkey in your profile.
|
||||||
|
description: "The passkey in your profile."
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: FileList
|
||||||
|
server: irc.filelist.io:6697
|
||||||
|
port: 6697
|
||||||
|
channels:
|
||||||
|
- "#announce"
|
||||||
|
announcers:
|
||||||
|
- Announce
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: single
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- 'New Torrent: This.Really.Old.Movie.1965.DVDRip.DD1.0.x264 -- [Filme SD] [1.91 GB] -- https://filelist.io/details.php?id=746781 -- by uploader1'
|
||||||
|
- 'New Torrent: This.New.Movie.2021.1080p.Blu-ray.AVC.DTS-HD.MA.5.1-BEATRIX -- [FreeLeech!] -- [Filme Blu-Ray] [26.78 GB] -- https://filelist.io/details.php?id=746782 -- by uploader1'
|
||||||
|
- 'New Torrent: This.New.Movie.2021.1080p.Remux.AVC.DTS-HD.MA.5.1-playBD -- [FreeLeech!] -- [Internal!] -- [Filme Blu-Ray] [17.69 GB] -- https://filelist.io/details.php?id=746789 -- by uploader1'
|
||||||
|
pattern: 'New Torrent: (.*?) (?:-- \[(FreeLeech!)] )?(?:-- \[(Internal!)] )?-- \[(.*)] \[(.*)] -- (https?:\/\/filelist.io\/).*id=(.*) -- by (.*)'
|
||||||
|
vars:
|
||||||
|
- torrentName
|
||||||
|
- freeleech
|
||||||
|
- internal
|
||||||
|
- category
|
||||||
|
- torrentSize
|
||||||
|
- baseUrl
|
||||||
|
- torrentId
|
||||||
|
- uploader
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "{{ .baseUrl }}download.php?id={{ .torrentId }}&file={{ .torrentName }}.torrent&passkey={{ .passkey }}"
|
||||||
|
encode:
|
||||||
|
- torrentName
|
56
internal/indexer/definitions/gazellegames.yaml
Normal file
56
internal/indexer/definitions/gazellegames.yaml
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
---
|
||||||
|
#id: gazellegames
|
||||||
|
name: GazelleGames
|
||||||
|
identifier: ggn
|
||||||
|
description: GazelleGames (GGn) is a private torrent tracker for GAMES
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://gazellegames.net/
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: gazelle
|
||||||
|
settings:
|
||||||
|
- name: authkey
|
||||||
|
type: text
|
||||||
|
label: Auth key
|
||||||
|
tooltip: Right click DL on a torrent and get the authkey.
|
||||||
|
description: Right click DL on a torrent and get the authkey.
|
||||||
|
- name: torrent_pass
|
||||||
|
type: text
|
||||||
|
label: Torrent pass
|
||||||
|
tooltip: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
description: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: GGn
|
||||||
|
server: irc.gazellegames.net:7000
|
||||||
|
port: 7000
|
||||||
|
channels:
|
||||||
|
- "#GGn-Announce"
|
||||||
|
announcers:
|
||||||
|
- Vertigo
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: single
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "Uploader :-: Nintendo 3DS :-: Yo-Kai.Watch.KOR.3DS-BigBlueBox in Yo-kai Watch [2013] ::Korean, Multi-Region, Scene:: https://gazellegames.net/torrents.php?torrentid=78851 - adventure, role_playing_game, nintendo;"
|
||||||
|
- "Uploader :-: Windows :-: Warriors.Wrath.Evil.Challenge-HI2U in Warriors' Wrath [2016] ::English, Scene:: FREELEECH! :: https://gazellegames.net/torrents.php?torrentid=78902 - action, adventure, casual, indie, role.playing.game;"
|
||||||
|
pattern: '^(.+) :-: (.+) :-: (.+) \[(\d+)\] ::(.+?):: ?(.+? ::)? https?:\/\/([^\/]+\/)torrents.php\?torrentid=(\d+) ?-? ?(.*?)?;?$'
|
||||||
|
vars:
|
||||||
|
- uploader
|
||||||
|
- category
|
||||||
|
- torrentName
|
||||||
|
- year
|
||||||
|
- flags
|
||||||
|
- bonus
|
||||||
|
- baseUrl
|
||||||
|
- torrentId
|
||||||
|
- tags
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "{{ .baseUrl }}torrents.php?action=download&id={{ .torrentId }}&authkey={{ .authkey }}&torrent_pass={{ .torrent_pass }}"
|
49
internal/indexer/definitions/hd-torrents.yaml
Normal file
49
internal/indexer/definitions/hd-torrents.yaml
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
#id: hdt
|
||||||
|
name: HD-Torrents
|
||||||
|
identifier: hdt
|
||||||
|
description: HD-Torrents (HD-T) is a private torrent tracker for HD MOVIES / TV
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://hd-torrents.org/
|
||||||
|
- https://hdts.ru
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: xbtit
|
||||||
|
settings:
|
||||||
|
- name: cookie
|
||||||
|
type: text
|
||||||
|
label: Cookie
|
||||||
|
description: "FireFox -> Preferences -> Privacy -> Show Cookies and find the uid and pass cookies. Example: uid=1234; pass=asdf12347asdf13"
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: P2P-NET
|
||||||
|
server: irc.p2p-network.net:6697
|
||||||
|
port: 6697
|
||||||
|
channels:
|
||||||
|
- "#HD-Torrents.Announce"
|
||||||
|
announcers:
|
||||||
|
- HoboLarry
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: single
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "New Torrent in category [XXX/Blu-ray] Erotische Fantasien 3D (2008) Blu-ray 1080p AVC DTS-HD MA 7 1 (14.60 GB) uploaded! Download: https://hd-torrents.org/download.php?id=806bc36530d146969d300c5352483a5e6e0639e9"
|
||||||
|
pattern: 'New Torrent in category \[([^\]]*)\] (.*) \(([^\)]*)\) uploaded! Download\: https?\:\/\/([^\/]+\/).*[&\?]id=([a-f0-9]+)'
|
||||||
|
vars:
|
||||||
|
- category
|
||||||
|
- torrentName
|
||||||
|
- torrentSize
|
||||||
|
- baseUrl
|
||||||
|
- torrentId
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "https://{{ .baseUrl }}download.php?id={{ .torrentId }}&f={{ .torrentName }}.torrent"
|
||||||
|
cookie: true
|
||||||
|
encode:
|
||||||
|
- torrentName
|
53
internal/indexer/definitions/iptorrents.yaml
Normal file
53
internal/indexer/definitions/iptorrents.yaml
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
---
|
||||||
|
#id: iptorrents
|
||||||
|
name: IPTorrents
|
||||||
|
identifier: ipt
|
||||||
|
description: IPTorrents (IPT) is a private torrent tracker for 0DAY / GENERAL.
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://iptorrents.com/
|
||||||
|
- https://iptorrents.me/
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: unknown
|
||||||
|
settings:
|
||||||
|
- name: passkey
|
||||||
|
type: text
|
||||||
|
label: Passkey
|
||||||
|
tooltip: Copy the passkey from your details page
|
||||||
|
description: "Copy the passkey from your details page."
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: IPTorrents
|
||||||
|
server: irc.iptorrents.com:6697
|
||||||
|
port: 6697
|
||||||
|
channels:
|
||||||
|
- "#ipt.announce"
|
||||||
|
- "#ipt.announce2"
|
||||||
|
announcers:
|
||||||
|
- IPT
|
||||||
|
- FunTimes
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: single
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "[Movie/XXX] Audrey Bitoni HD Pack FREELEECH - http://www.iptorrents.com/details.php?id=789421 - 14.112 GB"
|
||||||
|
- "[Movies/XviD] The First Men In The Moon 2010 DVDRip XviD-VoMiT - http://www.iptorrents.com/details.php?id=396589 - 716.219 MB"
|
||||||
|
pattern: '^\[([^\]]*)](.*?)\s*(FREELEECH)*\s*-\s+https?\:\/\/([^\/]+).*[&\?]id=(\d+)\s*-(.*)'
|
||||||
|
vars:
|
||||||
|
- category
|
||||||
|
- torrentName
|
||||||
|
- freeleech
|
||||||
|
- baseUrl
|
||||||
|
- torrentId
|
||||||
|
- torrentSize
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "{{ .baseUrl }}download.php?id={{ .torrentId }}&file={{ .torrentName }}.torrent&passkey={{ .passkey }}"
|
||||||
|
encode:
|
||||||
|
- torrentName
|
55
internal/indexer/definitions/nebulance.yaml
Normal file
55
internal/indexer/definitions/nebulance.yaml
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
---
|
||||||
|
#id: nebulance
|
||||||
|
name: Nebulance
|
||||||
|
identifier: nbl
|
||||||
|
description: Nebulance (NBL) is a ratioless private torrent tracker for TV
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://nebulance.io/
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: gazelle
|
||||||
|
settings:
|
||||||
|
- name: authkey
|
||||||
|
type: text
|
||||||
|
label: Auth key
|
||||||
|
tooltip: Right click DL on a torrent and get the authkey.
|
||||||
|
description: Right click DL on a torrent and get the authkey.
|
||||||
|
- name: torrent_pass
|
||||||
|
type: text
|
||||||
|
label: Torrent pass
|
||||||
|
tooltip: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
description: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: Nebulance
|
||||||
|
server: irc.nebulance.cc:6697
|
||||||
|
port: 6697
|
||||||
|
channels:
|
||||||
|
- "#nbl-announce"
|
||||||
|
announcers:
|
||||||
|
- DRADIS
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: single
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "[Episodes] The Vet Life - S02E08 [WebRip / x264 / MKV / 720p / HD / VLAD / The.Vet.Life.S02E08.Tuskegee.Reunion.720p.ANPL.WEBRip.AAC2.0.x264-VLAD.mkv] [702.00 MB - Uploader: UPLOADER] - http://nebulance.io/torrents.php?id=147 [Tags: comedy,subtitles,cbs]"
|
||||||
|
- "[Seasons] Police Interceptors - S10 [HDTV / x264 / MKV / MP4 / 480p / SD / BTN / Police.Interceptors.S10.HDTV.x264-BTN] [5.27 GB - Uploader: UPLOADER] - http://nebulance.io/torrents.php?id=1472 [Tags: comedy,subtitles,cbs]"
|
||||||
|
pattern: '\[(.*?)\] (.*?) \[(.*?)\] \[(.*?) - Uploader: (.*?)\] - (https?://.*)id=(\d+) \[Tags: (.*)\]'
|
||||||
|
vars:
|
||||||
|
- category
|
||||||
|
- torrentName
|
||||||
|
- releaseTags
|
||||||
|
- torrentSize
|
||||||
|
- uploader
|
||||||
|
- baseUrl
|
||||||
|
- torrentId
|
||||||
|
- tags
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "{{ .baseUrl }}action=download&id={{ .torrentId }}&authkey={{ .authkey }}&torrent_pass={{ .torrent_pass }}"
|
50
internal/indexer/definitions/orpheus.yaml
Normal file
50
internal/indexer/definitions/orpheus.yaml
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
---
|
||||||
|
#id: orpheus
|
||||||
|
name: Orpheus
|
||||||
|
identifier: ops
|
||||||
|
description: Orpheus (OPS) is a Private Torrent Tracker for MUSIC
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://orpheus.network/
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: gazelle
|
||||||
|
settings:
|
||||||
|
- name: authkey
|
||||||
|
type: text
|
||||||
|
label: Auth key
|
||||||
|
tooltip: Right click DL on a torrent and get the authkey.
|
||||||
|
description: Right click DL on a torrent and get the authkey.
|
||||||
|
- name: torrent_pass
|
||||||
|
type: text
|
||||||
|
label: Torrent pass
|
||||||
|
tooltip: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
description: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: Orpheus
|
||||||
|
server: irc.orpheus.network:7000
|
||||||
|
port: 7000
|
||||||
|
channels:
|
||||||
|
- "#announce"
|
||||||
|
announcers:
|
||||||
|
- hermes
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: single
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "TORRENT: Todd Edwards - You Came To Me [2002] [Single] - FLAC / Lossless / WEB - 2000s,house,uk.garage,garage.house - https://orpheus.network/torrents.php?id=756102 / https://orpheus.network/torrents.php?action=download&id=1647868"
|
||||||
|
- "TORRENT: THE BOOK [2021] [Album] - FLAC / Lossless / CD - - https://orpheus.network/torrents.php?id=693523 / https://orpheus.network/torrents.php?action=download&id=1647867"
|
||||||
|
pattern: 'TORRENT: (.*) - (.*) - https?://.* / (https?://.*id=\d+)'
|
||||||
|
vars:
|
||||||
|
- torrentName
|
||||||
|
- tags
|
||||||
|
- torrentId
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "{{ .baseUrl }}&authkey={{ .authkey }}&torrent_pass={{ .torrent_pass }}"
|
51
internal/indexer/definitions/ptp.yaml
Normal file
51
internal/indexer/definitions/ptp.yaml
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
#id: ptp
|
||||||
|
name: PassThePopcorn
|
||||||
|
identifier: ptp
|
||||||
|
description: PassThePopcorn (PTP) is a private torrent tracker for MOVIES
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://passthepopcorn.me
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: gazelle
|
||||||
|
settings:
|
||||||
|
- name: authkey
|
||||||
|
type: text
|
||||||
|
label: Auth key
|
||||||
|
tooltip: Right click DL on a torrent and get the authkey.
|
||||||
|
description: Right click DL on a torrent and get the authkey.
|
||||||
|
- name: torrent_pass
|
||||||
|
type: text
|
||||||
|
label: Torrent pass
|
||||||
|
tooltip: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
description: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: PassThePopcorn
|
||||||
|
server: irc.passthepopcorn.me:7000
|
||||||
|
port: 7000
|
||||||
|
channels:
|
||||||
|
- "#ptp-announce"
|
||||||
|
announcers:
|
||||||
|
- Hummingbird
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: single
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "Irene Huss - Nattrond AKA The Night Round [2008] by Anders Engström - XviD / DVD / AVI / 640x352 - http://passthepopcorn.me/torrents.php?id=51627 / http://passthepopcorn.me/torrents.php?action=download&id=97333 - crime, drama, mystery"
|
||||||
|
- "Dirty Rotten Scoundrels [1988] by Frank Oz - x264 / Blu-ray / MKV / 720p - http://passthepopcorn.me/torrents.php?id=10735 / http://passthepopcorn.me/torrents.php?action=download&id=97367 - comedy, crime"
|
||||||
|
pattern: '^(.*)-\s*https?:.*[&\?]id=.*https?\:\/\/([^\/]+\/).*[&\?]id=(\d+)\s*-\s*(.*)'
|
||||||
|
vars:
|
||||||
|
- torrentName
|
||||||
|
- baseUrl
|
||||||
|
- torrentId
|
||||||
|
- tags
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "https://{{ .baseUrl }}torrents.php?action=download&id={{ .torrentId }}&authkey={{ .authkey }}&torrent_pass={{ .torrent_pass }}"
|
51
internal/indexer/definitions/red.yaml
Normal file
51
internal/indexer/definitions/red.yaml
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
---
|
||||||
|
#id: red
|
||||||
|
name: Redacted
|
||||||
|
identifier: redacted
|
||||||
|
description: Redacted (RED) is a private torrent tracker for MUSIC
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://redacted.ch/
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: gazelle
|
||||||
|
settings:
|
||||||
|
- name: authkey
|
||||||
|
type: text
|
||||||
|
label: Auth key
|
||||||
|
tooltip: Right click DL on a torrent and get the authkey.
|
||||||
|
description: Right click DL on a torrent and get the authkey.
|
||||||
|
- name: torrent_pass
|
||||||
|
type: text
|
||||||
|
label: Torrent pass
|
||||||
|
tooltip: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
description: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: Scratch-Network
|
||||||
|
server: irc.scratch-network.net:6697
|
||||||
|
port: 6697
|
||||||
|
channels:
|
||||||
|
- "#red-announce"
|
||||||
|
announcers:
|
||||||
|
- Drone
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: single
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "JR Get Money - Nobody But You [2008] [Single] - FLAC / Lossless / Log / 100% / Cue / CD - https://redacted.ch/torrents.php?id=1592366 / https://redacted.ch/torrents.php?action=download&id=3372962 - hip.hop,rhythm.and.blues,2000s"
|
||||||
|
- "Johann Sebastian Bach performed by Festival Strings Lucerne under Rudolf Baumgartner - Brandenburg Concertos 5 and 6, Suite No 2 [1991] [Album] - FLAC / Lossless / Log / 100% / Cue / CD - https://redacted.ch/torrents.php?id=1592367 / https://redacted.ch/torrents.php?action=download&id=3372963 - classical"
|
||||||
|
pattern: '^(.*)\s+-\s+https?:.*[&\?]id=.*https?\:\/\/([^\/]+\/).*[&\?]id=(\d+)\s*-\s*(.*)'
|
||||||
|
vars:
|
||||||
|
- torrentName
|
||||||
|
- baseUrl
|
||||||
|
- torrentId
|
||||||
|
- tags
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "https://{{ .baseUrl }}torrents.php?action=download&id={{ .torrentId }}&authkey={{ .authkey }}&torrent_pass={{ .torrent_pass }}"
|
49
internal/indexer/definitions/superbits.yaml
Normal file
49
internal/indexer/definitions/superbits.yaml
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
#id: superbits
|
||||||
|
name: SuperBits
|
||||||
|
identifier: superbits
|
||||||
|
description: Superbits is a SWEDISH private torrent tracker for MOVIES / TV / 0DAY / GENERAL
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://superbits.org/
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: rartracker
|
||||||
|
settings:
|
||||||
|
- name: passkey
|
||||||
|
type: text
|
||||||
|
label: Passkey
|
||||||
|
tooltip: Copy the passkey from the /rss page
|
||||||
|
description: "Copy the passkey from the /rss page."
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: SuperBits
|
||||||
|
server: irc.superbits.org:6697
|
||||||
|
port: 6697
|
||||||
|
channels:
|
||||||
|
- "#autodl"
|
||||||
|
announcers:
|
||||||
|
- SuperBits
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: single
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "-[archive Film 1080]2[Asterix.Et.La.Surprise.De.Cesar.1985.FRENCH.1080p.BluRay.x264-TSuNaMi]3[844551]4[Size: 4.41 GB]5[FL: no]6[Scene: yes]"
|
||||||
|
- "-[new TV]2[Party.Down.South.S05E05.720p.WEB.h264-DiRT]3[844557]4[Size: 964.04 MB]5[FL: no]6[Scene: yes]7[Pred 1m 30s ago]"
|
||||||
|
pattern: '\-\[(.*)\]2\[(.*)\]3\[(\d+)\]4\[Size\:\s(.*)\]5\[FL\:\s(no|yes)\]6\[Scene\:\s(no|yes)\](?:7\[Pred\s(.*)\sago\])?'
|
||||||
|
vars:
|
||||||
|
- category
|
||||||
|
- torrentName
|
||||||
|
- torrentId
|
||||||
|
- torrentSize
|
||||||
|
- freeleech
|
||||||
|
- scene
|
||||||
|
- preTime
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "https://superbits.org/download.php?id={{ .torrentId }}&passkey={{ .passkey }}"
|
53
internal/indexer/definitions/torrentleech.yaml
Normal file
53
internal/indexer/definitions/torrentleech.yaml
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
---
|
||||||
|
#id: tracker01
|
||||||
|
name: TorrentLeech
|
||||||
|
identifier: torrentleech
|
||||||
|
description: TorrentLeech (TL) is a private torrent tracker for 0DAY / GENERAL.
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://www.torrentleech.org
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: custom
|
||||||
|
settings:
|
||||||
|
- name: rsskey
|
||||||
|
type: text
|
||||||
|
label: RSS key
|
||||||
|
tooltip: The rsskey in your TorrentLeech RSS feed link.
|
||||||
|
description: "Go to your profile and copy and paste your RSS link to extract the rsskey."
|
||||||
|
regex: /([\da-fA-F]{20})
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: TorrentLeech.org
|
||||||
|
server: irc.torrentleech.org:7021
|
||||||
|
port: 7021
|
||||||
|
channels:
|
||||||
|
- "#tlannounces"
|
||||||
|
announcers:
|
||||||
|
- _AnnounceBot_
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: single
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "New Torrent Announcement: <PC :: Iso> Name:'debian live 10 6 0 amd64 standard iso' uploaded by 'Anonymous' - http://www.tracker01.test/torrent/263302"
|
||||||
|
- "New Torrent Announcement: <PC :: Iso> Name:'debian live 10 6 0 amd64 standard iso' uploaded by 'Anonymous' freeleech - http://www.tracker01.test/torrent/263302"
|
||||||
|
pattern: New Torrent Announcement:\s*<([^>]*)>\s*Name:'(.*)' uploaded by '([^']*)'\s*(freeleech)*\s*-\s*https?\:\/\/([^\/]+\/)torrent\/(\d+)
|
||||||
|
vars:
|
||||||
|
- category
|
||||||
|
- torrentName
|
||||||
|
- uploader
|
||||||
|
- freeleech
|
||||||
|
- baseUrl
|
||||||
|
- torrentId
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "https://{{ .baseUrl }}rss/download/{{ .torrentId }}/{{ .rsskey }}/{{ .torrentName }}.torrent"
|
||||||
|
encode:
|
||||||
|
- torrentName
|
||||||
|
|
||||||
|
|
52
internal/indexer/definitions/uhdbits.yaml
Normal file
52
internal/indexer/definitions/uhdbits.yaml
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
---
|
||||||
|
#id: uhd
|
||||||
|
name: UHDBits
|
||||||
|
identifier: uhdbits
|
||||||
|
description: UHDBits (UHD) is a private torrent tracker for HD MOVIES / TV
|
||||||
|
language: en-us
|
||||||
|
urls:
|
||||||
|
- https://uhdbits.org/
|
||||||
|
privacy: private
|
||||||
|
protocol: torrent
|
||||||
|
supports:
|
||||||
|
- irc
|
||||||
|
- rss
|
||||||
|
source: gazelle
|
||||||
|
settings:
|
||||||
|
- name: authkey
|
||||||
|
type: text
|
||||||
|
label: Auth key
|
||||||
|
tooltip: Right click DL on a torrent and get the authkey.
|
||||||
|
description: Right click DL on a torrent and get the authkey.
|
||||||
|
- name: torrent_pass
|
||||||
|
type: text
|
||||||
|
label: Torrent pass
|
||||||
|
tooltip: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
description: Right click DL on a torrent and get the torrent_pass.
|
||||||
|
|
||||||
|
irc:
|
||||||
|
network: P2P-Network
|
||||||
|
server: irc.p2p-network.net:6697
|
||||||
|
port: 6697
|
||||||
|
channels:
|
||||||
|
- "#UHD.Announce"
|
||||||
|
announcers:
|
||||||
|
- UHDBot
|
||||||
|
- cr0nusbot
|
||||||
|
|
||||||
|
parse:
|
||||||
|
type: single
|
||||||
|
lines:
|
||||||
|
-
|
||||||
|
test:
|
||||||
|
- "New Torrent: D'Ardennen [2015] - TayTO Type: Movie / 1080p / Encode / Freeleech: 100 Size: 7.00GB - https://uhdbits.org/torrents.php?id=13882 / https://uhdbits.org/torrents.php?action=download&id=20488"
|
||||||
|
pattern: 'New Torrent: (.*) Type: (.*?) Freeleech: (.*) Size: (.*) - https?:\/\/.* \/ (https?:\/\/.*id=\d+)'
|
||||||
|
vars:
|
||||||
|
- torrentName
|
||||||
|
- releaseTags
|
||||||
|
- freeleechPercent
|
||||||
|
- torrentSize
|
||||||
|
- baseUrl
|
||||||
|
|
||||||
|
match:
|
||||||
|
torrenturl: "{{ .baseUrl }}&authkey={{ .authkey }}&torrent_pass={{ .torrent_pass }}"
|
252
internal/indexer/service.go
Normal file
252
internal/indexer/service.go
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
package indexer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Store(indexer domain.Indexer) (*domain.Indexer, error)
|
||||||
|
Update(indexer domain.Indexer) (*domain.Indexer, error)
|
||||||
|
Delete(id int) error
|
||||||
|
FindByFilterID(id int) ([]domain.Indexer, error)
|
||||||
|
List() ([]domain.Indexer, error)
|
||||||
|
GetAll() ([]*domain.IndexerDefinition, error)
|
||||||
|
GetTemplates() ([]domain.IndexerDefinition, error)
|
||||||
|
LoadIndexerDefinitions() error
|
||||||
|
GetIndexerByAnnounce(name string) *domain.IndexerDefinition
|
||||||
|
Start() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo domain.IndexerRepo
|
||||||
|
indexerDefinitions map[string]domain.IndexerDefinition
|
||||||
|
indexerInstances map[string]domain.IndexerDefinition
|
||||||
|
mapIndexerIRCToName map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo domain.IndexerRepo) Service {
|
||||||
|
return &service{
|
||||||
|
repo: repo,
|
||||||
|
indexerDefinitions: make(map[string]domain.IndexerDefinition),
|
||||||
|
indexerInstances: make(map[string]domain.IndexerDefinition),
|
||||||
|
mapIndexerIRCToName: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Store(indexer domain.Indexer) (*domain.Indexer, error) {
|
||||||
|
i, err := s.repo.Store(indexer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Update(indexer domain.Indexer) (*domain.Indexer, error) {
|
||||||
|
i, err := s.repo.Update(indexer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Delete(id int) error {
|
||||||
|
if err := s.repo.Delete(id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) FindByFilterID(id int) ([]domain.Indexer, error) {
|
||||||
|
filters, err := s.repo.FindByFilterID(id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filters, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) List() ([]domain.Indexer, error) {
|
||||||
|
i, err := s.repo.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetAll() ([]*domain.IndexerDefinition, error) {
|
||||||
|
indexers, err := s.repo.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var res = make([]*domain.IndexerDefinition, 0)
|
||||||
|
|
||||||
|
for _, indexer := range indexers {
|
||||||
|
in := s.getDefinitionByName(indexer.Identifier)
|
||||||
|
if in == nil {
|
||||||
|
// if no indexerDefinition found, continue
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
temp := domain.IndexerDefinition{
|
||||||
|
ID: indexer.ID,
|
||||||
|
Name: in.Name,
|
||||||
|
Identifier: in.Identifier,
|
||||||
|
Enabled: indexer.Enabled,
|
||||||
|
Description: in.Description,
|
||||||
|
Language: in.Language,
|
||||||
|
Privacy: in.Privacy,
|
||||||
|
Protocol: in.Protocol,
|
||||||
|
URLS: in.URLS,
|
||||||
|
Settings: nil,
|
||||||
|
SettingsMap: make(map[string]string),
|
||||||
|
IRC: in.IRC,
|
||||||
|
Parse: in.Parse,
|
||||||
|
}
|
||||||
|
|
||||||
|
// map settings
|
||||||
|
// add value to settings objects
|
||||||
|
for _, setting := range in.Settings {
|
||||||
|
if v, ok := indexer.Settings[setting.Name]; ok {
|
||||||
|
setting.Value = v
|
||||||
|
|
||||||
|
temp.SettingsMap[setting.Name] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
temp.Settings = append(temp.Settings, setting)
|
||||||
|
}
|
||||||
|
|
||||||
|
res = append(res, &temp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetTemplates() ([]domain.IndexerDefinition, error) {
|
||||||
|
|
||||||
|
definitions := s.indexerDefinitions
|
||||||
|
|
||||||
|
var ret []domain.IndexerDefinition
|
||||||
|
for _, definition := range definitions {
|
||||||
|
ret = append(ret, definition)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Start() error {
|
||||||
|
err := s.LoadIndexerDefinitions()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
indexers, err := s.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, indexer := range indexers {
|
||||||
|
if !indexer.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s.indexerInstances[indexer.Identifier] = *indexer
|
||||||
|
|
||||||
|
// map irc stuff to indexer.name
|
||||||
|
if indexer.IRC != nil {
|
||||||
|
server := indexer.IRC.Server
|
||||||
|
|
||||||
|
for _, channel := range indexer.IRC.Channels {
|
||||||
|
for _, announcer := range indexer.IRC.Announcers {
|
||||||
|
val := fmt.Sprintf("%v:%v:%v", server, channel, announcer)
|
||||||
|
s.mapIndexerIRCToName[val] = indexer.Identifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadIndexerDefinitions load definitions from golang embed fs
|
||||||
|
func (s *service) LoadIndexerDefinitions() error {
|
||||||
|
|
||||||
|
entries, err := fs.ReadDir(Definitions, "definitions")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal().Msgf("failed reading directory: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entries) == 0 {
|
||||||
|
log.Fatal().Msgf("failed reading directory: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range entries {
|
||||||
|
filePath := "definitions/" + f.Name()
|
||||||
|
|
||||||
|
if strings.Contains(f.Name(), ".yaml") {
|
||||||
|
log.Debug().Msgf("parsing: %v", filePath)
|
||||||
|
|
||||||
|
var d domain.IndexerDefinition
|
||||||
|
|
||||||
|
data, err := fs.ReadFile(Definitions, filePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msgf("failed reading file: %v", filePath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = yaml.Unmarshal(data, &d)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed unmarshal file: %v", filePath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.indexerDefinitions[d.Identifier] = d
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetIndexerByAnnounce(name string) *domain.IndexerDefinition {
|
||||||
|
|
||||||
|
if identifier, idOk := s.mapIndexerIRCToName[name]; idOk {
|
||||||
|
if indexer, ok := s.indexerInstances[identifier]; ok {
|
||||||
|
return &indexer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) getDefinitionByName(name string) *domain.IndexerDefinition {
|
||||||
|
|
||||||
|
if v, ok := s.indexerDefinitions[name]; ok {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) getDefinitionForAnnounce(name string) *domain.IndexerDefinition {
|
||||||
|
|
||||||
|
// map[network:channel:announcer] = indexer01
|
||||||
|
|
||||||
|
if v, ok := s.indexerDefinitions[name]; ok {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
260
internal/irc/handler.go
Normal file
260
internal/irc/handler.go
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/announce"
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gopkg.in/irc.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
connectTimeout = 15 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
network *domain.IrcNetwork
|
||||||
|
announceService announce.Service
|
||||||
|
|
||||||
|
conn net.Conn
|
||||||
|
ctx context.Context
|
||||||
|
stopped chan struct{}
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(network domain.IrcNetwork, announceService announce.Service) *Handler {
|
||||||
|
return &Handler{
|
||||||
|
conn: nil,
|
||||||
|
ctx: nil,
|
||||||
|
stopped: make(chan struct{}),
|
||||||
|
network: &network,
|
||||||
|
announceService: announceService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handler) Run() error {
|
||||||
|
//log.Debug().Msgf("server %+v", s.network)
|
||||||
|
|
||||||
|
if s.network.Addr == "" {
|
||||||
|
return errors.New("addr not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
s.ctx = ctx
|
||||||
|
s.cancel = cancel
|
||||||
|
|
||||||
|
dialer := net.Dialer{
|
||||||
|
Timeout: connectTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
var netConn net.Conn
|
||||||
|
var err error
|
||||||
|
|
||||||
|
addr := s.network.Addr
|
||||||
|
|
||||||
|
// decide to use SSL or not
|
||||||
|
if s.network.TLS {
|
||||||
|
tlsConf := &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
netConn, err = dialer.DialContext(s.ctx, "tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed to dial %v", addr)
|
||||||
|
return fmt.Errorf("failed to dial %q: %v", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
netConn = tls.Client(netConn, tlsConf)
|
||||||
|
s.conn = netConn
|
||||||
|
} else {
|
||||||
|
netConn, err = dialer.DialContext(s.ctx, "tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed to dial %v", addr)
|
||||||
|
return fmt.Errorf("failed to dial %q: %v", addr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s.conn = netConn
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msgf("Connected to: %v", addr)
|
||||||
|
|
||||||
|
config := irc.ClientConfig{
|
||||||
|
Nick: s.network.Nick,
|
||||||
|
User: s.network.Nick,
|
||||||
|
Name: s.network.Nick,
|
||||||
|
Pass: s.network.Pass,
|
||||||
|
Handler: irc.HandlerFunc(func(c *irc.Client, m *irc.Message) {
|
||||||
|
switch m.Command {
|
||||||
|
case "001":
|
||||||
|
// 001 is a welcome event, so we join channels there
|
||||||
|
err := s.onConnect(c, s.network.Channels)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("error joining channels %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case "366":
|
||||||
|
// TODO: handle joined
|
||||||
|
log.Debug().Msgf("JOINED: %v", m)
|
||||||
|
|
||||||
|
case "433":
|
||||||
|
// TODO: handle nick in use
|
||||||
|
log.Debug().Msgf("NICK IN USE: %v", m)
|
||||||
|
|
||||||
|
case "448", "475", "477":
|
||||||
|
// TODO: handle join failed
|
||||||
|
log.Debug().Msgf("JOIN FAILED: %v", m)
|
||||||
|
|
||||||
|
case "KICK":
|
||||||
|
log.Debug().Msgf("KICK: %v", m)
|
||||||
|
|
||||||
|
case "MODE":
|
||||||
|
// TODO: handle mode change
|
||||||
|
log.Debug().Msgf("MODE CHANGE: %v", m)
|
||||||
|
|
||||||
|
case "INVITE":
|
||||||
|
// TODO: handle invite
|
||||||
|
log.Debug().Msgf("INVITE: %v", m)
|
||||||
|
|
||||||
|
case "PART":
|
||||||
|
// TODO: handle parted
|
||||||
|
log.Debug().Msgf("PART: %v", m)
|
||||||
|
|
||||||
|
case "PRIVMSG":
|
||||||
|
err := s.onMessage(m)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("error on message %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the client
|
||||||
|
client := irc.NewClient(s.conn, config)
|
||||||
|
|
||||||
|
// Connect
|
||||||
|
err = client.RunContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not connect to %v", addr)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handler) GetNetwork() *domain.IrcNetwork {
|
||||||
|
return s.network
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handler) Stop() {
|
||||||
|
s.cancel()
|
||||||
|
|
||||||
|
//if !s.isStopped() {
|
||||||
|
// close(s.stopped)
|
||||||
|
//}
|
||||||
|
|
||||||
|
if s.conn != nil {
|
||||||
|
s.conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handler) isStopped() bool {
|
||||||
|
select {
|
||||||
|
case <-s.stopped:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handler) onConnect(client *irc.Client, channels []domain.IrcChannel) error {
|
||||||
|
// TODO check commands like nickserv before joining
|
||||||
|
|
||||||
|
for _, command := range s.network.ConnectCommands {
|
||||||
|
cmd := strings.TrimLeft(command, "/")
|
||||||
|
|
||||||
|
log.Info().Msgf("send connect command: %v to network: %s", cmd, s.network.Name)
|
||||||
|
|
||||||
|
err := client.Write(cmd)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error sending connect command %v to network: %v", command, s.network.Name)
|
||||||
|
continue
|
||||||
|
//return err
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ch := range channels {
|
||||||
|
myChan := fmt.Sprintf("JOIN %s", ch.Name)
|
||||||
|
|
||||||
|
// handle channel password
|
||||||
|
if ch.Password != "" {
|
||||||
|
myChan = fmt.Sprintf("JOIN %s %s", ch.Name, ch.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := client.Write(myChan)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error joining channel: %v", ch.Name)
|
||||||
|
continue
|
||||||
|
//return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info().Msgf("Monitoring channel %s", ch.Name)
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handler) OnJoin(msg string) (interface{}, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Handler) onMessage(msg *irc.Message) error {
|
||||||
|
log.Debug().Msgf("msg: %v", msg)
|
||||||
|
|
||||||
|
// parse announce
|
||||||
|
channel := &msg.Params[0]
|
||||||
|
announcer := &msg.Name
|
||||||
|
message := msg.Trailing()
|
||||||
|
// TODO add network
|
||||||
|
|
||||||
|
// add correlationID and tracing
|
||||||
|
|
||||||
|
announceID := fmt.Sprintf("%v:%v:%v", s.network.Addr, *channel, *announcer)
|
||||||
|
|
||||||
|
// clean message
|
||||||
|
cleanedMsg := cleanMessage(message)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := s.announceService.Parse(announceID, cleanedMsg)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not parse line: %v", cleanedMsg)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// irc line can contain lots of extra stuff like color so lets clean that
|
||||||
|
func cleanMessage(message string) string {
|
||||||
|
var regexMessageClean = `\x0f|\x1f|\x02|\x03(?:[\d]{1,2}(?:,[\d]{1,2})?)?`
|
||||||
|
|
||||||
|
rxp, err := regexp.Compile(regexMessageClean)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error compiling regex: %v", regexMessageClean)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return rxp.ReplaceAllString(message, "")
|
||||||
|
}
|
221
internal/irc/service.go
Normal file
221
internal/irc/service.go
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
package irc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/announce"
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
StartHandlers()
|
||||||
|
StopNetwork(name string) error
|
||||||
|
ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error)
|
||||||
|
GetNetworkByID(id int64) (*domain.IrcNetwork, error)
|
||||||
|
DeleteNetwork(ctx context.Context, id int64) error
|
||||||
|
StoreNetwork(network *domain.IrcNetwork) error
|
||||||
|
StoreChannel(networkID int64, channel *domain.IrcChannel) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
repo domain.IrcRepo
|
||||||
|
announceService announce.Service
|
||||||
|
indexerMap map[string]string
|
||||||
|
handlers map[string]*Handler
|
||||||
|
|
||||||
|
stopWG sync.WaitGroup
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(repo domain.IrcRepo, announceService announce.Service) Service {
|
||||||
|
return &service{
|
||||||
|
repo: repo,
|
||||||
|
announceService: announceService,
|
||||||
|
handlers: make(map[string]*Handler),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) StartHandlers() {
|
||||||
|
networks, err := s.repo.ListNetworks(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("failed to list networks: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, network := range networks {
|
||||||
|
if !network.Enabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if already in handlers
|
||||||
|
//v, ok := s.handlers[network.Name]
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
channels, err := s.repo.ListChannels(network.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed to list channels for network %q", network.Addr)
|
||||||
|
}
|
||||||
|
network.Channels = channels
|
||||||
|
|
||||||
|
handler := NewHandler(network, s.announceService)
|
||||||
|
|
||||||
|
s.handlers[network.Name] = handler
|
||||||
|
s.lock.Unlock()
|
||||||
|
|
||||||
|
log.Debug().Msgf("starting network: %+v", network.Name)
|
||||||
|
|
||||||
|
s.stopWG.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := handler.Run(); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed to start handler for network %q", network.Name)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.stopWG.Done()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) startNetwork(network domain.IrcNetwork) error {
|
||||||
|
// look if we have the network in handlers already, if so start it
|
||||||
|
if handler, found := s.handlers[network.Name]; found {
|
||||||
|
log.Debug().Msgf("starting network: %+v", network.Name)
|
||||||
|
|
||||||
|
if handler.conn != nil {
|
||||||
|
go func() {
|
||||||
|
if err := handler.Run(); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed to start handler for network %q", handler.network.Name)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// if not found in handlers, lets add it and run it
|
||||||
|
|
||||||
|
handler := NewHandler(network, s.announceService)
|
||||||
|
|
||||||
|
s.lock.Lock()
|
||||||
|
s.handlers[network.Name] = handler
|
||||||
|
s.lock.Unlock()
|
||||||
|
|
||||||
|
log.Debug().Msgf("starting network: %+v", network.Name)
|
||||||
|
|
||||||
|
s.stopWG.Add(1)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
if err := handler.Run(); err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed to start handler for network %q", network.Name)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.stopWG.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) StopNetwork(name string) error {
|
||||||
|
if handler, found := s.handlers[name]; found {
|
||||||
|
handler.Stop()
|
||||||
|
log.Debug().Msgf("stopped network: %+v", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) GetNetworkByID(id int64) (*domain.IrcNetwork, error) {
|
||||||
|
network, err := s.repo.GetNetworkByID(id)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed to get network: %v", id)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
channels, err := s.repo.ListChannels(network.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed to list channels for network %q", network.Addr)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
network.Channels = append(network.Channels, channels...)
|
||||||
|
|
||||||
|
return network, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) ListNetworks(ctx context.Context) ([]domain.IrcNetwork, error) {
|
||||||
|
networks, err := s.repo.ListNetworks(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("failed to list networks: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var ret []domain.IrcNetwork
|
||||||
|
|
||||||
|
for _, n := range networks {
|
||||||
|
channels, err := s.repo.ListChannels(n.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Msgf("failed to list channels for network %q: %v", n.Addr, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
n.Channels = append(n.Channels, channels...)
|
||||||
|
|
||||||
|
ret = append(ret, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) DeleteNetwork(ctx context.Context, id int64) error {
|
||||||
|
if err := s.repo.DeleteNetwork(ctx, id); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("delete network: %+v", id)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) StoreNetwork(network *domain.IrcNetwork) error {
|
||||||
|
if err := s.repo.StoreNetwork(network); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("store network: %+v", network)
|
||||||
|
|
||||||
|
if network.Channels != nil {
|
||||||
|
for _, channel := range network.Channels {
|
||||||
|
if err := s.repo.StoreChannel(network.ID, &channel); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop or start network
|
||||||
|
if !network.Enabled {
|
||||||
|
log.Debug().Msgf("stopping network: %+v", network.Name)
|
||||||
|
|
||||||
|
err := s.StopNetwork(network.Name)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not stop network: %+v", network.Name)
|
||||||
|
return fmt.Errorf("could not stop network: %v", network.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Debug().Msgf("starting network: %+v", network.Name)
|
||||||
|
|
||||||
|
err := s.startNetwork(*network)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not start network: %+v", network.Name)
|
||||||
|
return fmt.Errorf("could not start network: %v", network.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) StoreChannel(networkID int64, channel *domain.IrcChannel) error {
|
||||||
|
if err := s.repo.StoreChannel(networkID, channel); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
52
internal/logger/logger.go
Normal file
52
internal/logger/logger.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/config"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Setup(cfg config.Cfg) {
|
||||||
|
zerolog.TimeFieldFormat = time.RFC3339
|
||||||
|
|
||||||
|
switch cfg.LogLevel {
|
||||||
|
case "INFO":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||||
|
case "DEBUG":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
|
case "ERROR":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
case "WARN":
|
||||||
|
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||||
|
default:
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// setup console writer
|
||||||
|
consoleWriter := zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339}
|
||||||
|
|
||||||
|
writers := io.MultiWriter(consoleWriter)
|
||||||
|
|
||||||
|
// if logPath set create file writer
|
||||||
|
if cfg.LogPath != "" {
|
||||||
|
fileWriter := &lumberjack.Logger{
|
||||||
|
Filename: cfg.LogPath,
|
||||||
|
MaxSize: 100, // megabytes
|
||||||
|
MaxBackups: 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
// overwrite writers
|
||||||
|
writers = io.MultiWriter(consoleWriter, fileWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Logger = log.Output(writers)
|
||||||
|
|
||||||
|
log.Print("Starting autobrr")
|
||||||
|
log.Printf("Log-level: %v", cfg.LogLevel)
|
||||||
|
}
|
79
internal/release/process.go
Normal file
79
internal/release/process.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package release
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/action"
|
||||||
|
"github.com/autobrr/autobrr/internal/client"
|
||||||
|
"github.com/autobrr/autobrr/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
Process(announce domain.Announce) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
actionSvc action.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(actionService action.Service) Service {
|
||||||
|
return &service{actionSvc: actionService}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) Process(announce domain.Announce) error {
|
||||||
|
log.Debug().Msgf("start to process release: %+v", announce)
|
||||||
|
|
||||||
|
if announce.Filter.Actions == nil {
|
||||||
|
return fmt.Errorf("no actions for filter: %v", announce.Filter.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check can download
|
||||||
|
// smart episode?
|
||||||
|
// check against rules like active downloading torrents
|
||||||
|
|
||||||
|
// create http client
|
||||||
|
c := client.NewHttpClient()
|
||||||
|
|
||||||
|
// download torrent file
|
||||||
|
// TODO check extra headers, cookie
|
||||||
|
res, err := c.DownloadFile(announce.TorrentUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("could not download file: %v", announce.TorrentName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.FileName == "" {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
//log.Debug().Msgf("downloaded torrent file: %v", res.FileName)
|
||||||
|
|
||||||
|
// onTorrentDownloaded
|
||||||
|
|
||||||
|
// match more filters like torrent size
|
||||||
|
|
||||||
|
// Get meta info from file to find out the hash for later use
|
||||||
|
meta, err := metainfo.LoadFromFile(res.FileName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("metainfo could not open file: %v", res.FileName)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// torrent info hash used for re-announce
|
||||||
|
hash := meta.HashInfoBytes().String()
|
||||||
|
|
||||||
|
// take action (watchFolder, test, runProgram, qBittorrent, Deluge etc)
|
||||||
|
// actionService
|
||||||
|
err = s.actionSvc.RunActions(res.FileName, hash, *announce.Filter)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("error running actions for filter: %v", announce.Filter.Name)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// safe to delete tmp file
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
43
internal/server/server.go
Normal file
43
internal/server/server.go
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"github.com/autobrr/autobrr/internal/indexer"
|
||||||
|
"github.com/autobrr/autobrr/internal/irc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
Hostname string
|
||||||
|
Port int
|
||||||
|
|
||||||
|
indexerService indexer.Service
|
||||||
|
ircService irc.Service
|
||||||
|
|
||||||
|
stopWG sync.WaitGroup
|
||||||
|
lock sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer(ircSvc irc.Service, indexerSvc indexer.Service) *Server {
|
||||||
|
return &Server{
|
||||||
|
indexerService: indexerSvc,
|
||||||
|
ircService: ircSvc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() error {
|
||||||
|
log.Info().Msgf("Starting server. Listening on %v:%v", s.Hostname, s.Port)
|
||||||
|
|
||||||
|
// instantiate indexers
|
||||||
|
err := s.indexerService.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// instantiate and start irc networks
|
||||||
|
s.ircService.StartHandlers()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
12
internal/utils/strings.go
Normal file
12
internal/utils/strings.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package utils
|
||||||
|
|
||||||
|
// StrSliceContains check if slice contains string
|
||||||
|
func StrSliceContains(s []string, str string) bool {
|
||||||
|
for _, v := range s {
|
||||||
|
if v == str {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
176
pkg/qbittorrent/client.go
Normal file
176
pkg/qbittorrent/client.go
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
package qbittorrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/cookiejar"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
|
||||||
|
"golang.org/x/net/publicsuffix"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
settings Settings
|
||||||
|
http *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
Hostname string
|
||||||
|
Port uint
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
SSL bool
|
||||||
|
protocol string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(s Settings) *Client {
|
||||||
|
jarOptions := &cookiejar.Options{PublicSuffixList: publicsuffix.List}
|
||||||
|
//store cookies in jar
|
||||||
|
jar, err := cookiejar.New(jarOptions)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("new client cookie error")
|
||||||
|
}
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: time.Second * 10,
|
||||||
|
Jar: jar,
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &Client{
|
||||||
|
settings: s,
|
||||||
|
http: httpClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
c.settings.protocol = "http"
|
||||||
|
if c.settings.SSL {
|
||||||
|
c.settings.protocol = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) get(endpoint string, opts map[string]string) (*http.Response, error) {
|
||||||
|
reqUrl := fmt.Sprintf("%v://%v:%v/api/v2/%v", c.settings.protocol, c.settings.Hostname, c.settings.Port, endpoint)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", reqUrl, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("GET: error %v", reqUrl)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.http.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("GET: do %v", reqUrl)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) post(endpoint string, opts map[string]string) (*http.Response, error) {
|
||||||
|
// add optional parameters that the user wants
|
||||||
|
form := url.Values{}
|
||||||
|
if opts != nil {
|
||||||
|
for k, v := range opts {
|
||||||
|
form.Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reqUrl := fmt.Sprintf("%v://%v:%v/api/v2/%v", c.settings.protocol, c.settings.Hostname, c.settings.Port, endpoint)
|
||||||
|
req, err := http.NewRequest("POST", reqUrl, strings.NewReader(form.Encode()))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("POST: req %v", reqUrl)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add the content-type so qbittorrent knows what to expect
|
||||||
|
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
|
||||||
|
resp, err := c.http.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("POST: do %v", reqUrl)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) postFile(endpoint string, fileName string, opts map[string]string) (*http.Response, error) {
|
||||||
|
file, err := os.Open(fileName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("POST file: opening file %v", fileName)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Close the file later
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Buffer to store our request body as bytes
|
||||||
|
var requestBody bytes.Buffer
|
||||||
|
|
||||||
|
// Store a multipart writer
|
||||||
|
multiPartWriter := multipart.NewWriter(&requestBody)
|
||||||
|
|
||||||
|
// Initialize file field
|
||||||
|
fileWriter, err := multiPartWriter.CreateFormFile("torrents", fileName)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("POST file: initializing file field %v", fileName)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy the actual file content to the fields writer
|
||||||
|
_, err = io.Copy(fileWriter, file)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("POST file: could not copy file to writer %v", fileName)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate other fields
|
||||||
|
if opts != nil {
|
||||||
|
for key, val := range opts {
|
||||||
|
fieldWriter, err := multiPartWriter.CreateFormField(key)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("POST file: could not add other fields %v", fileName)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = fieldWriter.Write([]byte(val))
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("POST file: could not write field %v", fileName)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close multipart writer
|
||||||
|
multiPartWriter.Close()
|
||||||
|
|
||||||
|
reqUrl := fmt.Sprintf("%v://%v:%v/api/v2/%v", c.settings.protocol, c.settings.Hostname, c.settings.Port, endpoint)
|
||||||
|
req, err := http.NewRequest("POST", reqUrl, &requestBody)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("POST file: could not create request object %v", fileName)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set correct content type
|
||||||
|
req.Header.Set("Content-Type", multiPartWriter.FormDataContentType())
|
||||||
|
|
||||||
|
res, err := c.http.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("POST file: could not perform request %v", fileName)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) setCookies(cookies []*http.Cookie) {
|
||||||
|
cookieURL, _ := url.Parse(fmt.Sprintf("%v://%v:%v", c.settings.protocol, c.settings.Hostname, c.settings.Port))
|
||||||
|
c.http.Jar.SetCookies(cookieURL, cookies)
|
||||||
|
}
|
179
pkg/qbittorrent/domain.go
Normal file
179
pkg/qbittorrent/domain.go
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
package qbittorrent
|
||||||
|
|
||||||
|
type Torrent struct {
|
||||||
|
AddedOn int `json:"added_on"`
|
||||||
|
AmountLeft int `json:"amount_left"`
|
||||||
|
AutoManaged bool `json:"auto_tmm"`
|
||||||
|
Availability float32 `json:"availability"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Completed int `json:"completed"`
|
||||||
|
CompletionOn int `json:"completion_on"`
|
||||||
|
DlLimit int `json:"dl_limit"`
|
||||||
|
DlSpeed int `json:"dl_speed"`
|
||||||
|
Downloaded int `json:"downloaded"`
|
||||||
|
DownloadedSession int `json:"downloaded_session"`
|
||||||
|
ETA int `json:"eta"`
|
||||||
|
FirstLastPiecePrio bool `json:"f_l_piece_prio"`
|
||||||
|
ForceStart bool `json:"force_start"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
LastActivity int `json:"last_activity"`
|
||||||
|
MagnetURI string `json:"magnet_uri"`
|
||||||
|
MaxRatio float32 `json:"max_ratio"`
|
||||||
|
MaxSeedingTime int `json:"max_seeding_time"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
NumComplete int `json:"num_complete"`
|
||||||
|
NumIncomplete int `json:"num_incomplete"`
|
||||||
|
NumSeeds int `json:"num_seeds"`
|
||||||
|
Priority int `json:"priority"`
|
||||||
|
Progress float32 `json:"progress"`
|
||||||
|
Ratio float32 `json:"ratio"`
|
||||||
|
RatioLimit float32 `json:"ratio_limit"`
|
||||||
|
SavePath string `json:"save_path"`
|
||||||
|
SeedingTimeLimit int `json:"seeding_time_limit"`
|
||||||
|
SeenComplete int `json:"seen_complete"`
|
||||||
|
SequentialDownload bool `json:"seq_dl"`
|
||||||
|
Size int `json:"size"`
|
||||||
|
State TorrentState `json:"state"`
|
||||||
|
SuperSeeding bool `json:"super_seeding"`
|
||||||
|
Tags string `json:"tags"`
|
||||||
|
TimeActive int `json:"time_active"`
|
||||||
|
TotalSize int `json:"total_size"`
|
||||||
|
Tracker *string `json:"tracker"`
|
||||||
|
UpLimit int `json:"up_limit"`
|
||||||
|
Uploaded int `json:"uploaded"`
|
||||||
|
UploadedSession int `json:"uploaded_session"`
|
||||||
|
UpSpeed int `json:"upspeed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TorrentTrackersResponse struct {
|
||||||
|
Trackers []TorrentTracker `json:"trackers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TorrentTracker struct {
|
||||||
|
//Tier uint `json:"tier"` // can be both empty "" and int
|
||||||
|
Url string `json:"url"`
|
||||||
|
Status TrackerStatus `json:"status"`
|
||||||
|
NumPeers int `json:"num_peers"`
|
||||||
|
NumSeeds int `json:"num_seeds"`
|
||||||
|
NumLeechers int `json:"num_leechers"`
|
||||||
|
NumDownloaded int `json:"num_downloaded"`
|
||||||
|
Message string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TorrentState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Some error occurred, applies to paused torrents
|
||||||
|
TorrentStateError TorrentState = "error"
|
||||||
|
|
||||||
|
// Torrent data files is missing
|
||||||
|
TorrentStateMissingFiles TorrentState = "missingFiles"
|
||||||
|
|
||||||
|
// Torrent is being seeded and data is being transferred
|
||||||
|
TorrentStateUploading TorrentState = "uploading"
|
||||||
|
|
||||||
|
// Torrent is paused and has finished downloading
|
||||||
|
TorrentStatePausedUp TorrentState = "pausedUP"
|
||||||
|
|
||||||
|
// Queuing is enabled and torrent is queued for upload
|
||||||
|
TorrentStateQueuedUp TorrentState = "queuedUP"
|
||||||
|
|
||||||
|
// Torrent is being seeded, but no connection were made
|
||||||
|
TorrentStateStalledUp TorrentState = "stalledUP"
|
||||||
|
|
||||||
|
// Torrent has finished downloading and is being checked
|
||||||
|
TorrentStateCheckingUp TorrentState = "checkingUP"
|
||||||
|
|
||||||
|
// Torrent is forced to uploading and ignore queue limit
|
||||||
|
TorrentStateForcedUp TorrentState = "forcedUP"
|
||||||
|
|
||||||
|
// Torrent is allocating disk space for download
|
||||||
|
TorrentStateAllocating TorrentState = "allocating"
|
||||||
|
|
||||||
|
// Torrent is being downloaded and data is being transferred
|
||||||
|
TorrentStateDownloading TorrentState = "downloading"
|
||||||
|
|
||||||
|
// Torrent has just started downloading and is fetching metadata
|
||||||
|
TorrentStateMetaDl TorrentState = "metaDL"
|
||||||
|
|
||||||
|
// Torrent is paused and has NOT finished downloading
|
||||||
|
TorrentStatePausedDl TorrentState = "pausedDL"
|
||||||
|
|
||||||
|
// Queuing is enabled and torrent is queued for download
|
||||||
|
TorrentStateQueuedDl TorrentState = "queuedDL"
|
||||||
|
|
||||||
|
// Torrent is being downloaded, but no connection were made
|
||||||
|
TorrentStateStalledDl TorrentState = "stalledDL"
|
||||||
|
|
||||||
|
// Same as checkingUP, but torrent has NOT finished downloading
|
||||||
|
TorrentStateCheckingDl TorrentState = "checkingDL"
|
||||||
|
|
||||||
|
// Torrent is forced to downloading to ignore queue limit
|
||||||
|
TorrentStateForceDl TorrentState = "forceDL"
|
||||||
|
|
||||||
|
// Checking resume data on qBt startup
|
||||||
|
TorrentStateCheckingResumeData TorrentState = "checkingResumeData"
|
||||||
|
|
||||||
|
// Torrent is moving to another location
|
||||||
|
TorrentStateMoving TorrentState = "moving"
|
||||||
|
|
||||||
|
// Unknown status
|
||||||
|
TorrentStateUnknown TorrentState = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TorrentFilter string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Torrent is paused
|
||||||
|
TorrentFilterAll TorrentFilter = "all"
|
||||||
|
|
||||||
|
// Torrent is active
|
||||||
|
TorrentFilterActive TorrentFilter = "active"
|
||||||
|
|
||||||
|
// Torrent is inactive
|
||||||
|
TorrentFilterInactive TorrentFilter = "inactive"
|
||||||
|
|
||||||
|
// Torrent is completed
|
||||||
|
TorrentFilterCompleted TorrentFilter = "completed"
|
||||||
|
|
||||||
|
// Torrent is resumed
|
||||||
|
TorrentFilterResumed TorrentFilter = "resumed"
|
||||||
|
|
||||||
|
// Torrent is paused
|
||||||
|
TorrentFilterPaused TorrentFilter = "paused"
|
||||||
|
|
||||||
|
// Torrent is stalled
|
||||||
|
TorrentFilterStalled TorrentFilter = "stalled"
|
||||||
|
|
||||||
|
// Torrent is being seeded and data is being transferred
|
||||||
|
TorrentFilterUploading TorrentFilter = "uploading"
|
||||||
|
|
||||||
|
// Torrent is being seeded, but no connection were made
|
||||||
|
TorrentFilterStalledUploading TorrentFilter = "stalled_uploading"
|
||||||
|
|
||||||
|
// Torrent is being downloaded and data is being transferred
|
||||||
|
TorrentFilterDownloading TorrentFilter = "downloading"
|
||||||
|
|
||||||
|
// Torrent is being downloaded, but no connection were made
|
||||||
|
TorrentFilterStalledDownloading TorrentFilter = "stalled_downloading"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TrackerStatus https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-trackers
|
||||||
|
type TrackerStatus int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// 0 Tracker is disabled (used for DHT, PeX, and LSD)
|
||||||
|
TrackerStatusDisabled TrackerStatus = 0
|
||||||
|
|
||||||
|
// 1 Tracker has not been contacted yet
|
||||||
|
TrackerStatusNotContacted TrackerStatus = 1
|
||||||
|
|
||||||
|
// 2 Tracker has been contacted and is working
|
||||||
|
TrackerStatusOK TrackerStatus = 2
|
||||||
|
|
||||||
|
// 3 Tracker is updating
|
||||||
|
TrackerStatusUpdating TrackerStatus = 3
|
||||||
|
|
||||||
|
// 4 Tracker has been contacted, but it is not working (or doesn't send proper replies)
|
||||||
|
TrackerStatusNotWorking TrackerStatus = 4
|
||||||
|
)
|
222
pkg/qbittorrent/methods.go
Normal file
222
pkg/qbittorrent/methods.go
Normal file
|
@ -0,0 +1,222 @@
|
||||||
|
package qbittorrent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Login https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#authentication
|
||||||
|
func (c *Client) Login() error {
|
||||||
|
credentials := make(map[string]string)
|
||||||
|
credentials["username"] = c.settings.Username
|
||||||
|
credentials["password"] = c.settings.Password
|
||||||
|
|
||||||
|
resp, err := c.post("auth/login", credentials)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("login error")
|
||||||
|
return err
|
||||||
|
} else if resp.StatusCode == http.StatusForbidden {
|
||||||
|
log.Error().Err(err).Msg("User's IP is banned for too many failed login attempts")
|
||||||
|
return err
|
||||||
|
|
||||||
|
} else if resp.StatusCode != http.StatusOK { // check for correct status code
|
||||||
|
log.Error().Err(err).Msg("login bad status error")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
bodyBytes, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bodyString := string(bodyBytes)
|
||||||
|
|
||||||
|
// read output
|
||||||
|
if bodyString == "Fails." {
|
||||||
|
return errors.New("bad credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
// good response == "Ok."
|
||||||
|
|
||||||
|
// place cookies in jar for future requests
|
||||||
|
if cookies := resp.Cookies(); len(cookies) > 0 {
|
||||||
|
c.setCookies(cookies)
|
||||||
|
} else {
|
||||||
|
return errors.New("bad credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetTorrents() ([]Torrent, error) {
|
||||||
|
var torrents []Torrent
|
||||||
|
|
||||||
|
resp, err := c.get("torrents/info", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("get torrents error")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, readErr := ioutil.ReadAll(resp.Body)
|
||||||
|
if readErr != nil {
|
||||||
|
log.Error().Err(err).Msg("get torrents read error")
|
||||||
|
return nil, readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &torrents)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("get torrents unmarshal error")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return torrents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetTorrentsFilter(filter TorrentFilter) ([]Torrent, error) {
|
||||||
|
var torrents []Torrent
|
||||||
|
|
||||||
|
v := url.Values{}
|
||||||
|
v.Add("filter", string(filter))
|
||||||
|
params := v.Encode()
|
||||||
|
|
||||||
|
resp, err := c.get("torrents/info?"+params, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("get filtered torrents error: %v", filter)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, readErr := ioutil.ReadAll(resp.Body)
|
||||||
|
if readErr != nil {
|
||||||
|
log.Error().Err(err).Msgf("get filtered torrents read error: %v", filter)
|
||||||
|
return nil, readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &torrents)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("get filtered torrents unmarshal error: %v", filter)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return torrents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetTorrentsRaw() (string, error) {
|
||||||
|
resp, err := c.get("torrents/info", nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("get torrent trackers raw error")
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
data, _ := ioutil.ReadAll(resp.Body)
|
||||||
|
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetTorrentTrackers(hash string) ([]TorrentTracker, error) {
|
||||||
|
var trackers []TorrentTracker
|
||||||
|
|
||||||
|
params := url.Values{}
|
||||||
|
params.Add("hash", hash)
|
||||||
|
|
||||||
|
p := params.Encode()
|
||||||
|
|
||||||
|
resp, err := c.get("torrents/trackers?"+p, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("get torrent trackers error: %v", hash)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, readErr := ioutil.ReadAll(resp.Body)
|
||||||
|
if readErr != nil {
|
||||||
|
log.Error().Err(err).Msgf("get torrent trackers read error: %v", hash)
|
||||||
|
return nil, readErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &trackers)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("get torrent trackers: %v", hash)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return trackers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTorrentFromFile add new torrent from torrent file
|
||||||
|
func (c *Client) AddTorrentFromFile(file string, options map[string]string) error {
|
||||||
|
|
||||||
|
res, err := c.postFile("torrents/add", file, options)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("add torrents error: %v", file)
|
||||||
|
return err
|
||||||
|
} else if res.StatusCode != http.StatusOK {
|
||||||
|
log.Error().Err(err).Msgf("add torrents bad status: %v", file)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) DeleteTorrents(hashes []string, deleteFiles bool) error {
|
||||||
|
v := url.Values{}
|
||||||
|
|
||||||
|
// Add hashes together with | separator
|
||||||
|
hv := strings.Join(hashes, "|")
|
||||||
|
v.Add("hashes", hv)
|
||||||
|
v.Add("deleteFiles", strconv.FormatBool(deleteFiles))
|
||||||
|
|
||||||
|
encodedHashes := v.Encode()
|
||||||
|
|
||||||
|
resp, err := c.get("torrents/delete?"+encodedHashes, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("delete torrents error: %v", hashes)
|
||||||
|
return err
|
||||||
|
} else if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Error().Err(err).Msgf("delete torrents bad code: %v", hashes)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) ReAnnounceTorrents(hashes []string) error {
|
||||||
|
v := url.Values{}
|
||||||
|
|
||||||
|
// Add hashes together with | separator
|
||||||
|
hv := strings.Join(hashes, "|")
|
||||||
|
v.Add("hashes", hv)
|
||||||
|
|
||||||
|
encodedHashes := v.Encode()
|
||||||
|
|
||||||
|
resp, err := c.get("torrents/reannounce?"+encodedHashes, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msgf("re-announce error: %v", hashes)
|
||||||
|
return err
|
||||||
|
} else if resp.StatusCode != http.StatusOK {
|
||||||
|
log.Error().Err(err).Msgf("re-announce error bad status: %v", hashes)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
100
pkg/releaseinfo/parser.go
Normal file
100
pkg/releaseinfo/parser.go
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
package releaseinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReleaseInfo is the resulting structure returned by Parse
|
||||||
|
type ReleaseInfo struct {
|
||||||
|
Title string
|
||||||
|
Season int
|
||||||
|
Episode int
|
||||||
|
Year int
|
||||||
|
Resolution string
|
||||||
|
Source string
|
||||||
|
Codec string
|
||||||
|
Container string
|
||||||
|
Audio string
|
||||||
|
Group string
|
||||||
|
Region string
|
||||||
|
Extended bool
|
||||||
|
Hardcoded bool
|
||||||
|
Proper bool
|
||||||
|
Repack bool
|
||||||
|
Widescreen bool
|
||||||
|
Website string
|
||||||
|
Language string
|
||||||
|
Sbs string
|
||||||
|
Unrated bool
|
||||||
|
Size string
|
||||||
|
ThreeD bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func setField(tor *ReleaseInfo, field, raw, val string) {
|
||||||
|
ttor := reflect.TypeOf(tor)
|
||||||
|
torV := reflect.ValueOf(tor)
|
||||||
|
field = strings.Title(field)
|
||||||
|
v, _ := ttor.Elem().FieldByName(field)
|
||||||
|
//fmt.Printf(" field=%v, type=%+v, value=%v, raw=%v\n", field, v.Type, val, raw)
|
||||||
|
switch v.Type.Kind() {
|
||||||
|
case reflect.Bool:
|
||||||
|
torV.Elem().FieldByName(field).SetBool(true)
|
||||||
|
case reflect.Int:
|
||||||
|
clean, _ := strconv.ParseInt(val, 10, 64)
|
||||||
|
torV.Elem().FieldByName(field).SetInt(clean)
|
||||||
|
case reflect.Uint:
|
||||||
|
clean, _ := strconv.ParseUint(val, 10, 64)
|
||||||
|
torV.Elem().FieldByName(field).SetUint(clean)
|
||||||
|
case reflect.String:
|
||||||
|
torV.Elem().FieldByName(field).SetString(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse breaks up the given filename in TorrentInfo
|
||||||
|
func Parse(filename string) (*ReleaseInfo, error) {
|
||||||
|
tor := &ReleaseInfo{}
|
||||||
|
//fmt.Printf("filename %q\n", filename)
|
||||||
|
|
||||||
|
var startIndex, endIndex = 0, len(filename)
|
||||||
|
cleanName := strings.Replace(filename, "_", " ", -1)
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
matches := pattern.re.FindAllStringSubmatch(cleanName, -1)
|
||||||
|
if len(matches) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
matchIdx := 0
|
||||||
|
if pattern.last {
|
||||||
|
// Take last occurrence of element.
|
||||||
|
matchIdx = len(matches) - 1
|
||||||
|
}
|
||||||
|
//fmt.Printf(" %s: pattern:%q match:%#v\n", pattern.name, pattern.re, matches[matchIdx])
|
||||||
|
|
||||||
|
index := strings.Index(cleanName, matches[matchIdx][1])
|
||||||
|
if index == 0 {
|
||||||
|
startIndex = len(matches[matchIdx][1])
|
||||||
|
//fmt.Printf(" startIndex moved to %d [%q]\n", startIndex, filename[startIndex:endIndex])
|
||||||
|
} else if index < endIndex {
|
||||||
|
endIndex = index
|
||||||
|
//fmt.Printf(" endIndex moved to %d [%q]\n", endIndex, filename[startIndex:endIndex])
|
||||||
|
}
|
||||||
|
setField(tor, pattern.name, matches[matchIdx][1], matches[matchIdx][2])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start process for title
|
||||||
|
//fmt.Println(" title: <internal>")
|
||||||
|
raw := strings.Split(filename[startIndex:endIndex], "(")[0]
|
||||||
|
cleanName = raw
|
||||||
|
if strings.HasPrefix(cleanName, "- ") {
|
||||||
|
cleanName = raw[2:]
|
||||||
|
}
|
||||||
|
if strings.ContainsRune(cleanName, '.') && !strings.ContainsRune(cleanName, ' ') {
|
||||||
|
cleanName = strings.Replace(cleanName, ".", " ", -1)
|
||||||
|
}
|
||||||
|
cleanName = strings.Replace(cleanName, "_", " ", -1)
|
||||||
|
//cleanName = re.sub('([\[\(_]|- )$', '', cleanName).strip()
|
||||||
|
setField(tor, "title", raw, strings.TrimSpace(cleanName))
|
||||||
|
|
||||||
|
return tor, nil
|
||||||
|
}
|
331
pkg/releaseinfo/parser_test.go
Normal file
331
pkg/releaseinfo/parser_test.go
Normal file
|
@ -0,0 +1,331 @@
|
||||||
|
package releaseinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var updateGoldenFiles = flag.Bool("update", false, "update golden files in testdata/")
|
||||||
|
|
||||||
|
var testData = []string{
|
||||||
|
"The Walking Dead S05E03 720p HDTV x264-ASAP[ettv]",
|
||||||
|
"Hercules (2014) 1080p BrRip H264 - YIFY",
|
||||||
|
"Dawn.of.the.Planet.of.the.Apes.2014.HDRip.XViD-EVO",
|
||||||
|
"The Big Bang Theory S08E06 HDTV XviD-LOL [eztv]",
|
||||||
|
"22 Jump Street (2014) 720p BrRip x264 - YIFY",
|
||||||
|
"Hercules.2014.EXTENDED.1080p.WEB-DL.DD5.1.H264-RARBG",
|
||||||
|
"Hercules.2014.Extended.Cut.HDRip.XViD-juggs[ETRG]",
|
||||||
|
"Hercules (2014) WEBDL DVDRip XviD-MAX",
|
||||||
|
"WWE Hell in a Cell 2014 PPV WEB-DL x264-WD -={SPARROW}=-",
|
||||||
|
"UFC.179.PPV.HDTV.x264-Ebi[rartv]",
|
||||||
|
"Marvels Agents of S H I E L D S02E05 HDTV x264-KILLERS [eztv]",
|
||||||
|
"X-Men.Days.of.Future.Past.2014.1080p.WEB-DL.DD5.1.H264-RARBG",
|
||||||
|
"Guardians Of The Galaxy 2014 R6 720p HDCAM x264-JYK",
|
||||||
|
"Marvel's.Agents.of.S.H.I.E.L.D.S02E01.Shadows.1080p.WEB-DL.DD5.1",
|
||||||
|
"Marvels Agents of S.H.I.E.L.D. S02E06 HDTV x264-KILLERS[ettv]",
|
||||||
|
"Guardians of the Galaxy (CamRip / 2014)",
|
||||||
|
"The.Walking.Dead.S05E03.1080p.WEB-DL.DD5.1.H.264-Cyphanix[rartv]",
|
||||||
|
"Brave.2012.R5.DVDRip.XViD.LiNE-UNiQUE",
|
||||||
|
"Lets.Be.Cops.2014.BRRip.XViD-juggs[ETRG]",
|
||||||
|
"These.Final.Hours.2013.WBBRip XViD",
|
||||||
|
"Downton Abbey 5x06 HDTV x264-FoV [eztv]",
|
||||||
|
"Annabelle.2014.HC.HDRip.XViD.AC3-juggs[ETRG]",
|
||||||
|
"Lucy.2014.HC.HDRip.XViD-juggs[ETRG]",
|
||||||
|
"The Flash 2014 S01E04 HDTV x264-FUM[ettv]",
|
||||||
|
"South Park S18E05 HDTV x264-KILLERS [eztv]",
|
||||||
|
"The Flash 2014 S01E03 HDTV x264-LOL[ettv]",
|
||||||
|
"The Flash 2014 S01E01 HDTV x264-LOL[ettv]",
|
||||||
|
"Lucy 2014 Dual-Audio WEBRip 1400Mb",
|
||||||
|
"Teenage Mutant Ninja Turtles (HdRip / 2014)",
|
||||||
|
"Teenage Mutant Ninja Turtles (unknown_release_type / 2014)",
|
||||||
|
"The Simpsons S26E05 HDTV x264 PROPER-LOL [eztv]",
|
||||||
|
"2047 - Sights of Death (2014) 720p BrRip x264 - YIFY",
|
||||||
|
"Two and a Half Men S12E01 HDTV x264 REPACK-LOL [eztv]",
|
||||||
|
"Dinosaur 13 2014 WEBrip XviD AC3 MiLLENiUM",
|
||||||
|
"Teenage.Mutant.Ninja.Turtles.2014.HDRip.XviD.MP3-RARBG",
|
||||||
|
"Dawn.Of.The.Planet.of.The.Apes.2014.1080p.WEB-DL.DD51.H264-RARBG",
|
||||||
|
"Teenage.Mutant.Ninja.Turtles.2014.720p.HDRip.x264.AC3.5.1-RARBG",
|
||||||
|
"Gotham.S01E05.Viper.WEB-DL.x264.AAC",
|
||||||
|
"Into.The.Storm.2014.1080p.WEB-DL.AAC2.0.H264-RARBG",
|
||||||
|
"Lucy 2014 Dual-Audio 720p WEBRip",
|
||||||
|
"Into The Storm 2014 1080p BRRip x264 DTS-JYK",
|
||||||
|
"Sin.City.A.Dame.to.Kill.For.2014.1080p.BluRay.x264-SPARKS",
|
||||||
|
"WWE Monday Night Raw 3rd Nov 2014 HDTV x264-Sir Paul",
|
||||||
|
"Jack.And.The.Cuckoo-Clock.Heart.2013.BRRip XViD",
|
||||||
|
"WWE Hell in a Cell 2014 HDTV x264 SNHD",
|
||||||
|
"Dracula.Untold.2014.TS.XViD.AC3.MrSeeN-SiMPLE",
|
||||||
|
"The Missing 1x01 Pilot HDTV x264-FoV [eztv]",
|
||||||
|
"Doctor.Who.2005.8x11.Dark.Water.720p.HDTV.x264-FoV[rartv]",
|
||||||
|
"Gotham.S01E07.Penguins.Umbrella.WEB-DL.x264.AAC",
|
||||||
|
"One Shot [2014] DVDRip XViD-ViCKY",
|
||||||
|
"The Shaukeens 2014 Hindi (1CD) DvDScr x264 AAC...Hon3y",
|
||||||
|
"The Shaukeens (2014) 1CD DvDScr Rip x264 [DDR]",
|
||||||
|
"Annabelle.2014.1080p.PROPER.HC.WEBRip.x264.AAC.2.0-RARBG",
|
||||||
|
"Interstellar (2014) CAM ENG x264 AAC-CPG",
|
||||||
|
"Guardians of the Galaxy (2014) Dual Audio DVDRip AVI",
|
||||||
|
"Eliza Graves (2014) Dual Audio WEB-DL 720p MKV x264",
|
||||||
|
"WWE Monday Night Raw 2014 11 10 WS PDTV x264-RKOFAN1990 -={SPARR",
|
||||||
|
"Sons.of.Anarchy.S01E03",
|
||||||
|
"doctor_who_2005.8x12.death_in_heaven.720p_hdtv_x264-fov",
|
||||||
|
"breaking.bad.s01e01.720p.bluray.x264-reward",
|
||||||
|
"Game of Thrones - 4x03 - Breaker of Chains",
|
||||||
|
"[720pMkv.Com]_sons.of.anarchy.s05e10.480p.BluRay.x264-GAnGSteR",
|
||||||
|
"[ www.Speed.cd ] -Sons.of.Anarchy.S07E07.720p.HDTV.X264-DIMENSION",
|
||||||
|
"Community.s02e20.rus.eng.720p.Kybik.v.Kybe",
|
||||||
|
"The.Jungle.Book.2016.3D.1080p.BRRip.SBS.x264.AAC-ETRG",
|
||||||
|
"Ant-Man.2015.3D.1080p.BRRip.Half-SBS.x264.AAC-m2g",
|
||||||
|
"Ice.Age.Collision.Course.2016.READNFO.720p.HDRIP.X264.AC3.TiTAN",
|
||||||
|
"Red.Sonja.Queen.Of.Plagues.2016.BDRip.x264-W4F[PRiME]",
|
||||||
|
"The Purge: Election Year (2016) HC - 720p HDRiP - 900MB - ShAaNi",
|
||||||
|
"War Dogs (2016) HDTS 600MB - NBY",
|
||||||
|
"The Hateful Eight (2015) 720p BluRay - x265 HEVC - 999MB - ShAaN",
|
||||||
|
"The.Boss.2016.UNRATED.720p.BRRip.x264.AAC-ETRG",
|
||||||
|
"Return.To.Snowy.River.1988.iNTERNAL.DVDRip.x264-W4F[PRiME]",
|
||||||
|
"Akira (2016) - UpScaled - 720p - DesiSCR-Rip - Hindi - x264 - AC3 - 5.1 - Mafiaking - M2Tv",
|
||||||
|
"Ben Hur 2016 TELESYNC x264 AC3 MAXPRO",
|
||||||
|
"The.Secret.Life.of.Pets.2016.HDRiP.AAC-LC.x264-LEGi0N",
|
||||||
|
"[HorribleSubs] Clockwork Planet - 10 [480p].mkv",
|
||||||
|
"[HorribleSubs] Detective Conan - 862 [1080p].mkv",
|
||||||
|
"thomas.and.friends.s19e09_s20e14.convert.hdtv.x264-w4f[eztv].mkv",
|
||||||
|
"Blade.Runner.2049.2017.1080p.WEB-DL.DD5.1.H264-FGT-[rarbg.to]",
|
||||||
|
"2012(2009).1080p.Dual Audio(Hindi+English) 5.1 Audios",
|
||||||
|
"2012 (2009) 1080p BrRip x264 - 1.7GB - YIFY",
|
||||||
|
"2012 2009 x264 720p Esub BluRay 6.0 Dual Audio English Hindi GOPISAHI",
|
||||||
|
}
|
||||||
|
|
||||||
|
var moreTestData = []string{
|
||||||
|
"Tokyo Olympics 2020 Street Skateboarding Prelims and Final 25 07 2021 1080p WEB-DL AAC2 0 H 264-playWEB",
|
||||||
|
"Tokyo Olympics 2020 Taekwondo Day3 Finals 26 07 720pEN25fps ES",
|
||||||
|
"Die Freundin der Haie 2021 German DUBBED DL DOKU 1080p WEB x264-WiSHTV",
|
||||||
|
}
|
||||||
|
|
||||||
|
var movieTests = []string{
|
||||||
|
"The Last Letter from Your Lover 2021 2160p NF WEBRip DDP5 1 Atmos x265-KiNGS",
|
||||||
|
"Blade 1998 Hybrid 1080p BluRay REMUX AVC Atmos-EPSiLON",
|
||||||
|
"Forrest Gump 1994 1080p BluRay DDP7 1 x264-Geek",
|
||||||
|
"Deux sous de violettes 1951 1080p Blu-ray Remux AVC FLAC 2 0-EDPH",
|
||||||
|
"Predator 1987 2160p UHD BluRay DTS-HD MA 5 1 HDR x265-W4NK3R",
|
||||||
|
"Final Destination 2 2003 1080p BluRay x264-ETHOS",
|
||||||
|
"Hellboy.II.The.Golden.Army.2008.REMASTERED.NORDiC.1080p.BluRay.x264-PANDEMONiUM",
|
||||||
|
"Wonders of the Sea 2017 BluRay 1080p AVC DTS-HD MA 2.0-BeyondHD",
|
||||||
|
"A Week Away 2021 1080p NF WEB-DL DDP 5.1 Atmos DV H.265-SymBiOTes",
|
||||||
|
"Control 2004 BluRay 1080p DTS-HD MA 5.1 AVC REMUX-FraMeSToR",
|
||||||
|
"Mimi 2021 1080p Hybrid WEB-DL DDP 5.1 x264-Telly",
|
||||||
|
"She's So Lovely 1997 BluRay 1080p DTS-HD MA 5.1 AVC REMUX-FraMeSToR",
|
||||||
|
"Those Who Wish Me Dead 2021 BluRay 1080p DD5.1 x264-BHDStudio",
|
||||||
|
"The Last Letter from Your Lover 2021 2160p NF WEBRip DDP 5.1 Atmos x265-KiNGS",
|
||||||
|
"Spinning Man 2018 BluRay 1080p DTS 5.1 x264-MTeam",
|
||||||
|
"The Wicker Man 1973 Final Cut 1080p BluRay FLAC 1.0 x264-NTb",
|
||||||
|
"New Police Story 2004 720p BluRay DTS x264-HiFi",
|
||||||
|
"La Cienaga 2001 Criterion Collection NTSC DVD9 DD 2.0",
|
||||||
|
"The Thin Blue Line 1988 Criterion Collection NTSC DVD9 DD 2.0",
|
||||||
|
"The Thin Red Line 1998 Criterion Collection NTSC 2xDVD9 DD 5.1",
|
||||||
|
"The Sword of Doom AKA daibosatsu 1966 Criterion Collection NTSC DVD9 DD 1.0",
|
||||||
|
"Freaks 2018 Hybrid REPACK 1080p BluRay REMUX AVC DTS-HD MA 5.1-EPSiLON",
|
||||||
|
"The Oxford Murders 2008 1080p BluRay Remux AVC DTS-HD MA 7.1-Pootis",
|
||||||
|
"Berlin Babylon 2001 PAL DVD9 DD 5.1",
|
||||||
|
"Dillinger 1973 1080p BluRay REMUX AVC DTS-HD MA 1.0-HiDeFZeN",
|
||||||
|
"True Romance 1993 2160p UHD Blu-ray DV HDR HEVC DTS-HD MA 5.1",
|
||||||
|
"Family 2019 1080p AMZN WEB-DL DD+ 5.1 H.264-TEPES",
|
||||||
|
"Family 2019 720p AMZN WEB-DL DD+ 5.1 H.264-TEPES",
|
||||||
|
"The Banana Splits Movie 2019 NTSC DVD9 DD 5.1-(_10_)",
|
||||||
|
"Sex Is Zero AKA saegjeugsigong 2002 720p BluRay DD 5.1 x264-KiR",
|
||||||
|
"Sex Is Zero AKA saegjeugsigong 2002 1080p BluRay DTS 5.1 x264-KiR",
|
||||||
|
"Sex Is Zero AKA saegjeugsigong 2002 1080p KOR Blu-ray AVC DTS-HD MA 5.1-ARiN",
|
||||||
|
"The Stranger AKA aagntuk 1991 Criterion Collection NTSC DVD9 DD 1.0",
|
||||||
|
"The Taking of Power by Louis XIV AKA La prise de pouvoir par Louis XIV 1966 Criterion Collection NTSC DVD9 DD 1.0",
|
||||||
|
"La Cienaga 2001 Criterion Collection NTSC DVD9 DD 2.0",
|
||||||
|
"The Thin Blue Line 1988 Criterion Collection NTSC DVD9 DD 2.0",
|
||||||
|
"The Thin Red Line 1998 Criterion Collection NTSC 2xDVD9 DD 5.1",
|
||||||
|
"The Sword of Doom AKA daibosatsu 1966 Criterion Collection NTSC DVD9 DD 1.0",
|
||||||
|
"Freaks 2018 Hybrid REPACK 1080p BluRay REMUX AVC DTS-HD MA 5.1-EPSiLON",
|
||||||
|
"The Oxford Murders 2008 1080p BluRay Remux AVC DTS-HD MA 7.1-Pootis",
|
||||||
|
"Berlin Babylon 2001 PAL DVD9 DD 5.1",
|
||||||
|
"Dillinger 1973 1080p BluRay REMUX AVC DTS-HD MA 1.0-HiDeFZeN",
|
||||||
|
"True Romance 1993 2160p UHD Blu-ray DV HDR HEVC DTS-HD MA 5.1",
|
||||||
|
"La Cienaga 2001 Criterion Collection NTSC DVD9 DD 2.0",
|
||||||
|
"Freaks 2018 Hybrid REPACK 1080p BluRay REMUX AVC DTS-HD MA 5.1-EPSiLON",
|
||||||
|
"The Oxford Murders 2008 1080p BluRay Remux AVC DTS-HD MA 7.1-Pootis",
|
||||||
|
}
|
||||||
|
|
||||||
|
//func TestParse_Movies(t *testing.T) {
|
||||||
|
// type args struct {
|
||||||
|
// filename string
|
||||||
|
// }
|
||||||
|
// tests := []struct {
|
||||||
|
// filename string
|
||||||
|
// want *ReleaseInfo
|
||||||
|
// wantErr bool
|
||||||
|
// }{
|
||||||
|
// {filename: "", want: nil, wantErr: false},
|
||||||
|
// }
|
||||||
|
// for _, tt := range tests {
|
||||||
|
// t.Run(tt.filename, func(t *testing.T) {
|
||||||
|
// got, err := Parse(tt.filename)
|
||||||
|
// if (err != nil) != tt.wantErr {
|
||||||
|
// t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
// t.Errorf("Parse() got = %v, want %v", got, tt.want)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
var tvTests = []string{
|
||||||
|
"Melrose Place S04 480p web-dl eac3 x264",
|
||||||
|
"Privileged.S01E17.1080p.WEB.h264-DiRT",
|
||||||
|
"Banshee S02 BluRay 720p DD5.1 x264-NTb",
|
||||||
|
"Banshee S04 BluRay 720p DTS x264-NTb",
|
||||||
|
"Servant S01 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-FLUX",
|
||||||
|
"South Park S06 1080p BluRay DD5.1 x264-W4NK3R",
|
||||||
|
"The Walking Dead: Origins S01E01 1080p WEB-DL DDP 2.0 H.264-GOSSIP",
|
||||||
|
"Mythic Quest S01 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-FLUX",
|
||||||
|
"Masameer County S01 1080p NF WEB-DL DD+ 5.1 H.264-XIQ",
|
||||||
|
"Kevin Can F**K Himself 2021 S01 1080p AMZN WEB-DL DD+ 5.1 H.264-SaiTama",
|
||||||
|
"How to Sell Drugs Online (Fast) S03 1080p NF WEB-DL DD+ 5.1 x264-KnightKing",
|
||||||
|
"Power Book III: Raising Kanan S01E01 2160p WEB-DL DD+ 5.1 H265-GGEZ",
|
||||||
|
"Power Book III: Raising Kanan S01E02 2160p WEB-DL DD+ 5.1 H265-GGWP",
|
||||||
|
"Thea Walking Dead: Origins S01E01 1080p WEB-DL DD+ 2.0 H.264-GOSSIP",
|
||||||
|
"Mean Mums S01 1080p AMZN WEB-DL DD+ 2.0 H.264-FLUX",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse_TV(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
filename string
|
||||||
|
want *ReleaseInfo
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
filename: "Melrose Place S04 480p web-dl eac3 x264",
|
||||||
|
want: &ReleaseInfo{
|
||||||
|
Title: "Melrose Place",
|
||||||
|
Season: 4,
|
||||||
|
Resolution: "480p",
|
||||||
|
Source: "web-dl",
|
||||||
|
Codec: "x264",
|
||||||
|
Group: "dl eac3 x264",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filename: "Privileged.S01E17.1080p.WEB.h264-DiRT",
|
||||||
|
want: &ReleaseInfo{
|
||||||
|
Title: "Privileged",
|
||||||
|
Season: 1,
|
||||||
|
Episode: 17,
|
||||||
|
Resolution: "1080p",
|
||||||
|
Source: "WEB",
|
||||||
|
Codec: "h264",
|
||||||
|
Group: "DiRT",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filename: "Banshee S02 BluRay 720p DD5.1 x264-NTb",
|
||||||
|
want: &ReleaseInfo{
|
||||||
|
Title: "Banshee",
|
||||||
|
Season: 2,
|
||||||
|
Resolution: "720p",
|
||||||
|
Source: "BluRay",
|
||||||
|
Codec: "x264",
|
||||||
|
Audio: "DD5.1",
|
||||||
|
Group: "NTb",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
filename: "Banshee Season 2 BluRay 720p DD5.1 x264-NTb",
|
||||||
|
want: &ReleaseInfo{
|
||||||
|
Title: "Banshee",
|
||||||
|
Season: 2,
|
||||||
|
Resolution: "720p",
|
||||||
|
Source: "BluRay",
|
||||||
|
Codec: "x264",
|
||||||
|
Audio: "DD5.1",
|
||||||
|
Group: "NTb",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.filename, func(t *testing.T) {
|
||||||
|
got, err := Parse(tt.filename)
|
||||||
|
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
//if !reflect.DeepEqual(got, tt.want) {
|
||||||
|
// t.Errorf("Parse() got = %v, want %v", got, tt.want)
|
||||||
|
//}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var gamesTests = []string{
|
||||||
|
"Night Book NSW-LUMA",
|
||||||
|
"Evdeki Lanet-DARKSiDERS",
|
||||||
|
"Evdeki.Lanet-DARKSiDERS",
|
||||||
|
}
|
||||||
|
|
||||||
|
//func TestParser(t *testing.T) {
|
||||||
|
// for i, fname := range testData {
|
||||||
|
// t.Run(fmt.Sprintf("golden_file_%03d", i), func(t *testing.T) {
|
||||||
|
// tor, err := Parse(fname)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatalf("test %v: parser error:\n %v", i, err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var want ReleaseInfo
|
||||||
|
//
|
||||||
|
// if !reflect.DeepEqual(*tor, want) {
|
||||||
|
// t.Fatalf("test %v: wrong result for %q\nwant:\n %v\ngot:\n %v", i, fname, want, *tor)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
//func TestParserWriteToFiles(t *testing.T) {
|
||||||
|
// for i, fname := range testData {
|
||||||
|
// t.Run(fmt.Sprintf("golden_file_%03d", i), func(t *testing.T) {
|
||||||
|
// tor, err := Parse(fname)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatalf("test %v: parser error:\n %v", i, err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// goldenFilename := filepath.Join("testdata", fmt.Sprintf("golden_file_%03d.json", i))
|
||||||
|
//
|
||||||
|
// if *updateGoldenFiles {
|
||||||
|
// buf, err := json.MarshalIndent(tor, "", " ")
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatalf("error marshaling result: %v", err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if err = ioutil.WriteFile(goldenFilename, buf, 0644); err != nil {
|
||||||
|
// t.Fatalf("unable to update golden file: %v", err)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// buf, err := ioutil.ReadFile(goldenFilename)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatalf("error loading golden file: %v", err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// var want ReleaseInfo
|
||||||
|
// err = json.Unmarshal(buf, &want)
|
||||||
|
// if err != nil {
|
||||||
|
// t.Fatalf("error unmarshalling golden file %v: %v", goldenFilename, err)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if !reflect.DeepEqual(*tor, want) {
|
||||||
|
// t.Fatalf("test %v: wrong result for %q\nwant:\n %v\ngot:\n %v", i, fname, want, *tor)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
//}
|
58
pkg/releaseinfo/patterns.go
Normal file
58
pkg/releaseinfo/patterns.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package releaseinfo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var patterns = []struct {
|
||||||
|
name string
|
||||||
|
// Use the last matching pattern. E.g. Year.
|
||||||
|
last bool
|
||||||
|
kind reflect.Kind
|
||||||
|
// REs need to have 2 sub expressions (groups), the first one is "raw", and
|
||||||
|
// the second one for the "clean" value.
|
||||||
|
// E.g. Epiode matching on "S01E18" will result in: raw = "E18", clean = "18".
|
||||||
|
re *regexp.Regexp
|
||||||
|
}{
|
||||||
|
//{"season", false, reflect.Int, regexp.MustCompile(`(?i)(s?([0-9]{1,2}))[ex]`)},
|
||||||
|
{"season", false, reflect.Int, regexp.MustCompile(`(?i)((?:S|Season\s*)(\d{1,3}))`)},
|
||||||
|
{"episode", false, reflect.Int, regexp.MustCompile(`(?i)([ex]([0-9]{2})(?:[^0-9]|$))`)},
|
||||||
|
{"episode", false, reflect.Int, regexp.MustCompile(`(-\s+([0-9]+)(?:[^0-9]|$))`)},
|
||||||
|
{"year", true, reflect.Int, regexp.MustCompile(`\b(((?:19[0-9]|20[0-9])[0-9]))\b`)},
|
||||||
|
|
||||||
|
{"resolution", false, reflect.String, regexp.MustCompile(`\b(([0-9]{3,4}p|i))\b`)},
|
||||||
|
{"source", false, reflect.String, regexp.MustCompile(`(?i)\b(((?:PPV\.)?[HP]DTV|(?:HD)?CAM|B[DR]Rip|(?:HD-?)?TS|(?:PPV )?WEB-?DL(?: DVDRip)?|HDRip|DVDRip|DVDRIP|CamRip|WEB|W[EB]BRip|BluRay|DvDScr|telesync))\b`)},
|
||||||
|
{"codec", false, reflect.String, regexp.MustCompile(`(?i)\b((xvid|HEVC|[hx]\.?26[45]))\b`)},
|
||||||
|
{"container", false, reflect.String, regexp.MustCompile(`(?i)\b((MKV|AVI|MP4))\b`)},
|
||||||
|
|
||||||
|
{"audio", false, reflect.String, regexp.MustCompile(`(?i)\b((MP3|DD5\.?1|Dual[\- ]Audio|LiNE|DTS|AAC[.-]LC|AAC(?:\.?2\.0)?|AC3(?:\.5\.1)?))\b`)},
|
||||||
|
{"region", false, reflect.String, regexp.MustCompile(`(?i)\b(R([0-9]))\b`)},
|
||||||
|
{"size", false, reflect.String, regexp.MustCompile(`(?i)\b((\d+(?:\.\d+)?(?:GB|MB)))\b`)},
|
||||||
|
{"website", false, reflect.String, regexp.MustCompile(`^(\[ ?([^\]]+?) ?\])`)},
|
||||||
|
{"language", false, reflect.String, regexp.MustCompile(`(?i)\b((rus\.eng|ita\.eng))\b`)},
|
||||||
|
{"sbs", false, reflect.String, regexp.MustCompile(`(?i)\b(((?:Half-)?SBS))\b`)},
|
||||||
|
|
||||||
|
{"group", false, reflect.String, regexp.MustCompile(`\b(- ?([^-]+(?:-={[^-]+-?$)?))$`)},
|
||||||
|
|
||||||
|
{"extended", false, reflect.Bool, regexp.MustCompile(`(?i)\b(EXTENDED(:?.CUT)?)\b`)},
|
||||||
|
{"hardcoded", false, reflect.Bool, regexp.MustCompile(`(?i)\b((HC))\b`)},
|
||||||
|
|
||||||
|
{"proper", false, reflect.Bool, regexp.MustCompile(`(?i)\b((PROPER))\b`)},
|
||||||
|
{"repack", false, reflect.Bool, regexp.MustCompile(`(?i)\b((REPACK))\b`)},
|
||||||
|
|
||||||
|
{"widescreen", false, reflect.Bool, regexp.MustCompile(`(?i)\b((WS))\b`)},
|
||||||
|
{"unrated", false, reflect.Bool, regexp.MustCompile(`(?i)\b((UNRATED))\b`)},
|
||||||
|
{"threeD", false, reflect.Bool, regexp.MustCompile(`(?i)\b((3D))\b`)},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
for _, pat := range patterns {
|
||||||
|
if pat.re.NumSubexp() != 2 {
|
||||||
|
fmt.Printf("Pattern %q does not have enough capture groups. want 2, got %d\n", pat.name, pat.re.NumSubexp())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
51
pkg/wildcard/match.go
Normal file
51
pkg/wildcard/match.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package wildcard
|
||||||
|
|
||||||
|
// MatchSimple - finds whether the text matches/satisfies the pattern string.
|
||||||
|
// supports only '*' wildcard in the pattern.
|
||||||
|
// considers a file system path as a flat name space.
|
||||||
|
func MatchSimple(pattern, name string) bool {
|
||||||
|
if pattern == "" {
|
||||||
|
return name == pattern
|
||||||
|
}
|
||||||
|
if pattern == "*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Does only wildcard '*' match.
|
||||||
|
return deepMatchRune([]rune(name), []rune(pattern), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match - finds whether the text matches/satisfies the pattern string.
|
||||||
|
// supports '*' and '?' wildcards in the pattern string.
|
||||||
|
// unlike path.Match(), considers a path as a flat name space while matching the pattern.
|
||||||
|
// The difference is illustrated in the example here https://play.golang.org/p/Ega9qgD4Qz .
|
||||||
|
func Match(pattern, name string) (matched bool) {
|
||||||
|
if pattern == "" {
|
||||||
|
return name == pattern
|
||||||
|
}
|
||||||
|
if pattern == "*" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Does extended wildcard '*' and '?' match.
|
||||||
|
return deepMatchRune([]rune(name), []rune(pattern), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deepMatchRune(str, pattern []rune, simple bool) bool {
|
||||||
|
for len(pattern) > 0 {
|
||||||
|
switch pattern[0] {
|
||||||
|
default:
|
||||||
|
if len(str) == 0 || str[0] != pattern[0] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case '?':
|
||||||
|
if len(str) == 0 && !simple {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case '*':
|
||||||
|
return deepMatchRune(str, pattern[1:], simple) ||
|
||||||
|
(len(str) > 0 && deepMatchRune(str[1:], pattern, simple))
|
||||||
|
}
|
||||||
|
str = str[1:]
|
||||||
|
pattern = pattern[1:]
|
||||||
|
}
|
||||||
|
return len(str) == 0 && len(pattern) == 0
|
||||||
|
}
|
37
pkg/wildcard/match_test.go
Normal file
37
pkg/wildcard/match_test.go
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package wildcard
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
// TestMatch - Tests validate the logic of wild card matching.
|
||||||
|
// `Match` supports '*' and '?' wildcards.
|
||||||
|
// Sample usage: In resource matching for bucket policy validation.
|
||||||
|
func TestMatch(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
pattern string
|
||||||
|
text string
|
||||||
|
matched bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
pattern: "The?Simpsons*",
|
||||||
|
text: "The Simpsons S12",
|
||||||
|
matched: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "The?Simpsons*",
|
||||||
|
text: "The.Simpsons.S12",
|
||||||
|
matched: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: "The?Simpsons*",
|
||||||
|
text: "The.Simps.S12",
|
||||||
|
matched: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
// Iterating over the test cases, call the function under test and asert the output.
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
actualResult := Match(testCase.pattern, testCase.text)
|
||||||
|
if testCase.matched != actualResult {
|
||||||
|
t.Errorf("Test %d: Expected the result to be `%v`, but instead found it to be `%v`", i+1, testCase.matched, actualResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue