diff --git a/backup.go b/backup.go index c62f002..e060675 100644 --- a/backup.go +++ b/backup.go @@ -1,19 +1,59 @@ package main import ( + "crypto/sha256" + "encoding/hex" "io/ioutil" + "os" "path/filepath" - "strings" ) // BackupGame if a game has a custom image, backs it up by appending "(original)" to the // file name. -func BackupGame(game *Game) error { - if game.ImagePath != "" && game.ImageBytes != nil { - ext := filepath.Ext(game.ImagePath) - base := filepath.Base(game.ImagePath) - backupPath := filepath.Join(filepath.Dir(game.ImagePath), strings.TrimSuffix(base, ext)+" (original)"+ext) - return ioutil.WriteFile(backupPath, game.ImageBytes, 0666) - } +func BackupGame(gridDir string, game *Game) error { + if game.CleanImageBytes != nil { + return ioutil.WriteFile(getBackupPath(gridDir, game), game.CleanImageBytes, 0666) + } return nil } + +func getBackupPath(gridDir string, game *Game) string { + hash := sha256.Sum256(game.OverlayImageBytes) + // [:] is required to convert a fixed length byte array to a byte slice. + hexHash := hex.EncodeToString(hash[:]) + return filepath.Join(gridDir, game.ID+" backup "+hexHash+game.ImageExt) +} + +func loadImage(game *Game, sourceName string, imagePath string) error { + imageBytes, err := ioutil.ReadFile(imagePath) + if err == nil { + game.ImageExt = filepath.Ext(imagePath) + game.CleanImageBytes = imageBytes + game.ImageSource = sourceName + } + return err +} + +func LoadBackup(gridDir string, game *Game) { + // If there are any old-style backups (without hash), load them over the existing (with overlay) images. + oldBackups, err := filepath.Glob(filepath.Join(gridDir, game.ID+" (original)*")) + if err == nil && len(oldBackups) > 0 { + err = loadImage(game, "legacy backup (now converted)", oldBackups[0]) + if err == nil { + os.Remove(oldBackups[0]) + return + } + } + + files, err := filepath.Glob(filepath.Join(gridDir, game.ID+".*")) + if err == nil && len(files) > 0 { + err = loadImage(game, "manual customization", files[0]) + if err == nil { + game.OverlayImageBytes = game.CleanImageBytes + + // See if there exists a backup image with no overlays or modifications. + loadImage(game, "backup", getBackupPath(gridDir, game)) + } + } + +} diff --git a/download.go b/download.go index 3c9586f..3ff0162 100644 --- a/download.go +++ b/download.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "net/http" "net/url" + "path/filepath" "regexp" ) @@ -15,7 +16,7 @@ import ( const googleSearchFormat = `https://www.google.com.br/search?tbs=isz%3Aex%2Ciszw%3A460%2Ciszh%3A215&tbm=isch&num=5&q=` // Possible Google result formats -var googleSearchResultPatterns = []string{`imgurl=(.+?\.(jpg|png))&imgrefurl=`, `\"ou\":\"(.+?)\",\"`} +var googleSearchResultPatterns = []string{`imgurl=(.+?\.(jpeg|jpg|png))&imgrefurl=`, `\"ou\":\"(.+?)\",\"`} // Returns the first steam grid image URL found by Google search of a given // game name. @@ -115,10 +116,10 @@ func getImageAlternatives(game *Game) (response *http.Response, fromSearch bool, // DownloadImage tries to download the game images, saving it in game.ImageBytes. Returns // flags indicating if the operation succeeded and if the image downloaded was // from a search. -func DownloadImage(game *Game) error { +func DownloadImage(gridDir string, game *Game) (bool, error) { response, fromSearch, err := getImageAlternatives(game) if response == nil || err != nil { - return err + return false, err } imageBytes, err := ioutil.ReadAll(response.Body) @@ -130,6 +131,7 @@ func DownloadImage(game *Game) error { game.ImageSource = "download" } - game.ImageBytes = imageBytes - return nil + game.CleanImageBytes = imageBytes + game.ImageExt = filepath.Ext(response.Request.URL.Path) + return fromSearch, nil } diff --git a/games.go b/games.go index 1643b84..f3e71c3 100644 --- a/games.go +++ b/games.go @@ -8,7 +8,6 @@ import ( "path/filepath" "regexp" "strconv" - "strings" ) // Game in a steam library. May or may not be installed. @@ -19,10 +18,12 @@ type Game struct { Name string // Tags, including user-created category and Steam's "Favorite" tag. Tags []string - // Path for the grid image. - ImagePath string - // Raw bytes of the encoded image (usually jpg). - ImageBytes []byte + // Image format (.jpg, .jpeg, or .png). + ImageExt string + // Raw bytes of the encoded image (jpg or png) without overlays. + CleanImageBytes []byte + // Raw bytes of the encoded image (jpg or png) with overlays. + OverlayImageBytes []byte // Description of where the image was found (backup, official, search). ImageSource string } @@ -46,8 +47,7 @@ func addGamesFromProfile(user User, games map[string]*Game) (err error) { gameID := groups[1] gameName := groups[2] tags := []string{""} - imagePath := "" - games[gameID] = &Game{gameID, gameName, tags, imagePath, nil, ""} + games[gameID] = &Game{gameID, gameName, tags, "", nil, nil, ""} } return @@ -85,7 +85,7 @@ func addUnknownGames(user User, games map[string]*Game) { // If for some reason it wasn't included in the profile, create a new // entry for it now. Unfortunately we don't have a name. gameName := "" - games[gameID] = &Game{gameID, gameName, []string{tag}, "", nil, ""} + games[gameID] = &Game{gameID, gameName, []string{tag}, "", nil, nil, ""} } } } @@ -118,7 +118,7 @@ func addNonSteamGames(user User, games map[string]*Game) { // to 64bit Steam ID. No idea why Steam chose this operation. top := uint64(crc32.ChecksumIEEE(uniqueName)) | 0x80000000 gameID := strconv.FormatUint(top<<32|0x02000000, 10) - game := Game{gameID, string(gameName), []string{}, "", nil, ""} + game := Game{gameID, string(gameName), []string{}, "", nil, nil, ""} games[gameID] = &game tagsText := gameGroups[3] @@ -138,37 +138,5 @@ func GetGames(user User) map[string]*Game { addUnknownGames(user, games) addNonSteamGames(user, games) - suffixes := []string{ - " (original)..jpg", // Mistakes were made, own up to them. - " (original)..png", - " (original).jpg", - " (original).png", - ".jpg", - ".jpeg", - ".png", - } - - // Load existing and backup images. - for _, game := range games { - gridDir := filepath.Join(user.Dir, "config", "grid") - for _, suffix := range suffixes { - imagePath := filepath.Join(gridDir, game.ID+suffix) - imageBytes, err := ioutil.ReadFile(imagePath) - if err == nil { - game.ImagePath = filepath.Join(gridDir, game.ID+filepath.Ext(suffix)) - game.ImageBytes = imageBytes - if strings.HasPrefix(suffix, " (original)") { - game.ImageSource = "backup" - } else { - game.ImageSource = "manual customization" - } - break - } - } - if game.ImageBytes == nil { - game.ImagePath = filepath.Join(gridDir, game.ID+".jpg") - } - } - return games } diff --git a/overlays.go b/overlays.go index 073b614..44971a9 100644 --- a/overlays.go +++ b/overlays.go @@ -12,18 +12,6 @@ import ( "strings" ) -// Loads an image from a given path. -func loadImage(path string) (img image.Image, err error) { - reader, err := os.Open(path) - if err != nil { - return - } - defer reader.Close() - - img, _, err = image.Decode(reader) - return -} - // LoadOverlays from the given dir, returning a map of name -> image. func LoadOverlays(dir string) (overlays map[string]image.Image, err error) { overlays = make(map[string]image.Image, 0) @@ -48,7 +36,13 @@ func LoadOverlays(dir string) (overlays map[string]image.Image, err error) { continue } - img, err := loadImage(filepath.Join(dir, file.Name())) + reader, err := os.Open(filepath.Join(dir, file.Name())) + if err != nil { + return nil, err + } + defer reader.Close() + + img, _, err := image.Decode(reader) if err != nil { return overlays, err } @@ -64,16 +58,17 @@ func LoadOverlays(dir string) (overlays map[string]image.Image, err error) { // ApplyOverlay to the game image, depending on the category. The // resulting image is saved over the original. -func ApplyOverlay(game *Game, overlays map[string]image.Image) (applied bool, err error) { - if game.ImagePath == "" || game.ImageBytes == nil || len(game.Tags) == 0 { - return false, nil +func ApplyOverlay(game *Game, overlays map[string]image.Image) error { + if game.CleanImageBytes == nil || len(game.Tags) == 0 { + return nil } - gameImage, _, err := image.Decode(bytes.NewBuffer(game.ImageBytes)) + gameImage, _, err := image.Decode(bytes.NewBuffer(game.CleanImageBytes)) if err != nil { - return false, err + return err } + applied := false for _, tag := range game.Tags { // Normalize tag name by lower-casing it and remove trailing "s" from // plurals. Also, <, > and / are replaced with - because you can't have @@ -96,18 +91,18 @@ func ApplyOverlay(game *Game, overlays map[string]image.Image) (applied bool, er } if !applied { - return false, nil + return nil } buf := new(bytes.Buffer) - if strings.HasSuffix(game.ImagePath, "jpg") { - err = jpeg.Encode(buf, gameImage, &jpeg.Options{90}) - } else if strings.HasSuffix(game.ImagePath, "png") { + if game.ImageExt == ".jpg" || game.ImageExt == ".jpeg" { + err = jpeg.Encode(buf, gameImage, &jpeg.Options{95}) + } else if game.ImageExt == ".png" { err = png.Encode(buf, gameImage) } if err != nil { - return false, err + return err } - game.ImageBytes = buf.Bytes() - return true, nil + game.OverlayImageBytes = buf.Bytes() + return nil } diff --git a/steamgrid.go b/steamgrid.go index 000f8ca..c6dd0bc 100644 --- a/steamgrid.go +++ b/steamgrid.go @@ -32,8 +32,6 @@ func startApplication() { errorAndExit(err) } if len(overlays) == 0 { - // I'm trying to use a message box here, but for some reason the - // message appears twice and there's an error a closed channel. fmt.Println("No category overlays found. You can put overlay images in the folder 'overlays by category', where the filename is the game category.\n\nContinuing without overlays...") } @@ -55,12 +53,13 @@ func startApplication() { nOverlaysApplied := 0 nDownloaded := 0 var notFounds []*Game - var searchFounds []*Game - var errors []*Game + var searchedGames []*Game + var failedGames []*Game var errorMessages []string for _, user := range users { fmt.Println("Loading games for " + user.Name) + gridDir := filepath.Join(user.Dir, "config", "grid") games := GetGames(user) @@ -68,6 +67,8 @@ func startApplication() { for _, game := range games { i++ + LoadBackup(gridDir, game) + var name string if game.Name != "" { name = game.Name @@ -76,42 +77,50 @@ func startApplication() { } fmt.Printf("Processing %v (%v/%v)", name, i, len(games)) - if game.ImageBytes == nil { - err := DownloadImage(game) + if game.ImageSource == "" { + fromSearch, err := DownloadImage(gridDir, game) if err != nil { errorAndExit(err) } - if game.ImageBytes != nil { - nDownloaded++ - } else { + if game.ImageSource == "" { notFounds = append(notFounds, game) fmt.Printf(" not found\n") // Game has no image, skip it. continue + } else { + nDownloaded++ } - if game.ImageSource == "search" { - searchFounds = append(searchFounds, game) + + if fromSearch { + searchedGames = append(searchedGames, game) } } fmt.Printf(" found from %v\n", game.ImageSource) - err = BackupGame(game) - if err != nil { - errorAndExit(err) - } - - applied, err := ApplyOverlay(game, overlays) + err := ApplyOverlay(game, overlays) if err != nil { print(err.Error(), "\n") - errors = append(errors, game) + failedGames = append(failedGames, game) errorMessages = append(errorMessages, err.Error()) } - if applied { + if game.OverlayImageBytes != nil { nOverlaysApplied++ + } else { + game.OverlayImageBytes = game.CleanImageBytes + } + + err = BackupGame(gridDir, game) + if err != nil { + errorAndExit(err) + } + + if game.ImageExt == "" { + errorAndExit(errors.New("Failed to identify image format.")) } - err = ioutil.WriteFile(game.ImagePath, game.ImageBytes, 0666) + imagePath := filepath.Join(gridDir, game.ID+game.ImageExt) + err = ioutil.WriteFile(imagePath, game.OverlayImageBytes, 0666) if err != nil { fmt.Printf("Failed to write image for %v because: %v\n", game.Name, err.Error()) } @@ -119,9 +128,9 @@ func startApplication() { } fmt.Printf("\n\n%v images downloaded and %v overlays applied.\n\n", nDownloaded, nOverlaysApplied) - if len(searchFounds) >= 1 { - fmt.Printf("%v images were found with a Google search and may not be accurate:\n", len(searchFounds)) - for _, game := range searchFounds { + if len(searchedGames) >= 1 { + fmt.Printf("%v images were found with a Google search and may not be accurate:\n", len(searchedGames)) + for _, game := range searchedGames { fmt.Printf("* %v (steam id %v)\n", game.Name, game.ID) } @@ -137,9 +146,9 @@ func startApplication() { fmt.Printf("\n\n") } - if len(errors) >= 1 { - fmt.Printf("%v images were found but had errors and could not be overlaid:\n", len(errors)) - for i, game := range errors { + if len(failedGames) >= 1 { + fmt.Printf("%v images were found but had errors and could not be overlaid:\n", len(failedGames)) + for i, game := range failedGames { fmt.Printf("- %v (id %v) (%v)\n", game.Name, game.ID, errorMessages[i]) }