From 8a046bc5058c62573fb7ad34177a88dd23806236 Mon Sep 17 00:00:00 2001 From: nuxen Date: Tue, 7 May 2024 15:17:44 +0200 Subject: [PATCH] fix(processor): match filenames to seasonpack (#110) * fix(processor): match filenames to seasonpack * chore(formatting): proper import sorting * fix(tests): update matching file names tests * fix(processor): skip hardlink of mismatched episodes * chore(processor): move size logging to existing line * feat(formatting): move out episode comparing logic * feat(processor): improve matching logic and logging * fix(processor): wrong error used in if condition * chore(processor): logging cleanup * chore(processor): more logging cleanup * chore(processor): remove todo --- internal/http/processor.go | 52 +++++++----- internal/torrents/torrents.go | 34 +++++--- internal/utils/formatting.go | 43 +++++++--- internal/utils/formatting_test.go | 128 +++++++++++++++++++----------- 4 files changed, 173 insertions(+), 84 deletions(-) diff --git a/internal/http/processor.go b/internal/http/processor.go index 1fca108..db40866 100644 --- a/internal/http/processor.go +++ b/internal/http/processor.go @@ -75,6 +75,7 @@ type entryTime struct { type matchPaths struct { epPathClient string + epSizeClient int64 packPathAnnounce string } @@ -214,7 +215,7 @@ func (p *processor) processSeasonPack() (int, error) { if !ok { return StatusClientNotFound, fmt.Errorf("client not found in config") } - p.log.Info().Msgf("using %q client serving at %s:%d", clientName, client.Host, client.Port) + p.log.Info().Msgf("using %s client serving at %s:%d", clientName, client.Host, client.Port) if len(p.req.Name) == 0 { return StatusAnnounceNameError, fmt.Errorf("couldn't get announce name") @@ -236,7 +237,7 @@ func (p *processor) processSeasonPack() (int, error) { } packNameAnnounce := utils.FormatSeasonPackTitle(p.req.Name) - p.log.Debug().Msgf("formatted season pack name: %q", packNameAnnounce) + p.log.Debug().Msgf("formatted season pack name: %s", packNameAnnounce) for _, child := range v { if release.CheckCandidates(&requestrls, &child, p.cfg.Config.FuzzyMatching) == StatusAlreadyInClient { @@ -306,13 +307,15 @@ func (p *processor) processSeasonPack() (int, error) { case StatusSuccessfulMatch: m, err := p.getFiles(child.T.Hash) if err != nil { - p.log.Error().Err(err).Msgf("error getting files: %q", child.T.Name) + p.log.Error().Err(err).Msgf("error getting files: %s", child.T.Name) continue } fileName := "" - for _, v := range *m { - fileName = v.Name + var size int64 = 0 + for _, f := range *m { + fileName = f.Name + size = f.Size break } @@ -325,6 +328,7 @@ func (p *processor) processSeasonPack() (int, error) { currentMatch := []matchPaths{ { epPathClient: epPathClient, + epSizeClient: size, packPathAnnounce: packPathAnnounce, }, } @@ -336,7 +340,8 @@ func (p *processor) processSeasonPack() (int, error) { newMatches := append(oldMatches.([]matchPaths), currentMatch...) matchesMap.Store(p.req.Name, newMatches) - p.log.Debug().Msgf("matched torrent from client %q: %q %q", clientName, child.T.Name, child.T.Hash) + p.log.Debug().Msgf("matched torrent from client: name(%s), size(%d), hash(%s)", + child.T.Name, size, child.T.Hash) respCodes = append(respCodes, res) continue } @@ -376,11 +381,11 @@ func (p *processor) processSeasonPack() (int, error) { for _, match := range matches { if err := utils.CreateHardlink(match.epPathClient, match.packPathAnnounce); err != nil { - p.log.Error().Err(err).Msgf("error creating hardlink for: %q", match.epPathClient) + p.log.Error().Err(err).Msgf("error creating hardlink: %s", match.epPathClient) hardlinkRespCodes = append(hardlinkRespCodes, StatusFailedHardlink) continue } - p.log.Log().Msgf("created hardlink of %q into %q", match.epPathClient, match.packPathAnnounce) + p.log.Log().Msgf("created hardlink: source(%s), target(%s)", match.epPathClient, match.packPathAnnounce) hardlinkRespCodes = append(hardlinkRespCodes, StatusSuccessfulHardlink) } @@ -435,14 +440,14 @@ func (p *processor) parseTorrent() (int, error) { return StatusParseTorrentInfoError, err } packNameParsed := torrentInfo.BestName() - p.log.Debug().Msgf("parsed season pack name: %q", packNameParsed) + p.log.Debug().Msgf("parsed season pack name: %s", packNameParsed) torrentEps, err := torrents.GetEpisodesFromTorrentInfo(torrentInfo) if err != nil { return StatusGetEpisodesError, err } for _, torrentEp := range torrentEps { - p.log.Debug().Msgf("found episode in pack: %q", torrentEp) + p.log.Debug().Msgf("found episode in pack: name(%s), size(%d)", torrentEp.Name, torrentEp.Size) } matchesSlice, ok := matchesMap.Load(p.req.Name) @@ -452,24 +457,35 @@ func (p *processor) parseTorrent() (int, error) { matches := utils.DedupeSlice(matchesSlice.([]matchPaths)) var hardlinkRespCodes []int + var matchedPath string + var matchErr error for _, match := range matches { newPackPath := utils.ReplaceParentFolder(match.packPathAnnounce, packNameParsed) - // TODO: rework this functionality as it currently leads to overwritten hardlinks - /* - newPackPath, err = utils.MatchFileNameToSeasonPackNaming(newPackPath, torrentEps) - if err != nil { - p.log.Error().Err(err).Msgf("error matching episode to file in season pack: %q", match.epPathClient) + for _, torrentEp := range torrentEps { + matchedPath, matchErr = utils.MatchEpToSeasonPackEp(newPackPath, match.epSizeClient, torrentEp) + if matchErr != nil { + p.log.Debug().Err(matchErr).Msgf("episode did not match: client(%s), torrent(%s)", + filepath.Base(match.epPathClient), torrentEp.Name) + continue } - */ + break + } + if matchErr != nil { + p.log.Error().Err(matchErr).Msgf("error matching episode to file in pack, skipping hardlink: %s", + filepath.Base(match.epPathClient)) + hardlinkRespCodes = append(hardlinkRespCodes, StatusFailedHardlink) + continue + } + newPackPath = matchedPath if err = utils.CreateHardlink(match.epPathClient, newPackPath); err != nil { - p.log.Error().Err(err).Msgf("error creating hardlink for: %q", match.epPathClient) + p.log.Error().Err(err).Msgf("error creating hardlink: %s", match.epPathClient) hardlinkRespCodes = append(hardlinkRespCodes, StatusFailedHardlink) continue } - p.log.Log().Msgf("created hardlink of %q into %q", match.epPathClient, newPackPath) + p.log.Log().Msgf("created hardlink: source(%s), target(%s)", match.epPathClient, newPackPath) hardlinkRespCodes = append(hardlinkRespCodes, StatusSuccessfulHardlink) } diff --git a/internal/torrents/torrents.go b/internal/torrents/torrents.go index 70adba3..839c634 100644 --- a/internal/torrents/torrents.go +++ b/internal/torrents/torrents.go @@ -5,6 +5,7 @@ package torrents import ( "bytes" + "cmp" "fmt" "path/filepath" "slices" @@ -12,6 +13,11 @@ import ( "github.com/anacrolix/torrent/metainfo" ) +type Episode struct { + Name string + Size int64 +} + func ParseTorrentInfoFromTorrentBytes(torrentBytes []byte) (metainfo.Info, error) { r := bytes.NewReader(torrentBytes) @@ -28,23 +34,33 @@ func ParseTorrentInfoFromTorrentBytes(torrentBytes []byte) (metainfo.Info, error return info, nil } -func GetEpisodesFromTorrentInfo(info metainfo.Info) ([]string, error) { - var fileNames []string +func GetEpisodesFromTorrentInfo(info metainfo.Info) ([]Episode, error) { + var episodes []Episode if !info.IsDir() { - return []string{}, fmt.Errorf("not a directory") + return []Episode{}, fmt.Errorf("not a directory") } for _, file := range info.Files { - if filepath.Ext(file.BestPath()[0]) == ".mkv" { - fileNames = append(fileNames, file.BestPath()...) + for _, path := range file.BestPath() { + if filepath.Ext(path) != ".mkv" { + continue + } + + episodes = append(episodes, Episode{ + Name: path, + Size: file.Length, + }) + break } } - if len(fileNames) == 0 { - return []string{}, fmt.Errorf("no .mkv files found") + if len(episodes) == 0 { + return []Episode{}, fmt.Errorf("no .mkv files found") } - slices.Sort(fileNames) - return fileNames, nil + slices.SortStableFunc(episodes, func(a, b Episode) int { + return cmp.Compare(a.Name, b.Name) + }) + return episodes, nil } diff --git a/internal/utils/formatting.go b/internal/utils/formatting.go index 084a678..1731363 100644 --- a/internal/utils/formatting.go +++ b/internal/utils/formatting.go @@ -9,6 +9,8 @@ import ( "regexp" "strings" + "seasonpackarr/internal/torrents" + "github.com/moistari/rls" ) @@ -52,19 +54,38 @@ func ReplaceParentFolder(path, newFolder string) string { return newPath } -func MatchFileNameToSeasonPackNaming(episodeInClientPath string, torrentEpisodeNames []string) (string, error) { - episodeRls := rls.ParseString(filepath.Base(episodeInClientPath)) +func MatchEpToSeasonPackEp(epInClientPath string, epInClientSize int64, torrentEp torrents.Episode) (string, error) { + episodeRls := rls.ParseString(filepath.Base(epInClientPath)) + torrentEpRls := rls.ParseString(filepath.Base(torrentEp.Name)) + + err := compareEpisodes(episodeRls, torrentEpRls) + if err != nil { + return epInClientPath, err + } + + if epInClientSize != torrentEp.Size { + return epInClientPath, fmt.Errorf("size mismatch") + } + + return filepath.Join(filepath.Dir(epInClientPath), filepath.Base(torrentEp.Name)), nil +} + +func compareEpisodes(episodeRls, torrentEpRls rls.Release) error { + if episodeRls.Series != torrentEpRls.Series { + return fmt.Errorf("series mismatch") + } + + if episodeRls.Episode != torrentEpRls.Episode { + return fmt.Errorf("episode mismatch") + } - for _, packPath := range torrentEpisodeNames { - packRls := rls.ParseString(filepath.Base(packPath)) + if episodeRls.Resolution != torrentEpRls.Resolution { + return fmt.Errorf("resolution mismatch") + } - if (episodeRls.Series == packRls.Series) && - (episodeRls.Episode == packRls.Episode) && - (episodeRls.Resolution == packRls.Resolution) && - (episodeRls.Group == packRls.Group) { - return filepath.Join(filepath.Dir(episodeInClientPath), filepath.Base(packPath)), nil - } + if rls.MustNormalize(episodeRls.Group) != rls.MustNormalize(torrentEpRls.Group) { + return fmt.Errorf("group mismatch") } - return episodeInClientPath, fmt.Errorf("couldn't find matching episode in season pack, using existing file name") + return nil } diff --git a/internal/utils/formatting_test.go b/internal/utils/formatting_test.go index 10f4567..159e4ae 100644 --- a/internal/utils/formatting_test.go +++ b/internal/utils/formatting_test.go @@ -4,8 +4,11 @@ package utils import ( + "fmt" "testing" + "seasonpackarr/internal/torrents" + "github.com/moistari/rls" "github.com/stretchr/testify/assert" ) @@ -231,81 +234,114 @@ func Test_ReplaceParentFolder(t *testing.T) { } } -func TestMatchFileNameToSeasonPackNaming(t *testing.T) { +func Test_MatchEpToSeasonPackEp(t *testing.T) { + type args struct { + epInClientPath string + epInClientSize int64 + torrentEp torrents.Episode + } tests := []struct { - name string - epPathClient string - torrentEps []string - want string - wantErr bool + name string + args args + want string + wantErr assert.ErrorAssertionFunc }{ { - name: "found_match", - epPathClient: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - torrentEps: []string{ - "Series Title 2022 S02E01 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - "Series Title 2022 S02E02 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + name: "found_match", + args: args{ + epInClientPath: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + epInClientSize: 2316560346, + torrentEp: torrents.Episode{ + Name: "Series Title 2022 S02E01 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + Size: 2316560346, + }, }, want: "Series Title 2022 S02E01 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - wantErr: false, + wantErr: assert.NoError, }, { - name: "wrong_episode", - epPathClient: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - torrentEps: []string{ - "Series Title 2022 S02E02 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - "Series Title 2022 S02E03 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + name: "wrong_episode", + args: args{ + epInClientPath: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + epInClientSize: 2316560346, + torrentEp: torrents.Episode{ + Name: "Series Title 2022 S02E02 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + Size: 2316560346, + }, }, want: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - wantErr: true, + wantErr: assert.Error, }, { - name: "found_no_match", - epPathClient: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - torrentEps: []string{}, - want: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - wantErr: true, + name: "wrong_season", + args: args{ + epInClientPath: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + epInClientSize: 2316560346, + torrentEp: torrents.Episode{ + Name: "Series Title 2022 S03E01 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + Size: 2316560346, + }, + }, + want: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + wantErr: assert.Error, }, { - name: "wrong_season", - epPathClient: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - torrentEps: []string{ - "Series Title 2022 S03E01 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - "Series Title 2022 S03E02 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + name: "wrong_resolution", + args: args{ + epInClientPath: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + epInClientSize: 2316560346, + torrentEp: torrents.Episode{ + Name: "Series Title 2022 S02E01 2160p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + Size: 2316560346, + }, }, want: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - wantErr: true, + wantErr: assert.Error, }, { - name: "wrong_resolution", - epPathClient: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - torrentEps: []string{ - "Series Title 2022 S02E01 720p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - "Series Title 2022 S02E02 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + name: "wrong_rlsgrp", + args: args{ + epInClientPath: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + epInClientSize: 2316560346, + torrentEp: torrents.Episode{ + Name: "Series Title 2022 S02E01 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-OtherRlsGrp.mkv", + Size: 2316560346, + }, }, want: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - wantErr: true, + wantErr: assert.Error, }, { - name: "wrong_rlsgrp", - epPathClient: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - torrentEps: []string{ - "Series Title 2022 S02E01 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp2.mkv", - "Series Title 2022 S02E02 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + name: "wrong_size", + args: args{ + epInClientPath: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + epInClientSize: 2316560346, + torrentEp: torrents.Episode{ + Name: "Series Title 2022 S02E01 1080p Test ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + Size: 2278773077, + }, }, want: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", - wantErr: true, + wantErr: assert.Error, + }, + { + name: "found_no_match", + args: args{ + epInClientPath: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + epInClientSize: 2316560346, + torrentEp: torrents.Episode{}, + }, + want: "Series Title 2022 S02E01 1080p ATVP WEB-DL DDP 5.1 Atmos H.264-RlsGrp.mkv", + wantErr: assert.Error, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := MatchFileNameToSeasonPackNaming(tt.epPathClient, tt.torrentEps) - - if (err != nil) != tt.wantErr { - t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr) + got, err := MatchEpToSeasonPackEp(tt.args.epInClientPath, tt.args.epInClientSize, tt.args.torrentEp) + if !tt.wantErr(t, err, fmt.Sprintf("MatchEpToSeasonPackEp(%v, %v, %v)", tt.args.epInClientPath, tt.args.epInClientSize, tt.args.torrentEp)) { return } - assert.Equalf(t, tt.want, got, "MatchFileNameToSeasonPackNaming(%v, %v)", tt.epPathClient, tt.torrentEps) + assert.Equalf(t, tt.want, got, "MatchEpToSeasonPackEp(%v, %v, %v)", tt.args.epInClientPath, tt.args.epInClientSize, tt.args.torrentEp) }) } }