feat: improved logging with logrus

Propagated a logrus.Logger/Entry along for the processor, registry and
providers to use for events that may not require to stop processing but
are useful to know otherwise.

Changed some functions to return errors instead of failing with a logger
trying to centralice the logging or returning of the main failing points
in the main components of the application.

Closes #13
This commit is contained in:
Felipe M 2022-01-27 18:28:53 +01:00 committed by Felipe Martin Garcia
parent 76afe81086
commit 3d56f84762
16 changed files with 233 additions and 125 deletions

1
go.mod
View File

@ -5,4 +5,5 @@ go 1.15
require (
github.com/gosimple/slug v1.12.0
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
github.com/sirupsen/logrus v1.8.1
)

10
go.sum
View File

@ -1,6 +1,16 @@
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=

View File

@ -3,7 +3,6 @@ package cli
import (
"context"
"flag"
"log"
"os"
"github.com/fmartingr/games-screenshot-manager/internal/models"
@ -14,6 +13,7 @@ import (
"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/registry"
"github.com/sirupsen/logrus"
)
const defaultOutputPath string = "./Output"
@ -24,15 +24,16 @@ const defaultDryRun bool = false
const defaultDownloadCovers bool = false
func Start() {
registry := registry.NewProviderRegistry()
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())
logger := logrus.New()
flagSet := flag.NewFlagSet("gsm", flag.ExitOnError)
registry := registry.NewProviderRegistry(logger)
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,
}
@ -46,22 +47,33 @@ func Start() {
providerOptions := models.ProviderOptions{}
flagSet.StringVar(&providerOptions.InputPath, "input-path", defaultInputPath, "Input path for the provider that requires it")
flagSet.Parse(os.Args[1:])
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 {
log.Printf("Provider %s not found!", *providerName)
logger.Errorf("Provider %s not found!", *providerName)
return
}
games, err := provider.FindGames(providerOptions)
if err != nil {
log.Println(err)
logger.Errorf("Error obtaining game list: %s", err)
return
}
if len(games) > 0 {
ctx, cancel := context.WithCancel(context.Background())
processor := processor.NewProcessor(options)
processor := processor.NewProcessor(logger, options)
processor.Start(ctx)
for _, g := range games {

View File

@ -1,5 +1,7 @@
package models
import "github.com/sirupsen/logrus"
type ProviderOptions struct {
InputPath string
}
@ -7,3 +9,5 @@ type ProviderOptions struct {
type Provider interface {
FindGames(options ProviderOptions) ([]*Game, error)
}
type ProviderFactory func(logger *logrus.Logger) Provider

View File

@ -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)

View File

@ -3,7 +3,6 @@ package processor
import (
"bytes"
"context"
"log"
"os"
"path/filepath"
"strings"
@ -12,12 +11,15 @@ import (
"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 {
games chan *models.Game
logger *logrus.Entry
options models.Options
wg *sync.WaitGroup
games chan *models.Game
wg *sync.WaitGroup
}
func (p *Processor) Start(ctx context.Context) {
@ -32,7 +34,7 @@ func (p *Processor) Process(game *models.Game) {
}
func (p *Processor) process(ctx context.Context) {
log.Println("Started worker process")
p.logger.Debug("Worker started")
for {
select {
case <-ctx.Done():
@ -40,7 +42,7 @@ func (p *Processor) process(ctx context.Context) {
case game := <-p.games:
if err := p.processGame(game); err != nil {
log.Printf("[err] %s", err)
p.logger.Errorf("Error processing game %s from %s: %s", game.Name, game.Provider, err)
}
}
}
@ -54,7 +56,10 @@ func (p *Processor) Wait() {
func (p *Processor) processGame(game *models.Game) (err error) {
defer p.wg.Done()
log.Printf("Processing game: %s", game.Name)
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 {
@ -65,7 +70,7 @@ func (p *Processor) processGame(game *models.Game) (err error) {
if len(game.Name) > 0 {
destinationPath = filepath.Join(destinationPath, game.Name)
} else {
log.Printf("[IMPORTANT] Game ID %s has no name!", game.ID)
p.logger.Warnf("found game with ID: %s from %s without a name", game.ID, game.Provider)
destinationPath = filepath.Join(destinationPath, game.ID)
}
@ -73,7 +78,7 @@ func (p *Processor) processGame(game *models.Game) (err error) {
if _, err := os.Stat(destinationPath); os.IsNotExist(err) && !p.options.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))
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)
}
@ -83,12 +88,13 @@ func (p *Processor) processGame(game *models.Game) (err error) {
destinationCoverPath := filepath.Join(destinationPath, ".cover")
coverPath, err := helpers.DownloadURLIntoTempFile(game.CoverURL)
if err != nil {
log.Printf("[error] Error donwloading cover: %s", err)
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)
}
}
if _, err := os.Stat(destinationCoverPath); os.IsNotExist(err) {
helpers.CopyFile(coverPath, destinationCoverPath)
}
}
for _, screenshot := range game.Screenshots {
@ -97,26 +103,29 @@ func (p *Processor) processGame(game *models.Game) (err error) {
if _, err := os.Stat(destinationPath); !os.IsNotExist(err) {
sourceMd5, err := helpers.Md5File(screenshot.Path)
if err != nil {
log.Fatal(err)
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 {
log.Fatal(err)
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?
log.Println("Found different screenshot with equal timestamp for game ", game.Name, screenshot.Path)
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 {
log.Println(filepath.Base(screenshot.Path), " -> ", strings.Replace(destinationPath, helpers.ExpandUser(p.options.OutputPath), "", 1))
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 {
log.Printf("[error] error during operation: %s", err)
p.logger.WithFields(logrus.Fields{
"src": screenshot.Path,
"dest": destinationPath,
}).Errorf("Error during copy operation: %s", err)
}
}
}
@ -125,8 +134,9 @@ func (p *Processor) processGame(game *models.Game) (err error) {
return nil
}
func NewProcessor(options models.Options) *Processor {
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{},

View File

@ -1,8 +1,8 @@
package minecraft
import (
"fmt"
"io/ioutil"
"log"
"os"
"strings"
@ -10,12 +10,12 @@ import (
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
)
func getScreenshotsFromPath(game *models.Game, path string) {
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 {
log.Fatal(err)
return fmt.Errorf("error reading from %s: %s", path, err)
}
for _, file := range files {
@ -24,4 +24,5 @@ func getScreenshotsFromPath(game *models.Game, path string) {
}
}
}
return nil
}

View File

@ -7,11 +7,14 @@ import (
"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{}
type MinecraftProvider struct {
logger *logrus.Entry
}
func (p *MinecraftProvider) FindGames(options models.ProviderOptions) ([]*models.Game, error) {
var result []*models.Game
@ -19,24 +22,34 @@ func (p *MinecraftProvider) FindGames(options models.ProviderOptions) ([]*models
minecraftStandalone := models.Game{Name: "Minecraft", Platform: "PC", Notes: "Standalone"}
if runtime.GOOS == "linux" {
getScreenshotsFromPath(&minecraftStandalone, "~/.minecraft/screenshots")
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"} {
getScreenshotsFromPath(&minecraftFlatpak, path)
if err := getScreenshotsFromPath(&minecraftFlatpak, path); err != nil {
p.logger.Error(err)
}
}
result = append(result, &minecraftFlatpak)
} else if runtime.GOOS == "windows" {
getScreenshotsFromPath(&minecraftStandalone, filepath.Join(os.Getenv("APPDATA"), ".minecraft/screenshots"))
if err := getScreenshotsFromPath(&minecraftStandalone, filepath.Join(os.Getenv("APPDATA"), ".minecraft/screenshots")); err != nil {
p.logger.Error(err)
}
} else if runtime.GOOS == "darwin" {
getScreenshotsFromPath(&minecraftStandalone, filepath.Join(helpers.ExpandUser("~/Library/Application Support/minecraft/screenshots")))
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() *MinecraftProvider {
return &MinecraftProvider{}
func NewMinecraftProvider(logger *logrus.Logger) models.Provider {
return &MinecraftProvider{
logger: logger.WithField("from", "provider."+Name),
}
}

View File

@ -2,8 +2,8 @@ package nintendo_switch
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"strings"
"github.com/fmartingr/games-screenshot-manager/internal/models"
@ -26,10 +26,10 @@ func findGameByEncryptedID(gameList []SwitchGame, encryptedGameID string) Switch
return gameFound
}
func getSwitchGameList() []SwitchGame {
func getSwitchGameList() (result []SwitchGame, err error) {
response, err := helpers.DoRequest("GET", gameListURL)
if err != nil {
log.Panic(err)
return nil, fmt.Errorf("error getting switch game list: %s", err)
}
if response.Body != nil {
@ -38,18 +38,15 @@ func getSwitchGameList() []SwitchGame {
body, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Panic(err)
return nil, fmt.Errorf("error getting switch game list: %s", err)
}
switchGameList := []SwitchGame{}
jsonErr := json.Unmarshal(body, &switchGameList)
jsonErr := json.Unmarshal(body, &result)
if jsonErr != nil {
log.Fatal(jsonErr)
return nil, fmt.Errorf("error getting switch game list: %s", err)
}
log.Printf("Updated Nintendo Switch game list. Found %d games.", len(switchGameList))
return switchGameList
return
}
func addScreenshotToGame(userGames []*models.Game, switchGame SwitchGame, screenshot models.Screenshot) []*models.Game {

View File

@ -1,26 +1,33 @@
package nintendo_switch
import (
"log"
"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{}
type NintendoSwitchProvider struct {
logger *logrus.Entry
}
func (p *NintendoSwitchProvider) FindGames(options models.ProviderOptions) ([]*models.Game, error) {
switchGames := getSwitchGameList()
switchGames, err := getSwitchGameList()
if err != nil {
p.logger.Error(err)
return nil, err
}
var userGames []*models.Game
err := filepath.Walk(options.InputPath,
err = filepath.Walk(options.InputPath,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
@ -36,7 +43,7 @@ func (p *NintendoSwitchProvider) FindGames(options models.ProviderOptions) ([]*m
destinationName, err := time.Parse(layout, filenameParsed[0][0:14])
if err != nil {
log.Panic("Could not parse filename: ", err)
p.logger.Errorf("Could not parse filename '%s': %s", filename, err)
}
screenshot := models.Screenshot{Path: path, DestinationName: destinationName.Format(models.DatetimeFormat) + extension}
@ -45,11 +52,13 @@ func (p *NintendoSwitchProvider) FindGames(options models.ProviderOptions) ([]*m
return nil
})
if err != nil {
log.Panic(err)
return nil, err
}
return userGames, nil
}
func NewNintendoSwitchProvider() *NintendoSwitchProvider {
return &NintendoSwitchProvider{}
func NewNintendoSwitchProvider(logger *logrus.Logger) models.Provider {
return &NintendoSwitchProvider{
logger: logger.WithField("from", "provider."+Name),
}
}

View File

@ -1,19 +1,21 @@
package playstation4
import (
"log"
"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{}
type Playstation4Provider struct {
logger *logrus.Entry
}
func (p *Playstation4Provider) FindGames(options models.ProviderOptions) ([]*models.Game, error) {
var userGames []*models.Game
@ -34,12 +36,12 @@ func (p *Playstation4Provider) FindGames(options models.ProviderOptions) ([]*mod
if extension == ".jpg" {
fileDescriptor, errFileDescriptor := os.Open(filePath)
if errFileDescriptor != nil {
log.Printf("[warning] Couldn't open file %s: %s", fileName, errFileDescriptor)
p.logger.Warnf("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)
p.logger.Errorf("Decoding EXIF data from %s: %s", filePath, errExifData)
return nil
}
defer fileDescriptor.Close()
@ -54,11 +56,11 @@ func (p *Playstation4Provider) FindGames(options models.ProviderOptions) ([]*mod
if err == nil {
destinationName = videoDatetime.Format(models.DatetimeFormat)
} else {
log.Printf("[Warning] File does not follow datetime convention: %s. (%s) skipping...", fileName, err)
p.logger.Warnf("File %s does not follow datetime convention, skipping.", fileName, err)
return nil
}
} else {
log.Printf("[Warning] File does not follow datetime convention: %s, skipping...", fileName)
p.logger.Warnf("File %s does not follow datetime convention, skipping.", fileName)
return nil
}
}
@ -70,11 +72,13 @@ func (p *Playstation4Provider) FindGames(options models.ProviderOptions) ([]*mod
return nil
})
if err != nil {
log.Panic(err)
return nil, err
}
return userGames, nil
}
func NewPlaystation4Provider() *Playstation4Provider {
return &Playstation4Provider{}
func NewPlaystation4Provider(logger *logrus.Logger) models.Provider {
return &Playstation4Provider{
logger: logger.WithField("from", "provider."+Name),
}
}

View File

@ -2,8 +2,8 @@ package retroarch
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/url"
"os"
"path"
@ -13,6 +13,7 @@ import (
"github.com/fmartingr/games-screenshot-manager/internal/models"
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
"github.com/sirupsen/logrus"
)
type retroArchPlaylistItem struct {
@ -53,13 +54,13 @@ func cleanGameName(gameName string) string {
return splits[0]
}
func readPlaylists(playlistsPath string) 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 {
@ -67,18 +68,18 @@ func readPlaylists(playlistsPath string) map[string]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
@ -86,16 +87,16 @@ func readPlaylists(playlistsPath string) map[string]retroArchPlaylist {
}
}
}
return result
return result, nil
}
func findScreenshotsForGame(item retroArchPlaylistItem) []models.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 {
@ -119,7 +120,7 @@ func findScreenshotsForGame(item retroArchPlaylistItem) []models.Screenshot {
if err == nil {
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
}
}
@ -127,5 +128,5 @@ func findScreenshotsForGame(item retroArchPlaylistItem) []models.Screenshot {
result = append(result, models.Screenshot{Path: filepath.Join(filePath, file.Name()), DestinationName: screenshotDestinationName})
}
}
return result
return result, nil
}

View File

@ -14,6 +14,7 @@ package retroarch
import (
"github.com/fmartingr/games-screenshot-manager/internal/models"
"github.com/sirupsen/logrus"
)
const Name = "retroarch"
@ -22,20 +23,30 @@ 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 := readPlaylists(options.InputPath)
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: findScreenshotsForGame(item),
Screenshots: screenshots,
CoverURL: formatLibretroBoxartURL(playlistName, item.Label),
})
}
@ -44,6 +55,8 @@ func (p *RetroArchProvider) FindGames(options models.ProviderOptions) ([]*models
return userGames, nil
}
func NewRetroArchProvider() *RetroArchProvider {
return &RetroArchProvider{}
func NewRetroArchProvider(logger *logrus.Logger) models.Provider {
return &RetroArchProvider{
logger: logger.WithField("from", "provider."+Name),
}
}

View File

@ -2,8 +2,8 @@ package steam
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
@ -12,9 +12,10 @@ import (
"github.com/fmartingr/games-screenshot-manager/internal/models"
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
"github.com/sirupsen/logrus"
)
func getBasePathForOS() string {
func getBasePathForOS() (string, error) {
var path string
switch runtime.GOOS {
case "darwin":
@ -24,16 +25,17 @@ func getBasePathForOS() string {
case "windows":
path = "C:\\Program Files (x86)\\Steam"
default:
log.Panic("Unsupported OS: ", runtime.GOOS)
return "", fmt.Errorf("unsupported os: %s", runtime.GOOS)
}
return path
return path, nil
}
func getSteamAppList(c chan SteamAppList) {
log.Println("Updating steam game list...")
func getSteamAppList(logger *logrus.Entry, c chan SteamAppList) {
defer close(c)
response, err := helpers.DoRequest("GET", gameListURL)
if err != nil {
panic(err)
logger.Errorf("Error making request for Steam APP List: %s", err)
}
if response.Body != nil {
@ -42,49 +44,45 @@ func getSteamAppList(c chan SteamAppList) {
body, err := ioutil.ReadAll(response.Body)
if err != nil {
panic(err)
logger.Errorf("Error reading steam response: %s", err)
}
steamListResponse := SteamAppListResponse{}
jsonErr := json.Unmarshal(body, &steamListResponse)
if jsonErr != nil {
log.Fatal(jsonErr)
logger.Errorf("Error unmarshalling steam's response: %s", jsonErr)
}
log.Printf("Updated Steam game list. Found %d apps.", len(steamListResponse.AppList.Apps))
c <- steamListResponse.AppList
}
func guessUsers() []string {
func guessUsers(basePath string) ([]string, error) {
var users []string
var path string = filepath.Join(getBasePathForOS(), "userdata")
var path string = filepath.Join(basePath, "userdata")
if _, err := os.Stat(path); !os.IsNotExist(err) {
files, err := ioutil.ReadDir(path)
if err != nil {
log.Fatal(err)
return nil, 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
return users, nil
}
func getGamesFromUser(user string) []string {
log.Println("Getting Steam games for user: " + user)
func getGamesFromUser(basePath, user string) ([]string, error) {
var userGames []string
var path string = filepath.Join(getBasePathForOS(), "userdata", user, "760", "remote")
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 {
log.Fatal(err)
return nil, err
}
for _, file := range files {
@ -93,14 +91,14 @@ func getGamesFromUser(user string) []string {
}
}
return userGames
return userGames, nil
}
func getScreenshotsForGame(user string, game *models.Game) {
path := filepath.Join(getBasePathForOS(), "userdata", user, "/760/remote/", game.ID, "screenshots")
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 {
log.Fatal(err)
return fmt.Errorf("error reading game screenshot path: %s", err)
}
for _, file := range files {
@ -108,4 +106,6 @@ func getScreenshotsForGame(user string, game *models.Game) {
game.Screenshots = append(game.Screenshots, models.NewScreenshotWithoutDestination(path+"/"+file.Name()))
}
}
return nil
}

View File

@ -3,16 +3,18 @@ package steam
import (
"errors"
"fmt"
"log"
"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"`
@ -22,51 +24,77 @@ type SteamAppList struct {
Apps []SteamApp `json:"apps"`
}
func (appList SteamAppList) FindID(id string) (SteamApp, error) {
GameIDNotFound := errors.New("game ID not found")
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 {
log.Panic(err)
return result, fmt.Errorf("error parsing game ID: %s", err)
}
if game.AppID == uintGameID {
return game, nil
}
}
return SteamApp{}, GameIDNotFound
return result, errGameIDNotFound
}
type SteamAppListResponse struct {
AppList SteamAppList `json:"applist"`
}
type SteamProvider struct{}
type SteamProvider struct {
logger *logrus.Entry
}
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(c)
users := guessUsers()
go getSteamAppList(p.logger, 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 := getGamesFromUser(userID)
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 {
log.Print("[ERROR] Steam game ID not found: ", userGameID)
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)
log.Printf("Found Steam game for user %s: %s (%s)", userID, userGame.Name, userGame.ID)
getScreenshotsForGame(userID, &userGame)
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() *SteamProvider {
return &SteamProvider{}
func NewSteamProvider(logger *logrus.Logger) models.Provider {
return &SteamProvider{
logger: logger.WithField("from", "provider."+Name),
}
}

View File

@ -4,21 +4,24 @@ 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
providers map[string]*models.Provider
}
func (r *ProviderRegistry) Register(name string, provider models.Provider) error {
func (r *ProviderRegistry) Register(name string, providerFactory models.ProviderFactory) error {
_, exists := r.providers[name]
if exists {
return ErrProviderAlreadyRegistered
}
provider := providerFactory(r.logger.Logger)
r.providers[name] = &provider
return nil
@ -32,8 +35,9 @@ func (r *ProviderRegistry) Get(providerName string) (models.Provider, error) {
return *provider, nil
}
func NewProviderRegistry() *ProviderRegistry {
func NewProviderRegistry(logger *logrus.Logger) *ProviderRegistry {
return &ProviderRegistry{
logger: logger.WithField("from", "registry"),
providers: make(map[string]*models.Provider),
}
}