Skip to content

Commit

Permalink
Change backup system to include the clean image's hash
Browse files Browse the repository at this point in the history
The old backup system was to simply copy the "clean" (no overlays) file
to "{id} (original).jpg". When reapplying overlays, this clean file is
used as base. This works perfectly, but only when SteamGrid is the only
program touching these files.

If the user customizes an image directly on Steam (right click -> set
custom image), Steam changes the "{id}.jpg" file. But SteamGrid doesn't
know the file changed, and promptly deletes and reloads from the
"(original)" backup, discarding the user's change. This bug has wasted a
lot of users' time and effort. Sorry about that :(

The solution is to name the backup file (previously "{id}
(original).jpg") to include the overlaid file hash (now "{id} backup
{sha256}.jpg"). Now when processing a file, first SteamGrid reads
"{id}.jpg" and takes it hash. If there is a backup file with this hash,
it means the file was created by SteamGrid, and the backup file is the
just the clean version. If the user manually customizes an image, the
hash won't match any backups, and SteamGrid won't overwrite it.

The downside is that it may create files that will never be cleaned. As
the images are small, this shouldn't be an issue. Maybe a newer version
will automatically cleanup unused backups.

This commit also looks for old-style, legacy backups ("{id}
(original).jpg"), and converts them to new-style backups.
  • Loading branch information
boppreh committed May 13, 2017
1 parent e1e64fe commit bd4ed2a
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 105 deletions.
56 changes: 48 additions & 8 deletions 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))
}
}

}
12 changes: 7 additions & 5 deletions download.go
Expand Up @@ -6,6 +6,7 @@ import (
"io/ioutil"
"net/http"
"net/url"
"path/filepath"
"regexp"
)

Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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
}
50 changes: 9 additions & 41 deletions games.go
Expand Up @@ -8,7 +8,6 @@ import (
"path/filepath"
"regexp"
"strconv"
"strings"
)

// Game in a steam library. May or may not be installed.
Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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, ""}
}
}
}
Expand Down Expand Up @@ -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]
Expand All @@ -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
}
45 changes: 20 additions & 25 deletions overlays.go
Expand Up @@ -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)
Expand All @@ -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
}
Expand All @@ -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
Expand All @@ -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
}

0 comments on commit bd4ed2a

Please sign in to comment.