Publish current app and server
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ponzischeme89/memby/server/internal/sonarr"
|
||||
"github.com/ponzischeme89/memby/server/internal/store"
|
||||
)
|
||||
|
||||
type myShowResponse struct {
|
||||
store.UserShow
|
||||
SonarrStatus string `json:"sonarrStatus"`
|
||||
NextEpisode *time.Time `json:"nextEpisode,omitempty"`
|
||||
Lifecycle string `json:"lifecycle"`
|
||||
Monitored bool `json:"monitored"`
|
||||
}
|
||||
|
||||
type myShowsResponse struct {
|
||||
Shows []myShowResponse `json:"shows"`
|
||||
}
|
||||
|
||||
type notificationsResponse struct {
|
||||
Notifications []store.UserNotification `json:"notifications"`
|
||||
Preferences store.NotificationPreferences `json:"preferences"`
|
||||
}
|
||||
|
||||
func (s *Server) handleMyShows(w http.ResponseWriter, r *http.Request, sess store.Session) {
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
s.listMyShows(w, r, sess)
|
||||
case http.MethodPost:
|
||||
var show store.UserShow
|
||||
if !decodeJSON(w, r, &show) {
|
||||
return
|
||||
}
|
||||
show.ItemID = strings.TrimSpace(show.ItemID)
|
||||
show.Title = strings.TrimSpace(show.Title)
|
||||
if show.ItemID == "" || show.Title == "" {
|
||||
writeError(w, http.StatusBadRequest, "itemId and title are required")
|
||||
return
|
||||
}
|
||||
if err := s.store.SaveUserShow(r.Context(), sess.EmbyUserID, show); err != nil {
|
||||
s.log.Error("save user show failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "could not save show")
|
||||
return
|
||||
}
|
||||
s.listMyShows(w, r, sess)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleMyShow(w http.ResponseWriter, r *http.Request, sess store.Session) {
|
||||
itemID := strings.TrimSpace(r.PathValue("id"))
|
||||
if itemID == "" {
|
||||
writeError(w, http.StatusBadRequest, "show id is required")
|
||||
return
|
||||
}
|
||||
if err := s.store.DeleteUserShow(r.Context(), sess.EmbyUserID, itemID); err != nil {
|
||||
s.log.Error("delete user show failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "could not remove show")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (s *Server) listMyShows(w http.ResponseWriter, r *http.Request, sess store.Session) {
|
||||
saved, err := s.store.UserShows(r.Context(), sess.EmbyUserID)
|
||||
if err != nil {
|
||||
s.log.Error("list user shows failed", "error", err)
|
||||
writeError(w, http.StatusInternalServerError, "could not load shows")
|
||||
return
|
||||
}
|
||||
sonarrSeries := []sonarr.Series{}
|
||||
if s.sonarr != nil {
|
||||
if value, seriesErr := s.sonarr.Series(r.Context()); seriesErr == nil {
|
||||
sonarrSeries = value
|
||||
} else {
|
||||
s.log.Warn("Sonarr status unavailable for My Shows", "error", seriesErr)
|
||||
}
|
||||
}
|
||||
result := make([]myShowResponse, 0, len(saved))
|
||||
for _, show := range saved {
|
||||
matched := matchSonarrSeries(show, sonarrSeries)
|
||||
response := myShowResponse{
|
||||
UserShow: show,
|
||||
SonarrStatus: "Not found",
|
||||
Lifecycle: "Unknown",
|
||||
}
|
||||
if matched != nil {
|
||||
response.SonarrStatus = sonarrStatus(*matched)
|
||||
response.NextEpisode = matched.NextAiring
|
||||
response.Lifecycle = seriesLifecycle(matched.Status)
|
||||
response.Monitored = matched.Monitored
|
||||
}
|
||||
result = append(result, response)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, myShowsResponse{Shows: result})
|
||||
}
|
||||
|
||||
func matchSonarrSeries(show store.UserShow, all []sonarr.Series) *sonarr.Series {
|
||||
key := normalizedShowTitle(show.Title)
|
||||
for i := range all {
|
||||
candidate := &all[i]
|
||||
if normalizedShowTitle(candidate.Title) != key {
|
||||
continue
|
||||
}
|
||||
if show.Year == nil || candidate.Year == 0 || candidate.Year == *show.Year {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizedShowTitle(value string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if r >= 'A' && r <= 'Z' {
|
||||
return r + ('a' - 'A')
|
||||
}
|
||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
||||
return r
|
||||
}
|
||||
return -1
|
||||
}, value)
|
||||
}
|
||||
|
||||
func seriesLifecycle(status string) string {
|
||||
switch strings.ToLower(status) {
|
||||
case "continuing", "upcoming":
|
||||
return "Continuing"
|
||||
case "ended", "deleted":
|
||||
return "Cancelled"
|
||||
default:
|
||||
if status == "" {
|
||||
return "Unknown"
|
||||
}
|
||||
return strings.ToUpper(status[:1]) + strings.ToLower(status[1:])
|
||||
}
|
||||
}
|
||||
|
||||
func isContinuingSonarrStatus(status string) bool {
|
||||
return strings.EqualFold(status, "continuing") || strings.EqualFold(status, "upcoming")
|
||||
}
|
||||
|
||||
func sonarrStatus(series sonarr.Series) string {
|
||||
if series.Monitored {
|
||||
return "Monitored"
|
||||
}
|
||||
return "Not monitored"
|
||||
}
|
||||
|
||||
func (s *Server) handleNotifications(w http.ResponseWriter, r *http.Request, sess store.Session) {
|
||||
prefs, err := s.store.NotificationPreferences(r.Context(), sess.EmbyUserID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not load notification preferences")
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodPut {
|
||||
if !decodeJSON(w, r, &prefs) {
|
||||
return
|
||||
}
|
||||
if err := s.store.SetNotificationPreferences(r.Context(), sess.EmbyUserID, prefs); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not save notification preferences")
|
||||
return
|
||||
}
|
||||
}
|
||||
if prefs.Enabled && prefs.ShowReturnAlerts {
|
||||
s.syncReturnNotifications(r, sess, prefs)
|
||||
}
|
||||
notifications, err := s.store.UserNotifications(r.Context(), sess.EmbyUserID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not load notifications")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, notificationsResponse{
|
||||
Notifications: notifications,
|
||||
Preferences: prefs,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) syncReturnNotifications(
|
||||
r *http.Request, sess store.Session, prefs store.NotificationPreferences,
|
||||
) {
|
||||
if s.sonarr == nil {
|
||||
return
|
||||
}
|
||||
shows, err := s.store.UserShows(r.Context(), sess.EmbyUserID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
all, err := s.sonarr.Series(r.Context())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
until := now.AddDate(0, 0, prefs.LeadDays)
|
||||
for _, show := range shows {
|
||||
series := matchSonarrSeries(show, all)
|
||||
if series == nil || series.NextAiring == nil ||
|
||||
series.NextAiring.Before(now) || series.NextAiring.After(until) {
|
||||
continue
|
||||
}
|
||||
days := int(series.NextAiring.Sub(now).Hours()/24) + 1
|
||||
message := fmt.Sprintf("%s returns in %d days.", show.Title, days)
|
||||
if days <= 1 {
|
||||
message = show.Title + " returns tomorrow."
|
||||
} else if days == 7 {
|
||||
message = show.Title + " returns next week."
|
||||
}
|
||||
sourceKey := "show-return:" + show.ItemID + ":" + series.NextAiring.UTC().Format("2006-01-02")
|
||||
_ = s.store.UpsertNotification(
|
||||
r.Context(), sess.EmbyUserID, sourceKey, "show-return", show.ItemID,
|
||||
"New episode coming", message, series.NextAiring,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleNotificationAction(
|
||||
w http.ResponseWriter, r *http.Request, sess store.Session,
|
||||
) {
|
||||
id, err := strconv.ParseInt(r.PathValue("id"), 10, 64)
|
||||
if err != nil || id < 1 {
|
||||
writeError(w, http.StatusBadRequest, "invalid notification id")
|
||||
return
|
||||
}
|
||||
switch r.PathValue("action") {
|
||||
case "read":
|
||||
err = s.store.MarkNotificationRead(r.Context(), sess.EmbyUserID, id)
|
||||
case "dismiss":
|
||||
err = s.store.DismissNotification(r.Context(), sess.EmbyUserID, id)
|
||||
default:
|
||||
writeError(w, http.StatusNotFound, "unknown notification action")
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "could not update notification")
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func decodeJSON(w http.ResponseWriter, r *http.Request, out any) bool {
|
||||
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(out); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "invalid request")
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user