diff --git a/api/base_api_controller.go b/api/base_api_controller.go index bdca8df4e..f7a79e59a 100644 --- a/api/base_api_controller.go +++ b/api/base_api_controller.go @@ -29,8 +29,11 @@ func (c *BaseAPIController) ParamString(param string) string { return c.Input().Get(param) } -func (c *BaseAPIController) ParamTime(param string) time.Time { +func (c *BaseAPIController) ParamTime(param string, def time.Time) time.Time { var value int64 + if c.Input().Get(param) == "" { + return def + } c.Ctx.Input.Bind(&value, param) return utils.ToTime(value) } @@ -41,6 +44,12 @@ func (c *BaseAPIController) ParamInt(param string, def int) int { return value } +func (c *BaseAPIController) ParamBool(param string, def bool) bool { + value := def + c.Ctx.Input.Bind(&value, param) + return value +} + func (c *BaseAPIController) SendError(errorCode int, message ...interface{}) { response := responses.Subsonic{Version: beego.AppConfig.String("apiVersion"), Status: "fail"} var msg string diff --git a/api/browsing.go b/api/browsing.go index 1c3b4485c..9a1569e21 100644 --- a/api/browsing.go +++ b/api/browsing.go @@ -3,6 +3,8 @@ package api import ( "fmt" + "time" + "github.com/astaxie/beego" "github.com/deluan/gosonic/api/responses" "github.com/deluan/gosonic/engine" @@ -32,7 +34,7 @@ func (c *BrowsingController) GetMediaFolders() { // TODO: Shortcuts amd validate musicFolder parameter func (c *BrowsingController) GetIndexes() { - ifModifiedSince := c.ParamTime("ifModifiedSince") + ifModifiedSince := c.ParamTime("ifModifiedSince", time.Time{}) indexes, lastModified, err := c.browser.Indexes(ifModifiedSince) if err != nil { diff --git a/api/media_annotation.go b/api/media_annotation.go new file mode 100644 index 000000000..7ef670031 --- /dev/null +++ b/api/media_annotation.go @@ -0,0 +1,36 @@ +package api + +import ( + "time" + + "github.com/astaxie/beego" + "github.com/deluan/gosonic/api/responses" + "github.com/deluan/gosonic/itunesbridge" + "github.com/deluan/gosonic/utils" +) + +type MediaAnnotationController struct { + BaseAPIController + itunes itunesbridge.ItunesControl +} + +func (c *MediaAnnotationController) Prepare() { + utils.ResolveDependencies(&c.itunes) +} + +func (c *MediaAnnotationController) Scrobble() { + id := c.RequiredParamString("id", "Required id parameter is missing") + time := c.ParamTime("time", time.Now()) + submission := c.ParamBool("submission", true) + + if submission { + beego.Debug("Scrobbling", id, "at", time) + if err := c.itunes.Scrobble(id, time); err != nil { + beego.Error("Error scrobbling:", err) + c.SendError(responses.ERROR_GENERIC, "Internal error") + } + } + + response := c.NewEmpty() + c.SendResponse(response) +} diff --git a/api/users.go b/api/users.go index de15bb421..b2f616c05 100644 --- a/api/users.go +++ b/api/users.go @@ -11,5 +11,6 @@ func (c *UsersController) GetUser() { r.User.Username = c.RequiredParamString("username", "Required string parameter 'username' is not present") r.User.StreamRole = true r.User.DownloadRole = true + r.User.ScrobblingEnabled = true c.SendResponse(r) } diff --git a/conf/inject_definitions.go b/conf/inject_definitions.go index 32e115b79..82b81c386 100644 --- a/conf/inject_definitions.go +++ b/conf/inject_definitions.go @@ -7,6 +7,7 @@ import ( "github.com/deluan/gosonic/persistence" "github.com/deluan/gosonic/utils" + "github.com/deluan/gosonic/itunesbridge" "github.com/deluan/gosonic/scanner" ) @@ -28,14 +29,9 @@ func init() { utils.DefineSingleton(new(engine.Search), engine.NewSearch) // Other dependencies + utils.DefineSingleton(new(itunesbridge.ItunesControl), itunesbridge.NewItunesControl) utils.DefineSingleton(new(scanner.Scanner), scanner.NewItunesScanner) utils.DefineSingleton(new(gomate.DB), func() gomate.DB { return gomate.NewLedisEmbeddedDB(persistence.Db()) }) - //utils.DefineSingleton(new(gomate.Indexer), func() gomate.Indexer { - // return gomate.NewIndexer(gomate.NewLedisEmbeddedDB(persistence.Db())) - //}) - //utils.DefineSingleton(new(gomate.Searcher), func() gomate.Searcher { - // return gomate.NewSearcher(gomate.NewLedisEmbeddedDB(persistence.Db())) - //}) } diff --git a/conf/router.go b/conf/router.go index 0182c0617..41ea60d7d 100644 --- a/conf/router.go +++ b/conf/router.go @@ -29,6 +29,8 @@ func mapEndpoints() { beego.NSRouter("/stream.view", &api.StreamController{}, "*:Stream"), beego.NSRouter("/download.view", &api.StreamController{}, "*:Download"), + beego.NSRouter("/scrobble.view", &api.MediaAnnotationController{}, "*:Scrobble"), + beego.NSRouter("/getAlbumList.view", &api.GetAlbumListController{}, "*:Get"), beego.NSRouter("/getPlaylists.view", &api.PlaylistsController{}, "*:GetAll"), diff --git a/itunesbridge/itunes.go b/itunesbridge/itunes.go new file mode 100644 index 000000000..29e528942 --- /dev/null +++ b/itunesbridge/itunes.go @@ -0,0 +1,31 @@ +package itunesbridge + +import ( + "fmt" + "time" +) + +type ItunesControl interface { + Scrobble(id string, playDate time.Time) error +} + +func NewItunesControl() ItunesControl { + return itunesControl{} +} + +type itunesControl struct{} + +func (c itunesControl) Scrobble(id string, playDate time.Time) error { + script := Script{fmt.Sprintf( + `set theTrack to the first item of (every track whose database ID is equal to "%s")`, id), + `set c to (get played count of theTrack)`, + `tell theTrack`, + `set played count to c + 1`, + fmt.Sprintf(`set played date to date("%s")`, c.formatDateTime(playDate)), + `end tell`} + return script.Run() +} + +func (c itunesControl) formatDateTime(d time.Time) string { + return d.Format("Jan _2, 2006 3:04PM") +} diff --git a/itunesbridge/script.go b/itunesbridge/script.go new file mode 100644 index 000000000..7b769af5f --- /dev/null +++ b/itunesbridge/script.go @@ -0,0 +1,62 @@ +package itunesbridge + +import ( + "fmt" + "io" + "os" + "os/exec" +) + +type Script []string + +var CommandHost string + +func (s Script) lines() []string { + if len(s) == 0 { + panic("empty script") + } + + lines := make([]string, 0, 2) + tell := `tell application "iTunes"` + if CommandHost != "" { + tell += fmt.Sprintf(` of machine %q`, CommandHost) + } + if len(s) == 1 { + tell += " to " + s[0] + lines = append(lines, tell) + } else { + lines = append(lines, tell) + lines = append(lines, s...) + lines = append(lines, "end tell") + } + return lines +} + +func (s Script) args() []string { + var args []string + for _, line := range s.lines() { + args = append(args, "-e", line) + } + return args +} + +func (s Script) Command(w io.Writer, args ...string) *exec.Cmd { + command := exec.Command("osascript", append(s.args(), args...)...) + command.Stdout = w + command.Stderr = os.Stderr + return command +} + +func (s Script) Run(args ...string) error { + return s.Command(os.Stdout, args...).Run() +} + +func (s Script) Output(args ...string) ([]byte, error) { + return s.Command(nil, args...).Output() +} + +func (s Script) OutputString(args ...string) (string, error) { + p, err := s.Output(args...) + str := string(p) + return str, err +}