Skip to content

Commit

Permalink
feat(targets): emby (Cloudbox#13)
Browse files Browse the repository at this point in the history
  • Loading branch information
l3uddz authored Jul 28, 2020
1 parent e4d676b commit a53ef3f
Show file tree
Hide file tree
Showing 8 changed files with 346 additions and 1 deletion.
16 changes: 16 additions & 0 deletions cmd/autoscan/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"errors"
"fmt"
"github.com/cloudbox/autoscan/targets/emby"
"io"
"net/http"
"os"
Expand Down Expand Up @@ -44,6 +45,7 @@ type config struct {
// autoscan.Target
Targets struct {
Plex []plex.Config `yaml:"plex"`
Emby []emby.Config `yaml:"emby"`
} `yaml:"targets"`
}

Expand Down Expand Up @@ -230,8 +232,22 @@ func main() {
targets = append(targets, tp)
}

for _, t := range c.Targets.Emby {
tp, err := emby.New(t)
if err != nil {
log.Fatal().
Err(err).
Str("target", "emby").
Str("target_url", t.URL).
Msg("Failed initialising target")
}

targets = append(targets, tp)
}

log.Info().
Int("plex", len(c.Targets.Plex)).
Int("emby", len(c.Targets.Emby)).
Msg("Initialised targets")

// processor
Expand Down
4 changes: 4 additions & 0 deletions logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import (
)

func GetLogger(verbosity string) zerolog.Logger {
if verbosity == "" {
return log.Logger
}

level, err := zerolog.ParseLevel(verbosity)
if err != nil {
return log.Logger
Expand Down
37 changes: 37 additions & 0 deletions targets/emby/available.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package emby

import (
"fmt"
"net/http"

"github.com/cloudbox/autoscan"
)

func (t target) Available() error {
// create request
req, err := http.NewRequest("GET", autoscan.JoinURL(t.url, "emby", "System", "Info"), nil)
if err != nil {
return fmt.Errorf("%v: %w", err, autoscan.ErrFatal)
}

// set headers
req.Header.Set("X-Emby-Token", t.token)
req.Header.Set("Accept", "application/json")

// send request
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("could not check Emby availability: %v: %w",
err, autoscan.ErrTargetUnavailable)
}

defer res.Body.Close()

// validate response
if res.StatusCode != 200 {
return fmt.Errorf("could not check Emby availability: %v: %w",
res.StatusCode, autoscan.ErrTargetUnavailable)
}

return nil
}
86 changes: 86 additions & 0 deletions targets/emby/datastore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package emby

import (
"database/sql"
"fmt"

// database driver
_ "github.com/mattn/go-sqlite3"
)

func NewDatastore(path string) (*datastore, error) {
db, err := sql.Open("sqlite3", path)
if err != nil {
return nil, fmt.Errorf("could not open database: %v", err)
}

return &datastore{db: db}, nil
}

type datastore struct {
db *sql.DB
}

type library struct {
Name string
Path string
}

func (d *datastore) Libraries() ([]library, error) {
rows, err := d.db.Query(sqlSelectLibraries)
if err != nil {
return nil, fmt.Errorf("select libraries: %v", err)
}

defer rows.Close()

libraries := make([]library, 0)
for rows.Next() {
l := library{}
if err := rows.Scan(&l.Name, &l.Path); err != nil {
return nil, fmt.Errorf("scan library row: %v", err)
}

libraries = append(libraries, l)
}

return libraries, nil
}

type mediaPart struct {
ID int
File string
Size uint64
}

func (d *datastore) MediaPartByFile(path string) (*mediaPart, error) {
mp := new(mediaPart)

row := d.db.QueryRow(sqlSelectMediaPart, path)
err := row.Scan(&mp.ID, &mp.File, &mp.Size)
return mp, err
}

const (
sqlSelectLibraries = `
SELECT
mi.Name,
mi.Path
FROM
MediaItems mi
WHERE
mi.type = 3 AND mi.ParentId = 1
`
sqlSelectMediaPart = `
SELECT
mi.Id,
mi.Path,
mi.Size
FROM
MediaItems mi
WHERE
mi.Path = $1
LIMIT
1
`
)
60 changes: 60 additions & 0 deletions targets/emby/emby.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package emby

import (
"github.com/cloudbox/autoscan"
"github.com/rs/zerolog"
)

type Config struct {
Database string `yaml:"database"`
URL string `yaml:"url"`
Token string `yaml:"token"`
Rewrite autoscan.Rewrite `yaml:"rewrite"`
Verbosity string `yaml:"verbosity"`
}

type target struct {
url string
token string
libraries []library

log zerolog.Logger
rewrite autoscan.Rewriter
store *datastore
}

func New(c Config) (*target, error) {
rewriter, err := autoscan.NewRewriter(c.Rewrite)
if err != nil {
return nil, err
}

store, err := NewDatastore(c.Database)
if err != nil {
return nil, err
}

libraries, err := store.Libraries()
if err != nil {
return nil, err
}

l := autoscan.GetLogger(c.Verbosity).With().
Str("target", "emby").
Str("url", c.URL).
Logger()

l.Debug().
Interface("libraries", libraries).
Msg("Retrieved libraries")

return &target{
url: c.URL,
token: c.Token,
libraries: libraries,

log: l,
rewrite: rewriter,
store: store,
}, nil
}
141 changes: 141 additions & 0 deletions targets/emby/scan.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package emby

import (
"bytes"
"database/sql"
"encoding/json"
"errors"
"fmt"
"net/http"
"path/filepath"
"strings"

"github.com/cloudbox/autoscan"
)

type scanRequest struct {
Path string `json:"path"`
UpdateType string `json:"updateType"`
}

func (t target) Scan(scans []autoscan.Scan) error {
// ensure scan tasks present (should never fail)
if len(scans) == 0 {
return nil
}

// check for at-least one missing/changed file
process := false
for _, s := range scans {
targetFilePath := t.rewrite(filepath.Join(s.Folder, s.File))

targetFile, err := t.store.MediaPartByFile(targetFilePath)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// local file not found in target
t.log.Debug().
Str("path", targetFilePath).
Msg("At least one local file did not exist in target datastore")

process = true
break
}

// unexpected error
return fmt.Errorf("could not check emby datastore: %v: %w", err, autoscan.ErrFatal)
}

// local file was found in target
if targetFile.Size != s.Size {
// local file did not match in target
t.log.Debug().
Str("path", targetFilePath).
Uint64("target_size", targetFile.Size).
Uint64("local_size", s.Size).
Msg("Local file size does not match in target datastore")

process = true
break
}
}

if !process {
// all local files existed in target
t.log.Debug().
Interface("scans", scans).
Msg("All local files exist in target")
return nil
}

scanFolder := t.rewrite(scans[0].Folder)

// determine library for this scan
lib, err := t.getScanLibrary(scanFolder)
if err != nil {
t.log.Warn().
Err(err).
Msg("No target library found")
return fmt.Errorf("%v: %w", err, autoscan.ErrRetryScan)
}

l := t.log.With().
Str("path", scanFolder).
Str("library", lib.Name).
Logger()

l.Trace().Msg("Sending scan request")

// create request payload
payload := new(struct {
Updates []scanRequest `json:"Updates"`
})

payload.Updates = append(payload.Updates, scanRequest{
Path: scanFolder,
UpdateType: "Created",
})

b, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("failed encoding scan request payload: %v, %w", err, autoscan.ErrFatal)
}

// create request
reqURL := autoscan.JoinURL(t.url, "Library", "Media", "Updated")
req, err := http.NewRequest("POST", reqURL, bytes.NewBuffer(b))
if err != nil {
// May only occur when the user has provided an invalid URL
return fmt.Errorf("failed creating scan request: %v: %w", err, autoscan.ErrFatal)
}

// set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Emby-Token", t.token)

// send request
res, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("failed sending scan request: %v: %w", err, autoscan.ErrTargetUnavailable)
}

defer res.Body.Close()

// validate response
if res.StatusCode != 204 {
// 404 if some kind of proxy is in-front of Emby while it's offline.
return fmt.Errorf("%v: failed validating scan request response: %w", res.Status, autoscan.ErrTargetUnavailable)
}

l.Info().Msg("Scan queued")
return nil
}

func (t target) getScanLibrary(folder string) (*library, error) {
for _, l := range t.libraries {
if strings.HasPrefix(folder, l.Path) {
return &l, nil
}
}

return nil, fmt.Errorf("%v: failed determining library", folder)
}
1 change: 1 addition & 0 deletions targets/plex/datastore.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,5 +93,6 @@ FROM
media_parts mp
WHERE
mp.file = $1
LIMIT 1
`
)
2 changes: 1 addition & 1 deletion targets/plex/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (t target) Scan(scans []autoscan.Scan) error {
Str("target_path", fp).
Uint64("target_size", pf.Size).
Uint64("trigger_size", s.Size).
Msg("Trigger file size does not match targets file")
Msg("Trigger file size does not match target file")

process = true
break
Expand Down

0 comments on commit a53ef3f

Please sign in to comment.