diff --git a/cmd/autobrr/main.go b/cmd/autobrr/main.go new file mode 100644 index 0000000..b3870f3 --- /dev/null +++ b/cmd/autobrr/main.go @@ -0,0 +1,109 @@ +package main + +import ( + "database/sql" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/rs/zerolog/log" + "github.com/spf13/pflag" + _ "modernc.org/sqlite" + + "github.com/autobrr/autobrr/internal/action" + "github.com/autobrr/autobrr/internal/announce" + "github.com/autobrr/autobrr/internal/config" + "github.com/autobrr/autobrr/internal/database" + "github.com/autobrr/autobrr/internal/download_client" + "github.com/autobrr/autobrr/internal/filter" + "github.com/autobrr/autobrr/internal/http" + "github.com/autobrr/autobrr/internal/indexer" + "github.com/autobrr/autobrr/internal/irc" + "github.com/autobrr/autobrr/internal/logger" + "github.com/autobrr/autobrr/internal/release" + "github.com/autobrr/autobrr/internal/server" +) + +var ( + cfg config.Cfg +) + +func main() { + var configPath string + pflag.StringVar(&configPath, "config", "", "path to configuration file") + pflag.Parse() + + // read config + cfg = config.Read(configPath) + + // setup logger + logger.Setup(cfg) + + // if configPath is set then put database inside that path, otherwise create wherever it's run + var dataSource = database.DataSourceName(configPath, "autobrr.db") + + // open database connection + db, err := sql.Open("sqlite", dataSource) + if err != nil { + log.Fatal().Err(err).Msg("could not open db connection") + } + defer db.Close() + + if err = database.Migrate(db); err != nil { + log.Fatal().Err(err).Msg("could not migrate db") + } + + // setup repos + // var announceRepo = database.NewAnnounceRepo(db) + var ( + actionRepo = database.NewActionRepo(db) + downloadClientRepo = database.NewDownloadClientRepo(db) + filterRepo = database.NewFilterRepo(db) + indexerRepo = database.NewIndexerRepo(db) + ircRepo = database.NewIrcRepo(db) + ) + + var ( + downloadClientService = download_client.NewService(downloadClientRepo) + actionService = action.NewService(actionRepo, downloadClientService) + indexerService = indexer.NewService(indexerRepo) + filterService = filter.NewService(filterRepo, actionRepo, indexerService) + releaseService = release.NewService(actionService) + announceService = announce.NewService(filterService, indexerService, releaseService) + ircService = irc.NewService(ircRepo, announceService) + ) + + addr := fmt.Sprintf("%v:%v", cfg.Host, cfg.Port) + + errorChannel := make(chan error) + + go func() { + httpServer := http.NewServer(addr, cfg.BaseURL, actionService, downloadClientService, filterService, indexerService, ircService) + errorChannel <- httpServer.Open() + }() + + srv := server.NewServer(ircService, indexerService) + srv.Hostname = cfg.Host + srv.Port = cfg.Port + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) + + if err := srv.Start(); err != nil { + log.Fatal().Err(err).Msg("could not start server") + } + + for sig := range sigCh { + switch sig { + case syscall.SIGHUP: + log.Print("shutting down server") + os.Exit(1) + case syscall.SIGINT, syscall.SIGTERM: + log.Print("shutting down server") + //srv.Shutdown() + os.Exit(1) + return + } + } +} diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..8c94b53 --- /dev/null +++ b/config.toml @@ -0,0 +1,36 @@ +# config.toml + +# Hostname / IP +# +# Default: "localhost" +# +host = "127.0.0.1" + +# Port +# +# Default: 8989 +# +port = 8989 + +# Base url +# Set custom baseUrl eg /autobrr/ to serve in subdirectory. +# Not needed for subdomain, or by accessing with the :port directly. +# +# Optional +# +#baseUrl = "/autobrr/" + +# autobrr logs file +# If not defined, logs to stdout +# +# Optional +# +#logPath = "log/autobrr.log" + +# Log level +# +# Default: "DEBUG" +# +# Options: "ERROR", "DEBUG", "INFO", "WARN" +# +logLevel = "DEBUG" diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6840cba --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module github.com/autobrr/autobrr + +go 1.16 + +require ( + github.com/anacrolix/torrent v1.29.1 + github.com/fluffle/goirc v1.0.3 + github.com/go-chi/chi v1.5.4 + github.com/lib/pq v1.10.2 + github.com/pelletier/go-toml v1.6.0 // indirect + github.com/pkg/errors v0.9.1 + github.com/rs/zerolog v1.20.0 + github.com/smartystreets/assertions v1.0.0 // indirect + github.com/spf13/pflag v1.0.3 + github.com/spf13/viper v1.7.1 + github.com/stretchr/testify v1.7.0 + golang.org/x/net v0.0.0-20210427231257-85d9c07bbe3a + golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect + gopkg.in/irc.v3 v3.1.1 + gopkg.in/natefinch/lumberjack.v2 v2.0.0 + gopkg.in/yaml.v2 v2.4.0 + modernc.org/sqlite v1.12.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..d42f5c6 --- /dev/null +++ b/go.sum @@ -0,0 +1,1053 @@ +bazil.org/fuse v0.0.0-20180421153158-65cc252bf669/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +bazil.org/fuse v0.0.0-20200407214033-5883e5a4b512/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= +crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= +crawshaw.io/sqlite v0.3.3-0.20210127221821-98b1f83c5508/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= +github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= +github.com/RoaringBitmap/roaring v0.4.18/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= +github.com/RoaringBitmap/roaring v0.4.21/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= +github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= +github.com/RoaringBitmap/roaring v0.5.5/go.mod h1:puNo5VdzwbaIQxSiDIwfXl4Hnc+fbovcX4IW/dSTtUk= +github.com/RoaringBitmap/roaring v0.6.0/go.mod h1:WZ83fjBF/7uBHi6QoFyfGL4+xuV4Qn+xFkm4+vSzrhE= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/alangpierce/go-forceexport v0.0.0-20160317203124-8f1d6941cd75/go.mod h1:uAXEEpARkRhCZfEvy/y0Jcc888f9tHCc1W7/UeEtreE= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexflint/go-arg v1.1.0/go.mod h1:3Rj4baqzWaGGmZA2+bVTV8zQOZEjBQAPBnL5xLT+ftY= +github.com/alexflint/go-arg v1.2.0/go.mod h1:3Rj4baqzWaGGmZA2+bVTV8zQOZEjBQAPBnL5xLT+ftY= +github.com/alexflint/go-arg v1.3.0/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM= +github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw= +github.com/anacrolix/chansync v0.0.0-20210524073341-a336ebc2de92/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k= +github.com/anacrolix/confluence v1.7.1-0.20210221224747-9cb14aa2c53a/go.mod h1:T0JHvSaf9UfoiUdCtCOUuRroHm/tauUJTbLc6/vd5YA= +github.com/anacrolix/confluence v1.7.1-0.20210221225853-90405640e928/go.mod h1:NoLcfoRet+kYttjLXJRmh4qBVrylJsfIItik5GGj21A= +github.com/anacrolix/confluence v1.7.1-0.20210311004351-d642adb8546c/go.mod h1:KCZ3eObqKECNeZg0ekAoJVakHMP3gAdR8i0bQ26IkzM= +github.com/anacrolix/dht v0.0.0-20180412060941-24cbf25b72a4 h1:0yHJvFiGQhJ1gSHJOR8xzmnx45orEt7uiIB6guf0+zc= +github.com/anacrolix/dht v0.0.0-20180412060941-24cbf25b72a4/go.mod h1:hQfX2BrtuQsLQMYQwsypFAab/GvHg8qxwVi4OJdR1WI= +github.com/anacrolix/dht/v2 v2.0.1/go.mod h1:GbTT8BaEtfqab/LPd5tY41f3GvYeii3mmDUK300Ycyo= +github.com/anacrolix/dht/v2 v2.2.1-0.20191103020011-1dba080fb358/go.mod h1:d7ARx3WpELh9uOEEr0+8wvQeVTOkPse4UU6dKpv4q0E= +github.com/anacrolix/dht/v2 v2.3.2-0.20200103043204-8dce00767ebd/go.mod h1:cgjKyErDnKS6Mej5D1fEqBKg3KwFF2kpFZJp3L6/fGI= +github.com/anacrolix/dht/v2 v2.5.1-0.20200317023935-129f05e9b752/go.mod h1:7RLvyOjm+ZPA7vgFRP+1eRjFzrh27p/nF0VCk5LcjoU= +github.com/anacrolix/dht/v2 v2.8.0/go.mod h1:RjeKbveVwjnaVj5os4y/NQwqEoDWHigo5rdge9MP52k= +github.com/anacrolix/dht/v2 v2.8.1-0.20210221225335-7a6713a749f9/go.mod h1:p7fLHxqc1mtrFGXfJ226Fo2akG3Pv8ngCTnYAzVJXa4= +github.com/anacrolix/dht/v2 v2.8.1-0.20210311003418-13622df072ae/go.mod h1:wLmYr78fBu4KfUUkFZyGFFwDPDw9EHL5x8c632XCZzs= +github.com/anacrolix/dht/v2 v2.9.1 h1:pVvBJrP4tXu0DkRtkuVpmr1CMnqjcDccVJH0sQNVaH0= +github.com/anacrolix/dht/v2 v2.9.1/go.mod h1:ZyYcIQinN/TE3oKONCchQOLjhYR786Jaxz3jsBtih4A= +github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= +github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= +github.com/anacrolix/envpprof v1.0.1/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= +github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= +github.com/anacrolix/envpprof v1.1.1/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= +github.com/anacrolix/go-libutp v0.0.0-20180522111405-6baeb806518d/go.mod h1:beQSaSxwH2d9Eeu5ijrEnHei5Qhk+J6cDm1QkWFru4E= +github.com/anacrolix/go-libutp v1.0.2/go.mod h1:uIH0A72V++j0D1nnmTjjZUiH/ujPkFxYWkxQ02+7S0U= +github.com/anacrolix/go-libutp v1.0.4/go.mod h1:8vSGX5g0b4eebsDBNVQHUXSCwYaN18Lnkse0hUW8/5w= +github.com/anacrolix/log v0.0.0-20180412014343-2323884b361d/go.mod h1:sf/7c2aTldL6sRQj/4UKyjgVZBu2+M2z9wf7MmwPiew= +github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= +github.com/anacrolix/log v0.3.1-0.20190913000754-831e4ffe0174/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= +github.com/anacrolix/log v0.3.1-0.20191001111012-13cede988bcd/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= +github.com/anacrolix/log v0.4.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= +github.com/anacrolix/log v0.5.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= +github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= +github.com/anacrolix/log v0.6.1-0.20200416071330-f58a030e6149/go.mod h1:s5yBP/j046fm9odtUTbHOfDUq/zh1W8OkPpJtnX0oQI= +github.com/anacrolix/log v0.7.1-0.20200604014615-c244de44fd2d/go.mod h1:s5yBP/j046fm9odtUTbHOfDUq/zh1W8OkPpJtnX0oQI= +github.com/anacrolix/log v0.8.0/go.mod h1:s5yBP/j046fm9odtUTbHOfDUq/zh1W8OkPpJtnX0oQI= +github.com/anacrolix/log v0.9.0/go.mod h1:s5yBP/j046fm9odtUTbHOfDUq/zh1W8OkPpJtnX0oQI= +github.com/anacrolix/missinggo v0.0.0-20180522035225-b4a5853e62ff/go.mod h1:b0p+7cn+rWMIphK1gDH2hrDuwGOcbB6V4VXeSsEfHVk= +github.com/anacrolix/missinggo v0.0.0-20180725070939-60ef2fbf63df/go.mod h1:kwGiTUTZ0+p4vAz3VbAI5a30t2YbvemcmspjKwrAz5s= +github.com/anacrolix/missinggo v0.2.1-0.20190310234110-9fbdc9f242a8/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= +github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= +github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= +github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y= +github.com/anacrolix/missinggo v1.3.0 h1:06HlMsudotL7BAELRZs0yDZ4yVXsHXGi323QBjAVASw= +github.com/anacrolix/missinggo v1.3.0/go.mod h1:bqHm8cE8xr+15uVfMG3BFui/TxyB6//H5fwlq/TeqMc= +github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ= +github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY= +github.com/anacrolix/missinggo/v2 v2.2.1-0.20191103010835-12360f38ced0/go.mod h1:ZzG3/cc3t+5zcYWAgYrJW0MBsSwNwOkTlNquBbP51Bc= +github.com/anacrolix/missinggo/v2 v2.3.0/go.mod h1:ZzG3/cc3t+5zcYWAgYrJW0MBsSwNwOkTlNquBbP51Bc= +github.com/anacrolix/missinggo/v2 v2.3.1/go.mod h1:3XNH0OEmyMUZuvXmYdl+FDfXd0vvSZhvOLy8CFx8tLg= +github.com/anacrolix/missinggo/v2 v2.4.1-0.20200227072623-f02f6484f997/go.mod h1:KY+ij+mWvwGuqSuecLjjPv5LFw5ICUc1UvRems3VAZE= +github.com/anacrolix/missinggo/v2 v2.5.0/go.mod h1:HYuCbwvJXY3XbcmcIcTgZXHleoDXawxPWx/YiPzFzV0= +github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA= +github.com/anacrolix/missinggo/v2 v2.5.2-0.20210623112532-e21e4ddc477d h1:Z0oOzAPar3HzEwU00HnbK5JRe701kyaa+bnBJOaQ5zU= +github.com/anacrolix/missinggo/v2 v2.5.2-0.20210623112532-e21e4ddc477d/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA= +github.com/anacrolix/mmsg v0.0.0-20180515031531-a4a3ba1fc8bb/go.mod h1:x2/ErsYUmT77kezS63+wzZp8E3byYB0gzirM/WMBLfw= +github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc= +github.com/anacrolix/multiless v0.0.0-20191223025854-070b7994e841/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4= +github.com/anacrolix/multiless v0.0.0-20200413040533-acfd16f65d5d/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4= +github.com/anacrolix/multiless v0.0.0-20210222022749-ef43011a77ec/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4= +github.com/anacrolix/multiless v0.1.1-0.20210529082330-de2f6cf29619/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4= +github.com/anacrolix/stm v0.1.0/go.mod h1:ZKz7e7ERWvP0KgL7WXfRjBXHNRhlVRlbBQecqFtPq+A= +github.com/anacrolix/stm v0.1.1-0.20191106051447-e749ba3531cf/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg= +github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg= +github.com/anacrolix/stm v0.2.1-0.20201002073511-c35a2c748c6a/go.mod h1:spImf/rXwiAUoYYJK1YCZeWkpaHZ3kzjGFjwK5OStfU= +github.com/anacrolix/stm v0.2.1-0.20210310231625-45c211559de6/go.mod h1:spImf/rXwiAUoYYJK1YCZeWkpaHZ3kzjGFjwK5OStfU= +github.com/anacrolix/stm v0.3.0-alpha/go.mod h1:spImf/rXwiAUoYYJK1YCZeWkpaHZ3kzjGFjwK5OStfU= +github.com/anacrolix/sync v0.0.0-20171108081538-eee974e4f8c1/go.mod h1:+u91KiUuf0lyILI6x3n/XrW7iFROCZCG+TjgK8nW52w= +github.com/anacrolix/sync v0.0.0-20180611022320-3c4cb11f5a01/go.mod h1:+u91KiUuf0lyILI6x3n/XrW7iFROCZCG+TjgK8nW52w= +github.com/anacrolix/sync v0.0.0-20180808010631-44578de4e778/go.mod h1:s735Etp3joe/voe2sdaXLcqDdJSay1O0OPnM0ystjqk= +github.com/anacrolix/sync v0.2.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g= +github.com/anacrolix/sync v0.3.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DCOj/I0g= +github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= +github.com/anacrolix/tagflag v0.0.0-20180605133421-f477c8c2f14c/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= +github.com/anacrolix/tagflag v0.0.0-20180803105420-3a8ff5428f76/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= +github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= +github.com/anacrolix/tagflag v1.0.1/go.mod h1:gb0fiMQ02qU1djCSqaxGmruMvZGrMwSReidMB0zjdxo= +github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= +github.com/anacrolix/tagflag v1.1.1-0.20200411025953-9bb5209d56c2/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= +github.com/anacrolix/tagflag v1.2.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= +github.com/anacrolix/tagflag v1.3.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= +github.com/anacrolix/torrent v0.0.0-20180622074351-fefeef4ee9eb/go.mod h1:3vcFVxgOASslNXHdivT8spyMRBanMCenHRpe0u5vpBs= +github.com/anacrolix/torrent v1.7.1/go.mod h1:uvOcdpOjjrAq3uMP/u1Ide35f6MJ/o8kMnFG8LV3y6g= +github.com/anacrolix/torrent v1.9.0/go.mod h1:jJJ6lsd2LD1eLHkUwFOhy7I0FcLYH0tHKw2K7ZYMHCs= +github.com/anacrolix/torrent v1.11.0/go.mod h1:FwBai7SyOFlflvfEOaM88ag/jjcBWxTOqD6dVU/lKKA= +github.com/anacrolix/torrent v1.15.0/go.mod h1:MFc6KcbpAyfwGqOyRkdarUK9QnKA/FkVg0usFk1OQxU= +github.com/anacrolix/torrent v1.22.0/go.mod h1:GWTwQkOAilf0LR3C6A74XEkWPg0ejfFD9GcEIe57ess= +github.com/anacrolix/torrent v1.23.0/go.mod h1:737rU+al1LBWEs3IHBystZvsbg24iSP+8Gb25Vc/s5U= +github.com/anacrolix/torrent v1.25.1-0.20210221061757-051093ca31f5/go.mod h1:737rU+al1LBWEs3IHBystZvsbg24iSP+8Gb25Vc/s5U= +github.com/anacrolix/torrent v1.25.1-0.20210224024805-693c30dd889e/go.mod h1:d4V6QqkInfQidWVk8b8hMv8mtciswNitI1A2BiRSQV0= +github.com/anacrolix/torrent v1.29.1 h1:UIoi5RuKGf+Seia0deEDpMmASy8YEGcuNSx9EpzP9xk= +github.com/anacrolix/torrent v1.29.1/go.mod h1:40Hf2bWxFqTbTWbrdig57JnmYLCjShbWWjdbB3VN5n4= +github.com/anacrolix/upnp v0.1.1/go.mod h1:LXsbsp5h+WGN7YR+0A7iVXm5BL1LYryDev1zuJMWYQo= +github.com/anacrolix/upnp v0.1.2-0.20200416075019-5e9378ed1425/go.mod h1:Pz94W3kl8rf+wxH3IbCa9Sq+DTJr8OSbV2Q3/y51vYs= +github.com/anacrolix/utp v0.0.0-20180219060659-9e0e1d1d0572/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk= +github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNaBJ2Dk= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= +github.com/benbjohnson/immutable v0.3.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= +github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= +github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= +github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elliotchance/orderedmap v1.2.0/go.mod h1:8hdSl6jmveQw8ScByd3AaNHNk51RhbTazdqtTty+NFw= +github.com/elliotchance/orderedmap v1.3.0/go.mod h1:8hdSl6jmveQw8ScByd3AaNHNk51RhbTazdqtTty+NFw= +github.com/elliotchance/orderedmap v1.4.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fluffle/goirc v1.0.3 h1:xyH13ALiX3ZkspwAmUZUg82YXmcMrShiZHvzZT5ZGEY= +github.com/fluffle/goirc v1.0.3/go.mod h1:SqQ+D/FJYnNf6btZfM1NsOkESlVw39Q5bvjjRUUQ2Ho= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= +github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/go-unsnap-stream v0.0.0-20210130063903-47dfef350d96/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= +github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.5/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99 h1:twflg0XRTjwKpxb/jFExr4HGq6on2dEOmnL6FV+fgPw= +github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gosuri/uilive v0.0.0-20170323041506-ac356e6e42cd/go.mod h1:qkLSc0A5EXSP6B04TrN4oQoxqFI7A8XvoXSlJi8cwk8= +github.com/gosuri/uilive v0.0.3/go.mod h1:qkLSc0A5EXSP6B04TrN4oQoxqFI7A8XvoXSlJi8cwk8= +github.com/gosuri/uiprogress v0.0.0-20170224063937-d0567a9d84a1/go.mod h1:C1RTYn4Sc7iEyf6j8ft5dyoZ4212h8G1ol9QQluh5+0= +github.com/gosuri/uiprogress v0.0.1/go.mod h1:C1RTYn4Sc7iEyf6j8ft5dyoZ4212h8G1ol9QQluh5+0= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= +github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= +github.com/huandu/xstrings v1.2.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= +github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw= +github.com/lucas-clemente/quic-go v0.18.0/go.mod h1:yXttHsSNxQi8AWijC/vLP+OJczXqzHSOcJrM5ITUlCg= +github.com/lucas-clemente/quic-go v0.19.3/go.mod h1:ADXpNbTQjq1hIzCpB+y/k5iz4n4z4IwqoLb94Kh5Hu8= +github.com/lukechampine/stm v0.0.0-20191022212748-05486c32d236/go.mod h1:wTLsd5FC9rts7GkMpsPGk64CIuea+03yaLAp19Jmlg8= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= +github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/marten-seemann/qpack v0.2.0/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= +github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= +github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk= +github.com/marten-seemann/qtls v0.10.0/go.mod h1:UvMd1oaYDACI99/oZUYLzMCkBXQVT0aGm99sJhbT8hs= +github.com/marten-seemann/qtls-go1-15 v0.1.0/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I= +github.com/marten-seemann/qtls-go1-15 v0.1.1/go.mod h1:GyFwywLKkRt+6mfU99csTEY1joMZz5vmB1WNZH3P81I= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.7.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.13.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mattn/go-sqlite3 v2.0.2+incompatible h1:qzw9c2GNT8UFrgWNDhCTqRqYUSmu/Dav/9Z58LGpk7U= +github.com/mattn/go-sqlite3 v2.0.2+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= +github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.1/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= +github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg= +github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U= +github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I= +github.com/pion/dtls/v2 v2.0.4/go.mod h1:qAkFscX0ZHoI1E07RfYPoRw3manThveu+mlTDdOxoGI= +github.com/pion/dtls/v2 v2.0.7/go.mod h1:QuDII+8FVvk9Dp5t5vYIMTo7hh7uBkra+8QIm7QGm10= +github.com/pion/dtls/v2 v2.0.9/go.mod h1:O0Wr7si/Zj5/EBFlDzDd6UtVxx25CE1r7XM7BQKYQho= +github.com/pion/ice v0.7.18/go.mod h1:+Bvnm3nYC6Nnp7VV6glUkuOfToB/AtMRZpOU8ihuf4c= +github.com/pion/ice/v2 v2.0.15/go.mod h1:ZIiVGevpgAxF/cXiIVmuIUtCb3Xs4gCzCbXB6+nFkSI= +github.com/pion/ice/v2 v2.1.7/go.mod h1:kV4EODVD5ux2z8XncbLHIOtcXKtYXVgLVCeVqnpoeP0= +github.com/pion/interceptor v0.0.9/go.mod h1:dHgEP5dtxOTf21MObuBAjJeAayPxLUAZjerGH8Xr07c= +github.com/pion/interceptor v0.0.12/go.mod h1:qzeuWuD/ZXvPqOnxNcnhWfkCZ2e1kwwslicyyPnhoK4= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= +github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g= +github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k= +github.com/pion/quic v0.1.4/go.mod h1:dBhNvkLoQqRwfi6h3Vqj3IcPLgiW7rkZxBbRdp7Vzvk= +github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= +github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I= +github.com/pion/rtcp v1.2.4/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= +github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= +github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI= +github.com/pion/rtp v1.6.1/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/rtp v1.6.5/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= +github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= +github.com/pion/sctp v1.7.11/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0= +github.com/pion/sctp v1.7.12/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= +github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E= +github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= +github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA= +github.com/pion/srtp v1.5.2/go.mod h1:NiBff/MSxUwMUwx/fRNyD/xGE+dVvf8BOCeXhjCXZ9U= +github.com/pion/srtp/v2 v2.0.1/go.mod h1:c8NWHhhkFf/drmHTAblkdu8++lsISEBBdAuiyxgqIsE= +github.com/pion/srtp/v2 v2.0.2/go.mod h1:VEyLv4CuxrwGY8cxM+Ng3bmVy8ckz/1t6A0q/msKOw0= +github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= +github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= +github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8= +github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= +github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= +github.com/pion/transport v0.12.1/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= +github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= +github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A= +github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog= +github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw= +github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths= +github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= +github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc= +github.com/pion/webrtc/v3 v3.0.11/go.mod h1:WEvXneGTeqNmiR59v5jTsxMc4yXQyOQcRsrdAbNwSEU= +github.com/pion/webrtc/v3 v3.0.27/go.mod h1:QpLDmsU5a/a05n230gRtxZRvfHhFzn9ukGUL2x4G5ic= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk= +github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rs/dnscache v0.0.0-20190621150935-06bb5526f76b/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= +github.com/rs/dnscache v0.0.0-20210201191234-295bba877686/go.mod h1:qe5TWALJ8/a1Lqznoc5BDHpYX/8HU60Hm2AwRmqzxqA= +github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= +github.com/rs/zerolog v1.20.0 h1:38k9hgtUBdxFwE34yS8rTHmHBa4eN16E4DJlv177LNs= +github.com/rs/zerolog v1.20.0/go.mod h1:IzD0RJ65iWH0w97OQQebJEvTZYvsCUm9WVLWBQrJRjo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.0.0 h1:UVQPSSmc3qtTi+zPPkCXvZX9VvW/xT/NsRvKfwY81a8= +github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= +github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= +github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk= +github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/syncthing/syncthing v0.14.48-rc.4/go.mod h1:nw3siZwHPA6M8iSfjDCWQ402eqvEIasMQOE8nFOxy7M= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tinylib/msgp v1.1.1/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= +github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c/go.mod h1:hzIxponao9Kjc7aWznkXaL4U4TWaDSs8zcsY4Ka08nM= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/willf/bloom v0.0.0-20170505221640-54e3b963ee16/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8= +github.com/willf/bloom v2.0.3+incompatible/go.mod h1:MmAltL9pDMNTrvUkxdg0k0q5I0suxmuwp3KbyrZLOZ8= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180524181706-dfa909b99c79/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180926154720-4dfa2610cdf3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190318221613-d196dffd7c2b/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191125084936-ffdde1057850/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210331212208-0fccb6fa2b5c/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210420210106-798c2154c571/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210427231257-85d9c07bbe3a h1:njMmldwFTyDLqonHMagNXKBWptTBeDZOdblgaDsNEGQ= +golang.org/x/net v0.0.0-20210427231257-85d9c07bbe3a/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191126131656-8a8471f7e56d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201214210602-f9fddec55a1e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190828213141-aed303cbaa74/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200928182047-19e03678916f/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/irc.v3 v3.1.1 h1:o7Bq9EvyA0tLI1patP/DkhaxpbGVqaIsdRYijLrQcYc= +gopkg.in/irc.v3 v3.1.1/go.mod h1:shO2gz8+PVeS+4E6GAny88Z0YVVQSxQghdrMVGQsR9s= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +lukechampine.com/uint128 v1.1.1 h1:pnxCASz787iMf+02ssImqk6OLt+Z5QHMoZyUXR4z6JU= +lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.33.6/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/cc/v3 v3.33.7 h1:Rvxffgx6LHSpGS6IO8bffSYN1wpPsWHEWY9CV95vpro= +modernc.org/cc/v3 v3.33.7/go.mod h1:iPJg1pkwXqAV16SNgFBVYmggfMg6xhs+2oiO0vclK3g= +modernc.org/ccgo/v3 v3.9.5/go.mod h1:umuo2EP2oDSBnD3ckjaVUXMrmeAw8C8OSICVa0iFf60= +modernc.org/ccgo/v3 v3.9.6 h1:rCjLgu6iRxK2bqq8A0CCOnDP+tdA81LfbBUlM1L6ZIY= +modernc.org/ccgo/v3 v3.9.6/go.mod h1:KGOi0NhaT6CO19xeSXcpXBl0OkoD6T1U4dPd633G9Sg= +modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= +modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= +modernc.org/libc v1.7.13-0.20210308123627-12f642a52bb8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w= +modernc.org/libc v1.9.11 h1:QUxZMs48Ahg2F7SN41aERvMfGLY2HU/ADnB9DC4Yts8= +modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q= +modernc.org/mathutil v1.1.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/mathutil v1.4.1 h1:ij3fYGe8zBF4Vu+g0oT7mB06r8sqGWKuJu1yXeR4by8= +modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/memory v1.0.4 h1:utMBrFcpnQDdNsmM6asmyH/FM9TqLPS7XF7otpJmrwM= +modernc.org/memory v1.0.4/go.mod h1:nV2OApxradM3/OVbs2/0OsP6nPfakXpi50C7dcoHXlc= +modernc.org/opt v0.1.1 h1:/0RX92k9vwVeDXj+Xn23DKp2VJubL7k8qNffND6qn3A= +modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.12.0 h1:AMAOgk4CkblRJc6YLKSYtz3pZ6DW5wjQ1uYH/rN7/Kk= +modernc.org/sqlite v1.12.0/go.mod h1:ppqJ4cQ+R09YLzl9haEL9AYgj6wX8FcfwDTOI0nYykU= +modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs= +modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= +modernc.org/tcl v1.5.5 h1:N03RwthgTR/l/eQvz3UjfYnvVVj1G2sZqzFGfoD4HE4= +modernc.org/tcl v1.5.5/go.mod h1:ADkaTUuwukkrlhqwERyq0SM8OvyXo7+TjFz7yAF56EI= +modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk= +modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +modernc.org/z v1.0.1 h1:WyIDpEpAIx4Hel6q/Pcgj/VhaQV5XPJ2I6ryIYbjnpc= +modernc.org/z v1.0.1/go.mod h1:8/SRk5C/HgiQWCgXdfpb+1RvhORdkz5sw72d3jjtyqA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/internal/action/service.go b/internal/action/service.go new file mode 100644 index 0000000..9191979 --- /dev/null +++ b/internal/action/service.go @@ -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 +} diff --git a/internal/announce/parse.go b/internal/announce/parse.go new file mode 100644 index 0000000..97109dc --- /dev/null +++ b/internal/announce/parse.go @@ -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((?= 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 +} diff --git a/internal/announce/parse_test.go b/internal/announce/parse_test.go new file mode 100644 index 0000000..dcf89c3 --- /dev/null +++ b/internal/announce/parse_test.go @@ -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: 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: 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: 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: 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: 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) + } + }) + } +} diff --git a/internal/announce/service.go b/internal/announce/service.go new file mode 100644 index 0000000..ec79887 --- /dev/null +++ b/internal/announce/service.go @@ -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 +} diff --git a/internal/client/http.go b/internal/client/http.go new file mode 100644 index 0000000..66899a5 --- /dev/null +++ b/internal/client/http.go @@ -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 +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..2af44bd --- /dev/null +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/database/action.go b/internal/database/action.go new file mode 100644 index 0000000..c290a72 --- /dev/null +++ b/internal/database/action.go @@ -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, + } +} diff --git a/internal/database/announce.go b/internal/database/announce.go new file mode 100644 index 0000000..396824e --- /dev/null +++ b/internal/database/announce.go @@ -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 +} diff --git a/internal/database/download_client.go b/internal/database/download_client.go new file mode 100644 index 0000000..ae1773c --- /dev/null +++ b/internal/database/download_client.go @@ -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 +} diff --git a/internal/database/filter.go b/internal/database/filter.go new file mode 100644 index 0000000..410a600 --- /dev/null +++ b/internal/database/filter.go @@ -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 +} diff --git a/internal/database/indexer.go b/internal/database/indexer.go new file mode 100644 index 0000000..303c91a --- /dev/null +++ b/internal/database/indexer.go @@ -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 +} diff --git a/internal/database/irc.go b/internal/database/irc.go new file mode 100644 index 0000000..fc3c003 --- /dev/null +++ b/internal/database/irc.go @@ -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 +} diff --git a/internal/database/migrate.go b/internal/database/migrate.go new file mode 100644 index 0000000..6683e66 --- /dev/null +++ b/internal/database/migrate.go @@ -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() +} diff --git a/internal/database/utils.go b/internal/database/utils.go new file mode 100644 index 0000000..77cab77 --- /dev/null +++ b/internal/database/utils.go @@ -0,0 +1,13 @@ +package database + +import ( + "path" +) + +func DataSourceName(configPath string, name string) string { + if configPath != "" { + return path.Join(configPath, name) + } + + return name +} diff --git a/internal/database/utils_test.go b/internal/database/utils_test.go new file mode 100644 index 0000000..86a4424 --- /dev/null +++ b/internal/database/utils_test.go @@ -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) + }) + } +} diff --git a/internal/domain/action.go b/internal/domain/action.go new file mode 100644 index 0000000..4085248 --- /dev/null +++ b/internal/domain/action.go @@ -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" +) diff --git a/internal/domain/announce.go b/internal/domain/announce.go new file mode 100644 index 0000000..02e7ca3 --- /dev/null +++ b/internal/domain/announce.go @@ -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 +} diff --git a/internal/domain/client.go b/internal/domain/client.go new file mode 100644 index 0000000..ccf0c03 --- /dev/null +++ b/internal/domain/client.go @@ -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" +) diff --git a/internal/domain/config.go b/internal/domain/config.go new file mode 100644 index 0000000..74df8a2 --- /dev/null +++ b/internal/domain/config.go @@ -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"` +//} diff --git a/internal/domain/filter.go b/internal/domain/filter.go new file mode 100644 index 0000000..335be77 --- /dev/null +++ b/internal/domain/filter.go @@ -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"` +} diff --git a/internal/domain/indexer.go b/internal/domain/indexer.go new file mode 100644 index 0000000..29c1afc --- /dev/null +++ b/internal/domain/indexer.go @@ -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"` +} diff --git a/internal/domain/irc.go b/internal/domain/irc.go new file mode 100644 index 0000000..270c5fd --- /dev/null +++ b/internal/domain/irc.go @@ -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 +} diff --git a/internal/download_client/service.go b/internal/download_client/service.go new file mode 100644 index 0000000..7b6b4c7 --- /dev/null +++ b/internal/download_client/service.go @@ -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 +} diff --git a/internal/filter/service.go b/internal/filter/service.go new file mode 100644 index 0000000..b372ed0 --- /dev/null +++ b/internal/filter/service.go @@ -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 +} diff --git a/internal/filter/service_test.go b/internal/filter/service_test.go new file mode 100644 index 0000000..760f322 --- /dev/null +++ b/internal/filter/service_test.go @@ -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) + }) + } +} diff --git a/internal/http/action.go b/internal/http/action.go new file mode 100644 index 0000000..ae1a04b --- /dev/null +++ b/internal/http/action.go @@ -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) +} diff --git a/internal/http/config.go b/internal/http/config.go new file mode 100644 index 0000000..33624f6 --- /dev/null +++ b/internal/http/config.go @@ -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) +} diff --git a/internal/http/download_client.go b/internal/http/download_client.go new file mode 100644 index 0000000..c8b1348 --- /dev/null +++ b/internal/http/download_client.go @@ -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) +} diff --git a/internal/http/encoder.go b/internal/http/encoder.go new file mode 100644 index 0000000..80eb41a --- /dev/null +++ b/internal/http/encoder.go @@ -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) +} diff --git a/internal/http/filter.go b/internal/http/filter.go new file mode 100644 index 0000000..4f9b420 --- /dev/null +++ b/internal/http/filter.go @@ -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) +} diff --git a/internal/http/indexer.go b/internal/http/indexer.go new file mode 100644 index 0000000..17e21cb --- /dev/null +++ b/internal/http/indexer.go @@ -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) +} diff --git a/internal/http/irc.go b/internal/http/irc.go new file mode 100644 index 0000000..bba1921 --- /dev/null +++ b/internal/http/irc.go @@ -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) +} diff --git a/internal/http/service.go b/internal/http/service.go new file mode 100644 index 0000000..c8f2f81 --- /dev/null +++ b/internal/http/service.go @@ -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) +} diff --git a/internal/indexer/definitions.go b/internal/indexer/definitions.go new file mode 100644 index 0000000..f553542 --- /dev/null +++ b/internal/indexer/definitions.go @@ -0,0 +1,6 @@ +package indexer + +import "embed" + +//go:embed definitions +var Definitions embed.FS diff --git a/internal/indexer/definitions/alpharatio.yaml b/internal/indexer/definitions/alpharatio.yaml new file mode 100644 index 0000000..1d91556 --- /dev/null +++ b/internal/indexer/definitions/alpharatio.yaml @@ -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 }}" diff --git a/internal/indexer/definitions/beyondhd.yaml b/internal/indexer/definitions/beyondhd.yaml new file mode 100644 index 0000000..70db55b --- /dev/null +++ b/internal/indexer/definitions/beyondhd.yaml @@ -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 }}" diff --git a/internal/indexer/definitions/btn.yaml b/internal/indexer/definitions/btn.yaml new file mode 100644 index 0000000..98e2c93 --- /dev/null +++ b/internal/indexer/definitions/btn.yaml @@ -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 }}" diff --git a/internal/indexer/definitions/emp.yaml b/internal/indexer/definitions/emp.yaml new file mode 100644 index 0000000..d92f5a4 --- /dev/null +++ b/internal/indexer/definitions/emp.yaml @@ -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 }}" diff --git a/internal/indexer/definitions/filelist.yaml b/internal/indexer/definitions/filelist.yaml new file mode 100644 index 0000000..90fe79f --- /dev/null +++ b/internal/indexer/definitions/filelist.yaml @@ -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 diff --git a/internal/indexer/definitions/gazellegames.yaml b/internal/indexer/definitions/gazellegames.yaml new file mode 100644 index 0000000..90804b4 --- /dev/null +++ b/internal/indexer/definitions/gazellegames.yaml @@ -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 }}" diff --git a/internal/indexer/definitions/hd-torrents.yaml b/internal/indexer/definitions/hd-torrents.yaml new file mode 100644 index 0000000..cd09575 --- /dev/null +++ b/internal/indexer/definitions/hd-torrents.yaml @@ -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 diff --git a/internal/indexer/definitions/iptorrents.yaml b/internal/indexer/definitions/iptorrents.yaml new file mode 100644 index 0000000..a6b821d --- /dev/null +++ b/internal/indexer/definitions/iptorrents.yaml @@ -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 diff --git a/internal/indexer/definitions/nebulance.yaml b/internal/indexer/definitions/nebulance.yaml new file mode 100644 index 0000000..186b98e --- /dev/null +++ b/internal/indexer/definitions/nebulance.yaml @@ -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 }}" diff --git a/internal/indexer/definitions/orpheus.yaml b/internal/indexer/definitions/orpheus.yaml new file mode 100644 index 0000000..49a71f6 --- /dev/null +++ b/internal/indexer/definitions/orpheus.yaml @@ -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 }}" diff --git a/internal/indexer/definitions/ptp.yaml b/internal/indexer/definitions/ptp.yaml new file mode 100644 index 0000000..abcab69 --- /dev/null +++ b/internal/indexer/definitions/ptp.yaml @@ -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 }}" diff --git a/internal/indexer/definitions/red.yaml b/internal/indexer/definitions/red.yaml new file mode 100644 index 0000000..c0cfe72 --- /dev/null +++ b/internal/indexer/definitions/red.yaml @@ -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 }}" diff --git a/internal/indexer/definitions/superbits.yaml b/internal/indexer/definitions/superbits.yaml new file mode 100644 index 0000000..7c59310 --- /dev/null +++ b/internal/indexer/definitions/superbits.yaml @@ -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 }}" diff --git a/internal/indexer/definitions/torrentleech.yaml b/internal/indexer/definitions/torrentleech.yaml new file mode 100644 index 0000000..c949f53 --- /dev/null +++ b/internal/indexer/definitions/torrentleech.yaml @@ -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: Name:'debian live 10 6 0 amd64 standard iso' uploaded by 'Anonymous' - http://www.tracker01.test/torrent/263302" + - "New Torrent Announcement: 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 + + diff --git a/internal/indexer/definitions/uhdbits.yaml b/internal/indexer/definitions/uhdbits.yaml new file mode 100644 index 0000000..9b16add --- /dev/null +++ b/internal/indexer/definitions/uhdbits.yaml @@ -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 }}" diff --git a/internal/indexer/service.go b/internal/indexer/service.go new file mode 100644 index 0000000..92fc206 --- /dev/null +++ b/internal/indexer/service.go @@ -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 +} diff --git a/internal/irc/handler.go b/internal/irc/handler.go new file mode 100644 index 0000000..bd3033d --- /dev/null +++ b/internal/irc/handler.go @@ -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, "") +} diff --git a/internal/irc/service.go b/internal/irc/service.go new file mode 100644 index 0000000..5f82d6b --- /dev/null +++ b/internal/irc/service.go @@ -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 +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..11ac69d --- /dev/null +++ b/internal/logger/logger.go @@ -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) +} diff --git a/internal/release/process.go b/internal/release/process.go new file mode 100644 index 0000000..4f22ba8 --- /dev/null +++ b/internal/release/process.go @@ -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 +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..f2d4404 --- /dev/null +++ b/internal/server/server.go @@ -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 +} diff --git a/internal/utils/strings.go b/internal/utils/strings.go new file mode 100644 index 0000000..717f4eb --- /dev/null +++ b/internal/utils/strings.go @@ -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 +} diff --git a/pkg/qbittorrent/client.go b/pkg/qbittorrent/client.go new file mode 100644 index 0000000..32b58f1 --- /dev/null +++ b/pkg/qbittorrent/client.go @@ -0,0 +1,176 @@ +package qbittorrent + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "net/http/cookiejar" + "net/url" + "os" + "strings" + "time" + + "github.com/rs/zerolog/log" + + "golang.org/x/net/publicsuffix" +) + +type Client struct { + settings Settings + http *http.Client +} + +type Settings struct { + Hostname string + Port uint + Username string + Password string + SSL bool + protocol string +} + +func NewClient(s Settings) *Client { + jarOptions := &cookiejar.Options{PublicSuffixList: publicsuffix.List} + //store cookies in jar + jar, err := cookiejar.New(jarOptions) + if err != nil { + log.Error().Err(err).Msg("new client cookie error") + } + httpClient := &http.Client{ + Timeout: time.Second * 10, + Jar: jar, + } + + c := &Client{ + settings: s, + http: httpClient, + } + + c.settings.protocol = "http" + if c.settings.SSL { + c.settings.protocol = "https" + } + + return c +} + +func (c *Client) get(endpoint string, opts map[string]string) (*http.Response, error) { + reqUrl := fmt.Sprintf("%v://%v:%v/api/v2/%v", c.settings.protocol, c.settings.Hostname, c.settings.Port, endpoint) + + req, err := http.NewRequest("GET", reqUrl, nil) + if err != nil { + log.Error().Err(err).Msgf("GET: error %v", reqUrl) + return nil, err + } + + resp, err := c.http.Do(req) + if err != nil { + log.Error().Err(err).Msgf("GET: do %v", reqUrl) + return nil, err + } + + return resp, nil +} + +func (c *Client) post(endpoint string, opts map[string]string) (*http.Response, error) { + // add optional parameters that the user wants + form := url.Values{} + if opts != nil { + for k, v := range opts { + form.Add(k, v) + } + } + + reqUrl := fmt.Sprintf("%v://%v:%v/api/v2/%v", c.settings.protocol, c.settings.Hostname, c.settings.Port, endpoint) + req, err := http.NewRequest("POST", reqUrl, strings.NewReader(form.Encode())) + if err != nil { + log.Error().Err(err).Msgf("POST: req %v", reqUrl) + return nil, err + } + + // add the content-type so qbittorrent knows what to expect + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + + resp, err := c.http.Do(req) + if err != nil { + log.Error().Err(err).Msgf("POST: do %v", reqUrl) + return nil, err + } + + return resp, nil +} + +func (c *Client) postFile(endpoint string, fileName string, opts map[string]string) (*http.Response, error) { + file, err := os.Open(fileName) + if err != nil { + log.Error().Err(err).Msgf("POST file: opening file %v", fileName) + return nil, err + } + // Close the file later + defer file.Close() + + // Buffer to store our request body as bytes + var requestBody bytes.Buffer + + // Store a multipart writer + multiPartWriter := multipart.NewWriter(&requestBody) + + // Initialize file field + fileWriter, err := multiPartWriter.CreateFormFile("torrents", fileName) + if err != nil { + log.Error().Err(err).Msgf("POST file: initializing file field %v", fileName) + return nil, err + } + + // Copy the actual file content to the fields writer + _, err = io.Copy(fileWriter, file) + if err != nil { + log.Error().Err(err).Msgf("POST file: could not copy file to writer %v", fileName) + return nil, err + } + + // Populate other fields + if opts != nil { + for key, val := range opts { + fieldWriter, err := multiPartWriter.CreateFormField(key) + if err != nil { + log.Error().Err(err).Msgf("POST file: could not add other fields %v", fileName) + return nil, err + } + + _, err = fieldWriter.Write([]byte(val)) + if err != nil { + log.Error().Err(err).Msgf("POST file: could not write field %v", fileName) + return nil, err + } + } + } + + // Close multipart writer + multiPartWriter.Close() + + reqUrl := fmt.Sprintf("%v://%v:%v/api/v2/%v", c.settings.protocol, c.settings.Hostname, c.settings.Port, endpoint) + req, err := http.NewRequest("POST", reqUrl, &requestBody) + if err != nil { + log.Error().Err(err).Msgf("POST file: could not create request object %v", fileName) + return nil, err + } + + // Set correct content type + req.Header.Set("Content-Type", multiPartWriter.FormDataContentType()) + + res, err := c.http.Do(req) + if err != nil { + log.Error().Err(err).Msgf("POST file: could not perform request %v", fileName) + return nil, err + } + + return res, nil +} + +func (c *Client) setCookies(cookies []*http.Cookie) { + cookieURL, _ := url.Parse(fmt.Sprintf("%v://%v:%v", c.settings.protocol, c.settings.Hostname, c.settings.Port)) + c.http.Jar.SetCookies(cookieURL, cookies) +} diff --git a/pkg/qbittorrent/domain.go b/pkg/qbittorrent/domain.go new file mode 100644 index 0000000..1152564 --- /dev/null +++ b/pkg/qbittorrent/domain.go @@ -0,0 +1,179 @@ +package qbittorrent + +type Torrent struct { + AddedOn int `json:"added_on"` + AmountLeft int `json:"amount_left"` + AutoManaged bool `json:"auto_tmm"` + Availability float32 `json:"availability"` + Category string `json:"category"` + Completed int `json:"completed"` + CompletionOn int `json:"completion_on"` + DlLimit int `json:"dl_limit"` + DlSpeed int `json:"dl_speed"` + Downloaded int `json:"downloaded"` + DownloadedSession int `json:"downloaded_session"` + ETA int `json:"eta"` + FirstLastPiecePrio bool `json:"f_l_piece_prio"` + ForceStart bool `json:"force_start"` + Hash string `json:"hash"` + LastActivity int `json:"last_activity"` + MagnetURI string `json:"magnet_uri"` + MaxRatio float32 `json:"max_ratio"` + MaxSeedingTime int `json:"max_seeding_time"` + Name string `json:"name"` + NumComplete int `json:"num_complete"` + NumIncomplete int `json:"num_incomplete"` + NumSeeds int `json:"num_seeds"` + Priority int `json:"priority"` + Progress float32 `json:"progress"` + Ratio float32 `json:"ratio"` + RatioLimit float32 `json:"ratio_limit"` + SavePath string `json:"save_path"` + SeedingTimeLimit int `json:"seeding_time_limit"` + SeenComplete int `json:"seen_complete"` + SequentialDownload bool `json:"seq_dl"` + Size int `json:"size"` + State TorrentState `json:"state"` + SuperSeeding bool `json:"super_seeding"` + Tags string `json:"tags"` + TimeActive int `json:"time_active"` + TotalSize int `json:"total_size"` + Tracker *string `json:"tracker"` + UpLimit int `json:"up_limit"` + Uploaded int `json:"uploaded"` + UploadedSession int `json:"uploaded_session"` + UpSpeed int `json:"upspeed"` +} + +type TorrentTrackersResponse struct { + Trackers []TorrentTracker `json:"trackers"` +} + +type TorrentTracker struct { + //Tier uint `json:"tier"` // can be both empty "" and int + Url string `json:"url"` + Status TrackerStatus `json:"status"` + NumPeers int `json:"num_peers"` + NumSeeds int `json:"num_seeds"` + NumLeechers int `json:"num_leechers"` + NumDownloaded int `json:"num_downloaded"` + Message string `json:"msg"` +} + +type TorrentState string + +const ( + // Some error occurred, applies to paused torrents + TorrentStateError TorrentState = "error" + + // Torrent data files is missing + TorrentStateMissingFiles TorrentState = "missingFiles" + + // Torrent is being seeded and data is being transferred + TorrentStateUploading TorrentState = "uploading" + + // Torrent is paused and has finished downloading + TorrentStatePausedUp TorrentState = "pausedUP" + + // Queuing is enabled and torrent is queued for upload + TorrentStateQueuedUp TorrentState = "queuedUP" + + // Torrent is being seeded, but no connection were made + TorrentStateStalledUp TorrentState = "stalledUP" + + // Torrent has finished downloading and is being checked + TorrentStateCheckingUp TorrentState = "checkingUP" + + // Torrent is forced to uploading and ignore queue limit + TorrentStateForcedUp TorrentState = "forcedUP" + + // Torrent is allocating disk space for download + TorrentStateAllocating TorrentState = "allocating" + + // Torrent is being downloaded and data is being transferred + TorrentStateDownloading TorrentState = "downloading" + + // Torrent has just started downloading and is fetching metadata + TorrentStateMetaDl TorrentState = "metaDL" + + // Torrent is paused and has NOT finished downloading + TorrentStatePausedDl TorrentState = "pausedDL" + + // Queuing is enabled and torrent is queued for download + TorrentStateQueuedDl TorrentState = "queuedDL" + + // Torrent is being downloaded, but no connection were made + TorrentStateStalledDl TorrentState = "stalledDL" + + // Same as checkingUP, but torrent has NOT finished downloading + TorrentStateCheckingDl TorrentState = "checkingDL" + + // Torrent is forced to downloading to ignore queue limit + TorrentStateForceDl TorrentState = "forceDL" + + // Checking resume data on qBt startup + TorrentStateCheckingResumeData TorrentState = "checkingResumeData" + + // Torrent is moving to another location + TorrentStateMoving TorrentState = "moving" + + // Unknown status + TorrentStateUnknown TorrentState = "unknown" +) + +type TorrentFilter string + +const ( + // Torrent is paused + TorrentFilterAll TorrentFilter = "all" + + // Torrent is active + TorrentFilterActive TorrentFilter = "active" + + // Torrent is inactive + TorrentFilterInactive TorrentFilter = "inactive" + + // Torrent is completed + TorrentFilterCompleted TorrentFilter = "completed" + + // Torrent is resumed + TorrentFilterResumed TorrentFilter = "resumed" + + // Torrent is paused + TorrentFilterPaused TorrentFilter = "paused" + + // Torrent is stalled + TorrentFilterStalled TorrentFilter = "stalled" + + // Torrent is being seeded and data is being transferred + TorrentFilterUploading TorrentFilter = "uploading" + + // Torrent is being seeded, but no connection were made + TorrentFilterStalledUploading TorrentFilter = "stalled_uploading" + + // Torrent is being downloaded and data is being transferred + TorrentFilterDownloading TorrentFilter = "downloading" + + // Torrent is being downloaded, but no connection were made + TorrentFilterStalledDownloading TorrentFilter = "stalled_downloading" +) + +// TrackerStatus https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#get-torrent-trackers +type TrackerStatus int + +const ( + // 0 Tracker is disabled (used for DHT, PeX, and LSD) + TrackerStatusDisabled TrackerStatus = 0 + + // 1 Tracker has not been contacted yet + TrackerStatusNotContacted TrackerStatus = 1 + + // 2 Tracker has been contacted and is working + TrackerStatusOK TrackerStatus = 2 + + // 3 Tracker is updating + TrackerStatusUpdating TrackerStatus = 3 + + // 4 Tracker has been contacted, but it is not working (or doesn't send proper replies) + TrackerStatusNotWorking TrackerStatus = 4 +) diff --git a/pkg/qbittorrent/methods.go b/pkg/qbittorrent/methods.go new file mode 100644 index 0000000..f572285 --- /dev/null +++ b/pkg/qbittorrent/methods.go @@ -0,0 +1,222 @@ +package qbittorrent + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/rs/zerolog/log" +) + +// Login https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#authentication +func (c *Client) Login() error { + credentials := make(map[string]string) + credentials["username"] = c.settings.Username + credentials["password"] = c.settings.Password + + resp, err := c.post("auth/login", credentials) + if err != nil { + log.Error().Err(err).Msg("login error") + return err + } else if resp.StatusCode == http.StatusForbidden { + log.Error().Err(err).Msg("User's IP is banned for too many failed login attempts") + return err + + } else if resp.StatusCode != http.StatusOK { // check for correct status code + log.Error().Err(err).Msg("login bad status error") + return err + } + + defer resp.Body.Close() + + bodyBytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + bodyString := string(bodyBytes) + + // read output + if bodyString == "Fails." { + return errors.New("bad credentials") + } + + // good response == "Ok." + + // place cookies in jar for future requests + if cookies := resp.Cookies(); len(cookies) > 0 { + c.setCookies(cookies) + } else { + return errors.New("bad credentials") + } + + return nil +} + +func (c *Client) GetTorrents() ([]Torrent, error) { + var torrents []Torrent + + resp, err := c.get("torrents/info", nil) + if err != nil { + log.Error().Err(err).Msg("get torrents error") + return nil, err + } + + defer resp.Body.Close() + + body, readErr := ioutil.ReadAll(resp.Body) + if readErr != nil { + log.Error().Err(err).Msg("get torrents read error") + return nil, readErr + } + + err = json.Unmarshal(body, &torrents) + if err != nil { + log.Error().Err(err).Msg("get torrents unmarshal error") + return nil, err + } + + return torrents, nil +} + +func (c *Client) GetTorrentsFilter(filter TorrentFilter) ([]Torrent, error) { + var torrents []Torrent + + v := url.Values{} + v.Add("filter", string(filter)) + params := v.Encode() + + resp, err := c.get("torrents/info?"+params, nil) + if err != nil { + log.Error().Err(err).Msgf("get filtered torrents error: %v", filter) + return nil, err + } + + defer resp.Body.Close() + + body, readErr := ioutil.ReadAll(resp.Body) + if readErr != nil { + log.Error().Err(err).Msgf("get filtered torrents read error: %v", filter) + return nil, readErr + } + + err = json.Unmarshal(body, &torrents) + if err != nil { + log.Error().Err(err).Msgf("get filtered torrents unmarshal error: %v", filter) + return nil, err + } + + return torrents, nil +} + +func (c *Client) GetTorrentsRaw() (string, error) { + resp, err := c.get("torrents/info", nil) + if err != nil { + log.Error().Err(err).Msg("get torrent trackers raw error") + return "", err + } + + defer resp.Body.Close() + + data, _ := ioutil.ReadAll(resp.Body) + + return string(data), nil +} + +func (c *Client) GetTorrentTrackers(hash string) ([]TorrentTracker, error) { + var trackers []TorrentTracker + + params := url.Values{} + params.Add("hash", hash) + + p := params.Encode() + + resp, err := c.get("torrents/trackers?"+p, nil) + if err != nil { + log.Error().Err(err).Msgf("get torrent trackers error: %v", hash) + return nil, err + } + + defer resp.Body.Close() + + body, readErr := ioutil.ReadAll(resp.Body) + if readErr != nil { + log.Error().Err(err).Msgf("get torrent trackers read error: %v", hash) + return nil, readErr + } + + err = json.Unmarshal(body, &trackers) + if err != nil { + log.Error().Err(err).Msgf("get torrent trackers: %v", hash) + return nil, err + } + + return trackers, nil +} + +// AddTorrentFromFile add new torrent from torrent file +func (c *Client) AddTorrentFromFile(file string, options map[string]string) error { + + res, err := c.postFile("torrents/add", file, options) + if err != nil { + log.Error().Err(err).Msgf("add torrents error: %v", file) + return err + } else if res.StatusCode != http.StatusOK { + log.Error().Err(err).Msgf("add torrents bad status: %v", file) + return err + } + + defer res.Body.Close() + + return nil +} + +func (c *Client) DeleteTorrents(hashes []string, deleteFiles bool) error { + v := url.Values{} + + // Add hashes together with | separator + hv := strings.Join(hashes, "|") + v.Add("hashes", hv) + v.Add("deleteFiles", strconv.FormatBool(deleteFiles)) + + encodedHashes := v.Encode() + + resp, err := c.get("torrents/delete?"+encodedHashes, nil) + if err != nil { + log.Error().Err(err).Msgf("delete torrents error: %v", hashes) + return err + } else if resp.StatusCode != http.StatusOK { + log.Error().Err(err).Msgf("delete torrents bad code: %v", hashes) + return err + } + + defer resp.Body.Close() + + return nil +} + +func (c *Client) ReAnnounceTorrents(hashes []string) error { + v := url.Values{} + + // Add hashes together with | separator + hv := strings.Join(hashes, "|") + v.Add("hashes", hv) + + encodedHashes := v.Encode() + + resp, err := c.get("torrents/reannounce?"+encodedHashes, nil) + if err != nil { + log.Error().Err(err).Msgf("re-announce error: %v", hashes) + return err + } else if resp.StatusCode != http.StatusOK { + log.Error().Err(err).Msgf("re-announce error bad status: %v", hashes) + return err + } + + defer resp.Body.Close() + + return nil +} diff --git a/pkg/releaseinfo/parser.go b/pkg/releaseinfo/parser.go new file mode 100644 index 0000000..dba1725 --- /dev/null +++ b/pkg/releaseinfo/parser.go @@ -0,0 +1,100 @@ +package releaseinfo + +import ( + "reflect" + "strconv" + "strings" +) + +// ReleaseInfo is the resulting structure returned by Parse +type ReleaseInfo struct { + Title string + Season int + Episode int + Year int + Resolution string + Source string + Codec string + Container string + Audio string + Group string + Region string + Extended bool + Hardcoded bool + Proper bool + Repack bool + Widescreen bool + Website string + Language string + Sbs string + Unrated bool + Size string + ThreeD bool +} + +func setField(tor *ReleaseInfo, field, raw, val string) { + ttor := reflect.TypeOf(tor) + torV := reflect.ValueOf(tor) + field = strings.Title(field) + v, _ := ttor.Elem().FieldByName(field) + //fmt.Printf(" field=%v, type=%+v, value=%v, raw=%v\n", field, v.Type, val, raw) + switch v.Type.Kind() { + case reflect.Bool: + torV.Elem().FieldByName(field).SetBool(true) + case reflect.Int: + clean, _ := strconv.ParseInt(val, 10, 64) + torV.Elem().FieldByName(field).SetInt(clean) + case reflect.Uint: + clean, _ := strconv.ParseUint(val, 10, 64) + torV.Elem().FieldByName(field).SetUint(clean) + case reflect.String: + torV.Elem().FieldByName(field).SetString(val) + } +} + +// Parse breaks up the given filename in TorrentInfo +func Parse(filename string) (*ReleaseInfo, error) { + tor := &ReleaseInfo{} + //fmt.Printf("filename %q\n", filename) + + var startIndex, endIndex = 0, len(filename) + cleanName := strings.Replace(filename, "_", " ", -1) + for _, pattern := range patterns { + matches := pattern.re.FindAllStringSubmatch(cleanName, -1) + if len(matches) == 0 { + continue + } + matchIdx := 0 + if pattern.last { + // Take last occurrence of element. + matchIdx = len(matches) - 1 + } + //fmt.Printf(" %s: pattern:%q match:%#v\n", pattern.name, pattern.re, matches[matchIdx]) + + index := strings.Index(cleanName, matches[matchIdx][1]) + if index == 0 { + startIndex = len(matches[matchIdx][1]) + //fmt.Printf(" startIndex moved to %d [%q]\n", startIndex, filename[startIndex:endIndex]) + } else if index < endIndex { + endIndex = index + //fmt.Printf(" endIndex moved to %d [%q]\n", endIndex, filename[startIndex:endIndex]) + } + setField(tor, pattern.name, matches[matchIdx][1], matches[matchIdx][2]) + } + + // Start process for title + //fmt.Println(" title: ") + raw := strings.Split(filename[startIndex:endIndex], "(")[0] + cleanName = raw + if strings.HasPrefix(cleanName, "- ") { + cleanName = raw[2:] + } + if strings.ContainsRune(cleanName, '.') && !strings.ContainsRune(cleanName, ' ') { + cleanName = strings.Replace(cleanName, ".", " ", -1) + } + cleanName = strings.Replace(cleanName, "_", " ", -1) + //cleanName = re.sub('([\[\(_]|- )$', '', cleanName).strip() + setField(tor, "title", raw, strings.TrimSpace(cleanName)) + + return tor, nil +} diff --git a/pkg/releaseinfo/parser_test.go b/pkg/releaseinfo/parser_test.go new file mode 100644 index 0000000..6457550 --- /dev/null +++ b/pkg/releaseinfo/parser_test.go @@ -0,0 +1,331 @@ +package releaseinfo + +import ( + "flag" + "testing" + + "github.com/stretchr/testify/assert" +) + +var updateGoldenFiles = flag.Bool("update", false, "update golden files in testdata/") + +var testData = []string{ + "The Walking Dead S05E03 720p HDTV x264-ASAP[ettv]", + "Hercules (2014) 1080p BrRip H264 - YIFY", + "Dawn.of.the.Planet.of.the.Apes.2014.HDRip.XViD-EVO", + "The Big Bang Theory S08E06 HDTV XviD-LOL [eztv]", + "22 Jump Street (2014) 720p BrRip x264 - YIFY", + "Hercules.2014.EXTENDED.1080p.WEB-DL.DD5.1.H264-RARBG", + "Hercules.2014.Extended.Cut.HDRip.XViD-juggs[ETRG]", + "Hercules (2014) WEBDL DVDRip XviD-MAX", + "WWE Hell in a Cell 2014 PPV WEB-DL x264-WD -={SPARROW}=-", + "UFC.179.PPV.HDTV.x264-Ebi[rartv]", + "Marvels Agents of S H I E L D S02E05 HDTV x264-KILLERS [eztv]", + "X-Men.Days.of.Future.Past.2014.1080p.WEB-DL.DD5.1.H264-RARBG", + "Guardians Of The Galaxy 2014 R6 720p HDCAM x264-JYK", + "Marvel's.Agents.of.S.H.I.E.L.D.S02E01.Shadows.1080p.WEB-DL.DD5.1", + "Marvels Agents of S.H.I.E.L.D. S02E06 HDTV x264-KILLERS[ettv]", + "Guardians of the Galaxy (CamRip / 2014)", + "The.Walking.Dead.S05E03.1080p.WEB-DL.DD5.1.H.264-Cyphanix[rartv]", + "Brave.2012.R5.DVDRip.XViD.LiNE-UNiQUE", + "Lets.Be.Cops.2014.BRRip.XViD-juggs[ETRG]", + "These.Final.Hours.2013.WBBRip XViD", + "Downton Abbey 5x06 HDTV x264-FoV [eztv]", + "Annabelle.2014.HC.HDRip.XViD.AC3-juggs[ETRG]", + "Lucy.2014.HC.HDRip.XViD-juggs[ETRG]", + "The Flash 2014 S01E04 HDTV x264-FUM[ettv]", + "South Park S18E05 HDTV x264-KILLERS [eztv]", + "The Flash 2014 S01E03 HDTV x264-LOL[ettv]", + "The Flash 2014 S01E01 HDTV x264-LOL[ettv]", + "Lucy 2014 Dual-Audio WEBRip 1400Mb", + "Teenage Mutant Ninja Turtles (HdRip / 2014)", + "Teenage Mutant Ninja Turtles (unknown_release_type / 2014)", + "The Simpsons S26E05 HDTV x264 PROPER-LOL [eztv]", + "2047 - Sights of Death (2014) 720p BrRip x264 - YIFY", + "Two and a Half Men S12E01 HDTV x264 REPACK-LOL [eztv]", + "Dinosaur 13 2014 WEBrip XviD AC3 MiLLENiUM", + "Teenage.Mutant.Ninja.Turtles.2014.HDRip.XviD.MP3-RARBG", + "Dawn.Of.The.Planet.of.The.Apes.2014.1080p.WEB-DL.DD51.H264-RARBG", + "Teenage.Mutant.Ninja.Turtles.2014.720p.HDRip.x264.AC3.5.1-RARBG", + "Gotham.S01E05.Viper.WEB-DL.x264.AAC", + "Into.The.Storm.2014.1080p.WEB-DL.AAC2.0.H264-RARBG", + "Lucy 2014 Dual-Audio 720p WEBRip", + "Into The Storm 2014 1080p BRRip x264 DTS-JYK", + "Sin.City.A.Dame.to.Kill.For.2014.1080p.BluRay.x264-SPARKS", + "WWE Monday Night Raw 3rd Nov 2014 HDTV x264-Sir Paul", + "Jack.And.The.Cuckoo-Clock.Heart.2013.BRRip XViD", + "WWE Hell in a Cell 2014 HDTV x264 SNHD", + "Dracula.Untold.2014.TS.XViD.AC3.MrSeeN-SiMPLE", + "The Missing 1x01 Pilot HDTV x264-FoV [eztv]", + "Doctor.Who.2005.8x11.Dark.Water.720p.HDTV.x264-FoV[rartv]", + "Gotham.S01E07.Penguins.Umbrella.WEB-DL.x264.AAC", + "One Shot [2014] DVDRip XViD-ViCKY", + "The Shaukeens 2014 Hindi (1CD) DvDScr x264 AAC...Hon3y", + "The Shaukeens (2014) 1CD DvDScr Rip x264 [DDR]", + "Annabelle.2014.1080p.PROPER.HC.WEBRip.x264.AAC.2.0-RARBG", + "Interstellar (2014) CAM ENG x264 AAC-CPG", + "Guardians of the Galaxy (2014) Dual Audio DVDRip AVI", + "Eliza Graves (2014) Dual Audio WEB-DL 720p MKV x264", + "WWE Monday Night Raw 2014 11 10 WS PDTV x264-RKOFAN1990 -={SPARR", + "Sons.of.Anarchy.S01E03", + "doctor_who_2005.8x12.death_in_heaven.720p_hdtv_x264-fov", + "breaking.bad.s01e01.720p.bluray.x264-reward", + "Game of Thrones - 4x03 - Breaker of Chains", + "[720pMkv.Com]_sons.of.anarchy.s05e10.480p.BluRay.x264-GAnGSteR", + "[ www.Speed.cd ] -Sons.of.Anarchy.S07E07.720p.HDTV.X264-DIMENSION", + "Community.s02e20.rus.eng.720p.Kybik.v.Kybe", + "The.Jungle.Book.2016.3D.1080p.BRRip.SBS.x264.AAC-ETRG", + "Ant-Man.2015.3D.1080p.BRRip.Half-SBS.x264.AAC-m2g", + "Ice.Age.Collision.Course.2016.READNFO.720p.HDRIP.X264.AC3.TiTAN", + "Red.Sonja.Queen.Of.Plagues.2016.BDRip.x264-W4F[PRiME]", + "The Purge: Election Year (2016) HC - 720p HDRiP - 900MB - ShAaNi", + "War Dogs (2016) HDTS 600MB - NBY", + "The Hateful Eight (2015) 720p BluRay - x265 HEVC - 999MB - ShAaN", + "The.Boss.2016.UNRATED.720p.BRRip.x264.AAC-ETRG", + "Return.To.Snowy.River.1988.iNTERNAL.DVDRip.x264-W4F[PRiME]", + "Akira (2016) - UpScaled - 720p - DesiSCR-Rip - Hindi - x264 - AC3 - 5.1 - Mafiaking - M2Tv", + "Ben Hur 2016 TELESYNC x264 AC3 MAXPRO", + "The.Secret.Life.of.Pets.2016.HDRiP.AAC-LC.x264-LEGi0N", + "[HorribleSubs] Clockwork Planet - 10 [480p].mkv", + "[HorribleSubs] Detective Conan - 862 [1080p].mkv", + "thomas.and.friends.s19e09_s20e14.convert.hdtv.x264-w4f[eztv].mkv", + "Blade.Runner.2049.2017.1080p.WEB-DL.DD5.1.H264-FGT-[rarbg.to]", + "2012(2009).1080p.Dual Audio(Hindi+English) 5.1 Audios", + "2012 (2009) 1080p BrRip x264 - 1.7GB - YIFY", + "2012 2009 x264 720p Esub BluRay 6.0 Dual Audio English Hindi GOPISAHI", +} + +var moreTestData = []string{ + "Tokyo Olympics 2020 Street Skateboarding Prelims and Final 25 07 2021 1080p WEB-DL AAC2 0 H 264-playWEB", + "Tokyo Olympics 2020 Taekwondo Day3 Finals 26 07 720pEN25fps ES", + "Die Freundin der Haie 2021 German DUBBED DL DOKU 1080p WEB x264-WiSHTV", +} + +var movieTests = []string{ + "The Last Letter from Your Lover 2021 2160p NF WEBRip DDP5 1 Atmos x265-KiNGS", + "Blade 1998 Hybrid 1080p BluRay REMUX AVC Atmos-EPSiLON", + "Forrest Gump 1994 1080p BluRay DDP7 1 x264-Geek", + "Deux sous de violettes 1951 1080p Blu-ray Remux AVC FLAC 2 0-EDPH", + "Predator 1987 2160p UHD BluRay DTS-HD MA 5 1 HDR x265-W4NK3R", + "Final Destination 2 2003 1080p BluRay x264-ETHOS", + "Hellboy.II.The.Golden.Army.2008.REMASTERED.NORDiC.1080p.BluRay.x264-PANDEMONiUM", + "Wonders of the Sea 2017 BluRay 1080p AVC DTS-HD MA 2.0-BeyondHD", + "A Week Away 2021 1080p NF WEB-DL DDP 5.1 Atmos DV H.265-SymBiOTes", + "Control 2004 BluRay 1080p DTS-HD MA 5.1 AVC REMUX-FraMeSToR", + "Mimi 2021 1080p Hybrid WEB-DL DDP 5.1 x264-Telly", + "She's So Lovely 1997 BluRay 1080p DTS-HD MA 5.1 AVC REMUX-FraMeSToR", + "Those Who Wish Me Dead 2021 BluRay 1080p DD5.1 x264-BHDStudio", + "The Last Letter from Your Lover 2021 2160p NF WEBRip DDP 5.1 Atmos x265-KiNGS", + "Spinning Man 2018 BluRay 1080p DTS 5.1 x264-MTeam", + "The Wicker Man 1973 Final Cut 1080p BluRay FLAC 1.0 x264-NTb", + "New Police Story 2004 720p BluRay DTS x264-HiFi", + "La Cienaga 2001 Criterion Collection NTSC DVD9 DD 2.0", + "The Thin Blue Line 1988 Criterion Collection NTSC DVD9 DD 2.0", + "The Thin Red Line 1998 Criterion Collection NTSC 2xDVD9 DD 5.1", + "The Sword of Doom AKA daibosatsu 1966 Criterion Collection NTSC DVD9 DD 1.0", + "Freaks 2018 Hybrid REPACK 1080p BluRay REMUX AVC DTS-HD MA 5.1-EPSiLON", + "The Oxford Murders 2008 1080p BluRay Remux AVC DTS-HD MA 7.1-Pootis", + "Berlin Babylon 2001 PAL DVD9 DD 5.1", + "Dillinger 1973 1080p BluRay REMUX AVC DTS-HD MA 1.0-HiDeFZeN", + "True Romance 1993 2160p UHD Blu-ray DV HDR HEVC DTS-HD MA 5.1", + "Family 2019 1080p AMZN WEB-DL DD+ 5.1 H.264-TEPES", + "Family 2019 720p AMZN WEB-DL DD+ 5.1 H.264-TEPES", + "The Banana Splits Movie 2019 NTSC DVD9 DD 5.1-(_10_)", + "Sex Is Zero AKA saegjeugsigong 2002 720p BluRay DD 5.1 x264-KiR", + "Sex Is Zero AKA saegjeugsigong 2002 1080p BluRay DTS 5.1 x264-KiR", + "Sex Is Zero AKA saegjeugsigong 2002 1080p KOR Blu-ray AVC DTS-HD MA 5.1-ARiN", + "The Stranger AKA aagntuk 1991 Criterion Collection NTSC DVD9 DD 1.0", + "The Taking of Power by Louis XIV AKA La prise de pouvoir par Louis XIV 1966 Criterion Collection NTSC DVD9 DD 1.0", + "La Cienaga 2001 Criterion Collection NTSC DVD9 DD 2.0", + "The Thin Blue Line 1988 Criterion Collection NTSC DVD9 DD 2.0", + "The Thin Red Line 1998 Criterion Collection NTSC 2xDVD9 DD 5.1", + "The Sword of Doom AKA daibosatsu 1966 Criterion Collection NTSC DVD9 DD 1.0", + "Freaks 2018 Hybrid REPACK 1080p BluRay REMUX AVC DTS-HD MA 5.1-EPSiLON", + "The Oxford Murders 2008 1080p BluRay Remux AVC DTS-HD MA 7.1-Pootis", + "Berlin Babylon 2001 PAL DVD9 DD 5.1", + "Dillinger 1973 1080p BluRay REMUX AVC DTS-HD MA 1.0-HiDeFZeN", + "True Romance 1993 2160p UHD Blu-ray DV HDR HEVC DTS-HD MA 5.1", + "La Cienaga 2001 Criterion Collection NTSC DVD9 DD 2.0", + "Freaks 2018 Hybrid REPACK 1080p BluRay REMUX AVC DTS-HD MA 5.1-EPSiLON", + "The Oxford Murders 2008 1080p BluRay Remux AVC DTS-HD MA 7.1-Pootis", +} + +//func TestParse_Movies(t *testing.T) { +// type args struct { +// filename string +// } +// tests := []struct { +// filename string +// want *ReleaseInfo +// wantErr bool +// }{ +// {filename: "", want: nil, wantErr: false}, +// } +// for _, tt := range tests { +// t.Run(tt.filename, func(t *testing.T) { +// got, err := Parse(tt.filename) +// if (err != nil) != tt.wantErr { +// t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) +// return +// } +// if !reflect.DeepEqual(got, tt.want) { +// t.Errorf("Parse() got = %v, want %v", got, tt.want) +// } +// }) +// } +//} + +var tvTests = []string{ + "Melrose Place S04 480p web-dl eac3 x264", + "Privileged.S01E17.1080p.WEB.h264-DiRT", + "Banshee S02 BluRay 720p DD5.1 x264-NTb", + "Banshee S04 BluRay 720p DTS x264-NTb", + "Servant S01 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-FLUX", + "South Park S06 1080p BluRay DD5.1 x264-W4NK3R", + "The Walking Dead: Origins S01E01 1080p WEB-DL DDP 2.0 H.264-GOSSIP", + "Mythic Quest S01 2160p ATVP WEB-DL DDP 5.1 Atmos DV HEVC-FLUX", + "Masameer County S01 1080p NF WEB-DL DD+ 5.1 H.264-XIQ", + "Kevin Can F**K Himself 2021 S01 1080p AMZN WEB-DL DD+ 5.1 H.264-SaiTama", + "How to Sell Drugs Online (Fast) S03 1080p NF WEB-DL DD+ 5.1 x264-KnightKing", + "Power Book III: Raising Kanan S01E01 2160p WEB-DL DD+ 5.1 H265-GGEZ", + "Power Book III: Raising Kanan S01E02 2160p WEB-DL DD+ 5.1 H265-GGWP", + "Thea Walking Dead: Origins S01E01 1080p WEB-DL DD+ 2.0 H.264-GOSSIP", + "Mean Mums S01 1080p AMZN WEB-DL DD+ 2.0 H.264-FLUX", +} + +func TestParse_TV(t *testing.T) { + tests := []struct { + filename string + want *ReleaseInfo + wantErr bool + }{ + { + filename: "Melrose Place S04 480p web-dl eac3 x264", + want: &ReleaseInfo{ + Title: "Melrose Place", + Season: 4, + Resolution: "480p", + Source: "web-dl", + Codec: "x264", + Group: "dl eac3 x264", + }, + wantErr: false, + }, + { + filename: "Privileged.S01E17.1080p.WEB.h264-DiRT", + want: &ReleaseInfo{ + Title: "Privileged", + Season: 1, + Episode: 17, + Resolution: "1080p", + Source: "WEB", + Codec: "h264", + Group: "DiRT", + }, + wantErr: false, + }, + { + filename: "Banshee S02 BluRay 720p DD5.1 x264-NTb", + want: &ReleaseInfo{ + Title: "Banshee", + Season: 2, + Resolution: "720p", + Source: "BluRay", + Codec: "x264", + Audio: "DD5.1", + Group: "NTb", + }, + wantErr: false, + }, + { + filename: "Banshee Season 2 BluRay 720p DD5.1 x264-NTb", + want: &ReleaseInfo{ + Title: "Banshee", + Season: 2, + Resolution: "720p", + Source: "BluRay", + Codec: "x264", + Audio: "DD5.1", + Group: "NTb", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.filename, func(t *testing.T) { + got, err := Parse(tt.filename) + + if (err != nil) != tt.wantErr { + t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + return + } + + assert.Equal(t, tt.want, got) + //if !reflect.DeepEqual(got, tt.want) { + // t.Errorf("Parse() got = %v, want %v", got, tt.want) + //} + }) + } +} + +var gamesTests = []string{ + "Night Book NSW-LUMA", + "Evdeki Lanet-DARKSiDERS", + "Evdeki.Lanet-DARKSiDERS", +} + +//func TestParser(t *testing.T) { +// for i, fname := range testData { +// t.Run(fmt.Sprintf("golden_file_%03d", i), func(t *testing.T) { +// tor, err := Parse(fname) +// if err != nil { +// t.Fatalf("test %v: parser error:\n %v", i, err) +// } +// +// var want ReleaseInfo +// +// if !reflect.DeepEqual(*tor, want) { +// t.Fatalf("test %v: wrong result for %q\nwant:\n %v\ngot:\n %v", i, fname, want, *tor) +// } +// }) +// } +//} + +//func TestParserWriteToFiles(t *testing.T) { +// for i, fname := range testData { +// t.Run(fmt.Sprintf("golden_file_%03d", i), func(t *testing.T) { +// tor, err := Parse(fname) +// if err != nil { +// t.Fatalf("test %v: parser error:\n %v", i, err) +// } +// +// goldenFilename := filepath.Join("testdata", fmt.Sprintf("golden_file_%03d.json", i)) +// +// if *updateGoldenFiles { +// buf, err := json.MarshalIndent(tor, "", " ") +// if err != nil { +// t.Fatalf("error marshaling result: %v", err) +// } +// +// if err = ioutil.WriteFile(goldenFilename, buf, 0644); err != nil { +// t.Fatalf("unable to update golden file: %v", err) +// } +// } +// +// buf, err := ioutil.ReadFile(goldenFilename) +// if err != nil { +// t.Fatalf("error loading golden file: %v", err) +// } +// +// var want ReleaseInfo +// err = json.Unmarshal(buf, &want) +// if err != nil { +// t.Fatalf("error unmarshalling golden file %v: %v", goldenFilename, err) +// } +// +// if !reflect.DeepEqual(*tor, want) { +// t.Fatalf("test %v: wrong result for %q\nwant:\n %v\ngot:\n %v", i, fname, want, *tor) +// } +// }) +// } +//} diff --git a/pkg/releaseinfo/patterns.go b/pkg/releaseinfo/patterns.go new file mode 100644 index 0000000..1fb50e4 --- /dev/null +++ b/pkg/releaseinfo/patterns.go @@ -0,0 +1,58 @@ +package releaseinfo + +import ( + "fmt" + "os" + "reflect" + "regexp" +) + +var patterns = []struct { + name string + // Use the last matching pattern. E.g. Year. + last bool + kind reflect.Kind + // REs need to have 2 sub expressions (groups), the first one is "raw", and + // the second one for the "clean" value. + // E.g. Epiode matching on "S01E18" will result in: raw = "E18", clean = "18". + re *regexp.Regexp +}{ + //{"season", false, reflect.Int, regexp.MustCompile(`(?i)(s?([0-9]{1,2}))[ex]`)}, + {"season", false, reflect.Int, regexp.MustCompile(`(?i)((?:S|Season\s*)(\d{1,3}))`)}, + {"episode", false, reflect.Int, regexp.MustCompile(`(?i)([ex]([0-9]{2})(?:[^0-9]|$))`)}, + {"episode", false, reflect.Int, regexp.MustCompile(`(-\s+([0-9]+)(?:[^0-9]|$))`)}, + {"year", true, reflect.Int, regexp.MustCompile(`\b(((?:19[0-9]|20[0-9])[0-9]))\b`)}, + + {"resolution", false, reflect.String, regexp.MustCompile(`\b(([0-9]{3,4}p|i))\b`)}, + {"source", false, reflect.String, regexp.MustCompile(`(?i)\b(((?:PPV\.)?[HP]DTV|(?:HD)?CAM|B[DR]Rip|(?:HD-?)?TS|(?:PPV )?WEB-?DL(?: DVDRip)?|HDRip|DVDRip|DVDRIP|CamRip|WEB|W[EB]BRip|BluRay|DvDScr|telesync))\b`)}, + {"codec", false, reflect.String, regexp.MustCompile(`(?i)\b((xvid|HEVC|[hx]\.?26[45]))\b`)}, + {"container", false, reflect.String, regexp.MustCompile(`(?i)\b((MKV|AVI|MP4))\b`)}, + + {"audio", false, reflect.String, regexp.MustCompile(`(?i)\b((MP3|DD5\.?1|Dual[\- ]Audio|LiNE|DTS|AAC[.-]LC|AAC(?:\.?2\.0)?|AC3(?:\.5\.1)?))\b`)}, + {"region", false, reflect.String, regexp.MustCompile(`(?i)\b(R([0-9]))\b`)}, + {"size", false, reflect.String, regexp.MustCompile(`(?i)\b((\d+(?:\.\d+)?(?:GB|MB)))\b`)}, + {"website", false, reflect.String, regexp.MustCompile(`^(\[ ?([^\]]+?) ?\])`)}, + {"language", false, reflect.String, regexp.MustCompile(`(?i)\b((rus\.eng|ita\.eng))\b`)}, + {"sbs", false, reflect.String, regexp.MustCompile(`(?i)\b(((?:Half-)?SBS))\b`)}, + + {"group", false, reflect.String, regexp.MustCompile(`\b(- ?([^-]+(?:-={[^-]+-?$)?))$`)}, + + {"extended", false, reflect.Bool, regexp.MustCompile(`(?i)\b(EXTENDED(:?.CUT)?)\b`)}, + {"hardcoded", false, reflect.Bool, regexp.MustCompile(`(?i)\b((HC))\b`)}, + + {"proper", false, reflect.Bool, regexp.MustCompile(`(?i)\b((PROPER))\b`)}, + {"repack", false, reflect.Bool, regexp.MustCompile(`(?i)\b((REPACK))\b`)}, + + {"widescreen", false, reflect.Bool, regexp.MustCompile(`(?i)\b((WS))\b`)}, + {"unrated", false, reflect.Bool, regexp.MustCompile(`(?i)\b((UNRATED))\b`)}, + {"threeD", false, reflect.Bool, regexp.MustCompile(`(?i)\b((3D))\b`)}, +} + +func init() { + for _, pat := range patterns { + if pat.re.NumSubexp() != 2 { + fmt.Printf("Pattern %q does not have enough capture groups. want 2, got %d\n", pat.name, pat.re.NumSubexp()) + os.Exit(1) + } + } +} diff --git a/pkg/wildcard/match.go b/pkg/wildcard/match.go new file mode 100644 index 0000000..c00dbe5 --- /dev/null +++ b/pkg/wildcard/match.go @@ -0,0 +1,51 @@ +package wildcard + +// MatchSimple - finds whether the text matches/satisfies the pattern string. +// supports only '*' wildcard in the pattern. +// considers a file system path as a flat name space. +func MatchSimple(pattern, name string) bool { + if pattern == "" { + return name == pattern + } + if pattern == "*" { + return true + } + // Does only wildcard '*' match. + return deepMatchRune([]rune(name), []rune(pattern), true) +} + +// Match - finds whether the text matches/satisfies the pattern string. +// supports '*' and '?' wildcards in the pattern string. +// unlike path.Match(), considers a path as a flat name space while matching the pattern. +// The difference is illustrated in the example here https://play.golang.org/p/Ega9qgD4Qz . +func Match(pattern, name string) (matched bool) { + if pattern == "" { + return name == pattern + } + if pattern == "*" { + return true + } + // Does extended wildcard '*' and '?' match. + return deepMatchRune([]rune(name), []rune(pattern), false) +} + +func deepMatchRune(str, pattern []rune, simple bool) bool { + for len(pattern) > 0 { + switch pattern[0] { + default: + if len(str) == 0 || str[0] != pattern[0] { + return false + } + case '?': + if len(str) == 0 && !simple { + return false + } + case '*': + return deepMatchRune(str, pattern[1:], simple) || + (len(str) > 0 && deepMatchRune(str[1:], pattern, simple)) + } + str = str[1:] + pattern = pattern[1:] + } + return len(str) == 0 && len(pattern) == 0 +} diff --git a/pkg/wildcard/match_test.go b/pkg/wildcard/match_test.go new file mode 100644 index 0000000..37f0967 --- /dev/null +++ b/pkg/wildcard/match_test.go @@ -0,0 +1,37 @@ +package wildcard + +import "testing" + +// TestMatch - Tests validate the logic of wild card matching. +// `Match` supports '*' and '?' wildcards. +// Sample usage: In resource matching for bucket policy validation. +func TestMatch(t *testing.T) { + testCases := []struct { + pattern string + text string + matched bool + }{ + { + pattern: "The?Simpsons*", + text: "The Simpsons S12", + matched: true, + }, + { + pattern: "The?Simpsons*", + text: "The.Simpsons.S12", + matched: true, + }, + { + pattern: "The?Simpsons*", + text: "The.Simps.S12", + matched: false, + }, + } + // Iterating over the test cases, call the function under test and asert the output. + for i, testCase := range testCases { + actualResult := Match(testCase.pattern, testCase.text) + if testCase.matched != actualResult { + t.Errorf("Test %d: Expected the result to be `%v`, but instead found it to be `%v`", i+1, testCase.matched, actualResult) + } + } +}