Compare commits
8 Commits
387c4ea86b
...
af72c45833
Author | SHA1 | Date |
---|---|---|
Felipe Martin Garcia | af72c45833 | |
Felipe M | 3d56f84762 | |
Felipe M | 76afe81086 | |
Felipe M | d0c2d4ff07 | |
Felipe M | 01e3906a9a | |
Felipe M | 0d21736081 | |
Felipe M | dbbddc1242 | |
Felipe M | 0f3382ebf9 |
|
@ -2,3 +2,5 @@ Output
|
|||
Album
|
||||
build
|
||||
dist/
|
||||
.DS_Store
|
||||
Output*
|
||||
|
|
3
go.mod
3
go.mod
|
@ -3,6 +3,7 @@ module github.com/fmartingr/games-screenshot-manager
|
|||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/gosimple/slug v1.9.0
|
||||
github.com/gosimple/slug v1.12.0
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
)
|
||||
|
|
18
go.sum
18
go.sum
|
@ -1,6 +1,16 @@
|
|||
github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs=
|
||||
github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ=
|
||||
github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be/go.mod h1:MIDFMn7db1kT65GmV94GzpX9Qdi7N/pQlwb+AN8wh+Q=
|
||||
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=
|
||||
github.com/gosimple/slug v1.12.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
|
||||
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
|
||||
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
|
||||
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/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
|
@ -1,25 +1,22 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/providers"
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/cache"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/processor"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/providers/minecraft"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/providers/nintendo_switch"
|
||||
"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/gosimple/slug"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/registry"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var allowedProviders = [...]string{"steam", "minecraft", "nintendo-switch", "playstation-4", "retroarch"}
|
||||
|
||||
const defaultOutputPath string = "./Output"
|
||||
|
||||
const defaultInputPath string = ""
|
||||
|
@ -28,104 +25,65 @@ const defaultDryRun bool = false
|
|||
const defaultDownloadCovers bool = false
|
||||
|
||||
func Start() {
|
||||
cliOptions := providers.ProviderOptions{
|
||||
OutputPath: flag.String("output-path", defaultOutputPath, "The destination path of the screenshots"),
|
||||
InputPath: flag.String("input-path", defaultInputPath, "Input path for the provider that requires it"),
|
||||
DownloadCovers: flag.Bool("download-covers", defaultDownloadCovers, "use to enable the download of covers (if the provider supports it)"),
|
||||
DryRun: flag.Bool("dry-run", defaultDryRun, "Use to disable write actions on filesystem"),
|
||||
logger := logrus.New()
|
||||
flagSet := flag.NewFlagSet("gsm", flag.ExitOnError)
|
||||
|
||||
cache := cache.NewFileCache(logger)
|
||||
|
||||
registry := registry.NewProviderRegistry(logger, cache)
|
||||
registry.Register(minecraft.Name, minecraft.NewMinecraftProvider)
|
||||
registry.Register(nintendo_switch.Name, nintendo_switch.NewNintendoSwitchProvider)
|
||||
registry.Register(playstation4.Name, playstation4.NewPlaystation4Provider)
|
||||
registry.Register(steam.Name, steam.NewSteamProvider)
|
||||
registry.Register(retroarch.Name, retroarch.NewRetroArchProvider)
|
||||
|
||||
options := models.Options{
|
||||
ProcessBufferSize: 32,
|
||||
}
|
||||
var provider = flag.String("provider", defaultProvider, "steam")
|
||||
|
||||
flag.Parse()
|
||||
flagSet.StringVar(&options.OutputPath, "output-path", defaultOutputPath, "The destination path of the screenshots")
|
||||
flagSet.BoolVar(&options.DownloadCovers, "download-covers", defaultDownloadCovers, "use to enable the download of covers (if the provider supports it)")
|
||||
flagSet.BoolVar(&options.DryRun, "dry-run", defaultDryRun, "Use to disable write actions on filesystem")
|
||||
flagSet.IntVar(&options.WorkersNum, "workers-num", 2, "Number of workers to use to process games")
|
||||
|
||||
if helpers.SliceContainsString(allowedProviders[:], *provider, nil) {
|
||||
games := getGamesFromProvider(*provider, cliOptions)
|
||||
processGames(games, cliOptions)
|
||||
} else {
|
||||
log.Printf("Provider %s not found!", *provider)
|
||||
var providerName = flagSet.String("provider", defaultProvider, "steam")
|
||||
providerOptions := models.ProviderOptions{}
|
||||
flagSet.StringVar(&providerOptions.InputPath, "input-path", defaultInputPath, "Input path for the provider that requires it")
|
||||
|
||||
loglevelFlag := flagSet.String("log-level", logrus.InfoLevel.String(), "Log level")
|
||||
|
||||
if err := flagSet.Parse(os.Args[1:]); err != nil {
|
||||
logger.Errorf("error parsing args: %s", err)
|
||||
}
|
||||
|
||||
loglevel, err := logrus.ParseLevel(*loglevelFlag)
|
||||
if err != nil {
|
||||
logger.Warnf("Invalid loglevel %s, using %s instead.", *loglevelFlag, logrus.InfoLevel.String())
|
||||
loglevel = logrus.InfoLevel
|
||||
}
|
||||
logger.SetLevel(loglevel)
|
||||
|
||||
provider, err := registry.Get(*providerName)
|
||||
if err != nil {
|
||||
logger.Errorf("Provider %s not found!", *providerName)
|
||||
return
|
||||
}
|
||||
games, err := provider.FindGames(providerOptions)
|
||||
if err != nil {
|
||||
logger.Errorf("Error obtaining game list: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(games) > 0 {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
processor := processor.NewProcessor(logger, options)
|
||||
processor.Start(ctx)
|
||||
|
||||
for _, g := range games {
|
||||
processor.Process(g)
|
||||
}
|
||||
|
||||
processor.Wait()
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func getGamesFromProvider(provider string, cliOptions providers.ProviderOptions) []providers.Game {
|
||||
var games []providers.Game
|
||||
switch provider {
|
||||
case "steam":
|
||||
games = append(games, steam.GetGames(cliOptions)...)
|
||||
case "minecraft":
|
||||
games = append(games, minecraft.GetGames(cliOptions)...)
|
||||
case "nintendo-switch":
|
||||
games = append(games, nintendo_switch.GetGames(cliOptions)...)
|
||||
case "playstation-4":
|
||||
games = append(games, playstation4.GetGames(cliOptions)...)
|
||||
case "retroarch":
|
||||
games = append(games, retroarch.GetGames(cliOptions)...)
|
||||
}
|
||||
return games
|
||||
}
|
||||
|
||||
// TODO: Reduce into smaller functions
|
||||
func processGames(games []providers.Game, cliOptions providers.ProviderOptions) {
|
||||
for _, game := range games {
|
||||
destinationPath := filepath.Join(helpers.ExpandUser(*cliOptions.OutputPath), game.Platform)
|
||||
if len(game.Name) > 0 {
|
||||
destinationPath = filepath.Join(destinationPath, game.Name)
|
||||
} else {
|
||||
log.Printf("[IMPORTANT] Game ID %s has no name!", game.ID)
|
||||
destinationPath = filepath.Join(destinationPath, game.ID)
|
||||
}
|
||||
|
||||
// Do not continue if there's no screenshots
|
||||
if len(game.Screenshots) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if folder exists
|
||||
if _, err := os.Stat(destinationPath); os.IsNotExist(err) && !*cliOptions.DryRun {
|
||||
mkdirErr := os.MkdirAll(destinationPath, 0711)
|
||||
if mkdirErr != nil {
|
||||
log.Printf("[ERROR] Couldn't create directory with name %s, falling back to %s", game.Name, slug.Make(game.Name))
|
||||
destinationPath = filepath.Join(helpers.ExpandUser(*cliOptions.OutputPath), game.Platform, slug.Make(game.Name))
|
||||
os.MkdirAll(destinationPath, 0711)
|
||||
}
|
||||
}
|
||||
|
||||
if *cliOptions.DownloadCovers && !*cliOptions.DryRun && game.Cover.Path != "" {
|
||||
destinationCoverPath := filepath.Join(destinationPath, game.Cover.DestinationName)
|
||||
|
||||
if _, err := os.Stat(destinationCoverPath); os.IsNotExist(err) {
|
||||
helpers.CopyFile(game.Cover.Path, destinationCoverPath)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("=> Processing screenshots for %s %s", game.Name, game.Notes)
|
||||
for _, screenshot := range game.Screenshots {
|
||||
destinationPath := filepath.Join(destinationPath, screenshot.GetDestinationName())
|
||||
|
||||
if _, err := os.Stat(destinationPath); !os.IsNotExist(err) {
|
||||
sourceMd5, err := helpers.Md5File(screenshot.Path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
continue
|
||||
}
|
||||
destinationMd5, err := helpers.Md5File(destinationPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
continue
|
||||
}
|
||||
|
||||
if !bytes.Equal(sourceMd5, destinationMd5) {
|
||||
// Images are not equal, we should copy it anyway, but how?
|
||||
log.Println("Found different screenshot with equal timestamp for game ", game.Name, screenshot.Path)
|
||||
}
|
||||
|
||||
} else {
|
||||
if *cliOptions.DryRun {
|
||||
log.Println(filepath.Base(screenshot.Path), " -> ", strings.Replace(destinationPath, helpers.ExpandUser(*cliOptions.OutputPath), "", 1))
|
||||
} else {
|
||||
helpers.CopyFile(screenshot.Path, destinationPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrCacheKeyDontExist = errors.New("cache key don't exist")
|
||||
|
||||
type Cache interface {
|
||||
Delete(key string) error
|
||||
Get(key string) (string, error)
|
||||
GetExpiry(key string, expiration time.Duration) (string, error)
|
||||
Put(key, value string) error
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package providers
|
||||
package models
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
@ -8,13 +8,6 @@ import (
|
|||
|
||||
const DatetimeFormat = "2006-01-02_15-04-05"
|
||||
|
||||
type ProviderOptions struct {
|
||||
OutputPath *string
|
||||
InputPath *string
|
||||
DownloadCovers *bool
|
||||
DryRun *bool
|
||||
}
|
||||
|
||||
type Game struct {
|
||||
ID string
|
||||
Name string
|
||||
|
@ -22,7 +15,16 @@ type Game struct {
|
|||
Provider string
|
||||
Screenshots []Screenshot
|
||||
Notes string
|
||||
Cover Screenshot
|
||||
CoverURL string
|
||||
}
|
||||
|
||||
func NewGame(id, name, platform, provider string) Game {
|
||||
return Game{
|
||||
ID: id,
|
||||
Name: name,
|
||||
Platform: platform,
|
||||
Provider: provider,
|
||||
}
|
||||
}
|
||||
|
||||
type Screenshot struct {
|
||||
|
@ -40,3 +42,16 @@ func (screenshot Screenshot) GetDestinationName() string {
|
|||
}
|
||||
return fileStat.ModTime().Format(DatetimeFormat) + filepath.Ext(screenshot.Path)
|
||||
}
|
||||
|
||||
func NewScreenshot(path, destinationName string) Screenshot {
|
||||
return Screenshot{
|
||||
Path: path,
|
||||
DestinationName: destinationName,
|
||||
}
|
||||
}
|
||||
|
||||
func NewScreenshotWithoutDestination(path string) Screenshot {
|
||||
return Screenshot{
|
||||
Path: path,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
package models
|
||||
|
||||
type Options struct {
|
||||
OutputPath string
|
||||
DryRun bool
|
||||
DownloadCovers bool
|
||||
ProcessBufferSize int
|
||||
WorkersNum int
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package models
|
||||
|
||||
import "github.com/sirupsen/logrus"
|
||||
|
||||
type ProviderOptions struct {
|
||||
InputPath string
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
FindGames(options ProviderOptions) ([]*Game, error)
|
||||
}
|
||||
|
||||
type ProviderFactory func(logger *logrus.Logger, cache Cache) Provider
|
|
@ -0,0 +1,78 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type FileCache struct {
|
||||
logger *logrus.Entry
|
||||
path string
|
||||
}
|
||||
|
||||
func (c *FileCache) Get(key string) (result string, err error) {
|
||||
path := filepath.Join(c.path, key)
|
||||
|
||||
contents, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) || errors.Is(err, os.ErrPermission) {
|
||||
return result, models.ErrCacheKeyDontExist
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
return string(contents), nil
|
||||
}
|
||||
|
||||
func (c *FileCache) GetExpiry(key string, expiration time.Duration) (result string, err error) {
|
||||
path := filepath.Join(c.path, key)
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return result, models.ErrCacheKeyDontExist
|
||||
}
|
||||
|
||||
if info.ModTime().Add(expiration).Before(time.Now()) {
|
||||
c.Delete(key)
|
||||
return result, models.ErrCacheKeyDontExist
|
||||
}
|
||||
|
||||
return c.Get(key)
|
||||
}
|
||||
|
||||
func (c *FileCache) Put(key, value string) error {
|
||||
path := filepath.Join(c.path, key)
|
||||
|
||||
if err := os.WriteFile(path, []byte(value), 0766); err != nil {
|
||||
return fmt.Errorf("error writting cache file: %s", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *FileCache) Delete(key string) error {
|
||||
path := filepath.Join(c.path, key)
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
func NewFileCache(logger *logrus.Logger) *FileCache {
|
||||
userCacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
logger.Fatalf("error getting cache directory: %s", err)
|
||||
}
|
||||
path := filepath.Join(userCacheDir, "games-screenshot-manager")
|
||||
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
|
||||
return &FileCache{
|
||||
logger: logger.WithField("from", "cache.file"),
|
||||
path: path,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type MemoryCache struct {
|
||||
logger *logrus.Entry
|
||||
data map[string]string
|
||||
dataMu sync.RWMutex
|
||||
}
|
||||
|
||||
func (c *MemoryCache) Get(key string) (result string, err error) {
|
||||
c.dataMu.RLock()
|
||||
defer c.dataMu.RUnlock()
|
||||
|
||||
result, exists := c.data[key]
|
||||
if !exists {
|
||||
return result, models.ErrCacheKeyDontExist
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *MemoryCache) Put(key, value string) error {
|
||||
c.dataMu.Lock()
|
||||
c.data[key] = value
|
||||
c.dataMu.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MemoryCache) GetExpiry(key string, expiration time.Duration) (string, error) {
|
||||
// Since this is a in-memory storage, expiration is not required as of now.
|
||||
return c.Get(key)
|
||||
}
|
||||
|
||||
func (c *MemoryCache) Delete(key string) error {
|
||||
c.dataMu.Lock()
|
||||
delete(c.data, key)
|
||||
c.dataMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewMemoryCache(logger *logrus.Logger) *MemoryCache {
|
||||
return &MemoryCache{
|
||||
logger: logger.WithField("from", "cache.file"),
|
||||
data: make(map[string]string),
|
||||
}
|
||||
}
|
|
@ -2,12 +2,14 @@ package helpers
|
|||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var ErrCopyFileDestinationExists = errors.New("copy destination exists")
|
||||
|
||||
func CopyFile(src, dst string) (int64, error) {
|
||||
sourceFileStat, err := os.Stat(src)
|
||||
if err != nil {
|
||||
|
@ -26,8 +28,7 @@ func CopyFile(src, dst string) (int64, error) {
|
|||
|
||||
// Check if destination exists
|
||||
if _, err := os.Stat(dst); !os.IsNotExist(err) {
|
||||
log.Printf("- %s already exists, skipping...", dst)
|
||||
return 0, nil
|
||||
return 0, ErrCopyFileDestinationExists
|
||||
}
|
||||
|
||||
destination, err := os.Create(dst)
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
package processor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
|
||||
"github.com/gosimple/slug"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
logger *logrus.Entry
|
||||
options models.Options
|
||||
|
||||
games chan *models.Game
|
||||
wg *sync.WaitGroup
|
||||
}
|
||||
|
||||
func (p *Processor) Start(ctx context.Context) {
|
||||
for i := 0; i < p.options.WorkersNum; i++ {
|
||||
go p.process(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) Process(game *models.Game) {
|
||||
p.wg.Add(1)
|
||||
p.games <- game
|
||||
}
|
||||
|
||||
func (p *Processor) process(ctx context.Context) {
|
||||
p.logger.Debug("Worker started")
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case game := <-p.games:
|
||||
if err := p.processGame(game); err != nil {
|
||||
p.logger.Errorf("Error processing game %s from %s: %s", game.Name, game.Provider, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) Wait() {
|
||||
p.wg.Wait()
|
||||
}
|
||||
|
||||
// TODO: Reduce into smaller functions
|
||||
func (p *Processor) processGame(game *models.Game) (err error) {
|
||||
defer p.wg.Done()
|
||||
|
||||
p.logger.WithFields(logrus.Fields{
|
||||
"provider": game.Provider,
|
||||
"name": game.Name,
|
||||
}).Debugf("Processing game")
|
||||
|
||||
// Do not continue if there's no screenshots
|
||||
if len(game.Screenshots) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
destinationPath := filepath.Join(helpers.ExpandUser(p.options.OutputPath), game.Platform)
|
||||
if len(game.Name) > 0 {
|
||||
destinationPath = filepath.Join(destinationPath, game.Name)
|
||||
} else {
|
||||
p.logger.Warnf("found game with ID: %s from %s without a name", game.ID, game.Provider)
|
||||
destinationPath = filepath.Join(destinationPath, game.ID)
|
||||
}
|
||||
|
||||
// Check if folder exists (create otherwise)
|
||||
if _, err := os.Stat(destinationPath); os.IsNotExist(err) && !p.options.DryRun {
|
||||
mkdirErr := os.MkdirAll(destinationPath, 0711)
|
||||
if mkdirErr != nil {
|
||||
p.logger.Errorf("Couldn't create directory with name %s, falling back to %s", game.Name, slug.Make(game.Name))
|
||||
destinationPath = filepath.Join(helpers.ExpandUser(p.options.OutputPath), game.Platform, slug.Make(game.Name))
|
||||
os.MkdirAll(destinationPath, 0711)
|
||||
}
|
||||
}
|
||||
|
||||
if p.options.DownloadCovers && !p.options.DryRun && game.CoverURL != "" {
|
||||
destinationCoverPath := filepath.Join(destinationPath, ".cover")
|
||||
coverPath, err := helpers.DownloadURLIntoTempFile(game.CoverURL)
|
||||
if err != nil {
|
||||
p.logger.Errorf("Error donwloading cover for game %s from %s: %s", game.Name, game.Provider, err)
|
||||
} else {
|
||||
if _, err := os.Stat(destinationCoverPath); os.IsNotExist(err) {
|
||||
helpers.CopyFile(coverPath, destinationCoverPath)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, screenshot := range game.Screenshots {
|
||||
destinationPath := filepath.Join(destinationPath, screenshot.GetDestinationName())
|
||||
|
||||
if _, err := os.Stat(destinationPath); !os.IsNotExist(err) {
|
||||
sourceMd5, err := helpers.Md5File(screenshot.Path)
|
||||
if err != nil {
|
||||
p.logger.Errorf("Can't get hash of source file for game %s from %s: %s", game.Name, game.Provider, err)
|
||||
return err
|
||||
}
|
||||
destinationMd5, err := helpers.Md5File(destinationPath)
|
||||
if err != nil {
|
||||
p.logger.Errorf("Can't get hash of destination file for game %s from %s: %s", game.Name, game.Provider, err)
|
||||
return err
|
||||
}
|
||||
|
||||
if !bytes.Equal(sourceMd5, destinationMd5) {
|
||||
// Images are not equal, we should copy it anyway, but how?
|
||||
p.logger.Warnf("Found different screenshot with equal timestamp for game %s from %s on %s", game.Name, game.Provider, screenshot.Path)
|
||||
}
|
||||
|
||||
} else {
|
||||
if p.options.DryRun {
|
||||
p.logger.Infof("cp %s %s", filepath.Base(screenshot.Path), strings.Replace(destinationPath, helpers.ExpandUser(p.options.OutputPath), "", 1))
|
||||
} else {
|
||||
if _, err := helpers.CopyFile(screenshot.Path, destinationPath); err != nil {
|
||||
p.logger.WithFields(logrus.Fields{
|
||||
"src": screenshot.Path,
|
||||
"dest": destinationPath,
|
||||
}).Errorf("Error during copy operation: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewProcessor(logger *logrus.Logger, options models.Options) *Processor {
|
||||
return &Processor{
|
||||
logger: logger.WithField("from", "processor"),
|
||||
games: make(chan *models.Game, options.ProcessBufferSize),
|
||||
options: options,
|
||||
wg: &sync.WaitGroup{},
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package minecraft
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
|
||||
)
|
||||
|
||||
func getScreenshotsFromPath(game *models.Game, path string) error {
|
||||
path = helpers.ExpandUser(path)
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading from %s: %s", path, err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if strings.Contains(file.Name(), ".png") {
|
||||
game.Screenshots = append(game.Screenshots, models.Screenshot{Path: path + "/" + file.Name(), DestinationName: file.Name()})
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -1,53 +0,0 @@
|
|||
package minecraft
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/providers"
|
||||
)
|
||||
|
||||
func getScreenshotsFromPath(game *providers.Game, path string) {
|
||||
path = helpers.ExpandUser(path)
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if strings.Contains(file.Name(), ".png") {
|
||||
game.Screenshots = append(game.Screenshots, providers.Screenshot{Path: path + "/" + file.Name(), DestinationName: file.Name()})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetGames(cliOptions providers.ProviderOptions) []providers.Game {
|
||||
var result []providers.Game
|
||||
// Standalone minecraft
|
||||
minecraftStandalone := providers.Game{Name: "Minecraft", Platform: "PC", Notes: "Standalone"}
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
getScreenshotsFromPath(&minecraftStandalone, "~/.minecraft/screenshots")
|
||||
|
||||
// Flatpak minecraft
|
||||
minecraftFlatpak := providers.Game{Name: "Minecraft", Platform: "PC", Notes: "Flatpak"}
|
||||
for _, path := range [2]string{"~/.var/app/com.mojang.Minecraft/.minecraft/screenshots", "~/.var/app/com.mojang.Minecraft/data/minecraft/screenshots"} {
|
||||
getScreenshotsFromPath(&minecraftFlatpak, path)
|
||||
}
|
||||
result = append(result, minecraftFlatpak)
|
||||
} else if runtime.GOOS == "windows" {
|
||||
getScreenshotsFromPath(&minecraftStandalone, filepath.Join(os.Getenv("APPDATA"), ".minecraft/screenshots"))
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
getScreenshotsFromPath(&minecraftStandalone, filepath.Join(helpers.ExpandUser("~/Library/Application Support/minecraft/screenshots")))
|
||||
}
|
||||
result = append(result, minecraftStandalone)
|
||||
|
||||
return result
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
package minecraft
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const Name = "minecraft"
|
||||
|
||||
type MinecraftProvider struct {
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func (p *MinecraftProvider) FindGames(options models.ProviderOptions) ([]*models.Game, error) {
|
||||
var result []*models.Game
|
||||
// Standalone minecraft
|
||||
minecraftStandalone := models.Game{Name: "Minecraft", Platform: "PC", Notes: "Standalone"}
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
if err := getScreenshotsFromPath(&minecraftStandalone, "~/.minecraft/screenshots"); err != nil {
|
||||
p.logger.Error(err)
|
||||
}
|
||||
|
||||
// Flatpak minecraft
|
||||
minecraftFlatpak := models.Game{Name: "Minecraft", Platform: "PC", Notes: "Flatpak"}
|
||||
for _, path := range [2]string{"~/.var/app/com.mojang.Minecraft/.minecraft/screenshots", "~/.var/app/com.mojang.Minecraft/data/minecraft/screenshots"} {
|
||||
if err := getScreenshotsFromPath(&minecraftFlatpak, path); err != nil {
|
||||
p.logger.Error(err)
|
||||
}
|
||||
}
|
||||
result = append(result, &minecraftFlatpak)
|
||||
} else if runtime.GOOS == "windows" {
|
||||
if err := getScreenshotsFromPath(&minecraftStandalone, filepath.Join(os.Getenv("APPDATA"), ".minecraft/screenshots")); err != nil {
|
||||
p.logger.Error(err)
|
||||
}
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
if err := getScreenshotsFromPath(&minecraftStandalone, filepath.Join(helpers.ExpandUser("~/Library/Application Support/minecraft/screenshots"))); err != nil {
|
||||
p.logger.Error(err)
|
||||
}
|
||||
}
|
||||
result = append(result, &minecraftStandalone)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func NewMinecraftProvider(logger *logrus.Logger, cache models.Cache) models.Provider {
|
||||
return &MinecraftProvider{
|
||||
logger: logger.WithField("from", "provider."+Name),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package nintendo_switch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
|
||||
)
|
||||
|
||||
type SwitchGame struct {
|
||||
Name string `json:"title_normalized"`
|
||||
EncryptedGameID string `json:"encrypted_game_id"`
|
||||
}
|
||||
|
||||
func findGameByEncryptedID(gameList []SwitchGame, encryptedGameID string) SwitchGame {
|
||||
var gameFound SwitchGame = SwitchGame{EncryptedGameID: encryptedGameID}
|
||||
for _, game := range gameList {
|
||||
if strings.EqualFold(game.EncryptedGameID, encryptedGameID) {
|
||||
gameFound = game
|
||||
}
|
||||
}
|
||||
|
||||
return gameFound
|
||||
}
|
||||
|
||||
func getSwitchGameList() (result []SwitchGame, err error) {
|
||||
response, err := helpers.DoRequest("GET", gameListURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting switch game list: %s", err)
|
||||
}
|
||||
|
||||
if response.Body != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting switch game list: %s", err)
|
||||
}
|
||||
|
||||
jsonErr := json.Unmarshal(body, &result)
|
||||
if jsonErr != nil {
|
||||
return nil, fmt.Errorf("error getting switch game list: %s", err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func addScreenshotToGame(userGames []*models.Game, switchGame SwitchGame, screenshot models.Screenshot) []*models.Game {
|
||||
var foundGame *models.Game
|
||||
for gameIndex, game := range userGames {
|
||||
if game.ID == switchGame.EncryptedGameID {
|
||||
foundGame = game
|
||||
userGames[gameIndex].Screenshots = append(userGames[gameIndex].Screenshots, screenshot)
|
||||
}
|
||||
}
|
||||
|
||||
if foundGame == nil {
|
||||
foundGame := models.Game{Name: switchGame.Name, ID: switchGame.EncryptedGameID, Platform: platformName, Provider: platformName}
|
||||
foundGame.Screenshots = append(foundGame.Screenshots, screenshot)
|
||||
userGames = append(userGames, &foundGame)
|
||||
}
|
||||
|
||||
return userGames
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package nintendo_switch
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const Name = "nintendo-switch"
|
||||
const platformName = "Nintendo Switch"
|
||||
const gameListURL = "https://fmartingr.github.io/switch-games-json/switch_id_names.json"
|
||||
|
||||
type NintendoSwitchProvider struct {
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func (p *NintendoSwitchProvider) FindGames(options models.ProviderOptions) ([]*models.Game, error) {
|
||||
switchGames, err := getSwitchGameList()
|
||||
if err != nil {
|
||||
p.logger.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var userGames []*models.Game
|
||||
|
||||
err = filepath.Walk(options.InputPath,
|
||||
func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
filename := filepath.Base(path)
|
||||
extension := filepath.Ext(filepath.Base(path))
|
||||
|
||||
filenameParsed := strings.Split(filename[:len(filename)-len(extension)], "-")
|
||||
switchGame := findGameByEncryptedID(switchGames, filenameParsed[1])
|
||||
|
||||
layout := "20060102150405"
|
||||
destinationName, err := time.Parse(layout, filenameParsed[0][0:14])
|
||||
|
||||
if err != nil {
|
||||
p.logger.Errorf("Could not parse filename '%s': %s", filename, err)
|
||||
}
|
||||
|
||||
screenshot := models.Screenshot{Path: path, DestinationName: destinationName.Format(models.DatetimeFormat) + extension}
|
||||
userGames = addScreenshotToGame(userGames, switchGame, screenshot)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return userGames, nil
|
||||
}
|
||||
|
||||
func NewNintendoSwitchProvider(logger *logrus.Logger, cache models.Cache) models.Provider {
|
||||
return &NintendoSwitchProvider{
|
||||
logger: logger.WithField("from", "provider."+Name),
|
||||
}
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
package nintendo_switch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/providers"
|
||||
)
|
||||
|
||||
const providerName = "nintendo-switch"
|
||||
const platformName = "Nintendo Switch"
|
||||
const gameListURL = "https://fmartingr.github.io/switch-games-json/switch_id_names.json"
|
||||
|
||||
type SwitchGame struct {
|
||||
Name string `json:"title_normalized"`
|
||||
EncryptedGameID string `json:"encrypted_game_id"`
|
||||
}
|
||||
|
||||
func findGameByEncryptedID(gameList []SwitchGame, encryptedGameID string) SwitchGame {
|
||||
var gameFound SwitchGame = SwitchGame{EncryptedGameID: encryptedGameID}
|
||||
for _, game := range gameList {
|
||||
if strings.EqualFold(game.EncryptedGameID, encryptedGameID) {
|
||||
gameFound = game
|
||||
}
|
||||
}
|
||||
|
||||
return gameFound
|
||||
}
|
||||
|
||||
func getSwitchGameList() []SwitchGame {
|
||||
response, err := helpers.DoRequest("GET", gameListURL)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
if response.Body != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
|
||||
switchGameList := []SwitchGame{}
|
||||
jsonErr := json.Unmarshal(body, &switchGameList)
|
||||
if jsonErr != nil {
|
||||
log.Fatal(jsonErr)
|
||||
}
|
||||
|
||||
log.Printf("Updated Nintendo Switch game list. Found %d providers.", len(switchGameList))
|
||||
|
||||
return switchGameList
|
||||
}
|
||||
|
||||
func addScreenshotToGame(userGames []providers.Game, switchGame SwitchGame, screenshot providers.Screenshot) []providers.Game {
|
||||
var foundGame providers.Game
|
||||
for gameIndex, game := range userGames {
|
||||
if game.ID == switchGame.EncryptedGameID {
|
||||
foundGame = game
|
||||
userGames[gameIndex].Screenshots = append(userGames[gameIndex].Screenshots, screenshot)
|
||||
}
|
||||
}
|
||||
|
||||
if foundGame.ID == "" {
|
||||
foundGame := providers.Game{Name: switchGame.Name, ID: switchGame.EncryptedGameID, Platform: platformName, Provider: providerName}
|
||||
foundGame.Screenshots = append(foundGame.Screenshots, screenshot)
|
||||
userGames = append(userGames, foundGame)
|
||||
}
|
||||
|
||||
return userGames
|
||||
}
|
||||
|
||||
func GetGames(cliOptions providers.ProviderOptions) []providers.Game {
|
||||
switchGames := getSwitchGameList()
|
||||
var userGames []providers.Game
|
||||
|
||||
err := filepath.Walk(*cliOptions.InputPath,
|
||||
func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
filename := filepath.Base(path)
|
||||
extension := filepath.Ext(filepath.Base(path))
|
||||
|
||||
filenameParsed := strings.Split(filename[:len(filename)-len(extension)], "-")
|
||||
switchGame := findGameByEncryptedID(switchGames, filenameParsed[1])
|
||||
|
||||
layout := "20060102150405"
|
||||
destinationName, err := time.Parse(layout, filenameParsed[0][0:14])
|
||||
|
||||
if err != nil {
|
||||
log.Panic("Could not parse filename: ", err)
|
||||
}
|
||||
|
||||
screenshot := providers.Screenshot{Path: path, DestinationName: destinationName.Format(providers.DatetimeFormat) + extension}
|
||||
userGames = addScreenshotToGame(userGames, switchGame, screenshot)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
return userGames
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package playstation4
|
||||
|
||||
import "github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
|
||||
func addScreenshotToGame(userGames []*models.Game, gameName string, screenshot models.Screenshot) []*models.Game {
|
||||
var foundGame *models.Game
|
||||
for gameIndex, game := range userGames {
|
||||
if game.Name == gameName {
|
||||
foundGame = game
|
||||
userGames[gameIndex].Screenshots = append(userGames[gameIndex].Screenshots, screenshot)
|
||||
}
|
||||
}
|
||||
|
||||
if foundGame == nil {
|
||||
foundGame := models.Game{Name: gameName, ID: gameName, Platform: platformName, Provider: platformName}
|
||||
foundGame.Screenshots = append(foundGame.Screenshots, screenshot)
|
||||
userGames = append(userGames, &foundGame)
|
||||
}
|
||||
|
||||
return userGames
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package playstation4
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/providers"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
)
|
||||
|
||||
const providerName = "playstation-4"
|
||||
const platformName = "PlayStation 4"
|
||||
|
||||
func addScreenshotToGame(userGames []providers.Game, gameName string, screenshot providers.Screenshot) []providers.Game {
|
||||
var foundGame providers.Game
|
||||
for gameIndex, game := range userGames {
|
||||
if game.Name == gameName {
|
||||
foundGame = game
|
||||
userGames[gameIndex].Screenshots = append(userGames[gameIndex].Screenshots, screenshot)
|
||||
}
|
||||
}
|
||||
|
||||
// Game not found
|
||||
if foundGame.Name == "" {
|
||||
foundGame := providers.Game{Name: gameName, ID: gameName, Platform: platformName, Provider: providerName}
|
||||
foundGame.Screenshots = append(foundGame.Screenshots, screenshot)
|
||||
userGames = append(userGames, foundGame)
|
||||
}
|
||||
|
||||
return userGames
|
||||
}
|
||||
|
||||
func GetGames(cliOptions providers.ProviderOptions) []providers.Game {
|
||||
var userGames []providers.Game
|
||||
|
||||
err := filepath.Walk(*cliOptions.InputPath,
|
||||
func(filePath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
var destinationName string
|
||||
gameName := filepath.Base(filepath.Dir(filePath))
|
||||
fileName := filepath.Base(filePath)
|
||||
extension := filepath.Ext(filepath.Base(filePath))
|
||||
layout := "20060102150405"
|
||||
|
||||
if extension == ".jpg" {
|
||||
fileDescriptor, errFileDescriptor := os.Open(filePath)
|
||||
if errFileDescriptor != nil {
|
||||
log.Printf("[warning] Couldn't open file %s: %s", fileName, errFileDescriptor)
|
||||
return nil
|
||||
}
|
||||
exifData, errExifData := exif.Decode(fileDescriptor)
|
||||
if errExifData != nil {
|
||||
log.Printf("[Error] Decoding EXIF data from %s: %s", filePath, errExifData)
|
||||
return nil
|
||||
}
|
||||
defer fileDescriptor.Close()
|
||||
|
||||
exifDateTime, _ := exifData.DateTime()
|
||||
destinationName = exifDateTime.Format(providers.DatetimeFormat)
|
||||
|
||||
} else if extension == ".mp4" {
|
||||
if len(fileName) >= len(layout)+len(extension) {
|
||||
videoDatetime, err := time.Parse(layout, fileName[len(fileName)-len(extension)-len(layout):len(fileName)-len(extension)])
|
||||
|
||||
if err == nil {
|
||||
destinationName = videoDatetime.Format(providers.DatetimeFormat)
|
||||
} else {
|
||||
log.Printf("[Warning] File does not follow datetime convention: %s. (%s) skipping...", fileName, err)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
log.Printf("[Warning] File does not follow datetime convention: %s, skipping...", fileName)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
screenshot := providers.Screenshot{Path: filePath, DestinationName: destinationName + extension}
|
||||
userGames = addScreenshotToGame(userGames, gameName, screenshot)
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
return userGames
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package playstation4
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const Name = "playstation-4"
|
||||
const platformName = "PlayStation 4"
|
||||
|
||||
type Playstation4Provider struct {
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func (p *Playstation4Provider) FindGames(options models.ProviderOptions) ([]*models.Game, error) {
|
||||
var userGames []*models.Game
|
||||
|
||||
err := filepath.Walk(options.InputPath,
|
||||
func(filePath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
var destinationName string
|
||||
gameName := filepath.Base(filepath.Dir(filePath))
|
||||
fileName := filepath.Base(filePath)
|
||||
extension := filepath.Ext(filepath.Base(filePath))
|
||||
layout := "20060102150405"
|
||||
|
||||
if extension == ".jpg" {
|
||||
fileDescriptor, errFileDescriptor := os.Open(filePath)
|
||||
if errFileDescriptor != nil {
|
||||
p.logger.Warnf("Couldn't open file %s: %s", fileName, errFileDescriptor)
|
||||
return nil
|
||||
}
|
||||
exifData, errExifData := exif.Decode(fileDescriptor)
|
||||
if errExifData != nil {
|
||||
p.logger.Errorf("Decoding EXIF data from %s: %s", filePath, errExifData)
|
||||
return nil
|
||||
}
|
||||
defer fileDescriptor.Close()
|
||||
|
||||
exifDateTime, _ := exifData.DateTime()
|
||||
destinationName = exifDateTime.Format(models.DatetimeFormat)
|
||||
|
||||
} else if extension == ".mp4" {
|
||||
if len(fileName) >= len(layout)+len(extension) {
|
||||
videoDatetime, err := time.Parse(layout, fileName[len(fileName)-len(extension)-len(layout):len(fileName)-len(extension)])
|
||||
|
||||
if err == nil {
|
||||
destinationName = videoDatetime.Format(models.DatetimeFormat)
|
||||
} else {
|
||||
p.logger.Warnf("File %s does not follow datetime convention, skipping.", fileName, err)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
p.logger.Warnf("File %s does not follow datetime convention, skipping.", fileName)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
screenshot := models.Screenshot{Path: filePath, DestinationName: destinationName + extension}
|
||||
addScreenshotToGame(userGames, gameName, screenshot)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return userGames, nil
|
||||
}
|
||||
|
||||
func NewPlaystation4Provider(logger *logrus.Logger, cache models.Cache) models.Provider {
|
||||
return &Playstation4Provider{
|
||||
logger: logger.WithField("from", "provider."+Name),
|
||||
}
|
||||
}
|
|
@ -1,21 +1,9 @@
|
|||
// RetroArch screenshot provider
|
||||
|
||||
// Notes:
|
||||
// This provider only works if the following retroarch configuration is set:
|
||||
// screenshots_in_content_dir = "true"
|
||||
// auto_screenshot_filename = "true"
|
||||
// This way the screenshots will be stored in the same folders as the games
|
||||
// We will read the playlists from retroarch to determine the Platforms and games
|
||||
// from there, and screenshots will be extracted from the content folders, so you can
|
||||
// sort your games the way you like most, but screenshots need to be renamed
|
||||
// by retroarch for us to parse them properly.
|
||||
|
||||
package retroarch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
|
@ -23,16 +11,12 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/providers"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const providerName = "retroarch"
|
||||
|
||||
const libretroCoverURLBase = "http://thumbnails.libretro.com/"
|
||||
const datetimeLayout = "060102-150405"
|
||||
|
||||
type RetroArchPlaylistItem struct {
|
||||
type retroArchPlaylistItem struct {
|
||||
Path string `json:"path"`
|
||||
Label string `json:"label"`
|
||||
CorePath string `json:"core_path"`
|
||||
|
@ -41,7 +25,7 @@ type RetroArchPlaylistItem struct {
|
|||
DBName string `json:"db_name"`
|
||||
}
|
||||
|
||||
type RetroArchPlaylist struct {
|
||||
type retroArchPlaylist struct {
|
||||
Version string `json:"version"`
|
||||
DefaultCorePath string `json:"default_core_path"`
|
||||
DefaultCoreName string `json:"default_core_name"`
|
||||
|
@ -49,7 +33,7 @@ type RetroArchPlaylist struct {
|
|||
RightThumbnailMode int `json:"right_thumbnail_mode"`
|
||||
LeftThumbnailMode int `json:"left_thumbnail_mode"`
|
||||
SortMode int `json:"sort_mode"`
|
||||
Items []RetroArchPlaylistItem `json:"items"`
|
||||
Items []retroArchPlaylistItem `json:"items"`
|
||||
}
|
||||
|
||||
func formatLibretroBoxartURL(platform string, game string) string {
|
||||
|
@ -70,32 +54,32 @@ func cleanGameName(gameName string) string {
|
|||
return splits[0]
|
||||
}
|
||||
|
||||
func readPlaylists(playlistsPath string) map[string]RetroArchPlaylist {
|
||||
var result = make(map[string]RetroArchPlaylist)
|
||||
func readPlaylists(logger *logrus.Entry, playlistsPath string) (map[string]retroArchPlaylist, error) {
|
||||
var result = make(map[string]retroArchPlaylist)
|
||||
playlistsPath = helpers.ExpandUser(playlistsPath)
|
||||
if _, err := os.Stat(playlistsPath); !os.IsNotExist(err) {
|
||||
files, err := ioutil.ReadDir(playlistsPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return result, fmt.Errorf("error reading playlist directory: %s", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if strings.Contains(file.Name(), ".lpl") {
|
||||
var item RetroArchPlaylist
|
||||
var item retroArchPlaylist
|
||||
source, errOpen := os.Open(filepath.Join(playlistsPath, file.Name()))
|
||||
if errOpen != nil {
|
||||
log.Printf("[ERROR] Error reading playlist %s: %s", file.Name(), errOpen)
|
||||
logger.Errorf("Error reading playlist %s: %s", file.Name(), errOpen)
|
||||
continue
|
||||
}
|
||||
fileContents, errReadContent := ioutil.ReadAll(source)
|
||||
if errReadContent != nil {
|
||||
log.Printf("[ERROR] Reading contents of %s: %s", file.Name(), err)
|
||||
logger.Errorf("Error reading contents of %s: %s", file.Name(), err)
|
||||
continue
|
||||
}
|
||||
|
||||
errUnmarshal := json.Unmarshal(fileContents, &item)
|
||||
if errUnmarshal != nil {
|
||||
log.Printf("[ERROR] Formatting %s: %s", file.Name(), errUnmarshal)
|
||||
logger.Errorf("Error formatting %s: %s", file.Name(), errUnmarshal)
|
||||
continue
|
||||
}
|
||||
result[strings.Replace(file.Name(), ".lpl", "", 1)] = item
|
||||
|
@ -103,16 +87,16 @@ func readPlaylists(playlistsPath string) map[string]RetroArchPlaylist {
|
|||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func findScreenshotsForGame(item RetroArchPlaylistItem) []providers.Screenshot {
|
||||
var result []providers.Screenshot
|
||||
func findScreenshotsForGame(logger *logrus.Entry, item retroArchPlaylistItem) ([]models.Screenshot, error) {
|
||||
var result []models.Screenshot
|
||||
filePath := filepath.Dir(item.Path)
|
||||
fileName := strings.Replace(filepath.Base(item.Path), filepath.Ext(item.Path), "", 1)
|
||||
files, err := ioutil.ReadDir(filePath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
|
@ -130,51 +114,19 @@ func findScreenshotsForGame(item RetroArchPlaylistItem) []providers.Screenshot {
|
|||
if strings.Contains(file.Name(), "-cheevo-") {
|
||||
filenameParts := strings.Split(file.Name(), "-")
|
||||
achievementID := strings.Replace(filenameParts[len(filenameParts)-1], extension, "", 1)
|
||||
screenshotDestinationName = file.ModTime().Format(providers.DatetimeFormat) + "_retroachievement-" + achievementID + extension
|
||||
screenshotDestinationName = file.ModTime().Format(models.DatetimeFormat) + "_retroachievement-" + achievementID + extension
|
||||
} else {
|
||||
screenshotDate, err := time.Parse(datetimeLayout, file.Name()[len(file.Name())-len(extension)-len(datetimeLayout):len(file.Name())-len(extension)])
|
||||
if err == nil {
|
||||
screenshotDestinationName = screenshotDate.Format(providers.DatetimeFormat) + extension
|
||||
screenshotDestinationName = screenshotDate.Format(models.DatetimeFormat) + extension
|
||||
} else {
|
||||
log.Printf("[error] Formatting screenshot %s: %s", file.Name(), err)
|
||||
logger.Errorf("Error formatting screenshot %s: %s", file.Name(), err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, providers.Screenshot{Path: filepath.Join(filePath, file.Name()), DestinationName: screenshotDestinationName})
|
||||
result = append(result, models.Screenshot{Path: filepath.Join(filePath, file.Name()), DestinationName: screenshotDestinationName})
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func GetGames(cliOptions providers.ProviderOptions) []providers.Game {
|
||||
var userGames []providers.Game
|
||||
|
||||
playlists := readPlaylists(*cliOptions.InputPath)
|
||||
|
||||
for playlistName := range playlists {
|
||||
for _, item := range playlists[playlistName].Items {
|
||||
var cover providers.Screenshot
|
||||
|
||||
if *cliOptions.DownloadCovers {
|
||||
coverURL := formatLibretroBoxartURL(playlistName, item.Label)
|
||||
boxartPath, err := helpers.DownloadURLIntoTempFile(coverURL)
|
||||
if err == nil {
|
||||
cover = providers.Screenshot{Path: boxartPath, DestinationName: ".cover"}
|
||||
} else {
|
||||
log.Printf("[error] Error downloading cover for %s: %s", item.Label, err)
|
||||
}
|
||||
}
|
||||
|
||||
userGames = append(userGames, providers.Game{
|
||||
Platform: cleanPlatformName(playlistName),
|
||||
Name: cleanGameName(item.Label),
|
||||
Provider: providerName,
|
||||
Screenshots: findScreenshotsForGame(item),
|
||||
Cover: cover,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return userGames
|
||||
return result, nil
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
// RetroArch screenshot provider
|
||||
|
||||
// Notes:
|
||||
// This provider only works if the following retroarch configuration is set:
|
||||
// screenshots_in_content_dir = "true"
|
||||
// auto_screenshot_filename = "true"
|
||||
// This way the screenshots will be stored in the same folders as the games
|
||||
// We will read the playlists from retroarch to determine the Platforms and games
|
||||
// from there, and screenshots will be extracted from the content folders, so you can
|
||||
// sort your games the way you like most, but screenshots need to be renamed
|
||||
// by retroarch for us to parse them properly.
|
||||
|
||||
package retroarch
|
||||
|
||||
import (
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const Name = "retroarch"
|
||||
|
||||
const libretroCoverURLBase = "http://thumbnails.libretro.com/"
|
||||
const datetimeLayout = "060102-150405"
|
||||
|
||||
type RetroArchProvider struct {
|
||||
logger *logrus.Entry
|
||||
}
|
||||
|
||||
func (p *RetroArchProvider) FindGames(options models.ProviderOptions) ([]*models.Game, error) {
|
||||
var userGames []*models.Game
|
||||
|
||||
playlists, err := readPlaylists(p.logger, options.InputPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for playlistName := range playlists {
|
||||
for _, item := range playlists[playlistName].Items {
|
||||
screenshots, err := findScreenshotsForGame(p.logger, item)
|
||||
if err != nil {
|
||||
p.logger.Errorf("Error retrieving game screenshots: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
userGames = append(userGames, &models.Game{
|
||||
Platform: cleanPlatformName(playlistName),
|
||||
Name: cleanGameName(item.Label),
|
||||
Provider: Name,
|
||||
Screenshots: screenshots,
|
||||
CoverURL: formatLibretroBoxartURL(playlistName, item.Label),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return userGames, nil
|
||||
}
|
||||
|
||||
func NewRetroArchProvider(logger *logrus.Logger, cache models.Cache) models.Provider {
|
||||
return &RetroArchProvider{
|
||||
logger: logger.WithField("from", "provider."+Name),
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
package steam
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func getBasePathForOS() (string, error) {
|
||||
var path string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
path = helpers.ExpandUser("~/Library/Application Support/Steam")
|
||||
case "linux":
|
||||
path = helpers.ExpandUser("~/.local/share/Steam")
|
||||
case "windows":
|
||||
path = "C:\\Program Files (x86)\\Steam"
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported os: %s", runtime.GOOS)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func getSteamAppList(logger *logrus.Entry, cache models.Cache, c chan SteamAppList) {
|
||||
defer close(c)
|
||||
cacheKey := "steam-applist"
|
||||
download := true
|
||||
var payload []byte
|
||||
|
||||
result, err := cache.GetExpiry(cacheKey, 24*time.Hour)
|
||||
if err != nil && !errors.Is(err, models.ErrCacheKeyDontExist) {
|
||||
logger.Errorf("error retrieving cache: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if len(result) > 0 {
|
||||
download = false
|
||||
payload = []byte(result)
|
||||
}
|
||||
|
||||
if download {
|
||||
response, err := helpers.DoRequest("GET", gameListURL)
|
||||
if err != nil {
|
||||
logger.Errorf("Error making request for Steam APP List: %s", err)
|
||||
}
|
||||
|
||||
if response.Body != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
|
||||
payload, err = ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
logger.Errorf("Error reading steam response: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
if download {
|
||||
if err := cache.Put(cacheKey, string(payload)); err != nil {
|
||||
logger.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
steamListResponse := SteamAppListResponse{}
|
||||
jsonErr := json.Unmarshal(payload, &steamListResponse)
|
||||
if jsonErr != nil {
|
||||
logger.Errorf("Error unmarshalling steam's response: %s", jsonErr)
|
||||
}
|
||||
|
||||
c <- steamListResponse.AppList
|
||||
}
|
||||
|
||||
func guessUsers(basePath string) ([]string, error) {
|
||||
var users []string
|
||||
var path string = filepath.Join(basePath, "userdata")
|
||||
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if _, err := strconv.ParseInt(file.Name(), 10, 64); err == nil {
|
||||
users = append(users, file.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func getGamesFromUser(basePath, user string) ([]string, error) {
|
||||
var userGames []string
|
||||
var path string = filepath.Join(basePath, "userdata", user, "760", "remote")
|
||||
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
// len(file.Name()) == 20 -> Custom added Game to steam
|
||||
userGames = append(userGames, file.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return userGames, nil
|
||||
}
|
||||
|
||||
func getScreenshotsForGame(basePath, user string, game *models.Game) error {
|
||||
path := filepath.Join(basePath, "userdata", user, "/760/remote/", game.ID, "screenshots")
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading game screenshot path: %s", err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if strings.Contains(file.Name(), ".jpg") {
|
||||
game.Screenshots = append(game.Screenshots, models.NewScreenshotWithoutDestination(path+"/"+file.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
package steam
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const Name = "steam"
|
||||
const gameListURL = "https://api.steampowered.com/ISteamApps/GetAppList/v2/"
|
||||
const baseGameHeaderURL = "https://cdn.cloudflare.steamstatic.com/steam/apps/%d/header.jpg"
|
||||
|
||||
var errGameIDNotFound = errors.New("game ID not found")
|
||||
|
||||
type SteamApp struct {
|
||||
AppID uint64 `json:"appid"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type SteamAppList struct {
|
||||
Apps []SteamApp `json:"apps"`
|
||||
}
|
||||
|
||||
func (appList SteamAppList) FindID(id string) (result SteamApp, err error) {
|
||||
for _, game := range appList.Apps {
|
||||
uintGameID, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("error parsing game ID: %s", err)
|
||||
}
|
||||
if game.AppID == uintGameID {
|
||||
return game, nil
|
||||
}
|
||||
}
|
||||
return result, errGameIDNotFound
|
||||
}
|
||||
|
||||
type SteamAppListResponse struct {
|
||||
AppList SteamAppList `json:"applist"`
|
||||
}
|
||||
|
||||
type SteamProvider struct {
|
||||
logger *logrus.Entry
|
||||
cache models.Cache
|
||||
}
|
||||
|
||||
func (p *SteamProvider) FindGames(options models.ProviderOptions) ([]*models.Game, error) {
|
||||
basePath, err := getBasePathForOS()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting steam's base path: %s", err)
|
||||
}
|
||||
|
||||
var localGames []*models.Game
|
||||
c := make(chan SteamAppList)
|
||||
go getSteamAppList(p.logger, p.cache, c)
|
||||
|
||||
users, err := guessUsers(basePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting users: %s", err)
|
||||
}
|
||||
|
||||
p.logger.Debugf("Found %d users", len(users))
|
||||
|
||||
steamApps := <-c
|
||||
|
||||
if len(steamApps.Apps) == 0 {
|
||||
return nil, fmt.Errorf("coulnd't get steam app list")
|
||||
}
|
||||
|
||||
for _, userID := range users {
|
||||
userGames, err := getGamesFromUser(basePath, userID)
|
||||
if err != nil {
|
||||
p.logger.Errorf("error retrieving user's %s games: %s", userID, err)
|
||||
continue
|
||||
}
|
||||
for _, userGameID := range userGames {
|
||||
steamGame, err := steamApps.FindID(userGameID)
|
||||
if err != nil {
|
||||
p.logger.Errorf("Steam game ID not found: %s", userGameID)
|
||||
}
|
||||
p.logger.WithField("userID", userID).Debugf("Found game: %s", steamGame.Name)
|
||||
userGame := models.NewGame(userGameID, steamGame.Name, "PC", Name)
|
||||
|
||||
userGame.CoverURL = fmt.Sprintf(baseGameHeaderURL, steamGame.AppID)
|
||||
|
||||
if err := getScreenshotsForGame(basePath, userID, &userGame); err != nil {
|
||||
p.logger.Errorf("error getting screenshots: %s", err)
|
||||
}
|
||||
localGames = append(localGames, &userGame)
|
||||
}
|
||||
}
|
||||
return localGames, nil
|
||||
}
|
||||
|
||||
func NewSteamProvider(logger *logrus.Logger, cache models.Cache) models.Provider {
|
||||
return &SteamProvider{
|
||||
cache: cache,
|
||||
logger: logger.WithField("from", "provider."+Name),
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
package steam
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/providers"
|
||||
)
|
||||
|
||||
const providerName string = "steam"
|
||||
const gameListURL string = "https://api.steampowered.com/ISteamApps/GetAppList/v2/"
|
||||
const baseGameHeaderURL string = "https://cdn.cloudflare.steamstatic.com/steam/apps/%s/header.jpg"
|
||||
|
||||
type SteamApp struct {
|
||||
AppID uint64 `json:"appid"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type SteamAppList struct {
|
||||
Apps []SteamApp `json:"apps"`
|
||||
}
|
||||
|
||||
func (appList SteamAppList) FindID(id string) (SteamApp, error) {
|
||||
GameIDNotFound := errors.New("game ID not found")
|
||||
for _, game := range appList.Apps {
|
||||
uintGameID, err := strconv.ParseUint(id, 10, 64)
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
if game.AppID == uintGameID {
|
||||
return game, nil
|
||||
}
|
||||
}
|
||||
return SteamApp{}, GameIDNotFound
|
||||
}
|
||||
|
||||
type SteamAppListResponse struct {
|
||||
AppList SteamAppList `json:"applist"`
|
||||
}
|
||||
|
||||
func getBasePathForOS() string {
|
||||
var path string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
path = helpers.ExpandUser("~/Library/Application Support/Steam")
|
||||
case "linux":
|
||||
path = helpers.ExpandUser("~/.local/share/Steam")
|
||||
case "windows":
|
||||
path = "C:\\Program Files (x86)\\Steam"
|
||||
default:
|
||||
log.Panic("Unsupported OS: ", runtime.GOOS)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func getSteamAppList(c chan SteamAppList) {
|
||||
log.Println("Updating steam game list...")
|
||||
response, err := helpers.DoRequest("GET", gameListURL)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if response.Body != nil {
|
||||
defer response.Body.Close()
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
steamListResponse := SteamAppListResponse{}
|
||||
jsonErr := json.Unmarshal(body, &steamListResponse)
|
||||
if jsonErr != nil {
|
||||
log.Fatal(jsonErr)
|
||||
}
|
||||
|
||||
log.Printf("Updated Steam game list. Found %d apps.", len(steamListResponse.AppList.Apps))
|
||||
|
||||
c <- steamListResponse.AppList
|
||||
}
|
||||
|
||||
func downloadGameHeaderImage(appId string) (string, error) {
|
||||
url := fmt.Sprintf(baseGameHeaderURL, appId)
|
||||
coverURL, err := helpers.DownloadURLIntoTempFile(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return coverURL, nil
|
||||
}
|
||||
|
||||
func GuessUsers() []string {
|
||||
var users []string
|
||||
var path string = filepath.Join(getBasePathForOS(), "userdata")
|
||||
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if _, err := strconv.ParseInt(file.Name(), 10, 64); err == nil {
|
||||
log.Printf("Found local install Steam user: %s", file.Name())
|
||||
users = append(users, file.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
func GetGamesFromUser(user string) []string {
|
||||
log.Println("Getting Steam games for user: " + user)
|
||||
var userGames []string
|
||||
var path string = filepath.Join(getBasePathForOS(), "userdata", user, "760", "remote")
|
||||
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
// len(file.Name()) == 20 -> Custom added Game to steam
|
||||
userGames = append(userGames, file.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return userGames
|
||||
}
|
||||
|
||||
func GetScreenshotsForGame(user string, game *providers.Game) {
|
||||
path := filepath.Join(getBasePathForOS(), "userdata", user, "/760/remote/", game.ID, "screenshots")
|
||||
files, err := ioutil.ReadDir(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if strings.Contains(file.Name(), ".jpg") {
|
||||
game.Screenshots = append(game.Screenshots, providers.Screenshot{Path: path + "/" + file.Name()})
|
||||
// log.Printf("Found screenshot for user %s and game %d: %s", user, game.ID, path+"/"+file.Name())
|
||||
}
|
||||
}
|
||||
|
||||
if len(game.Screenshots) > 0 {
|
||||
log.Printf("Found %d screenshots", len(game.Screenshots))
|
||||
}
|
||||
}
|
||||
|
||||
func GetGames(cliOptions providers.ProviderOptions) []providers.Game {
|
||||
var localGames []providers.Game
|
||||
c := make(chan SteamAppList)
|
||||
go getSteamAppList(c)
|
||||
users := GuessUsers()
|
||||
steamApps := <-c
|
||||
for _, userID := range users {
|
||||
userGames := GetGamesFromUser(userID)
|
||||
for _, userGameID := range userGames {
|
||||
steamGame, err := steamApps.FindID(userGameID)
|
||||
if err != nil {
|
||||
log.Print("[ERROR] Steam game ID not found: ", userGameID)
|
||||
}
|
||||
userGame := providers.Game{ID: userGameID, Name: steamGame.Name, Provider: providerName, Platform: "PC"}
|
||||
|
||||
if *cliOptions.DownloadCovers {
|
||||
coverPath, err := downloadGameHeaderImage(userGameID)
|
||||
if err == nil {
|
||||
userGame.Cover = providers.Screenshot{Path: coverPath, DestinationName: ".cover"}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Found Steam game for user %s: %s (%s)", userID, userGame.Name, userGame.ID)
|
||||
GetScreenshotsForGame(userID, &userGame)
|
||||
localGames = append(localGames, userGame)
|
||||
}
|
||||
}
|
||||
return localGames
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var ErrProviderAlreadyRegistered = errors.New("provider already registered")
|
||||
var ErrProviderNotRegistered = errors.New("provider not registered")
|
||||
|
||||
type ProviderRegistry struct {
|
||||
logger *logrus.Entry
|
||||
cache models.Cache
|
||||
|
||||
providers map[string]*models.Provider
|
||||
}
|
||||
|
||||
func (r *ProviderRegistry) Register(name string, providerFactory models.ProviderFactory) error {
|
||||
_, exists := r.providers[name]
|
||||
if exists {
|
||||
return ErrProviderAlreadyRegistered
|
||||
}
|
||||
|
||||
provider := providerFactory(r.logger.Logger, r.cache)
|
||||
r.providers[name] = &provider
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *ProviderRegistry) Get(providerName string) (models.Provider, error) {
|
||||
provider, exists := r.providers[providerName]
|
||||
if !exists {
|
||||
return nil, ErrProviderNotRegistered
|
||||
}
|
||||
return *provider, nil
|
||||
}
|
||||
|
||||
func NewProviderRegistry(logger *logrus.Logger, cache models.Cache) *ProviderRegistry {
|
||||
return &ProviderRegistry{
|
||||
logger: logger.WithField("from", "registry"),
|
||||
cache: cache,
|
||||
providers: make(map[string]*models.Provider),
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue