diff --git a/go.mod b/go.mod index 7a541c8..ba283a3 100644 --- a/go.mod +++ b/go.mod @@ -17,4 +17,4 @@ require ( github.com/google/uuid v1.3.0 // indirect ) -replace github.com/jrudio/go-plex-client v0.0.0-20220106065909-9e1d590b99aa => github.com/RoyXiang/go-plex-client v0.0.0-20220305043318-06cfadcf82d7 +replace github.com/jrudio/go-plex-client v0.0.0-20220106065909-9e1d590b99aa => github.com/RoyXiang/go-plex-client v0.0.0-20220310030059-ef5991e7e4e2 diff --git a/go.sum b/go.sum index ee4b448..fc5bdfc 100644 --- a/go.sum +++ b/go.sum @@ -1,8 +1,8 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/RoyXiang/go-plex-client v0.0.0-20220305043318-06cfadcf82d7 h1:UirnG494f6HbVpTONQeAh3UtG8OWGuaCz6H6ezVkIHE= -github.com/RoyXiang/go-plex-client v0.0.0-20220305043318-06cfadcf82d7/go.mod h1:twidbPLE4eUk3CgDno5uCzpnPRboBTElH+iJrQO7S4w= +github.com/RoyXiang/go-plex-client v0.0.0-20220310030059-ef5991e7e4e2 h1:CV52ZCM7Xjtk3524V31RoX3mRvBHa2PZL6YZDOVb87U= +github.com/RoyXiang/go-plex-client v0.0.0-20220310030059-ef5991e7e4e2/go.mod h1:twidbPLE4eUk3CgDno5uCzpnPRboBTElH+iJrQO7S4w= github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= diff --git a/handler/const.go b/handler/const.go index 81d391b..29d29cb 100644 --- a/handler/const.go +++ b/handler/const.go @@ -46,5 +46,7 @@ const ( const ( sessionUnplayed sessionStatus = iota sessionPlaying + sessionPaused + sessionStopped sessionWatched ) diff --git a/handler/plex.go b/handler/plex.go index 5a55d9c..ff0e2a2 100644 --- a/handler/plex.go +++ b/handler/plex.go @@ -18,7 +18,6 @@ import ( "time" "github.com/RoyXiang/plexproxy/common" - "github.com/go-chi/chi/v5/middleware" "github.com/jrudio/go-plex-client" "github.com/xanderstrike/plexhooks" ) @@ -102,31 +101,52 @@ func (u *plexUser) UnmarshalBinary(data []byte) error { return json.Unmarshal(data, u) } -func (c *PlexClient) ServeHTTP(w http.ResponseWriter, r *http.Request) { - path := r.URL.EscapedPath() - switch { - case path == "/video/:/transcode/universal/decision": - if c.disableTranscode { - r = c.disableTranscoding(r) - } - case strings.HasPrefix(path, "/web/"): - if c.redirectWebApp && r.Method == http.MethodGet { - http.Redirect(w, r, "https://app.plex.tv/desktop", http.StatusFound) - return +func (a sessionData) Check(b sessionData) (bool, bool) { + if a.status != b.status { + return true, true + } + if a.progress != b.progress { + if a.status == sessionPlaying { + return true, false } + return true, true + } + if a.lastEvent != b.lastEvent { + return true, false } + if len(a.guids) != len(b.guids) { + return true, false + } + return false, false +} - c.proxy.ServeHTTP(w, r) +func (c *PlexClient) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := r.URL.EscapedPath() + if c.redirectWebApp && strings.HasPrefix(path, "/web/") && r.Method == http.MethodGet { + http.Redirect(w, r, "https://app.plex.tv/desktop", http.StatusFound) + return + } - if w.(middleware.WrapResponseWriter).Status() == http.StatusOK { - query := r.URL.Query() - switch path { - case "/:/scrobble", "/:/unscrobble": - go clearCachedMetadata(query.Get("key"), r.Header.Get(headerToken)) - case "/:/timeline": - go c.syncTimelineWithPlaxt(r) + if token := r.Header.Get(headerToken); token != "" { + // If it is an authorized request + if user := c.GetUser(token); user != nil { + switch path { + case "/:/scrobble", "/:/unscrobble": + ratingKey := r.URL.Query().Get("key") + if ratingKey != "" { + go clearCachedMetadata(ratingKey, user.Id) + } + case "/:/timeline": + go c.syncTimelineWithPlaxt(r, user) + case "/video/:/transcode/universal/decision": + if c.disableTranscode { + r = c.disableTranscoding(r) + } + } } } + + c.proxy.ServeHTTP(w, r) } func (c *PlexClient) IsTokenSet() bool { @@ -238,22 +258,16 @@ func (c *PlexClient) GetAccountInfo(token string) (user plex.UserPlexTV) { return } -func (c *PlexClient) syncTimelineWithPlaxt(r *http.Request) { +func (c *PlexClient) syncTimelineWithPlaxt(r *http.Request, user *plexUser) { if c.plaxtUrl == "" || !c.IsTokenSet() { return } - token := r.Header.Get(headerToken) clientUuid := r.Header.Get(headerClientIdentity) ratingKey := r.URL.Query().Get("ratingKey") playbackTime := r.URL.Query().Get("time") state := r.URL.Query().Get("state") - if token == "" || clientUuid == "" || ratingKey == "" || playbackTime == "" || state == "" { - return - } - - user := c.GetUser(token) - if user == nil { + if clientUuid == "" || ratingKey == "" || playbackTime == "" || state == "" { return } @@ -274,21 +288,19 @@ func (c *PlexClient) syncTimelineWithPlaxt(r *http.Request) { return } session := c.sessions[sessionKey] - sessionChanged := false - - serverIdentifier := c.getServerIdentifier() - if serverIdentifier == "" { + if session.status == sessionWatched { return } - var section plex.Directory - sectionId := session.metadata.LibrarySectionID.String() - if c.getLibrarySection(sectionId) { - section = c.sections[sectionId] - if section.Type != "show" && section.Type != "movie" { + + progress := int(math.Round(float64(viewOffset) / float64(session.metadata.Duration) * 100.0)) + if progress == 0 { + if session.progress >= watchedThreshold { + // time would become 0 once a playback session was finished + progress = 100 + viewOffset = session.metadata.Duration + } else if session.status != sessionUnplayed { return } - } else { - return } externalGuids := make([]plexhooks.ExternalGuid, 0) @@ -305,52 +317,65 @@ func (c *PlexClient) syncTimelineWithPlaxt(r *http.Request) { }) } session.guids = externalGuids - sessionChanged = true } else { externalGuids = session.guids } var event string - progress := int(math.Round(float64(viewOffset) / float64(session.metadata.Duration) * 100.0)) + var threshold int switch state { case "playing": - if session.status == sessionPlaying { - if progress >= 100 { - event = webhookEventScrobble - } else { - event = webhookEventResume - } - } else { + threshold = 100 + if session.status == sessionUnplayed || session.status == sessionStopped { event = webhookEventPlay - } - case "paused": - if progress >= watchedThreshold && session.status == sessionPlaying { - event = webhookEventScrobble } else { - event = webhookEventPause + event = webhookEventResume } + case "paused": + threshold = watchedThreshold + event = webhookEventPause case "stopped": - if progress >= watchedThreshold && session.status == sessionPlaying { - event = webhookEventScrobble - } else { - event = webhookEventStop - } + threshold = watchedThreshold + event = webhookEventStop } - if event == "" || session.status == sessionWatched { + if event == "" { return - } else if event == webhookEventScrobble { - session.status = sessionWatched - sessionChanged = true - go clearCachedMetadata(ratingKey, r.Header.Get(headerToken)) - } else if event == webhookEventStop { - go clearCachedMetadata(ratingKey, r.Header.Get(headerToken)) - } else if session.status == sessionUnplayed { + } else if progress >= threshold { + event = webhookEventScrobble + } + switch event { + case webhookEventPlay, webhookEventResume: session.status = sessionPlaying - sessionChanged = true + case webhookEventPause: + session.status = sessionPaused + case webhookEventStop: + session.status = sessionStopped + go clearCachedMetadata(ratingKey, user.Id) + case webhookEventScrobble: + session.status = sessionWatched + go clearCachedMetadata(ratingKey, user.Id) } - if sessionChanged || event != session.lastEvent { - session.lastEvent = event + session.lastEvent = event + session.progress = progress + shouldUpdate, shouldScrobble := session.Check(c.sessions[sessionKey]) + if shouldUpdate { c.sessions[sessionKey] = session + } + if !shouldScrobble { + return + } + + serverIdentifier := c.getServerIdentifier() + if serverIdentifier == "" { + return + } + var section plex.Directory + sectionId := session.metadata.LibrarySectionID.String() + if c.getLibrarySection(sectionId) { + section = c.sections[sectionId] + if section.Type != "show" && section.Type != "movie" { + return + } } else { return } @@ -391,9 +416,10 @@ func (c *PlexClient) getServerIdentifier() string { defer c.mu.RUnlock() identity, err := c.client.GetServerIdentity() - if err == nil { - c.serverIdentifier = &identity.MediaContainer.MachineIdentifier + if err != nil { + return "" } + c.serverIdentifier = &identity.MediaContainer.MachineIdentifier } return *c.serverIdentifier } diff --git a/handler/structs.go b/handler/structs.go index 18af95a..8506f48 100644 --- a/handler/structs.go +++ b/handler/structs.go @@ -23,6 +23,7 @@ type sessionData struct { guids []plexhooks.ExternalGuid lastEvent string status sessionStatus + progress int } type plexUser struct { diff --git a/handler/utils.go b/handler/utils.go index 7faf476..cb51b9a 100644 --- a/handler/utils.go +++ b/handler/utils.go @@ -118,7 +118,7 @@ func writeToCache(key string, resp *http.Response, ttl time.Duration) { redisClient.Set(context.Background(), key, b, ttl) } -func clearCachedMetadata(ratingKey, token string) { +func clearCachedMetadata(ratingKey string, userId int) { if redisClient == nil { return } @@ -130,11 +130,8 @@ func clearCachedMetadata(ratingKey, token string) { if ratingKey != "" { pattern += fmt.Sprintf("/library/metadata/%s", ratingKey) } - if token != "" { - user := plexClient.GetUser(token) - if user != nil { - pattern += fmt.Sprintf("*%s=%d", headerUserId, user.Id) - } + if userId > 0 { + pattern += fmt.Sprintf("*%s=%d", headerUserId, userId) } pattern += "*" diff --git a/handler/websocket.go b/handler/websocket.go index 181ffbf..2611b27 100644 --- a/handler/websocket.go +++ b/handler/websocket.go @@ -69,6 +69,6 @@ func wsOnActivity(n plex.NotificationContainer) { } } if isMetadataChanged { - clearCachedMetadata("", "") + clearCachedMetadata("", 0) } }