From 5d814b8539bf0f719670b1be7c90f03e0572d761 Mon Sep 17 00:00:00 2001 From: Felipe M Date: Thu, 14 Jan 2021 20:28:37 +0100 Subject: [PATCH] RetroArch support - Added the retroarch provider - Fixed some `path.X` references (moved to filepath) Note: In order to use RetroArch specific settings on the app are required. Read the top comment on the provider module to know how to start. --- README.md | 3 +- pkg/cli/cli.go | 8 +- pkg/games/structs.go | 4 +- pkg/providers/retroarch/retroarch.go | 155 +++++++++++++++++++++++++++ 4 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 pkg/providers/retroarch/retroarch.go diff --git a/README.md b/README.md index 9d4a0a9..edb7432 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,11 @@ Use the appropriate ID with the `-provider` flag. [See examples below](#Usage) | Name | ID | Notes | Covers | | --- | --- | --- | --- | -| Steam | `steam` | Linux, macOS, Windows | Yes [Example](https://steamcdn-a.akamaihd.net/steam/apps/377840/header.jpg) | +| Steam | `steam` | Linux, macOS, Windows | Yes ([Example](https://steamcdn-a.akamaihd.net/steam/apps/377840/header.jpg)) | | Minecraft | `minecraft` | Linux, Linux Flatpak, macOS, Windows | No | | Nintendo Switch | `nintendo-switch` | Requires `-input-path` | No | | PlayStation 4 | `playstation-4` | Requires `-input-path` | No | +| RetroArch | `retroarch` | Requires `-input-path` | No | ## How it works diff --git a/pkg/cli/cli.go b/pkg/cli/cli.go index c55733b..07dad15 100644 --- a/pkg/cli/cli.go +++ b/pkg/cli/cli.go @@ -5,7 +5,6 @@ import ( "flag" "log" "os" - "path" "path/filepath" "strings" @@ -14,11 +13,12 @@ import ( "github.com/fmartingr/games-screenshot-mananger/pkg/providers/minecraft" "github.com/fmartingr/games-screenshot-mananger/pkg/providers/nintendo_switch" "github.com/fmartingr/games-screenshot-mananger/pkg/providers/playstation4" + "github.com/fmartingr/games-screenshot-mananger/pkg/providers/retroarch" "github.com/fmartingr/games-screenshot-mananger/pkg/providers/steam" "github.com/gosimple/slug" ) -var allowedProviders = [...]string{"steam", "minecraft", "nintendo-switch", "playstation-4"} +var allowedProviders = [...]string{"steam", "minecraft", "nintendo-switch", "playstation-4", "retroarch"} const defaultOutputPath string = "./Output" @@ -56,6 +56,8 @@ func getGamesFromProvider(provider string, inputPath string, downloadCovers bool games = append(games, nintendo_switch.GetGames(inputPath)...) case "playstation-4": games = append(games, playstation4.GetGames(inputPath)...) + case "retroarch": + games = append(games, retroarch.GetGames(inputPath, downloadCovers)...) } return games } @@ -111,7 +113,7 @@ func processGames(games []games.Game, outputPath string, dryRun bool, downloadCo } else { if dryRun { - log.Println(path.Base(screenshot.Path), " -> ", strings.Replace(destinationPath, helpers.ExpandUser(outputPath), "", 1)) + log.Println(filepath.Base(screenshot.Path), " -> ", strings.Replace(destinationPath, helpers.ExpandUser(outputPath), "", 1)) } else { helpers.CopyFile(screenshot.Path, destinationPath) } diff --git a/pkg/games/structs.go b/pkg/games/structs.go index 8baa637..9293bd1 100644 --- a/pkg/games/structs.go +++ b/pkg/games/structs.go @@ -3,7 +3,7 @@ package games import ( "log" "os" - "path" + "path/filepath" ) const DatetimeFormat = "2006-01-02_15-04-05" @@ -31,5 +31,5 @@ func (screenshot Screenshot) GetDestinationName() string { if statErr != nil { log.Fatal(statErr) } - return fileStat.ModTime().Format(DatetimeFormat) + path.Ext(screenshot.Path) + return fileStat.ModTime().Format(DatetimeFormat) + filepath.Ext(screenshot.Path) } diff --git a/pkg/providers/retroarch/retroarch.go b/pkg/providers/retroarch/retroarch.go new file mode 100644 index 0000000..1df1705 --- /dev/null +++ b/pkg/providers/retroarch/retroarch.go @@ -0,0 +1,155 @@ +// 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" + "io/ioutil" + "log" + "net/url" + "os" + "path" + "path/filepath" + "strings" + "time" + + "github.com/fmartingr/games-screenshot-mananger/pkg/games" + "github.com/fmartingr/games-screenshot-mananger/pkg/helpers" +) + +const providerName = "retroarch" + +const libretroCoverURLBase = "http://thumbnails.libretro.com/" +const datetimeLayout = "060102-150405" + +type RetroArchPlaylistItem struct { + Path string `json:"path"` + Label string `json:"label"` + CorePath string `json:"core_path"` + CoreName string `json:"core_name"` + CRC32 string `json:"crc32"` + DBName string `json:"db_name"` +} + +type RetroArchPlaylist struct { + Version string `json:"version"` + DefaultCorePath string `json:"default_core_path"` + DefaultCoreName string `json:"default_core_name"` + LabelDisplayMode int `json:"label_display_mode"` + RightThumbnailMode int `json:"right_thumbnail_mode"` + LeftThumbnailMode int `json:"left_thumbnail_mode"` + SortMode int `json:"sort_mode"` + Items []RetroArchPlaylistItem `json:"items"` +} + +func formatLibretroBoxartURL(platform string, game string) string { + return libretroCoverURLBase + url.PathEscape(path.Join(platform, "Named_Boxarts", game)) + ".png" +} + +func cleanPlatformName(platformName string) string { + // Removes the "Nintendo - " portion of nintendo systems + // Could probably be extended to others (Sony, Microsoft) by removing all until the first hyphen + if strings.Contains(platformName, "Nintendo") { + return strings.Replace(platformName, "Nintendo - ", "", 1) + } + return platformName +} + +func cleanGameName(gameName string) string { + splits := strings.Split(gameName, "(") + return splits[0] +} + +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) + if err != nil { + log.Fatal(err) + } + + for _, file := range files { + if strings.Contains(file.Name(), ".lpl") { + 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) + continue + } + fileContents, errReadContent := ioutil.ReadAll(source) + if errReadContent != nil { + log.Printf("[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) + continue + } + result[strings.Replace(file.Name(), ".lpl", "", 1)] = item + source.Close() + } + } + } + return result +} + +func findScreenshotsForGame(item RetroArchPlaylistItem) []games.Screenshot { + var result []games.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) + } + + for _, file := range files { + // Get all screenshots for the game, excluding state screenshots + if !file.IsDir() && strings.Contains(file.Name(), fileName) && strings.Contains(file.Name(), ".png") { + if strings.Contains(file.Name(), ".state.") || strings.Contains(file.Name(), "-cheevo-") { + // Ignore state and achievement screenshots? + continue + } + extension := filepath.Ext(file.Name()) + screenshotDate, err := time.Parse(datetimeLayout, file.Name()[len(file.Name())-len(extension)-len(datetimeLayout):len(file.Name())-len(extension)]) + if err == nil { + result = append(result, games.Screenshot{Path: filepath.Join(filePath, file.Name()), DestinationName: screenshotDate.Format(games.DatetimeFormat) + extension}) + } else { + log.Printf("[error] Formatting screenshot %s: %s", file.Name(), err) + } + + } + } + return result +} + +func GetGames(inputPath string, downloadCovers bool) []games.Game { + var userGames []games.Game + + playlists := readPlaylists(inputPath) + + for playlistName := range playlists { + for _, item := range playlists[playlistName].Items { + userGames = append(userGames, games.Game{ + Platform: cleanPlatformName(playlistName), + Name: cleanGameName(item.Label), + Provider: providerName, + Screenshots: findScreenshotsForGame(item), + }) + } + } + + return userGames +}