diff --git a/go.mod b/go.mod index e91a6a9..8b3aaa0 100644 --- a/go.mod +++ b/go.mod @@ -4,12 +4,15 @@ go 1.21.6 require ( github.com/rs/zerolog v1.32.0 + github.com/stretchr/testify v1.8.4 golang.org/x/time v0.5.0 gopkg.in/yaml.v3 v3.0.1 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.12.0 // indirect ) diff --git a/go.sum b/go.sum index a16daa0..6353bc2 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -6,9 +8,13 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= diff --git a/internal/discovery/discovery.go b/internal/discovery/discovery.go index 0abc9b1..6eb62be 100644 --- a/internal/discovery/discovery.go +++ b/internal/discovery/discovery.go @@ -14,6 +14,12 @@ import ( "github.com/sonalys/animeman/internal/parser" ) +type TaggedNyaa struct { + meta parser.ParsedTitle + seasonEpisodeTag string + entry nyaa.Entry +} + func (c *Controller) RunDiscovery(ctx context.Context) error { t1 := time.Now() entries, err := c.dep.MAL.GetAnimeList(ctx, @@ -41,15 +47,18 @@ func (c *Controller) RunDiscovery(ctx context.Context) error { return nil } -type TaggedNyaa struct { - meta parser.ParsedTitle - seasonEpisodeTag string - entry nyaa.Entry +func filterNyaaBatch(entries []nyaa.Entry) []nyaa.Entry { + for _, entry := range entries { + if meta := parser.ParseTitle(entry.Title); meta.IsMultiEpisode { + return []nyaa.Entry{entry} + } + } + return entries } -func buildTaggedNyaaList(torrents []nyaa.Entry) []TaggedNyaa { - out := make([]TaggedNyaa, 0, len(torrents)) - for _, entry := range torrents { +func buildTaggedNyaaList(entries []nyaa.Entry) []TaggedNyaa { + out := make([]TaggedNyaa, 0, len(entries)) + for _, entry := range entries { meta := parser.ParseTitle(entry.Title) out = append(out, TaggedNyaa{ meta: meta, @@ -63,6 +72,19 @@ func buildTaggedNyaaList(torrents []nyaa.Entry) []TaggedNyaa { return out } +func filterEpisodes(list []TaggedNyaa, latestTag string) []TaggedNyaa { + out := make([]TaggedNyaa, 0, len(list)) + for _, nyaaEntry := range list { + // Make sure we only add episodes ahead of the current ones in the qBittorrent. + if compareTags(nyaaEntry.seasonEpisodeTag, latestTag) <= 0 { + continue + } + latestTag = nyaaEntry.seasonEpisodeTag + out = append(out, nyaaEntry) + } + return out +} + func (c *Controller) DigestMALEntry(ctx context.Context, entry myanimelist.AnimeListEntry) (count int, err error) { // Build search query for Nyaa. // For title we filter for english and original titles. @@ -70,13 +92,13 @@ func (c *Controller) DigestMALEntry(ctx context.Context, entry myanimelist.Anime sourceQuery := nyaa.OrQuery(c.dep.Config.Sources) qualityQuery := nyaa.OrQuery(c.dep.Config.Qualitites) - torrents, err := c.dep.NYAA.List(ctx, titleQuery, sourceQuery, qualityQuery) - log.Debug().Str("entry", entry.GetTitle()).Msgf("found %d torrents", len(torrents)) + nyaaEntries, err := c.dep.NYAA.List(ctx, titleQuery, sourceQuery, qualityQuery) + log.Debug().Str("entry", entry.GetTitle()).Msgf("found %d torrents", len(nyaaEntries)) if err != nil { return 0, fmt.Errorf("getting nyaa list: %w", err) } // There should always be torrents for entries, if there aren't we can just exit the routine. - if len(torrents) == 0 { + if len(nyaaEntries) == 0 { log.Error().Msgf("no torrents found for entry '%s'", entry.GetTitle()) return 0, nil } @@ -84,22 +106,18 @@ func (c *Controller) DigestMALEntry(ctx context.Context, entry myanimelist.Anime if err != nil { return count, fmt.Errorf("getting latest tag: %w", err) } - taggedNyaaList := buildTaggedNyaaList(torrents) + // If we don't have any episodes, and show is released, try to find a batch for all episodes. + if latestTag == "" && entry.AiringStatus == myanimelist.AiringStatusAired { + nyaaEntries = filterNyaaBatch(nyaaEntries) + } + taggedNyaaList := buildTaggedNyaaList(nyaaEntries) + taggedNyaaList = filterEpisodes(taggedNyaaList, latestTag) for _, nyaaEntry := range taggedNyaaList { - // Make sure we only add episodes ahead of the current ones in the qBittorrent. - if compareTags(nyaaEntry.seasonEpisodeTag, latestTag) <= 0 { - continue - } - latestTag = nyaaEntry.seasonEpisodeTag - log.Debug().Str("entry", entry.GetTitle()).Msgf("analyzing torrent '%s'", nyaaEntry.meta.Title) - added, err := c.DigestNyaaTorrent(ctx, entry, nyaaEntry) - if err != nil { + if err := c.DigestNyaaTorrent(ctx, entry, nyaaEntry); err != nil { log.Error().Msgf("failed to digest nyaa entry: %s", err) continue } - if added { - count++ - } + count++ } return count, nil } diff --git a/internal/discovery/discovery_test.go b/internal/discovery/discovery_test.go new file mode 100644 index 0000000..ab500be --- /dev/null +++ b/internal/discovery/discovery_test.go @@ -0,0 +1,93 @@ +package discovery + +import ( + "reflect" + "testing" + + "github.com/sonalys/animeman/integrations/nyaa" + "github.com/stretchr/testify/require" +) + +func Test_filterEpisodes(t *testing.T) { + type args struct { + list []TaggedNyaa + latestTag string + } + tests := []struct { + name string + args args + want []TaggedNyaa + }{ + { + name: "empty", + args: args{}, + want: []TaggedNyaa{}, + }, + { + name: "no tag", + args: args{ + latestTag: "", + list: []TaggedNyaa{ + {seasonEpisodeTag: "!Show3 S03E01"}, + {seasonEpisodeTag: "!Show3 S03E02"}, + }, + }, + want: []TaggedNyaa{ + {seasonEpisodeTag: "!Show3 S03E01"}, + {seasonEpisodeTag: "!Show3 S03E02"}, + }, + }, + { + name: "tag", + args: args{ + latestTag: "!Show3 S03E01", + list: []TaggedNyaa{ + {seasonEpisodeTag: "!Show3 S03E01"}, + {seasonEpisodeTag: "!Show3 S03E02"}, + }, + }, + want: []TaggedNyaa{ + {seasonEpisodeTag: "!Show3 S03E02"}, + }, + }, + { + name: "season batch", + args: args{ + latestTag: "!Show3 S03", + list: []TaggedNyaa{ + {seasonEpisodeTag: "!Show3 S03E01"}, + {seasonEpisodeTag: "!Show3 S03E02"}, + }, + }, + want: []TaggedNyaa{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := filterEpisodes(tt.args.list, tt.args.latestTag); !reflect.DeepEqual(got, tt.want) { + t.Errorf("filterEpisodes() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_buildTaggedNyaaList(t *testing.T) { + t.Run("empty", func(t *testing.T) { + got := buildTaggedNyaaList([]nyaa.Entry{}) + require.Empty(t, got) + }) + t.Run("sort by tag", func(t *testing.T) { + input := []nyaa.Entry{ + {Title: "Show3: S03E03"}, + {Title: "Show3: S03E02"}, + {Title: "Show3: S03E01"}, + {Title: "Show3: S03"}, + } + got := buildTaggedNyaaList(input) + require.Len(t, got, len(input)) + latestTag := got[len(got)-1].seasonEpisodeTag + for i := range got { + require.True(t, compareTags(got[i].seasonEpisodeTag, latestTag) <= 0) + } + }) +} diff --git a/internal/discovery/torrent.go b/internal/discovery/torrent.go index 0d24ada..148e7e5 100644 --- a/internal/discovery/torrent.go +++ b/internal/discovery/torrent.go @@ -86,10 +86,10 @@ func (c *Controller) GetLatestTag(ctx context.Context, entry myanimelist.AnimeLi return getLatestTag(append(torrents1, torrents2...)...), nil } -func (c *Controller) DigestNyaaTorrent(ctx context.Context, entry myanimelist.AnimeListEntry, nyaaEntry TaggedNyaa) (bool, error) { +func (c *Controller) DigestNyaaTorrent(ctx context.Context, entry myanimelist.AnimeListEntry, nyaaEntry TaggedNyaa) error { if nyaaEntry.meta.IsMultiEpisode && entry.AiringStatus == myanimelist.AiringStatusAiring { log.Debug().Msgf("torrent dropped: multi-episode for currently airing") - return false, nil + return nil } var savePath qbittorrent.SavePath if c.dep.Config.CreateShowFolder { @@ -105,11 +105,11 @@ func (c *Controller) DigestNyaaTorrent(ctx context.Context, entry myanimelist.An qbittorrent.Category(c.dep.Config.Category), ) if err != nil { - return false, fmt.Errorf("adding torrents: %w", err) + return fmt.Errorf("adding torrents: %w", err) } log.Info(). Str("savePath", string(savePath)). Strs("tag", tags). Msgf("torrent added") - return true, nil + return nil }