diff --git a/README.md b/README.md index 75120b59..637524ba 100644 --- a/README.md +++ b/README.md @@ -261,6 +261,13 @@ To add your webhook to Sonarr, Radarr or Lidarr, do: 7. Set the URL to Autoscan's URL and add `/triggers/:name` where name is the name set in the trigger's config. 8. Optional: set username and password. +##### Experimental support for more events + +Autoscan also supports the new `On Rename`, `On Series Delete` and `On Episode File Delete` Sonarr events. +We have marked support for these events as experimental as the webhook payload may still change. +In addition, we are not 100% sure whether these three events cover all the possible file system interactions. +So for now, please do keep using Bernard or the Inotify trigger to fetch all scans. + ### Processor Triggers pass the Scans they receive to the processor. diff --git a/triggers/lidarr/lidarr.go b/triggers/lidarr/lidarr.go index 90d035e1..f7c72b3d 100644 --- a/triggers/lidarr/lidarr.go +++ b/triggers/lidarr/lidarr.go @@ -66,7 +66,7 @@ func (h handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { l.Trace().Interface("event", event).Msg("Received JSON body") if strings.EqualFold(event.Type, "Test") { - l.Debug().Msg("Received test event") + l.Info().Msg("Received test event") rw.WriteHeader(http.StatusOK) return } @@ -105,6 +105,7 @@ func (h handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusOK) l.Info(). Str("path", scans[0].Folder). + Str("event", event.Type). Msg("Scan moved to processor") } diff --git a/triggers/radarr/radarr.go b/triggers/radarr/radarr.go index e0ecf059..f0a5d355 100644 --- a/triggers/radarr/radarr.go +++ b/triggers/radarr/radarr.go @@ -70,7 +70,7 @@ func (h handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { rlog.Trace().Interface("event", event).Msg("Received JSON body") if strings.EqualFold(event.Type, "Test") { - rlog.Debug().Msg("Received test event") + rlog.Info().Msg("Received test event") rw.WriteHeader(http.StatusOK) return } @@ -100,6 +100,7 @@ func (h handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { rw.WriteHeader(http.StatusOK) rlog.Info(). Str("path", folderPath). + Str("event", event.Type). Msg("Scan moved to processor") } diff --git a/triggers/sonarr/sonarr.go b/triggers/sonarr/sonarr.go index f1fded83..3fc9631e 100644 --- a/triggers/sonarr/sonarr.go +++ b/triggers/sonarr/sonarr.go @@ -53,6 +53,12 @@ type sonarrEvent struct { Series struct { Path string } `json:"series"` + + RenamedFiles []struct { + // use PreviousPath as the Series.Path might have changed. + PreviousPath string + RelativePath string + } `json:"renamedEpisodeFiles"` } func (h handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { @@ -70,37 +76,93 @@ func (h handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { rlog.Trace().Interface("event", event).Msg("Received JSON body") if strings.EqualFold(event.Type, "Test") { - rlog.Debug().Msg("Received test event") + rlog.Info().Msg("Received test event") rw.WriteHeader(http.StatusOK) return } - if !strings.EqualFold(event.Type, "Download") || event.File.RelativePath == "" || event.Series.Path == "" { - rlog.Error().Msg("Required fields are missing") - rw.WriteHeader(http.StatusBadRequest) - return + var paths []string + + // a Download event is either an upgrade or a new file. + // the EpisodeFileDelete event shares the same request format as Download. + if strings.EqualFold(event.Type, "Download") || strings.EqualFold(event.Type, "EpisodeFileDelete") { + if event.File.RelativePath == "" || event.Series.Path == "" { + rlog.Error().Msg("Required fields are missing") + return + } + + // Use path.Dir to get the directory in which the file is located + folderPath := path.Dir(path.Join(event.Series.Path, event.File.RelativePath)) + paths = append(paths, folderPath) + } + + // An entire show has been deleted + if strings.EqualFold(event.Type, "SeriesDelete") { + if event.Series.Path == "" { + rlog.Error().Msg("Required fields are missing") + return + } + + // Scan the folder of the show + paths = append(paths, event.Series.Path) + } + + if strings.EqualFold(event.Type, "Rename") { + if event.Series.Path == "" { + rlog.Error().Msg("Required fields are missing") + return + } + + // Keep track of which paths we have already added to paths. + encountered := make(map[string]bool) + + for _, renamedFile := range event.RenamedFiles { + previousPath := path.Dir(renamedFile.PreviousPath) + currentPath := path.Dir(path.Join(event.Series.Path, renamedFile.RelativePath)) + + // if previousPath not in paths, then add it. + if _, ok := encountered[previousPath]; !ok { + encountered[previousPath] = true + paths = append(paths, previousPath) + } + + // if currentPath not in paths, then add it. + if _, ok := encountered[currentPath]; !ok { + encountered[currentPath] = true + paths = append(paths, currentPath) + } + } } - // Rewrite the path based on the provided rewriter. - folderPath := path.Dir(h.rewrite(path.Join(event.Series.Path, event.File.RelativePath))) + var scans []autoscan.Scan + + for _, folderPath := range paths { + folderPath := h.rewrite(folderPath) + + scan := autoscan.Scan{ + Folder: folderPath, + Priority: h.priority, + Time: now(), + } - scan := autoscan.Scan{ - Folder: folderPath, - Priority: h.priority, - Time: now(), + scans = append(scans, scan) } - err = h.callback(scan) + err = h.callback(scans...) if err != nil { - rlog.Error().Err(err).Msg("Processor could not process scan") + rlog.Error().Err(err).Msg("Processor could not process scans") rw.WriteHeader(http.StatusInternalServerError) return } + for _, scan := range scans { + rlog.Info(). + Str("path", scan.Folder). + Str("event", event.Type). + Msg("Scan moved to processor") + } + rw.WriteHeader(http.StatusOK) - rlog.Info(). - Str("path", folderPath). - Msg("Scan moved to processor") } var now = time.Now diff --git a/triggers/sonarr/sonarr_test.go b/triggers/sonarr/sonarr_test.go index d1d93537..ca68fd66 100644 --- a/triggers/sonarr/sonarr_test.go +++ b/triggers/sonarr/sonarr_test.go @@ -45,7 +45,7 @@ func TestHandler(t *testing.T) { var testCases = []Test{ { - "Scan has all the correct fields", + "Scan has all the correct fields on Download event", Given{ Config: standardConfig, Fixture: "testdata/westworld.json", @@ -61,6 +61,72 @@ func TestHandler(t *testing.T) { }, }, }, + { + "Scan on EpisodeFileDelete", + Given{ + Config: standardConfig, + Fixture: "testdata/episode_delete.json", + }, + Expected{ + StatusCode: 200, + Scans: []autoscan.Scan{ + { + Folder: "/mnt/unionfs/Media/TV/Westworld/Season 2", + Priority: 5, + Time: currentTime, + }, + }, + }, + }, + { + "Picks up the Rename event without duplicates", + Given{ + Config: standardConfig, + Fixture: "testdata/rename.json", + }, + Expected{ + StatusCode: 200, + Scans: []autoscan.Scan{ + { + Folder: "/mnt/unionfs/Media/TV/Westworld/Season 1", + Priority: 5, + Time: currentTime, + }, + { + Folder: "/mnt/unionfs/Media/TV/Westworld [imdb:tt0475784]/Season 1", + Priority: 5, + Time: currentTime, + }, + { + Folder: "/mnt/unionfs/Media/TV/Westworld/Season 2", + Priority: 5, + Time: currentTime, + }, + { + Folder: "/mnt/unionfs/Media/TV/Westworld [imdb:tt0475784]/Season 2", + Priority: 5, + Time: currentTime, + }, + }, + }, + }, + { + "Scans show folder on SeriesDelete event", + Given{ + Config: standardConfig, + Fixture: "testdata/series_delete.json", + }, + Expected{ + StatusCode: 200, + Scans: []autoscan.Scan{ + { + Folder: "/mnt/unionfs/Media/TV/Westworld", + Priority: 5, + Time: currentTime, + }, + }, + }, + }, { "Returns bad request on invalid JSON", Given{ diff --git a/triggers/sonarr/testdata/episode_delete.json b/triggers/sonarr/testdata/episode_delete.json new file mode 100644 index 00000000..582c3e02 --- /dev/null +++ b/triggers/sonarr/testdata/episode_delete.json @@ -0,0 +1,9 @@ +{ + "eventType": "EpisodeFileDelete", + "episodeFile": { + "relativePath": "Season 2/Westworld.S02E01.mkv" + }, + "series": { + "path": "/TV/Westworld" + } +} \ No newline at end of file diff --git a/triggers/sonarr/testdata/rename.json b/triggers/sonarr/testdata/rename.json new file mode 100644 index 00000000..41161485 --- /dev/null +++ b/triggers/sonarr/testdata/rename.json @@ -0,0 +1,20 @@ +{ + "eventType": "Rename", + "series": { + "path": "/TV/Westworld [imdb:tt0475784]" + }, + "renamedEpisodeFiles": [ + { + "previousPath": "/TV/Westworld/Season 1/Westworld.S01E01.mkv", + "relativePath": "Season 1/Westworld.S01E01.mkv" + }, + { + "previousPath": "/TV/Westworld/Season 1/Westworld.S01E02.mkv", + "relativePath": "Season 1/Westworld.S01E02.mkv" + }, + { + "previousPath": "/TV/Westworld/Season 2/Westworld.S01E02.mkv", + "relativePath": "Season 2/Westworld.S02E01.mkv" + } + ] +} \ No newline at end of file diff --git a/triggers/sonarr/testdata/series_delete.json b/triggers/sonarr/testdata/series_delete.json new file mode 100644 index 00000000..13b9d985 --- /dev/null +++ b/triggers/sonarr/testdata/series_delete.json @@ -0,0 +1,6 @@ +{ + "eventType": "SeriesDelete", + "series": { + "path": "/TV/Westworld" + } +} \ No newline at end of file diff --git a/triggers/sonarr/testdata/westworld.json b/triggers/sonarr/testdata/westworld.json index d176eede..59e68747 100644 --- a/triggers/sonarr/testdata/westworld.json +++ b/triggers/sonarr/testdata/westworld.json @@ -1,11 +1,9 @@ { "eventType": "Download", - "isUpgrade": false, "episodeFile": { - "relativePath": "Season 1/Westworld.S01E01.The.Original.2160p.TrueHD.Atmos.7.1.HEVC.REMUX.mkv" + "relativePath": "Season 1/Westworld.S01E01.mkv" }, "series": { - "tvdbId": 296762, "path": "/TV/Westworld" } } \ No newline at end of file