refactor: working on new version
- Refactored all providers, all following the same interface - Added the registry component where all providers get initializated - Added the processor component in charge of processing screenshots/covers - Split configuration into Options and ProviderOptions - Refactored the workaround for the covers, now the game provide a CoverURL and the processor decides to download it or not - Made the providers folder hierarchy more clear, and moved helper functions to other files for sanity - Simplified CLI
This commit is contained in:
parent
b608e9e53e
commit
c03232381c
|
@ -2,3 +2,5 @@ Output
|
|||
Album
|
||||
build
|
||||
dist/
|
||||
.DS_Store
|
||||
Output*
|
||||
|
|
|
@ -1,25 +1,20 @@
|
|||
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/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"
|
||||
)
|
||||
|
||||
var allowedProviders = [...]string{"steam", "minecraft", "nintendo-switch", "playstation-4", "retroarch"}
|
||||
|
||||
const defaultOutputPath string = "./Output"
|
||||
|
||||
const defaultInputPath string = ""
|
||||
|
@ -28,104 +23,49 @@ 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"),
|
||||
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())
|
||||
|
||||
options := models.Options{
|
||||
OutputPath: *flag.String("output-path", defaultOutputPath, "The destination path of the screenshots"),
|
||||
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"),
|
||||
ProcessBufferSize: 0, // Unbuffered for now
|
||||
}
|
||||
var providerName = flag.String("provider", defaultProvider, "steam")
|
||||
providerOptions := models.ProviderOptions{
|
||||
InputPath: *flag.String("input-path", defaultInputPath, "Input path for the provider that requires it"),
|
||||
}
|
||||
var provider = flag.String("provider", defaultProvider, "steam")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
if helpers.SliceContainsString(allowedProviders[:], *provider, nil) {
|
||||
games := getGamesFromProvider(*provider, cliOptions)
|
||||
processGames(games, cliOptions)
|
||||
} else {
|
||||
log.Printf("Provider %s not found!", *provider)
|
||||
provider, err := registry.Get(*providerName)
|
||||
if err != nil {
|
||||
log.Printf("Provider %s not found!", *providerName)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
games, err := provider.FindGames(providerOptions)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
processor := processor.NewProcessor(options)
|
||||
|
||||
if len(games) > 0 {
|
||||
processor.Start(ctx)
|
||||
|
||||
for _, g := range games {
|
||||
processor.Process(g)
|
||||
}
|
||||
|
||||
processor.Wait()
|
||||
}
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
|
|
@ -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,16 @@
|
|||
package models
|
||||
|
||||
type Options struct {
|
||||
OutputPath string
|
||||
DryRun bool
|
||||
DownloadCovers bool
|
||||
ProcessBufferSize int
|
||||
}
|
||||
|
||||
type ProviderOptions struct {
|
||||
InputPath string
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
FindGames(options ProviderOptions) ([]*Game, error)
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package processor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log"
|
||||
"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"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
input chan *models.Game
|
||||
options models.Options
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func (p *Processor) Start(ctx context.Context) {
|
||||
go p.process(ctx)
|
||||
}
|
||||
|
||||
func (p *Processor) Process(game *models.Game) {
|
||||
p.input <- game
|
||||
}
|
||||
|
||||
func (p *Processor) process(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case game := <-p.input:
|
||||
p.wg.Add(1)
|
||||
if err := p.processGame(game); err != nil {
|
||||
log.Printf("[err] %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) Wait() {
|
||||
p.wg.Wait()
|
||||
}
|
||||
|
||||
func NewProcessor(options models.Options) *Processor {
|
||||
return &Processor{
|
||||
input: make(chan *models.Game, options.ProcessBufferSize),
|
||||
options: options,
|
||||
wg: sync.WaitGroup{},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Reduce into smaller functions
|
||||
func (p *Processor) processGame(game *models.Game) (err error) {
|
||||
defer p.wg.Done()
|
||||
|
||||
// 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 {
|
||||
log.Printf("[IMPORTANT] Game ID %s has no name!", game.ID)
|
||||
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 {
|
||||
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(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 {
|
||||
log.Printf("[error] Error donwloading cover: %s", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Fatal(err)
|
||||
return err
|
||||
}
|
||||
destinationMd5, err := helpers.Md5File(destinationPath)
|
||||
if err != nil {
|
||||
log.Fatal(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)
|
||||
}
|
||||
|
||||
} else {
|
||||
if p.options.DryRun {
|
||||
log.Println(filepath.Base(screenshot.Path), " -> ", strings.Replace(destinationPath, helpers.ExpandUser(p.options.OutputPath), "", 1))
|
||||
} else {
|
||||
helpers.CopyFile(screenshot.Path, destinationPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package minecraft
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"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) {
|
||||
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, models.Screenshot{Path: path + "/" + file.Name(), DestinationName: file.Name()})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,42 @@
|
|||
package minecraft
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
|
||||
)
|
||||
|
||||
const Name = "minecraft"
|
||||
|
||||
type MinecraftProvider struct{}
|
||||
|
||||
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" {
|
||||
getScreenshotsFromPath(&minecraftStandalone, "~/.minecraft/screenshots")
|
||||
|
||||
// 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)
|
||||
}
|
||||
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, nil
|
||||
}
|
||||
|
||||
func NewMinecraftProvider() *MinecraftProvider {
|
||||
return &MinecraftProvider{}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package nintendo_switch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"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() []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 games.", len(switchGameList))
|
||||
|
||||
return switchGameList
|
||||
}
|
||||
|
||||
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.ID == "" {
|
||||
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,55 @@
|
|||
package nintendo_switch
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
)
|
||||
|
||||
const Name = "nintendo-switch"
|
||||
const platformName = "Nintendo Switch"
|
||||
const gameListURL = "https://fmartingr.github.io/switch-games-json/switch_id_names.json"
|
||||
|
||||
type NintendoSwitchProvider struct{}
|
||||
|
||||
func (p *NintendoSwitchProvider) FindGames(options models.ProviderOptions) ([]*models.Game, error) {
|
||||
switchGames := getSwitchGameList()
|
||||
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 {
|
||||
log.Panic("Could not parse filename: ", err)
|
||||
}
|
||||
|
||||
screenshot := models.Screenshot{Path: path, DestinationName: destinationName.Format(models.DatetimeFormat) + extension}
|
||||
userGames = addScreenshotToGame(userGames, switchGame, screenshot)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
return userGames, nil
|
||||
}
|
||||
|
||||
func NewNintendoSwitchProvider() *NintendoSwitchProvider {
|
||||
return &NintendoSwitchProvider{}
|
||||
}
|
|
@ -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 games.", 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.ID == "" {
|
||||
foundGame := models.Game{Name: gameName, ID: gameName, Platform: platformName, Provider: platformName}
|
||||
foundGame.Screenshots = append(foundGame.Screenshots, screenshot)
|
||||
userGames = append(userGames, &foundGame)
|
||||
}
|
||||
|
||||
return userGames
|
||||
}
|
|
@ -6,36 +6,19 @@ import (
|
|||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/providers"
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/rwcarlsen/goexif/exif"
|
||||
)
|
||||
|
||||
const providerName = "playstation-4"
|
||||
const Name = "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)
|
||||
}
|
||||
}
|
||||
type Playstation4Provider struct{}
|
||||
|
||||
// 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)
|
||||
}
|
||||
func (p *Playstation4Provider) FindGames(options models.ProviderOptions) ([]*models.Game, error) {
|
||||
var userGames []*models.Game
|
||||
|
||||
return userGames
|
||||
}
|
||||
|
||||
func GetGames(cliOptions providers.ProviderOptions) []providers.Game {
|
||||
var userGames []providers.Game
|
||||
|
||||
err := filepath.Walk(*cliOptions.InputPath,
|
||||
err := filepath.Walk(options.InputPath,
|
||||
func(filePath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -62,14 +45,14 @@ func GetGames(cliOptions providers.ProviderOptions) []providers.Game {
|
|||
defer fileDescriptor.Close()
|
||||
|
||||
exifDateTime, _ := exifData.DateTime()
|
||||
destinationName = exifDateTime.Format(providers.DatetimeFormat)
|
||||
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(providers.DatetimeFormat)
|
||||
destinationName = videoDatetime.Format(models.DatetimeFormat)
|
||||
} else {
|
||||
log.Printf("[Warning] File does not follow datetime convention: %s. (%s) skipping...", fileName, err)
|
||||
return nil
|
||||
|
@ -80,9 +63,8 @@ func GetGames(cliOptions providers.ProviderOptions) []providers.Game {
|
|||
}
|
||||
}
|
||||
|
||||
screenshot := providers.Screenshot{Path: filePath, DestinationName: destinationName + extension}
|
||||
userGames = addScreenshotToGame(userGames, gameName, screenshot)
|
||||
|
||||
screenshot := models.Screenshot{Path: filePath, DestinationName: destinationName + extension}
|
||||
addScreenshotToGame(userGames, gameName, screenshot)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -90,5 +72,9 @@ func GetGames(cliOptions providers.ProviderOptions) []providers.Game {
|
|||
if err != nil {
|
||||
log.Panic(err)
|
||||
}
|
||||
return userGames
|
||||
return userGames, nil
|
||||
}
|
||||
|
||||
func NewPlaystation4Provider() *Playstation4Provider {
|
||||
return &Playstation4Provider{}
|
||||
}
|
|
@ -1,15 +1,3 @@
|
|||
// 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 (
|
||||
|
@ -23,16 +11,11 @@ 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"
|
||||
)
|
||||
|
||||
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 +24,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 +32,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,8 +53,8 @@ func cleanGameName(gameName string) string {
|
|||
return splits[0]
|
||||
}
|
||||
|
||||
func readPlaylists(playlistsPath string) map[string]RetroArchPlaylist {
|
||||
var result = make(map[string]RetroArchPlaylist)
|
||||
func readPlaylists(playlistsPath string) map[string]retroArchPlaylist {
|
||||
var result = make(map[string]retroArchPlaylist)
|
||||
playlistsPath = helpers.ExpandUser(playlistsPath)
|
||||
if _, err := os.Stat(playlistsPath); !os.IsNotExist(err) {
|
||||
files, err := ioutil.ReadDir(playlistsPath)
|
||||
|
@ -81,7 +64,7 @@ func readPlaylists(playlistsPath string) map[string]RetroArchPlaylist {
|
|||
|
||||
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)
|
||||
|
@ -106,8 +89,8 @@ func readPlaylists(playlistsPath string) map[string]RetroArchPlaylist {
|
|||
return result
|
||||
}
|
||||
|
||||
func findScreenshotsForGame(item RetroArchPlaylistItem) []providers.Screenshot {
|
||||
var result []providers.Screenshot
|
||||
func findScreenshotsForGame(item retroArchPlaylistItem) []models.Screenshot {
|
||||
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)
|
||||
|
@ -130,51 +113,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)
|
||||
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
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
// 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"
|
||||
)
|
||||
|
||||
const Name = "retroarch"
|
||||
|
||||
const libretroCoverURLBase = "http://thumbnails.libretro.com/"
|
||||
const datetimeLayout = "060102-150405"
|
||||
|
||||
type RetroArchProvider struct {
|
||||
}
|
||||
|
||||
func (p *RetroArchProvider) FindGames(options models.ProviderOptions) ([]*models.Game, error) {
|
||||
var userGames []*models.Game
|
||||
|
||||
playlists := readPlaylists(options.InputPath)
|
||||
|
||||
for playlistName := range playlists {
|
||||
for _, item := range playlists[playlistName].Items {
|
||||
userGames = append(userGames, &models.Game{
|
||||
Platform: cleanPlatformName(playlistName),
|
||||
Name: cleanGameName(item.Label),
|
||||
Provider: Name,
|
||||
Screenshots: findScreenshotsForGame(item),
|
||||
CoverURL: formatLibretroBoxartURL(playlistName, item.Label),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return userGames, nil
|
||||
}
|
||||
|
||||
func NewRetroArchProvider() *RetroArchProvider {
|
||||
return &RetroArchProvider{}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package steam
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
"github.com/fmartingr/games-screenshot-manager/pkg/helpers"
|
||||
)
|
||||
|
||||
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 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 *models.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, models.NewScreenshotWithoutDestination(path+"/"+file.Name()))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
package steam
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strconv"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
)
|
||||
|
||||
const Name = "steam"
|
||||
const gameListURL = "https://api.steampowered.com/ISteamApps/GetAppList/v2/"
|
||||
const baseGameHeaderURL = "https://cdn.cloudflare.steamstatic.com/steam/apps/%d/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"`
|
||||
}
|
||||
|
||||
type SteamProvider struct{}
|
||||
|
||||
func (p *SteamProvider) FindGames(options models.ProviderOptions) ([]*models.Game, error) {
|
||||
var localGames []*models.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 := 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)
|
||||
localGames = append(localGames, &userGame)
|
||||
}
|
||||
}
|
||||
return localGames, nil
|
||||
}
|
||||
|
||||
func NewSteamProvider() *SteamProvider {
|
||||
return &SteamProvider{}
|
||||
}
|
|
@ -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,39 @@
|
|||
package registry
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/fmartingr/games-screenshot-manager/internal/models"
|
||||
)
|
||||
|
||||
var ErrProviderAlreadyRegistered = errors.New("provider already registered")
|
||||
var ErrProviderNotRegistered = errors.New("provider not registered")
|
||||
|
||||
type ProviderRegistry struct {
|
||||
providers map[string]*models.Provider
|
||||
}
|
||||
|
||||
func (r *ProviderRegistry) Register(name string, provider models.Provider) error {
|
||||
_, exists := r.providers[name]
|
||||
if exists {
|
||||
return ErrProviderAlreadyRegistered
|
||||
}
|
||||
|
||||
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() *ProviderRegistry {
|
||||
return &ProviderRegistry{
|
||||
providers: make(map[string]*models.Provider),
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue