initial prototype complete, starting to package for docker

This commit is contained in:
2023-09-25 19:45:36 -04:00
parent e1e64cdad9
commit 28df368156
6 changed files with 442 additions and 0 deletions

16
go.mod Normal file
View File

@@ -0,0 +1,16 @@
module gitea.derajnet.duckdns.org/deranjer/plex-prometheus-exporter
go 1.19
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
golang.org/x/sys v0.8.0 // indirect
google.golang.org/protobuf v1.30.0 // indirect
)

28
go.sum Normal file
View File

@@ -0,0 +1,28 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4=
github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w=
github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM=
github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc=
github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg=
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=

91
main.go Normal file
View File

@@ -0,0 +1,91 @@
package main
import (
"fmt"
"log"
"net/http"
"os"
"strconv"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
type serverSettings struct {
port int
plexAddr string
plexToken string
plexTimeout int
metricsPrefix string
metricsMediaCollectingIntervalSeconds int
}
func envGetter(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func setupServerSettings() serverSettings {
port, err := strconv.Atoi(envGetter("PORT", "9545"))
if err != nil {
log.Fatal("Unable to parse port string to int, failing....")
}
plexToken, value := os.LookupEnv("PLEX_TOKEN")
if !value {
log.Fatal("No plex token provided, failing....")
}
plexTimeout, err := strconv.Atoi(envGetter("PLEX_TIMEOUT", "10"))
if err != nil {
log.Fatal("Unable to parse plextimeout string to int, failing....")
}
metricsMediaCollectingIntervalSeconds, err := strconv.Atoi(envGetter("METRICS_MEDIA_COLLECTING_INTERVAL_SECONDS", "300"))
if err != nil {
log.Fatal("Unable to parse METRICS_MEDIA_COLLECTING_INTERVAL_SECONDS string to int, failing....")
}
return serverSettings{
port: port,
plexAddr: envGetter("PLEX_ADDR", "http://localhost:32400"),
plexToken: plexToken,
plexTimeout: plexTimeout,
metricsPrefix: envGetter("METRICS_PREFIX", "PLEX"),
metricsMediaCollectingIntervalSeconds: metricsMediaCollectingIntervalSeconds,
}
}
func main() {
os.Setenv("PORT", "9545")
os.Setenv("PLEX_ADDR", "https://plex.derajnet.duckdns.org:32400")
os.Setenv("PLEX_TOKEN", "5ezJu5cjnhoAbPJRKngs")
os.Setenv("PLEX_TIMEOUT", "10")
os.Setenv("METRICS_PREFIX", "PLEX")
os.Setenv("METRICS_MEDIA_COLLECTING_INTERVAL_SECONDS", "300")
settings := setupServerSettings()
// Setup plex client
pc := setupClient(&settings)
prometheus.MustRegister(plexActiveSessions)
prometheus.MustRegister(plexNumMovies)
prometheus.MustRegister(plexNumTVShows)
go func() {
for {
time.Sleep(time.Second * 5)
fmt.Println("Collecting info...")
stats := pc.gatherAllStats()
plexActiveSessions.Set(stats.currentSessions)
plexNumMovies.Set(stats.numMovies)
plexNumTVShows.Set(stats.numTV)
}
}()
// m := NewMetrics(reg)
http.Handle("/metrics", promhttp.Handler())
http.ListenAndServe(fmt.Sprintf(":%d", settings.port), nil)
}

105
plex_client.go Normal file
View File

@@ -0,0 +1,105 @@
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"net/http"
)
type plexStats struct {
currentSessions float64
numMovies float64
numTV float64
}
type PlexClient struct {
baseUrl string
client *http.Client
plexToken string
}
func setupClient(settings *serverSettings) PlexClient {
transCfg := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
return PlexClient{
baseUrl: settings.plexAddr,
client: &http.Client{Transport: transCfg},
plexToken: settings.plexToken,
}
}
// sendRequest sends a request and resturns response (always assuming a get request)
func (pc *PlexClient) sendRequest(url string, body []byte) (result []byte) {
fullUrl := pc.baseUrl + url
req, err := http.NewRequest("GET", fullUrl, bytes.NewBuffer(body))
if err != nil {
fmt.Println("Error creating new request", err)
return
}
req.Header.Add("Accept", "application/json")
q := req.URL.Query()
q.Add("X-Plex-Token", pc.plexToken)
req.URL.RawQuery = q.Encode()
res, err := pc.client.Do(req)
if err != nil {
fmt.Println("Error sending request", err)
return
}
defer res.Body.Close()
resBody, err := io.ReadAll(res.Body)
if err != nil {
fmt.Println("Unable to read response body: ", err)
return
}
return resBody
}
func (pc *PlexClient) getActiveSessions() *ActiveSessions {
result := pc.sendRequest("/status/sessions", nil)
currentActiveSessions := ActiveSessions{}
err := json.Unmarshal(result, &currentActiveSessions)
if err != nil {
fmt.Println("Error unmarshalling current active sessions: ", err)
}
return &currentActiveSessions
}
func (pc *PlexClient) getNumMovies() *ActiveSessions {
result := pc.sendRequest("/library/sections/3/all", nil) //Hard code library ID for now TODO: Grab from library/sections call
libraryAll := ActiveSessions{}
err := json.Unmarshal(result, &libraryAll)
if err != nil {
fmt.Println("Error unmarshalling current active sessions: ", err)
}
return &libraryAll
}
func (pc *PlexClient) getNumTV() *ActiveSessions {
result := pc.sendRequest("/library/sections/2/all", nil) //Hard code library ID for now TODO: Grab from library/sections call
libraryAll := ActiveSessions{}
err := json.Unmarshal(result, &libraryAll)
if err != nil {
fmt.Println("Error unmarshalling current active sessions: ", err)
}
return &libraryAll
}
func (pc *PlexClient) gatherAllStats() *plexStats {
allStats := plexStats{}
sessionsData := pc.getActiveSessions()
libraryDetails := pc.getNumMovies()
tvshowDetails := pc.getNumTV()
allStats.currentSessions = float64(sessionsData.MediaContainer.Size)
allStats.numMovies = float64(libraryDetails.MediaContainer.Size)
allStats.numTV = float64(tvshowDetails.MediaContainer.Size)
return &allStats
}

188
plex_models.go Normal file
View File

@@ -0,0 +1,188 @@
package main
type ActiveSessions struct {
MediaContainer struct {
Size int `json:"size"`
Metadata []struct {
AddedAt int `json:"addedAt"`
Art string `json:"art"`
Duration int `json:"duration"`
GrandparentArt string `json:"grandparentArt"`
GrandparentGUID string `json:"grandparentGuid"`
GrandparentKey string `json:"grandparentKey"`
GrandparentRatingKey string `json:"grandparentRatingKey"`
GrandparentThumb string `json:"grandparentThumb"`
GrandparentTitle string `json:"grandparentTitle"`
GUID string `json:"guid"`
Key string `json:"key"`
LastViewedAt int `json:"lastViewedAt"`
LibrarySectionID string `json:"librarySectionID"`
LibrarySectionKey string `json:"librarySectionKey"`
LibrarySectionTitle string `json:"librarySectionTitle"`
MusicAnalysisVersion string `json:"musicAnalysisVersion"`
OriginalTitle string `json:"originalTitle"`
ParentGUID string `json:"parentGuid"`
ParentIndex int `json:"parentIndex"`
ParentKey string `json:"parentKey"`
ParentRatingKey string `json:"parentRatingKey"`
ParentTitle string `json:"parentTitle"`
RatingKey string `json:"ratingKey"`
SessionKey string `json:"sessionKey"`
Title string `json:"title"`
Type string `json:"type"`
ViewOffset int `json:"viewOffset"`
Media []struct {
AudioChannels int `json:"audioChannels"`
AudioCodec string `json:"audioCodec"`
Bitrate int `json:"bitrate"`
Container string `json:"container"`
Duration int `json:"duration"`
ID string `json:"id"`
Part []struct {
Container string `json:"container"`
Duration int `json:"duration"`
File string `json:"file"`
ID string `json:"id"`
Key string `json:"key"`
Size int `json:"size"`
Stream []struct {
AlbumGain string `json:"albumGain"`
AlbumPeak string `json:"albumPeak"`
AlbumRange string `json:"albumRange"`
AudioChannelLayout string `json:"audioChannelLayout"`
Bitrate int `json:"bitrate"`
Channels int `json:"channels"`
Codec string `json:"codec"`
DisplayTitle string `json:"displayTitle"`
ExtendedDisplayTitle string `json:"extendedDisplayTitle"`
Gain string `json:"gain"`
ID string `json:"id"`
Index int `json:"index"`
Loudness string `json:"loudness"`
Lra string `json:"lra"`
Peak string `json:"peak"`
SamplingRate int `json:"samplingRate"`
Selected bool `json:"selected"`
StreamType int `json:"streamType"`
} `json:"Stream"`
} `json:"Part"`
} `json:"Media"`
User struct {
ID string `json:"id"`
Thumb string `json:"thumb"`
Title string `json:"title"`
} `json:"User"`
Player struct {
Address string `json:"address"`
Device string `json:"device"`
MachineIdentifier string `json:"machineIdentifier"`
Platform string `json:"platform"`
PlatformVersion string `json:"platformVersion"`
Product string `json:"product"`
Profile string `json:"profile"`
RemotePublicAddress string `json:"remotePublicAddress"`
State string `json:"state"`
Title string `json:"title"`
Version string `json:"version"`
Local bool `json:"local"`
Relayed bool `json:"relayed"`
Secure bool `json:"secure"`
UserID int `json:"userID"`
} `json:"Player"`
} `json:"Metadata"`
} `json:"MediaContainer"`
}
type LibraryAll struct {
MediaContainer struct {
Size int `json:"size"`
AllowSync bool `json:"allowSync"`
Art string `json:"art"`
Identifier string `json:"identifier"`
LibrarySectionID int `json:"librarySectionID"`
LibrarySectionTitle string `json:"librarySectionTitle"`
LibrarySectionUUID string `json:"librarySectionUUID"`
MediaTagPrefix string `json:"mediaTagPrefix"`
MediaTagVersion int `json:"mediaTagVersion"`
Thumb string `json:"thumb"`
Title1 string `json:"title1"`
Title2 string `json:"title2"`
ViewGroup string `json:"viewGroup"`
ViewMode int `json:"viewMode"`
Metadata []struct {
RatingKey string `json:"ratingKey"`
Key string `json:"key"`
GUID string `json:"guid"`
Studio string `json:"studio,omitempty"`
Type string `json:"type"`
Title string `json:"title"`
ContentRating string `json:"contentRating,omitempty"`
Summary string `json:"summary"`
Rating float64 `json:"rating,omitempty"`
AudienceRating float64 `json:"audienceRating,omitempty"`
Year int `json:"year,omitempty"`
Tagline string `json:"tagline,omitempty"`
Thumb string `json:"thumb"`
Art string `json:"art,omitempty"`
Duration int `json:"duration"`
OriginallyAvailableAt string `json:"originallyAvailableAt,omitempty"`
AddedAt int `json:"addedAt"`
UpdatedAt int `json:"updatedAt"`
AudienceRatingImage string `json:"audienceRatingImage,omitempty"`
ChapterSource string `json:"chapterSource,omitempty"`
PrimaryExtraKey string `json:"primaryExtraKey,omitempty"`
RatingImage string `json:"ratingImage,omitempty"`
Media []struct {
ID int `json:"id"`
Duration int `json:"duration"`
Bitrate int `json:"bitrate"`
Width int `json:"width"`
Height int `json:"height"`
AspectRatio float64 `json:"aspectRatio"`
AudioChannels int `json:"audioChannels"`
AudioCodec string `json:"audioCodec"`
VideoCodec string `json:"videoCodec"`
VideoResolution string `json:"videoResolution"`
Container string `json:"container"`
VideoFrameRate string `json:"videoFrameRate"`
OptimizedForStreaming int `json:"optimizedForStreaming"`
Has64BitOffsets bool `json:"has64bitOffsets"`
VideoProfile string `json:"videoProfile"`
Part []struct {
ID int `json:"id"`
Key string `json:"key"`
Duration int `json:"duration"`
File string `json:"file"`
Size int `json:"size"`
Container string `json:"container"`
Has64BitOffsets bool `json:"has64bitOffsets"`
OptimizedForStreaming bool `json:"optimizedForStreaming"`
VideoProfile string `json:"videoProfile"`
} `json:"Part"`
} `json:"Media"`
Genre []struct {
Tag string `json:"tag"`
} `json:"Genre,omitempty"`
Country []struct {
Tag string `json:"tag"`
} `json:"Country,omitempty"`
Director []struct {
Tag string `json:"tag"`
} `json:"Director,omitempty"`
Writer []struct {
Tag string `json:"tag"`
} `json:"Writer,omitempty"`
Role []struct {
Tag string `json:"tag"`
} `json:"Role,omitempty"`
TitleSort string `json:"titleSort,omitempty"`
ViewCount int `json:"viewCount,omitempty"`
LastViewedAt int `json:"lastViewedAt,omitempty"`
SkipCount int `json:"skipCount,omitempty"`
OriginalTitle string `json:"originalTitle,omitempty"`
ViewOffset int `json:"viewOffset,omitempty"`
CreatedAtAccuracy string `json:"createdAtAccuracy,omitempty"`
CreatedAtTZOffset string `json:"createdAtTZOffset,omitempty"`
} `json:"Metadata"`
} `json:"MediaContainer"`
}

14
prom_metrics.go Normal file
View File

@@ -0,0 +1,14 @@
package main
import "github.com/prometheus/client_golang/prometheus"
var plexActiveSessions = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "plex_active_sessions", Help: "Shows number of active sessions in plex"})
var plexNumMovies = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "plex_num_movies", Help: "Number of movies in the plex database",
})
var plexNumTVShows = prometheus.NewGauge(prometheus.GaugeOpts{
Name: "plex_num_tv_shows", Help: "Number of TV Shows in the plex database",
})