mirror of
https://github.com/idanoo/autobrr
synced 2025-07-23 08:49:13 +00:00
feat: add backend
This commit is contained in:
parent
bc418ff248
commit
a838d994a6
68 changed files with 9561 additions and 0 deletions
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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue