Denmaseno 8 роки тому
батько
коміт
104fd1f61e
4 змінених файлів з 702 додано та 0 видалено
  1. 195 0
      plex.go
  2. 34 0
      plex_directory.go
  3. 113 0
      plex_media.go
  4. 360 0
      plex_server.go

+ 195 - 0
plex.go

@@ -0,0 +1,195 @@
+package plexapi
+
+import (
+	"crypto/tls"
+	"encoding/xml"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"net/http/cookiejar"
+	"strings"
+	"sync"
+	"time"
+
+	"gopkg.in/yaml.v2"
+)
+
+// API struct
+type API struct {
+	User     string     `yaml:"user"`
+	Password string     `yaml:"password"`
+	HTTP     HTTPConfig `yaml:"http"`
+	client   *http.Client
+	userInfo UserInfo
+	servers  map[string]*Server
+}
+
+// HTTPConfig struct
+type HTTPConfig struct {
+	Timeout    time.Duration `yaml:"timeout"`
+	WorkerSize int           `yaml:"workerSize"`
+}
+
+// UserInfo struct
+type UserInfo struct {
+	XMLName  xml.Name `xml:"user"`
+	UserName string   `xml:"username"`
+	Token    string   `xml:"authentication-token"`
+}
+
+// MediaContainer struct
+type MediaContainer struct {
+	Paths                         []string    `xml:"-"`
+	Keys                          []KeyInfo   `xml:"-"`
+	XMLName                       xml.Name    `xml:"MediaContainer"`
+	Servers                       []Server    `xml:"Server"`
+	Directories                   []Directory `xml:"Directory"`
+	Videos                        []Video     `xml:"Video"`
+	Size                          int         `xml:"size,attr"`
+	AllowCameraUpload             int         `xml:"allowCameraUpload,attr"`
+	AllowSync                     int         `xml:"allowSync,attr"`
+	AllowChannelAccess            int         `xml:"allowChannelAccess,attr"`
+	RequestParametersInCookie     int         `xml:"requestParametersInCookie,attr"`
+	Sync                          int         `xml:"sync,attr"`
+	TranscoderActiveVideoSessions int         `xml:"transcoderActiveVideoSessions,attr"`
+	TranscoderAudio               int         `xml:"transcoderAudio,attr"`
+	TranscoderVideo               int         `xml:"transcoderVideo,attr"`
+	TranscoderVideoBitrates       string      `xml:"transcoderVideoBitrates,attr"`
+	TranscoderVideoQualities      string      `xml:"transcoderVideoQualities,attr"`
+	TranscoderVideoResolutions    string      `xml:"transcoderVideoResolutions,attr"`
+	FriendlyName                  string      `xml:"friendlyName,attr"`
+	MachineIdentifier             string      `xml:"machineIdentifier,attr"`
+}
+
+// KeyInfo struct
+type KeyInfo struct {
+	Key   string
+	Type  string
+	Title string
+}
+
+// Progress struct
+type Progress struct {
+	Server  string
+	Command string
+	Delta   int
+}
+
+// LoadConfig func
+func (api *API) LoadConfig(name string) {
+	cfg, err := ioutil.ReadFile(name)
+	if err != nil {
+		log.Fatal("Unable to load config", err)
+	}
+	yaml.Unmarshal(cfg, &api)
+}
+
+func (api *API) setHeader(req *http.Request) {
+	req.Header.Add("X-Plex-Product", "plex-sync")
+	req.Header.Add("X-Plex-Version", "1.0.0")
+	req.Header.Add("X-Plex-Client-Identifier", "donkey")
+	if api.userInfo.Token != "" {
+		req.Header.Add("X-Plex-Token", api.userInfo.Token)
+	}
+}
+
+// Login func
+func (api *API) login() error {
+	if api.client == nil {
+		cookieJar, _ := cookiejar.New(nil)
+		tr := &http.Transport{
+			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+		}
+		api.client = &http.Client{
+			Timeout:   time.Duration(time.Second * api.HTTP.Timeout),
+			Transport: tr,
+			Jar:       cookieJar,
+		}
+	}
+
+	reqBody := fmt.Sprintf("user[login]=%s&user[password]=%s", api.User, api.Password)
+	req, err := http.NewRequest("POST", "https://plex.tv/users/sign_in.xml", strings.NewReader(reqBody))
+	api.setHeader(req)
+	if err != nil {
+		return err
+	}
+
+	resp, err := api.client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return err
+	}
+	return xml.Unmarshal(body, &api.userInfo)
+}
+
+// GetServers func
+func (api *API) GetServers() (servers map[string]*Server, err error) {
+	if api.userInfo.Token == "" {
+		err := api.login()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	req, err := http.NewRequest("GET", "https://plex.tv/pms/servers.xml", nil)
+	if err != nil {
+		return nil, err
+	}
+	api.setHeader(req)
+
+	resp, err := api.client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+
+	var data MediaContainer
+	err = xml.Unmarshal(body, &data)
+	api.servers = make(map[string]*Server)
+	for _, s := range data.Servers {
+		ns := s
+		ns.api = api
+		api.servers[s.Name] = &ns
+	}
+
+	var wg sync.WaitGroup
+	wg.Add(len(api.servers))
+
+	for _, ps := range api.servers {
+		server := ps
+		go func() {
+			server.Check()
+			wg.Done()
+		}()
+	}
+
+	wg.Wait()
+	return api.servers, err
+}
+
+// GetServer func
+func (api *API) GetServer(name string) (server *Server, err error) {
+	if api.servers == nil {
+		_, err := api.GetServers()
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	s, ok := api.servers[name]
+	if ok {
+		return s, nil
+	}
+	return nil, fmt.Errorf("Server '%s' not found", name)
+}

+ 34 - 0
plex_directory.go

@@ -0,0 +1,34 @@
+package plexapi
+
+import "encoding/xml"
+
+// Directory struct
+type Directory struct {
+	Keys       []KeyInfo  `xml:"-"`
+	Paths      []string   `xml:"-"`
+	XMLName    xml.Name   `xml:"Directory"`
+	Locations  []Location `xml:"Location"`
+	Count      int        `xml:"count,attr"`
+	Key        string     `xml:"key,attr"`
+	Title      string     `xml:"title,attr"`
+	Art        string     `xml:"art,attr"`
+	Composite  string     `xml:"composite,attr"`
+	Filters    int        `xml:"filters,attr"`
+	Refreshing int        `xml:"refreshing,attr"`
+	Thumb      string     `xml:"thumb,attr"`
+	Type       string     `xml:"type,attr"`
+	Agent      string     `xml:"agent,attr"`
+	Scanner    string     `xml:"scanner,attr"`
+	Language   string     `xml:"language,attr"`
+	UUID       string     `xml:"uuid,attr"`
+	UpdatedAt  string     `xml:"updatedAt,attr"`
+	CreatedAt  string     `xml:"createdAt,attr"`
+	AllowSync  int        `xml:"allowSync,attr"`
+}
+
+// Location struct
+type Location struct {
+	XMLName xml.Name `xml:"Location"`
+	ID      int      `xml:"id,attr"`
+	Path    string   `xml:"path,attr"`
+}

+ 113 - 0
plex_media.go

@@ -0,0 +1,113 @@
+package plexapi
+
+import "encoding/xml"
+
+// Video struct
+type Video struct {
+	server                *Server
+	Keys                  []KeyInfo `xml:"-"`
+	Paths                 []string  `xml:"-"`
+	FID                   string    `xml:"-"`
+	XMLName               xml.Name  `xml:"Video"`
+	GUID                  string    `xml:"guid,attr"`
+	RatingKey             string    `xml:"ratingKey,attr"`
+	Key                   string    `xml:"key,attr"`
+	Studio                string    `xml:"studio,attr"`
+	Type                  string    `xml:"type,attr"`
+	Title                 string    `xml:"title,attr"`
+	TitleSort             string    `xml:"titleSort,attr"`
+	ContentRating         string    `xml:"contentRating,attr"`
+	Summary               string    `xml:"summary,attr"`
+	Rating                string    `xml:"rating,attr"`
+	ViewCount             string    `xml:"viewCount,attr"`
+	ViewOffset            string    `xml:"viewOffset,attr"`
+	LastViewedAt          string    `xml:"lastViewedAt,attr"`
+	Year                  string    `xml:"year,attr"`
+	Tagline               string    `xml:"tagline,attr"`
+	Thumb                 string    `xml:"thumb,attr"`
+	Art                   string    `xml:"art,attr"`
+	Duration              string    `xml:"duration,attr"`
+	OriginallyAvailableAt string    `xml:"originallyAvailableAt,attr"`
+	AddedAt               string    `xml:"addedAt,attr"`
+	UpdatedAt             string    `xml:"updatedAt,attr"`
+	ChapterSource         string    `xml:"chapterSource,attr"`
+	Media                 Media     `xml:"Media"`
+	Genre                 Genre     `xml:"Genre"`
+	Writer                Writer    `xml:"Writer"`
+	Country               Country   `xml:"Country"`
+	Role                  Role      `xml:"Role"`
+	Director              Director  `xml:"Director"`
+}
+
+// Media struct
+type Media struct {
+	XMLName         xml.Name `xml:"Media"`
+	VideoResolution string   `xml:"videoResolution,attr"`
+	ID              string   `xml:"id,attr"`
+	Duration        string   `xml:"duration,attr"`
+	Bitrate         string   `xml:"bitrate,attr"`
+	Width           string   `xml:"width,attr"`
+	Height          string   `xml:"height,attr"`
+	AspectRatio     string   `xml:"aspectRatio,attr"`
+	AudioChannels   string   `xml:"audioChannels,attr"`
+	AudioCodec      string   `xml:"audioCodec,attr"`
+	VideoCodec      string   `xml:"videoCodec,attr"`
+	Container       string   `xml:"container,attr"`
+	VideoFrameRate  string   `xml:"videoFrameRate,attr"`
+	VideoProfile    string   `xml:"videoProfile,attr"`
+	Parts           []Part   `xml:"Part"`
+}
+
+// Part struct
+type Part struct {
+	XMLName             xml.Name `xml:"Part"`
+	ID                  string   `xml:"id,attr"`
+	Key                 string   `xml:"key,attr"`
+	Duration            string   `xml:"duration,attr"`
+	File                string   `xml:"file,attr"`
+	Sizecontainer       string   `xml:"sizecontainer,attr"`
+	DeepAnalysisVersion string   `xml:"deepAnalysisVersion,attr"`
+	RequiredBandwidths  string   `xml:"requiredBandwidths,attr"`
+	VideoProfile        string   `xml:"videoProfile,attr"`
+}
+
+// Genre struct
+type Genre struct {
+	XMLName xml.Name `xml:"Genre"`
+	Tag     string   `xml:"tag,attr"`
+}
+
+// Writer struct
+type Writer struct {
+	XMLName xml.Name `xml:"Writer"`
+	Tag     string   `xml:"tag,attr"`
+}
+
+// Country struct
+type Country struct {
+	XMLName xml.Name `xml:"Country"`
+	Tag     string   `xml:"tag,attr"`
+}
+
+// Role struct
+type Role struct {
+	XMLName xml.Name `xml:"Role"`
+	Tag     string   `xml:"tag,attr"`
+}
+
+// Director struct
+type Director struct {
+	XMLName xml.Name `xml:"Director"`
+	Tag     string   `xml:"tag,attr"`
+}
+
+// Data struct
+type Data struct {
+	Videos    map[string]Video
+	UpdatedAt map[string]int64
+}
+
+// GetServer func
+func (v *Video) GetServer() *Server {
+	return v.server
+}

+ 360 - 0
plex_server.go

@@ -0,0 +1,360 @@
+package plexapi
+
+import (
+	"encoding/xml"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+	"sync"
+
+	"code.senomas.com/go/util"
+	log "github.com/Sirupsen/logrus"
+)
+
+// Server struct
+type Server struct {
+	api               *API
+	host              string
+	XMLName           xml.Name `xml:"Server"`
+	AccessToken       string   `xml:"accessToken,attr"`
+	Name              string   `xml:"name,attr"`
+	Address           string   `xml:"address,attr"`
+	Port              int      `xml:"port,attr"`
+	Version           string   `xml:"version,attr"`
+	Scheme            string   `xml:"scheme,attr"`
+	Host              string   `xml:"host,attr"`
+	LocalAddresses    string   `xml:"localAddresses,attr"`
+	MachineIdentifier string   `xml:"machineIdentifier,attr"`
+	CreatedAt         int      `xml:"createdAt,attr"`
+	UpdatedAt         int      `xml:"updatedAt,attr"`
+	Owned             int      `xml:"owned,attr"`
+	Synced            int      `xml:"synced,attr"`
+}
+
+func (server *Server) setHeader(req *http.Request) {
+	req.Header.Add("X-Plex-Product", "plex-sync")
+	req.Header.Add("X-Plex-Version", "1.0.0")
+	req.Header.Add("X-Plex-Client-Identifier", "donkey")
+	if server.api.userInfo.Token != "" {
+		req.Header.Add("X-Plex-Token", server.api.userInfo.Token)
+	}
+}
+
+func (server *Server) getContainer(url string) (container MediaContainer, err error) {
+	log.WithField("url", url).Debugf("GET")
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		log.WithField("url", url).WithError(err).Errorf("http.GET")
+		return container, err
+	}
+	server.setHeader(req)
+
+	resp, err := server.api.client.Do(req)
+	if err != nil {
+		return container, err
+	}
+	defer resp.Body.Close()
+
+	log.WithField("url", url).WithField("status", resp.Status).Debugf("RESP")
+
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		log.WithField("url", url).WithError(err).Errorf("RESP")
+		return container, err
+	}
+
+	err = xml.Unmarshal(body, &container)
+	return container, err
+}
+
+// Check func
+func (server *Server) Check() {
+	var urls []string
+	urls = append(urls, fmt.Sprintf("https://%s:%v", server.Address, server.Port))
+	for _, la := range strings.Split(server.LocalAddresses, ",") {
+		urls = append(urls, fmt.Sprintf("https://%s:32400", la))
+	}
+
+	var checkOnce util.CheckOnce
+	var wg sync.WaitGroup
+
+	out := make(chan string)
+	defer close(out)
+
+	for _, pu := range urls {
+		url := pu
+		wg.Add(1)
+		go func() error {
+			defer wg.Done()
+
+			if !checkOnce.IsDone() {
+				log.Debugf("CHECK %s %s", server.Name, url)
+				c, err := server.getContainer(url)
+				if err != nil {
+					log.Debugf("ERROR GET %s '%s': %v", server.Name, url, err)
+					return err
+				}
+
+				if c.FriendlyName != server.Name {
+					log.Fatal("WRONG SERVER ", c.FriendlyName, "  ", server.Name)
+				}
+				// log.Debugf("RESP BODY\n%s", util.JSONPrettyPrint(c))
+				checkOnce.Done(func() {
+					out <- url
+				})
+			}
+			return nil
+		}()
+	}
+
+	go func() {
+		wg.Wait()
+		checkOnce.Done(func() {
+			out <- ""
+		})
+	}()
+
+	server.host = <-out
+	log.Debugf("URL %s  %s", server.Name, server.host)
+}
+
+// Perform func
+func (server *Server) Perform(cmd, path string) error {
+	if server.host != "" {
+		url := server.host + path
+		log.WithField("url", url).Debugf(cmd)
+		req, err := http.NewRequest(cmd, url, nil)
+		if err != nil {
+			log.WithField("url", url).WithError(err).Errorf("http.%s", cmd)
+			return err
+		}
+		server.setHeader(req)
+
+		resp, err := server.api.client.Do(req)
+		if err != nil {
+			return err
+		}
+		defer resp.Body.Close()
+
+		log.WithField("url", url).WithField("status", resp.Status).Debugf("RESP")
+
+		body, err := ioutil.ReadAll(resp.Body)
+		if err != nil {
+			log.WithField("url", url).WithError(err).Errorf("RESP")
+			return err
+		}
+
+		log.Debugf("RESP BODY\n%s", body)
+		return err
+	}
+	return fmt.Errorf("NO HOST")
+}
+
+// GetContainer func
+func (server *Server) GetContainer(path string) (container MediaContainer, err error) {
+	if server.host != "" {
+		container, err = server.getContainer(server.host + path)
+		return container, err
+	}
+	return container, fmt.Errorf("NO HOST")
+}
+
+// GetDirectories func
+func (server *Server) GetDirectories() (directories []Directory, err error) {
+	container, err := server.GetContainer("/library/sections")
+	if err != nil {
+		return directories, err
+	}
+	for _, d := range container.Directories {
+		nd := d
+		// nd.server = server
+		directories = append(directories, nd)
+	}
+	return directories, err
+}
+
+// GetVideos func
+func (server *Server) GetVideos(wg *sync.WaitGroup, out chan<- interface{}) {
+	cs := make(chan MediaContainer)
+	dirs := make(chan Directory)
+
+	wg.Add(1)
+	go func() {
+		container, err := server.GetContainer("/library/sections")
+		if err == nil {
+			cs <- container
+		} else {
+			wg.Done()
+		}
+	}()
+	for i, il := 0, server.api.HTTP.WorkerSize; i < il; i++ {
+		go func() {
+			for c := range cs {
+				func() {
+					defer wg.Done()
+					for _, d := range c.Directories {
+						wg.Add(1)
+						d.Paths = c.Paths
+						d.Keys = c.Keys
+						d.Keys = append(d.Keys, KeyInfo{Key: d.Key, Type: d.Type, Title: d.Title})
+						dirs <- d
+					}
+					for _, v := range c.Videos {
+						// if v.GUID == "" {
+						// 	meta, err := server.GetMeta(v)
+						// 	if err != nil {
+						// 		log.Fatal("GetMeta ", err)
+						// 	}
+						// 	v = meta
+						// }
+						v.server = server
+						v.Paths = c.Paths
+						v.Keys = c.Keys
+						var idx []string
+						for _, px := range v.Media.Parts {
+							for _, kk := range v.Paths {
+								if strings.HasPrefix(px.File, kk) {
+									idx = append(idx, px.File[len(kk):])
+								}
+							}
+						}
+						if len(idx) > 0 {
+							v.FID = strings.Join(idx, ":")
+							v.FID = strings.Replace(v.FID, "\\", "/", -1)
+						} else {
+							v.FID = v.GUID
+						}
+						wg.Add(1)
+						out <- v
+					}
+				}()
+			}
+		}()
+		go func() {
+			for d := range dirs {
+				func() {
+					defer wg.Done()
+
+					if strings.HasPrefix(d.Key, "/library/") {
+						cc, err := server.GetContainer(d.Key)
+						if err == nil {
+							wg.Add(1)
+							cc.Keys = d.Keys
+							cc.Paths = d.Paths
+							for _, l := range d.Locations {
+								if l.Path != "" {
+									cc.Paths = append(cc.Paths, l.Path)
+								}
+							}
+							cs <- cc
+						}
+					} else {
+						cc, err := server.GetContainer(fmt.Sprintf("/library/sections/%v/all", d.Key))
+						if err == nil {
+							wg.Add(1)
+							cc.Keys = d.Keys
+							cc.Paths = d.Paths
+							for _, l := range d.Locations {
+								if l.Path != "" {
+									cc.Paths = append(cc.Paths, l.Path)
+								}
+							}
+							cs <- cc
+						}
+					}
+				}()
+			}
+		}()
+	}
+}
+
+// GetMeta func
+func (server *Server) GetMeta(video Video) (meta Video, err error) {
+	if video.GUID != "" {
+		return video, nil
+	}
+	var mc MediaContainer
+	mc, err = server.GetContainer(video.Key)
+	if err != nil {
+		return video, err
+	}
+	return mc.Videos[0], nil
+}
+
+// MarkWatched func
+func (server *Server) MarkWatched(key string) error {
+	url := server.host + "/:/scrobble?identifier=com.plexapp.plugins.library&key=" + key
+	log.WithField("url", url).WithField("server", server.Name).Debug("MarkWatched.GET")
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return err
+	}
+	server.setHeader(req)
+
+	resp, err := server.api.client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return err
+	}
+	log.WithField("url", url).WithField("server", server.Name).Debugf("MarkWatched.RESULT\n%s", body)
+
+	return err
+}
+
+// MarkUnwatched func
+func (server *Server) MarkUnwatched(key string) error {
+	url := server.host + "/:/unscrobble?identifier=com.plexapp.plugins.library&key=" + key
+	log.WithField("url", url).WithField("server", server.Name).Debug("MarkUnwatched.GET")
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return err
+	}
+	server.setHeader(req)
+
+	resp, err := server.api.client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return err
+	}
+
+	log.WithField("url", url).WithField("server", server.Name).Debugf("MarkUnwatched.RESULT\n%s", body)
+
+	return err
+}
+
+// SetViewOffset func
+func (server *Server) SetViewOffset(key, offset string) error {
+	url := server.host + "/:/progress?key=" + key + "&identifier=com.plexapp.plugins.library&time=" + offset
+	log.WithField("url", url).WithField("server", server.Name).Debug("SetViewOffset.GET")
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return err
+	}
+	server.setHeader(req)
+
+	resp, err := server.api.client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return err
+	}
+
+	log.WithField("url", url).WithField("server", server.Name).Debugf("SetViewOffset.RESULT\n%s", body)
+
+	return err
+}