github.com/artpar/rclone@v1.67.3/backend/cache/plex.go (about) 1 //go:build !plan9 && !js 2 3 package cache 4 5 import ( 6 "bytes" 7 "crypto/tls" 8 "encoding/json" 9 "fmt" 10 "io" 11 "net/http" 12 "net/url" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/artpar/rclone/fs" 18 cache "github.com/patrickmn/go-cache" 19 "golang.org/x/net/websocket" 20 ) 21 22 const ( 23 // defPlexLoginURL is the default URL for Plex login 24 defPlexLoginURL = "https://plex.tv/users/sign_in.json" 25 defPlexNotificationURL = "%s/:/websockets/notifications?X-Plex-Token=%s" 26 ) 27 28 // PlaySessionStateNotification is part of the API response of Plex 29 type PlaySessionStateNotification struct { 30 SessionKey string `json:"sessionKey"` 31 GUID string `json:"guid"` 32 Key string `json:"key"` 33 ViewOffset int64 `json:"viewOffset"` 34 State string `json:"state"` 35 TranscodeSession string `json:"transcodeSession"` 36 } 37 38 // NotificationContainer is part of the API response of Plex 39 type NotificationContainer struct { 40 Type string `json:"type"` 41 Size int `json:"size"` 42 PlaySessionState []PlaySessionStateNotification `json:"PlaySessionStateNotification"` 43 } 44 45 // PlexNotification is part of the API response of Plex 46 type PlexNotification struct { 47 Container NotificationContainer `json:"NotificationContainer"` 48 } 49 50 // plexConnector is managing the cache integration with Plex 51 type plexConnector struct { 52 url *url.URL 53 username string 54 password string 55 token string 56 insecure bool 57 f *Fs 58 mu sync.Mutex 59 running bool 60 runningMu sync.Mutex 61 stateCache *cache.Cache 62 saveToken func(string) 63 } 64 65 // newPlexConnector connects to a Plex server and generates a token 66 func newPlexConnector(f *Fs, plexURL, username, password string, insecure bool, saveToken func(string)) (*plexConnector, error) { 67 u, err := url.ParseRequestURI(strings.TrimRight(plexURL, "/")) 68 if err != nil { 69 return nil, err 70 } 71 72 pc := &plexConnector{ 73 f: f, 74 url: u, 75 username: username, 76 password: password, 77 token: "", 78 insecure: insecure, 79 stateCache: cache.New(time.Hour, time.Minute), 80 saveToken: saveToken, 81 } 82 83 return pc, nil 84 } 85 86 // newPlexConnector connects to a Plex server and generates a token 87 func newPlexConnectorWithToken(f *Fs, plexURL, token string, insecure bool) (*plexConnector, error) { 88 u, err := url.ParseRequestURI(strings.TrimRight(plexURL, "/")) 89 if err != nil { 90 return nil, err 91 } 92 93 pc := &plexConnector{ 94 f: f, 95 url: u, 96 token: token, 97 insecure: insecure, 98 stateCache: cache.New(time.Hour, time.Minute), 99 } 100 pc.listenWebsocket() 101 102 return pc, nil 103 } 104 105 func (p *plexConnector) closeWebsocket() { 106 p.runningMu.Lock() 107 defer p.runningMu.Unlock() 108 fs.Infof("plex", "stopped Plex watcher") 109 p.running = false 110 } 111 112 func (p *plexConnector) websocketDial() (*websocket.Conn, error) { 113 u := strings.TrimRight(strings.Replace(strings.Replace( 114 p.url.String(), "http://", "ws://", 1), "https://", "wss://", 1), "/") 115 url := fmt.Sprintf(defPlexNotificationURL, u, p.token) 116 117 config, err := websocket.NewConfig(url, "http://localhost") 118 if err != nil { 119 return nil, err 120 } 121 if p.insecure { 122 config.TlsConfig = &tls.Config{InsecureSkipVerify: true} 123 } 124 return websocket.DialConfig(config) 125 } 126 127 func (p *plexConnector) listenWebsocket() { 128 p.runningMu.Lock() 129 defer p.runningMu.Unlock() 130 131 conn, err := p.websocketDial() 132 if err != nil { 133 fs.Errorf("plex", "%v", err) 134 return 135 } 136 137 p.running = true 138 go func() { 139 for { 140 if !p.isConnected() { 141 break 142 } 143 144 notif := &PlexNotification{} 145 err := websocket.JSON.Receive(conn, notif) 146 if err != nil { 147 fs.Debugf("plex", "%v", err) 148 p.closeWebsocket() 149 break 150 } 151 // we're only interested in play events 152 if notif.Container.Type == "playing" { 153 // we loop through each of them 154 for _, v := range notif.Container.PlaySessionState { 155 // event type of playing 156 if v.State == "playing" { 157 // if it's not cached get the details and cache them 158 if _, found := p.stateCache.Get(v.Key); !found { 159 req, err := http.NewRequest("GET", fmt.Sprintf("%s%s", p.url.String(), v.Key), nil) 160 if err != nil { 161 continue 162 } 163 p.fillDefaultHeaders(req) 164 resp, err := http.DefaultClient.Do(req) 165 if err != nil { 166 continue 167 } 168 var data []byte 169 data, err = io.ReadAll(resp.Body) 170 if err != nil { 171 continue 172 } 173 p.stateCache.Set(v.Key, data, cache.DefaultExpiration) 174 } 175 } else if v.State == "stopped" { 176 p.stateCache.Delete(v.Key) 177 } 178 } 179 } 180 } 181 }() 182 } 183 184 // fillDefaultHeaders will add common headers to requests 185 func (p *plexConnector) fillDefaultHeaders(req *http.Request) { 186 req.Header.Add("X-Plex-Client-Identifier", fmt.Sprintf("rclone (%v)", p.f.String())) 187 req.Header.Add("X-Plex-Product", fmt.Sprintf("rclone (%v)", p.f.Name())) 188 req.Header.Add("X-Plex-Version", fs.Version) 189 req.Header.Add("Accept", "application/json") 190 if p.token != "" { 191 req.Header.Add("X-Plex-Token", p.token) 192 } 193 } 194 195 // authenticate will generate a token based on a username/password 196 func (p *plexConnector) authenticate() error { 197 p.mu.Lock() 198 defer p.mu.Unlock() 199 200 form := url.Values{} 201 form.Set("user[login]", p.username) 202 form.Add("user[password]", p.password) 203 req, err := http.NewRequest("POST", defPlexLoginURL, strings.NewReader(form.Encode())) 204 if err != nil { 205 return err 206 } 207 p.fillDefaultHeaders(req) 208 resp, err := http.DefaultClient.Do(req) 209 if err != nil { 210 return err 211 } 212 var data map[string]interface{} 213 err = json.NewDecoder(resp.Body).Decode(&data) 214 if err != nil { 215 return fmt.Errorf("failed to obtain token: %w", err) 216 } 217 tokenGen, ok := get(data, "user", "authToken") 218 if !ok { 219 return fmt.Errorf("failed to obtain token: %v", data) 220 } 221 token, ok := tokenGen.(string) 222 if !ok { 223 return fmt.Errorf("failed to obtain token: %v", data) 224 } 225 p.token = token 226 if p.token != "" { 227 if p.saveToken != nil { 228 p.saveToken(p.token) 229 } 230 fs.Infof(p.f.Name(), "Connected to Plex server: %v", p.url.String()) 231 } 232 p.listenWebsocket() 233 234 return nil 235 } 236 237 // isConnected checks if this rclone is authenticated to Plex 238 func (p *plexConnector) isConnected() bool { 239 p.runningMu.Lock() 240 defer p.runningMu.Unlock() 241 return p.running 242 } 243 244 // isConfigured checks if this rclone is configured to use a Plex server 245 func (p *plexConnector) isConfigured() bool { 246 return p.url != nil 247 } 248 249 func (p *plexConnector) isPlaying(co *Object) bool { 250 var err error 251 if !p.isConnected() { 252 p.listenWebsocket() 253 } 254 255 remote := co.Remote() 256 if cr, yes := p.f.isWrappedByCrypt(); yes { 257 remote, err = cr.DecryptFileName(co.Remote()) 258 if err != nil { 259 fs.Debugf("plex", "can not decrypt wrapped file: %v", err) 260 return false 261 } 262 } 263 264 isPlaying := false 265 for _, v := range p.stateCache.Items() { 266 if bytes.Contains(v.Object.([]byte), []byte(remote)) { 267 isPlaying = true 268 break 269 } 270 } 271 272 return isPlaying 273 } 274 275 // adapted from: https://stackoverflow.com/a/28878037 (credit) 276 func get(m interface{}, path ...interface{}) (interface{}, bool) { 277 for _, p := range path { 278 switch idx := p.(type) { 279 case string: 280 if mm, ok := m.(map[string]interface{}); ok { 281 if val, found := mm[idx]; found { 282 m = val 283 continue 284 } 285 } 286 return nil, false 287 case int: 288 if mm, ok := m.([]interface{}); ok { 289 if len(mm) > idx { 290 m = mm[idx] 291 continue 292 } 293 } 294 return nil, false 295 } 296 } 297 return m, true 298 }