0.3.35
This commit is contained in:
@@ -26,10 +26,21 @@ type myShowsResponse struct {
|
||||
}
|
||||
|
||||
type notificationsResponse struct {
|
||||
Notifications []store.UserNotification `json:"notifications"`
|
||||
Notifications []notificationResponse `json:"notifications"`
|
||||
Preferences store.NotificationPreferences `json:"preferences"`
|
||||
}
|
||||
|
||||
type notificationAction struct {
|
||||
Kind string `json:"kind"`
|
||||
Label string `json:"label"`
|
||||
CompletedLabel string `json:"completedLabel"`
|
||||
}
|
||||
|
||||
type notificationResponse struct {
|
||||
store.UserNotification
|
||||
Action *notificationAction `json:"action,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) handleMyShows(w http.ResponseWriter, r *http.Request, sess store.Session) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
@@ -178,12 +189,73 @@ func (s *Server) handleNotifications(w http.ResponseWriter, r *http.Request, ses
|
||||
writeError(w, http.StatusInternalServerError, "could not load notifications")
|
||||
return
|
||||
}
|
||||
visible := filterStoredNotifications(notifications, prefs)
|
||||
shows := []store.UserShow{}
|
||||
if value, showsErr := s.store.UserShows(r.Context(), sess.EmbyUserID); showsErr == nil {
|
||||
shows = value
|
||||
} else {
|
||||
s.loggerFor(r.Context()).Warn("My Shows unavailable for notification actions", "error", showsErr)
|
||||
}
|
||||
sonarrSeries := []sonarr.Series{}
|
||||
if s.sonarrEnabled(r.Context()) {
|
||||
if value, seriesErr := s.sonarrSeriesCatalogue(r.Context()); seriesErr == nil {
|
||||
sonarrSeries = value
|
||||
} else {
|
||||
s.loggerFor(r.Context()).Warn("Sonarr status unavailable for notifications", "error", seriesErr)
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, notificationsResponse{
|
||||
Notifications: filterStoredNotifications(notifications, prefs),
|
||||
Notifications: notificationResponses(visible, shows, sonarrSeries),
|
||||
Preferences: prefs,
|
||||
})
|
||||
}
|
||||
|
||||
func notificationResponses(
|
||||
notifications []store.UserNotification, shows []store.UserShow, series []sonarr.Series,
|
||||
) []notificationResponse {
|
||||
showItemsBySeriesKey := map[string]string{}
|
||||
for _, show := range shows {
|
||||
matched := matchSonarrSeries(show, series)
|
||||
if matched == nil {
|
||||
continue
|
||||
}
|
||||
seriesKey := sonarrSeriesStatusKey(*matched)
|
||||
if seriesKey == "" || strings.TrimSpace(show.ItemID) == "" {
|
||||
continue
|
||||
}
|
||||
showItemsBySeriesKey[seriesKey] = show.ItemID
|
||||
}
|
||||
result := make([]notificationResponse, 0, len(notifications))
|
||||
for _, notification := range notifications {
|
||||
response := notificationResponse{UserNotification: notification}
|
||||
if notification.Kind == "show-cancelled" {
|
||||
if itemID := showItemsBySeriesKey[notificationSeriesKey(notification.SourceKey)]; itemID != "" {
|
||||
response.ItemID = itemID
|
||||
response.Action = ¬ificationAction{
|
||||
Kind: "remove-my-show",
|
||||
Label: "Remove from My Shows",
|
||||
CompletedLabel: "Removed from My Shows",
|
||||
}
|
||||
}
|
||||
}
|
||||
result = append(result, response)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func notificationSeriesKey(sourceKey string) string {
|
||||
const prefix = "show-cancelled:"
|
||||
if !strings.HasPrefix(sourceKey, prefix) {
|
||||
return ""
|
||||
}
|
||||
trimmed := strings.TrimPrefix(sourceKey, prefix)
|
||||
cut := strings.LastIndex(trimmed, ":")
|
||||
if cut <= 0 {
|
||||
return ""
|
||||
}
|
||||
return trimmed[:cut]
|
||||
}
|
||||
|
||||
func (s *Server) syncReturnNotifications(
|
||||
r *http.Request, sess store.Session, prefs store.NotificationPreferences,
|
||||
) {
|
||||
@@ -314,6 +386,8 @@ func (s *Server) handleNotificationAction(
|
||||
err = s.store.MarkNotificationUnread(r.Context(), sess.EmbyUserID, id)
|
||||
case "dismiss":
|
||||
err = s.store.DismissNotification(r.Context(), sess.EmbyUserID, id)
|
||||
case "remove-my-show":
|
||||
err = s.removeMyShowFromNotification(r, sess, id)
|
||||
default:
|
||||
writeError(w, http.StatusNotFound, "unknown notification action")
|
||||
return
|
||||
@@ -325,6 +399,34 @@ func (s *Server) handleNotificationAction(
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) removeMyShowFromNotification(
|
||||
r *http.Request, sess store.Session, notificationID int64,
|
||||
) error {
|
||||
notifications, err := s.store.UserNotifications(r.Context(), sess.EmbyUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shows, err := s.store.UserShows(r.Context(), sess.EmbyUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
series := []sonarr.Series{}
|
||||
if s.sonarrEnabled(r.Context()) {
|
||||
value, err := s.sonarrSeriesCatalogue(r.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
series = value
|
||||
}
|
||||
for _, notification := range notificationResponses(notifications, shows, series) {
|
||||
if notification.ID != notificationID || notification.Action == nil || notification.ItemID == "" {
|
||||
continue
|
||||
}
|
||||
return s.store.DeleteUserShow(r.Context(), sess.EmbyUserID, notification.ItemID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeJSON(w http.ResponseWriter, r *http.Request, out any) bool {
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/appupdate"
|
||||
"github.com/ponzischeme89/memby/server/internal/sonarr"
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
@@ -81,3 +82,45 @@ func TestClearableNotificationIDsHonourPreferences(t *testing.T) {
|
||||
t.Fatalf("an empty list should clear nothing, got %v", ids)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotificationResponsesShowRemoveMyShowsActionOnlyForCurrentFollow(t *testing.T) {
|
||||
shows := []store.UserShow{
|
||||
{ItemID: "emby-1", Title: "The Peripheral", Year: intPtr(2022)},
|
||||
{ItemID: "emby-2", Title: "Shogun", Year: intPtr(2024)},
|
||||
}
|
||||
series := []sonarr.Series{
|
||||
{ID: 10, TVDBID: 123, Title: "The Peripheral", Year: 2022},
|
||||
{ID: 11, TVDBID: 456, Title: "Shogun", Year: 2024},
|
||||
}
|
||||
notifications := []store.UserNotification{
|
||||
{ID: 1, Kind: "show-cancelled", SourceKey: "show-cancelled:tvdb:123:9", Title: "The Peripheral - Cancelled"},
|
||||
{ID: 2, Kind: "show-cancelled", SourceKey: "show-cancelled:tvdb:999:8", Title: "Unknown Show - Cancelled"},
|
||||
{ID: 3, Kind: "show-return", SourceKey: "show-return:emby-2:2026-08-27", Title: "Shogun returns"},
|
||||
}
|
||||
|
||||
got := notificationResponses(notifications, shows, series)
|
||||
if got[0].Action == nil {
|
||||
t.Fatalf("cancelled followed show should offer an action: %#v", got[0])
|
||||
}
|
||||
if got[0].Action.Kind != "remove-my-show" || got[0].Action.Label != "Remove from My Shows" {
|
||||
t.Fatalf("unexpected action = %#v", got[0].Action)
|
||||
}
|
||||
if got[0].ItemID != "emby-1" {
|
||||
t.Fatalf("cancelled followed show item = %q, want emby-1", got[0].ItemID)
|
||||
}
|
||||
if got[1].Action != nil {
|
||||
t.Fatalf("unfollowed cancelled show should not offer an action: %#v", got[1])
|
||||
}
|
||||
if got[2].Action != nil {
|
||||
t.Fatalf("non-cancelled notification should not offer an action: %#v", got[2])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelledNotificationCopy(t *testing.T) {
|
||||
if got := cancelledNotificationTitle("The Peripheral"); got != "The Peripheral - Cancelled" {
|
||||
t.Fatalf("title = %q", got)
|
||||
}
|
||||
if got := cancelledNotificationBody("The Peripheral"); got != "The Peripheral has been cancelled by the network." {
|
||||
t.Fatalf("body = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,8 +131,8 @@ func (s *Server) scanSonarrLifecycle(ctx context.Context) (sonarrLifecycleResult
|
||||
Source: notifySourceSonarrLifecycle,
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
Title: "Show cancelled",
|
||||
Body: change.Current.Title + " has been cancelled.",
|
||||
Title: cancelledNotificationTitle(change.Current.Title),
|
||||
Body: cancelledNotificationBody(change.Current.Title),
|
||||
SourceKey: sourceKey,
|
||||
EventAt: &eventAt,
|
||||
Metadata: map[string]any{"series": change.Current.Title, "status": change.Current.Status},
|
||||
@@ -163,6 +163,14 @@ func sonarrLifecycleNotificationKind(previous, current string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func cancelledNotificationTitle(title string) string {
|
||||
return strings.TrimSpace(title) + " - Cancelled"
|
||||
}
|
||||
|
||||
func cancelledNotificationBody(title string) string {
|
||||
return strings.TrimSpace(title) + " has been cancelled by the network."
|
||||
}
|
||||
|
||||
func sonarrSeriesStatusKey(series sonarr.Series) string {
|
||||
if series.TVDBID > 0 {
|
||||
return "tvdb:" + strconv.Itoa(series.TVDBID)
|
||||
|
||||
Reference in New Issue
Block a user