From 4053a18316c68d999443784a3c40cffdd176f362 Mon Sep 17 00:00:00 2001 From: Felipe Martin Garcia Date: Sun, 27 Mar 2022 23:37:00 +0200 Subject: [PATCH] Xbox Game Bar support (#20) * feat: xbox game bar support with exif handlin --- .gitignore | 1 + README.md | 7 +- go.mod | 8 +- go.sum | 10 +- internal/cli/cli.go | 2 + internal/exif/exif.go | 39 ++++++++ pkg/providers/xbox_game_bar/provider.go | 118 ++++++++++++++++++++++++ 7 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 internal/exif/exif.go create mode 100644 pkg/providers/xbox_game_bar/provider.go diff --git a/.gitignore b/.gitignore index 4ea4230..199ff61 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build dist/ .DS_Store Output* +Input* diff --git a/README.md b/README.md index ad99ce9..45392ec 100644 --- a/README.md +++ b/README.md @@ -10,10 +10,15 @@ Use the appropriate ID with the `-provider` flag. [See examples below](#Usage) | Name | ID | Linux | Windows | macOS | Covers | Notes | | --------------- | ----------------- | ----- | ------- | ----- | ------ | --------------------------------------------------- | -| Steam | `steam` | Yes | Yes | Yes | Yes | | Minecraft | `minecraft` | Yes | Yes | Yes | No | | PlayStation 4 | `playstation-4` | - | - | - | No | Requires `-input-path` pointing to PS4 folder | | RetroArch | `retroarch` | - | - | - | Yes | Requires `-input-path` pointing to Playlists folder | +| Steam | `steam` | Yes | Yes | Yes | Yes | +| Xbox Game Bar | `xbox-game-bar` | - | - | - | No | Requires `-input-path` pointing to the folder holding the captures | + +## Requirements + +- [exiftool](https://exiftool.org/) to parse EXIF data from files. ## How it works diff --git a/go.mod b/go.mod index b78dcda..a9363e4 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,15 @@ module github.com/fmartingr/games-screenshot-manager -go 1.15 +go 1.17 require ( + github.com/barasher/go-exiftool v1.7.0 github.com/gosimple/slug v1.12.0 github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd github.com/sirupsen/logrus v1.8.1 ) + +require ( + github.com/gosimple/unidecode v1.0.1 // indirect + golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 // indirect +) diff --git a/go.sum b/go.sum index 7d6e407..a4088eb 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,6 @@ +github.com/barasher/go-exiftool v1.7.0 h1:EOGb5D6TpWXmqsnEjJ0ai6+tIW2gZFwIoS9O/33Nixs= +github.com/barasher/go-exiftool v1.7.0/go.mod h1:F9s/a3uHSM8YniVfwF+sbQUtP8Gmh9nyzigNF+8vsWo= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gosimple/slug v1.12.0 h1:xzuhj7G7cGtd34NXnW/yF0l+AGNfWqwgh/IXgFy7dnc= @@ -10,7 +13,10 @@ github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= +golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/cli/cli.go b/internal/cli/cli.go index a728e29..80853b1 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -12,6 +12,7 @@ import ( "github.com/fmartingr/games-screenshot-manager/pkg/providers/playstation4" "github.com/fmartingr/games-screenshot-manager/pkg/providers/retroarch" "github.com/fmartingr/games-screenshot-manager/pkg/providers/steam" + "github.com/fmartingr/games-screenshot-manager/pkg/providers/xbox_game_bar" "github.com/fmartingr/games-screenshot-manager/pkg/registry" "github.com/sirupsen/logrus" ) @@ -32,6 +33,7 @@ func Start() { registry := registry.NewProviderRegistry(logger, cache) registry.Register(minecraft.Name, minecraft.NewMinecraftProvider) registry.Register(playstation4.Name, playstation4.NewPlaystation4Provider) + registry.Register(xbox_game_bar.Name, xbox_game_bar.NewXboxGameGarProvider) registry.Register(steam.Name, steam.NewSteamProvider) registry.Register(retroarch.Name, retroarch.NewRetroArchProvider) diff --git a/internal/exif/exif.go b/internal/exif/exif.go new file mode 100644 index 0000000..2cba49a --- /dev/null +++ b/internal/exif/exif.go @@ -0,0 +1,39 @@ +package exif + +import ( + "fmt" + "log" + + "github.com/barasher/go-exiftool" +) + +func GetTags(path string) (map[string]string, error) { + et, err := exiftool.NewExiftool() + if err != nil { + return nil, fmt.Errorf("error intializing exiftool: %v\n", err) + } + defer et.Close() + + fileInfos := et.ExtractMetadata(path) + + if len(fileInfos) == 0 { + return nil, fmt.Errorf("no metadata found for %s", path) + } + + result := make(map[string]string, len(fileInfos[0].Fields)) + + for _, fileInfo := range fileInfos { + if fileInfo.Err != nil { + return nil, fmt.Errorf("error parsing file exif for %v: %v", fileInfo.File, fileInfo.Err) + } + + for k := range fileInfo.Fields { + result[k], err = fileInfo.GetString(k) + if err != nil { + log.Printf("error getting tag %s: %s", k, err) + } + } + } + + return result, nil +} diff --git a/pkg/providers/xbox_game_bar/provider.go b/pkg/providers/xbox_game_bar/provider.go new file mode 100644 index 0000000..f448f16 --- /dev/null +++ b/pkg/providers/xbox_game_bar/provider.go @@ -0,0 +1,118 @@ +package xbox_game_bar + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "path/filepath" + "strings" + "time" + + "github.com/fmartingr/games-screenshot-manager/internal/exif" + "github.com/fmartingr/games-screenshot-manager/internal/models" + "github.com/fmartingr/games-screenshot-manager/pkg/helpers" + "github.com/gosimple/slug" + "github.com/sirupsen/logrus" +) + +const ( + Name = "xbox-game-bar" + platformName = "PC" + dateTimeLayout = "2006:01:02 15:04:05" +) + +type dvrMetadata struct { + StartTime time.Time `json:"startTime"` +} + +type XboxGameBarProvider struct { + logger *logrus.Entry +} + +func (p *XboxGameBarProvider) FindGames(options models.ProviderOptions) ([]*models.Game, error) { + var userGames []*models.Game + + path := helpers.ExpandUser(options.InputPath) + + files, err := ioutil.ReadDir(path) + if err != nil { + return nil, fmt.Errorf("error reading from path %s: %s", options.InputPath, err) + } + + games := make(map[string]*models.Game) + + for _, file := range files { + fullPath := filepath.Join(path, file.Name()) + + if strings.Contains(file.Name(), ".png") || strings.Contains(file.Name(), ".mp4") { + tags, err := exif.GetTags(fullPath) + if err != nil { + p.logger.Errorf("err: %s", err) + continue + } + + titleTag := "MicrosoftGameDVRTitle" + if strings.Contains(file.Name(), ".mp4") { + titleTag = "Title" + } + + gameName := tags[titleTag] + + game, exists := games[tags[titleTag]] + if !exists { + game = &models.Game{ + ID: slug.Make(gameName), + Name: gameName, + Platform: platformName, + Provider: Name, + } + games[tags[titleTag]] = game + } + + var destinationName string + + if strings.Contains(file.Name(), ".png") { + metadataTag := "MicrosoftGameDVRExtended" + metadataString, exists := tags[metadataTag] + if !exists { + p.logger.Warnf("no metadata found for %s", file.Name()) + } + var metadata dvrMetadata + if err := json.Unmarshal([]byte(metadataString), &metadata); err != nil { + p.logger.Errorf("error parsing metadata for %s: %s", file.Name(), err) + } + + destinationName = metadata.StartTime.Format(models.DatetimeFormat) + ".png" + } else { + mediaCreateTag := "MediaCreateDate" + mediaCreateString, exists := tags[mediaCreateTag] + if !exists { + p.logger.Warnf("no media creation time found for %s", file.Name()) + continue + } + + mediaCreationTime, err := time.Parse(dateTimeLayout, mediaCreateString) + if err != nil { + p.logger.Warnf("error parsing media creation time for %s: %s", file.Name(), err) + continue + } + + destinationName = mediaCreationTime.Format(models.DatetimeFormat) + ".mp4" + } + + game.Screenshots = append(game.Screenshots, models.NewScreenshot(path+"/"+file.Name(), destinationName)) + } + } + + for _, g := range games { + userGames = append(userGames, g) + } + + return userGames, nil +} + +func NewXboxGameGarProvider(logger *logrus.Logger, cache models.Cache) models.Provider { + return &XboxGameBarProvider{ + logger: logger.WithField("from", "provider."+Name), + } +}