Skip to content

Commit

Permalink
improve resiliency for qBittorrent disconnects
Browse files Browse the repository at this point in the history
  • Loading branch information
sonalys committed Feb 7, 2024
1 parent 3c6ecc9 commit fe54abd
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 53 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

Animeman is a service for fetching your MyAnimeList currently watching animes from Nyaa.si RSS feed.

Currently it manages qBitTorrent through it's WebUI, creating and managing a category of torrents.
Currently it manages qBittorrent through it's WebUI, creating and managing a category of torrents.

It automatically parses the torrent titles for tagging the show, season and episodes, while also searching in Nyaa.si for new releases.

Expand Down Expand Up @@ -46,7 +46,7 @@ downloadPath: /downloads/animes
createShowFolder: true
malUser: YOUR_USER
qBitTorrentHost: http://192.168.1.240:8088 # qBittorrent WebUI Host.
qBitTorrentUsername: admin # change with qBitTorrent credentials.
qBitTorrentUsername: admin # change with qBittorrent credentials.
qBitTorrentPassword: adminadmin
pollFrequency: 15m0s # How often should we seek for updates?
```
Expand Down
17 changes: 3 additions & 14 deletions cmd/service/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ package main
import (
"context"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"syscall"

"github.com/rs/zerolog"
Expand All @@ -19,28 +16,21 @@ import (
"github.com/sonalys/animeman/internal/utils"
)

func isLaunchedByDebugger() bool {
// gops executable must be in the path. See https://github.com/google/gops
gopsOut, err := exec.Command("gops", strconv.Itoa(os.Getppid())).Output()
return err == nil && strings.Contains(string(gopsOut), "dlv")
}

func init() {
log.Logger = log.Output(zerolog.ConsoleWriter{
Out: os.Stderr,
})
if !isLaunchedByDebugger() {
zerolog.SetGlobalLevel(zerolog.InfoLevel)
}
zerolog.SetGlobalLevel(zerolog.InfoLevel)
}

func main() {
log.Info().Msg("starting Animeman")
config := config.ReadConfig(utils.Coalesce(os.Getenv("CONFIG_PATH"), "config.yaml"))
ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
c := discovery.New(discovery.Dependencies{
MAL: myanimelist.New(config.MALUser),
NYAA: nyaa.New(),
QB: qbittorrent.New(config.QBitTorrentHost, config.QBitTorrentUsername, config.QBitTorrentPassword),
QB: qbittorrent.New(ctx, config.QBitTorrentHost, config.QBitTorrentUsername, config.QBitTorrentPassword),
Config: discovery.Config{
Sources: config.Sources,
Qualitites: config.Qualities,
Expand All @@ -50,7 +40,6 @@ func main() {
PollFrequency: config.PollFrequency,
},
})
ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
if err := c.Start(ctx); err != nil {
log.Error().Msgf("failed to finish discover: %s", err)
} else {
Expand Down
50 changes: 28 additions & 22 deletions integrations/qbittorrent/api.go
Original file line number Diff line number Diff line change
@@ -1,58 +1,64 @@
package qbittorrent

import (
"context"
"errors"
"fmt"
"net/http"
"net/http/cookiejar"
"syscall"
"time"

"github.com/rs/zerolog/log"
)

type (
API struct {
host string
client *http.Client
host string
username, password string
client *http.Client
}
)

func New(host, username, password string) *API {
func New(ctx context.Context, host, username, password string) *API {
jar, err := cookiejar.New(nil)
if err != nil {
panic(err)
}
api := &API{
host: fmt.Sprintf("%s/api/v2", host),
host: fmt.Sprintf("%s/api/v2", host),
username: username,
password: password,
client: &http.Client{
Timeout: 3 * time.Second,
Jar: jar,
},
}
var version string
api.Wait()
api.Wait(ctx)
if version, err = api.Version(); err != nil {
if !errors.Is(err, ErrUnauthorized) {
log.Fatal().Msgf("failed to initialize: %s", err)
}
if err := api.Login(username, password); err != nil {
log.Fatal().Msgf("could not initialize qBittorrent: %s", err)
}
if version, err = api.Version(); err == ErrUnauthorized {
log.Fatal().Msgf("could not check version: %s", err)
}
log.Fatal().Msgf("failed to connect to qBittorrent: %s", err)
}
log.Info().Msgf("connected to qBitTorrent:%s", version)
log.Info().Msgf("connected to qBittorrent:%s", version)
return api
}

func (api *API) Do(req *http.Request) (*http.Response, error) {
resp, err := api.client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode >= 400 {
return nil, ErrUnauthorized
ctx := context.WithoutCancel(req.Context())
localReq := req.Clone(ctx)
resp, err := api.client.Do(localReq)
switch {
case errors.Is(err, syscall.ECONNREFUSED) ||
errors.Is(err, syscall.ECONNABORTED) ||
errors.Is(err, syscall.ECONNRESET):
log.Warn().Msgf("qBittorrent disconnected")
api.Wait(ctx)
return api.Do(req)
case err == nil && resp.StatusCode >= 400:
if loginErr := api.Login(api.username, api.password); loginErr != nil {
return resp, loginErr
}
return api.Do(req)
}
return resp, nil
return resp, err
}
16 changes: 10 additions & 6 deletions integrations/qbittorrent/ping.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package qbittorrent

import (
"context"
"time"

"github.com/rs/zerolog/log"
)

func (api *API) Wait() {
for retries := 5; retries >= 0; retries-- {
_, err := api.client.Get(api.host + "/app/version")
if err == nil {
func (api *API) Wait(ctx context.Context) {
log.Info().Msgf("probing for qBittorrent")
for {
if ctx.Err() != nil {
return
}
log.Info().Msgf("waiting for qBitTorrent")
time.Sleep(time.Duration(6-retries) * 3 * time.Second)
if _, err := api.client.Get(api.host + "/app/version"); err == nil {
log.Info().Msgf("qBittorrent is ready")
return
}
time.Sleep(time.Second)
}
}
6 changes: 1 addition & 5 deletions integrations/qbittorrent/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,5 @@ func (api *API) Version() (string, error) {
return "", NewErrConnection(err)
}
defer resp.Body.Close()
version, err := io.ReadAll(resp.Body)
if err != nil {
panic(err)
}
return string(version), nil
return string(utils.Must(io.ReadAll(resp.Body))), nil
}
5 changes: 2 additions & 3 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,13 @@ func GenerateBoilerplateConfig() {
}

func ReadConfig(path string) Config {
file, err := os.ReadFile(path)
file, err := os.Open(path)
if err != nil {
GenerateBoilerplateConfig()
log.Fatal().Msg("file config.yaml not detected, please open the created file and configure it correctly")
}
var config Config
err = yaml.Unmarshal(file, &config)
if err != nil {
if err = yaml.NewDecoder(file).Decode(&config); err != nil {
log.Fatal().Msgf("could not read config.yaml: %s", err)
}
return config
Expand Down
2 changes: 1 addition & 1 deletion internal/discovery/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func New(dep Dependencies) *Controller {

func (c *Controller) Start(ctx context.Context) error {
if err := c.UpdateExistingTorrentsTags(ctx); err != nil {
return fmt.Errorf("updating qBitTorrent entries: %w", err)
return fmt.Errorf("updating qBittorrent entries: %w", err)
}
log.Info().Msgf("starting polling with frequency %s", c.dep.Config.PollFrequency.String())
timer := time.NewTicker(c.dep.Config.PollFrequency)
Expand Down

0 comments on commit fe54abd

Please sign in to comment.