mirror of
https://github.com/rommapp/grout.git
synced 2026-04-23 06:54:36 +00:00
Caching work and fixing save sync
This commit is contained in:
+20
-85
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"grout/artwork"
|
||||
"grout/cache"
|
||||
"grout/cfw"
|
||||
"grout/constants"
|
||||
@@ -44,7 +43,6 @@ const (
|
||||
saveSyncSettings gaba.StateName = "save_sync_settings"
|
||||
info gaba.StateName = "info"
|
||||
logoutConfirmation gaba.StateName = "logout_confirmation"
|
||||
clearCacheConfirmation gaba.StateName = "clear_cache_confirmation"
|
||||
refreshCache gaba.StateName = "refresh_cache"
|
||||
saveSync gaba.StateName = "save_sync"
|
||||
biosDownload gaba.StateName = "bios_download"
|
||||
@@ -124,12 +122,7 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
|
||||
}
|
||||
|
||||
// Validate artwork cache in background
|
||||
go artwork.ValidateCache()
|
||||
|
||||
// Index any existing artwork files
|
||||
if cm := cache.GetCacheManager(); cm != nil {
|
||||
go cm.ScanAndIndexArtwork()
|
||||
}
|
||||
cache.RunArtworkValidation()
|
||||
|
||||
gaba.AddState(fsm, platformSelection, func(ctx *gaba.Context) (ui.PlatformSelectionOutput, gaba.ExitCode) {
|
||||
platforms, _ := gaba.Get[[]romm.Platform](ctx)
|
||||
@@ -562,6 +555,7 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
|
||||
}).
|
||||
On(constants.ExitCodeGeneralSettings, generalSettings).
|
||||
On(constants.ExitCodeCollectionsSettings, collectionsSettings).
|
||||
On(constants.ExitCodeEditMappings, settingsPlatformMapping).
|
||||
On(constants.ExitCodeAdvancedSettings, advancedSettings).
|
||||
On(constants.ExitCodeSaveSyncSettings, saveSyncSettings).
|
||||
On(constants.ExitCodeInfo, info).
|
||||
@@ -665,7 +659,6 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
|
||||
}).
|
||||
On(gaba.ExitCodeSuccess, settings).
|
||||
On(constants.ExitCodeEditMappings, settingsPlatformMapping).
|
||||
On(constants.ExitCodeClearCache, clearCacheConfirmation).
|
||||
On(constants.ExitCodeRefreshCache, refreshCache).
|
||||
On(constants.ExitCodeSyncArtwork, artworkSync).
|
||||
On(gaba.ExitCodeBack, settings)
|
||||
@@ -744,6 +737,12 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
|
||||
config, _ := gaba.Get[*internal.Config](ctx)
|
||||
currentCFW, _ := gaba.Get[cfw.CFW](ctx)
|
||||
|
||||
// Delete the entire cache folder on logout
|
||||
if err := cache.DeleteCacheFolder(); err != nil {
|
||||
gaba.GetLogger().Error("Failed to delete cache folder", "error", err)
|
||||
// Continue with logout even if cache deletion fails
|
||||
}
|
||||
|
||||
config.Hosts = nil
|
||||
config.DirectoryMappings = nil
|
||||
config.PlatformOrder = nil
|
||||
@@ -787,6 +786,11 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
|
||||
}
|
||||
}
|
||||
|
||||
// Re-initialize cache manager for the new host
|
||||
if err := cache.InitCacheManager(config.Hosts[0], config); err != nil {
|
||||
gaba.GetLogger().Error("Failed to initialize cache manager after re-login", "error", err)
|
||||
}
|
||||
|
||||
platforms, err := internal.GetMappedPlatforms(config.Hosts[0], config.DirectoryMappings, config.ApiTimeout)
|
||||
if err != nil {
|
||||
gaba.GetLogger().Error("Failed to load platforms after re-login", "error", err)
|
||||
@@ -794,44 +798,8 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
|
||||
}
|
||||
gaba.Set(ctx, platforms)
|
||||
|
||||
nav, _ := gaba.Get[*NavState](ctx)
|
||||
nav.ResetGameList()
|
||||
nav.PlatformListPos = ListPosition{}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
gaba.AddState(fsm, clearCacheConfirmation, func(ctx *gaba.Context) (ui.ClearCacheOutput, gaba.ExitCode) {
|
||||
screen := ui.NewClearCacheScreen()
|
||||
result, err := screen.Draw()
|
||||
|
||||
if err != nil {
|
||||
return ui.ClearCacheOutput{}, gaba.ExitCodeError
|
||||
}
|
||||
|
||||
return result.Value, result.ExitCode
|
||||
}).
|
||||
On(gaba.ExitCodeBack, advancedSettings).
|
||||
OnWithHook(gaba.ExitCodeSuccess, advancedSettings, func(ctx *gaba.Context) error {
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
// If games cache was cleared, re-fetch platforms and re-populate
|
||||
cm := cache.GetCacheManager()
|
||||
if cm != nil && cm.IsFirstRun() {
|
||||
host, _ := gaba.Get[romm.Host](ctx)
|
||||
config, _ := gaba.Get[*internal.Config](ctx)
|
||||
|
||||
// Fetch fresh platform list from API
|
||||
platforms, err := internal.GetMappedPlatforms(host, config.DirectoryMappings, config.ApiTimeout)
|
||||
if err != nil {
|
||||
logger.Error("Failed to fetch platforms after cache clear", "error", err)
|
||||
return nil // Don't fail, just log
|
||||
}
|
||||
|
||||
// Update platforms in context
|
||||
gaba.Set(ctx, platforms)
|
||||
|
||||
// Re-populate cache with progress
|
||||
// Populate cache for the new login
|
||||
if cm := cache.GetCacheManager(); cm != nil && cm.IsFirstRun() {
|
||||
progress := uatomic.NewFloat64(0)
|
||||
gaba.ProcessMessage(
|
||||
i18n.Localize(&goi18n.Message{ID: "cache_building", Other: "Building cache..."}, nil),
|
||||
@@ -844,10 +812,12 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
|
||||
return nil, cm.PopulateFullCacheWithProgress(platforms, progress)
|
||||
},
|
||||
)
|
||||
|
||||
logger.Info("Cache rebuilt after clear")
|
||||
}
|
||||
|
||||
nav, _ := gaba.Get[*NavState](ctx)
|
||||
nav.ResetGameList()
|
||||
nav.PlatformListPos = ListPosition{}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -876,7 +846,6 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
|
||||
|
||||
refreshGames := false
|
||||
refreshCollections := false
|
||||
refreshArtwork := false
|
||||
|
||||
for _, t := range result.SelectedTypes {
|
||||
switch t {
|
||||
@@ -884,8 +853,6 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
|
||||
refreshGames = true
|
||||
case ui.RefreshCacheCollections:
|
||||
refreshCollections = true
|
||||
case ui.RefreshCacheArtwork:
|
||||
refreshArtwork = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -900,11 +867,6 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
|
||||
logger.Error("Failed to clear collections cache", "error", err)
|
||||
}
|
||||
}
|
||||
if refreshArtwork {
|
||||
if err := cm.ClearArtwork(); err != nil {
|
||||
logger.Error("Failed to clear artwork cache", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// If we need to refresh games or collections, re-populate
|
||||
if refreshGames || refreshCollections {
|
||||
@@ -933,36 +895,9 @@ func buildFSM(config *internal.Config, c cfw.CFW, platforms []romm.Platform, qui
|
||||
)
|
||||
}
|
||||
|
||||
// If only refreshing artwork, sync it in background
|
||||
if refreshArtwork && !refreshGames && !refreshCollections {
|
||||
platforms, _ := gaba.Get[[]romm.Platform](ctx)
|
||||
go func() {
|
||||
for _, p := range platforms {
|
||||
games, err := cm.GetPlatformGames(p.ID)
|
||||
if err == nil {
|
||||
artwork.SyncInBackground(host, games)
|
||||
}
|
||||
}
|
||||
// Record artwork refresh time after sync completes
|
||||
cm.RecordRefreshTime(cache.MetaKeyArtworkRefreshedAt)
|
||||
}()
|
||||
|
||||
gaba.ProcessMessage(
|
||||
i18n.Localize(&goi18n.Message{ID: "artwork_sync_started", Other: "Artwork sync started in background"}, nil),
|
||||
gaba.ProcessMessageOptions{ShowThemeBackground: true},
|
||||
func() (interface{}, error) {
|
||||
return nil, nil
|
||||
},
|
||||
)
|
||||
} else if refreshArtwork {
|
||||
// Record artwork refresh time (it was cleared and will be re-downloaded on demand)
|
||||
cm.RecordRefreshTime(cache.MetaKeyArtworkRefreshedAt)
|
||||
}
|
||||
|
||||
logger.Info("Cache refresh completed",
|
||||
"games", refreshGames,
|
||||
"collections", refreshCollections,
|
||||
"artwork", refreshArtwork)
|
||||
"collections", refreshCollections)
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package artwork
|
||||
|
||||
import (
|
||||
"grout/cache"
|
||||
"grout/internal/fileutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
|
||||
)
|
||||
|
||||
func GetCacheDir() string {
|
||||
return cache.GetArtworkCacheDir()
|
||||
}
|
||||
|
||||
func ClearCache() error {
|
||||
// Clear SQLite metadata
|
||||
if cm := cache.GetCacheManager(); cm != nil {
|
||||
if err := cm.ClearArtwork(); err != nil {
|
||||
gaba.GetLogger().Debug("Failed to clear artwork metadata", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear files from disk
|
||||
cacheDir := GetCacheDir()
|
||||
if !fileutil.FileExists(cacheDir) {
|
||||
return nil
|
||||
}
|
||||
return os.RemoveAll(cacheDir)
|
||||
}
|
||||
|
||||
func HasCache() bool {
|
||||
// Check SQLite first for speed
|
||||
if cm := cache.GetCacheManager(); cm != nil {
|
||||
if cm.GetArtworkCount() > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to filesystem check
|
||||
cacheDir := GetCacheDir()
|
||||
entries, err := os.ReadDir(cacheDir)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return len(entries) > 0
|
||||
}
|
||||
|
||||
func GetCachePath(platformFSSlug string, romID int) string {
|
||||
return filepath.Join(GetCacheDir(), platformFSSlug, strconv.Itoa(romID)+".png")
|
||||
}
|
||||
|
||||
func Exists(platformFSSlug string, romID int) bool {
|
||||
// Check SQLite metadata first (fast)
|
||||
if cm := cache.GetCacheManager(); cm != nil {
|
||||
if cm.IsArtworkCached(platformFSSlug, romID) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to filesystem check
|
||||
return fileutil.FileExists(GetCachePath(platformFSSlug, romID))
|
||||
}
|
||||
|
||||
func EnsureCacheDir(platformFSSlug string) error {
|
||||
dir := filepath.Join(GetCacheDir(), platformFSSlug)
|
||||
return os.MkdirAll(dir, 0755)
|
||||
}
|
||||
|
||||
// MarkCached records that artwork has been cached for a ROM
|
||||
func MarkCached(platformFSSlug string, romID int) {
|
||||
if cm := cache.GetCacheManager(); cm != nil {
|
||||
path := GetCachePath(platformFSSlug, romID)
|
||||
if err := cm.MarkArtworkCached(platformFSSlug, romID, path); err != nil {
|
||||
gaba.GetLogger().Debug("Failed to mark artwork cached", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ValidateCache() {
|
||||
if cm := cache.GetCacheManager(); cm != nil {
|
||||
go func() {
|
||||
removed, err := cm.ValidateArtworkCache()
|
||||
if err != nil {
|
||||
gaba.GetLogger().Debug("Failed to validate artwork cache", "error", err)
|
||||
return
|
||||
}
|
||||
if removed > 0 {
|
||||
gaba.GetLogger().Debug("Removed invalid artwork entries", "count", removed)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -1,203 +0,0 @@
|
||||
package artwork
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"grout/constants"
|
||||
"grout/internal/imageutil"
|
||||
"grout/romm"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
|
||||
)
|
||||
|
||||
// GetWithArtwork returns all ROMs that have artwork URLs available
|
||||
func GetWithArtwork(roms []romm.Rom) []romm.Rom {
|
||||
var withArtwork []romm.Rom
|
||||
for _, rom := range roms {
|
||||
if HasURL(rom) {
|
||||
withArtwork = append(withArtwork, rom)
|
||||
}
|
||||
}
|
||||
return withArtwork
|
||||
}
|
||||
|
||||
func GetMissing(roms []romm.Rom) []romm.Rom {
|
||||
var missing []romm.Rom
|
||||
for _, rom := range roms {
|
||||
if !HasURL(rom) {
|
||||
continue
|
||||
}
|
||||
if !Exists(rom.PlatformFSSlug, rom.ID) {
|
||||
missing = append(missing, rom)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
func CheckRemoteLastModified(url string, authHeader string) (time.Time, error) {
|
||||
req, err := http.NewRequest("HEAD", url, nil)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if authHeader != "" {
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * constants.DefaultHTTPTimeout}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return time.Time{}, fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
lastModified := resp.Header.Get("Last-Modified")
|
||||
if lastModified == "" {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
|
||||
return http.ParseTime(lastModified)
|
||||
}
|
||||
|
||||
func NeedsUpdate(rom romm.Rom, host romm.Host) bool {
|
||||
cachePath := GetCachePath(rom.PlatformFSSlug, rom.ID)
|
||||
|
||||
localInfo, err := os.Stat(cachePath)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
coverPath := GetCoverPath(rom)
|
||||
if coverPath == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
artURL := host.URL() + coverPath
|
||||
artURL = strings.ReplaceAll(artURL, " ", "%20")
|
||||
|
||||
remoteModTime, err := CheckRemoteLastModified(artURL, host.BasicAuthHeader())
|
||||
if err != nil || remoteModTime.IsZero() {
|
||||
return false // On error or no Last-Modified header, skip re-download
|
||||
}
|
||||
|
||||
return remoteModTime.After(localInfo.ModTime())
|
||||
}
|
||||
|
||||
func GetOutdated(roms []romm.Rom, host romm.Host) []romm.Rom {
|
||||
var outdated []romm.Rom
|
||||
for _, rom := range roms {
|
||||
if !HasURL(rom) {
|
||||
continue
|
||||
}
|
||||
if Exists(rom.PlatformFSSlug, rom.ID) && NeedsUpdate(rom, host) {
|
||||
outdated = append(outdated, rom)
|
||||
}
|
||||
}
|
||||
return outdated
|
||||
}
|
||||
|
||||
func HasURL(rom romm.Rom) bool {
|
||||
return rom.PathCoverSmall != "" || rom.PathCoverLarge != "" || rom.URLCover != ""
|
||||
}
|
||||
|
||||
func GetCoverPath(rom romm.Rom) string {
|
||||
if rom.PathCoverSmall != "" {
|
||||
return rom.PathCoverSmall
|
||||
}
|
||||
if rom.PathCoverLarge != "" {
|
||||
return rom.PathCoverLarge
|
||||
}
|
||||
return rom.URLCover
|
||||
}
|
||||
|
||||
func DownloadAndCache(rom romm.Rom, host romm.Host) error {
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
coverPath := GetCoverPath(rom)
|
||||
if coverPath == "" {
|
||||
return nil // No artwork available
|
||||
}
|
||||
|
||||
if err := EnsureCacheDir(rom.PlatformFSSlug); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
cachePath := GetCachePath(rom.PlatformFSSlug, rom.ID)
|
||||
|
||||
artURL := host.URL() + coverPath
|
||||
artURL = strings.ReplaceAll(artURL, " ", "%20")
|
||||
|
||||
req, err := http.NewRequest("GET", artURL, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", host.BasicAuthHeader())
|
||||
|
||||
client := &http.Client{Timeout: constants.DefaultClientTimeout}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download artwork: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
outFile, err := os.Create(cachePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create cache file: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
if _, err = io.Copy(outFile, resp.Body); err != nil {
|
||||
os.Remove(cachePath)
|
||||
return fmt.Errorf("failed to write cache file: %w", err)
|
||||
}
|
||||
outFile.Close()
|
||||
|
||||
if err := imageutil.ProcessArtImage(cachePath); err != nil {
|
||||
logger.Warn("Failed to process artwork image", "path", cachePath, "error", err)
|
||||
os.Remove(cachePath)
|
||||
return fmt.Errorf("failed to process artwork: %w", err)
|
||||
}
|
||||
|
||||
file, err := os.Open(cachePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open processed artwork: %w", err)
|
||||
}
|
||||
_, err = png.DecodeConfig(file)
|
||||
file.Close()
|
||||
if err != nil {
|
||||
os.Remove(cachePath)
|
||||
return fmt.Errorf("processed artwork is not a valid PNG: %w", err)
|
||||
}
|
||||
|
||||
// Record in SQLite metadata
|
||||
MarkCached(rom.PlatformFSSlug, rom.ID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncInBackground(host romm.Host, games []romm.Rom) {
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
missing := GetMissing(games)
|
||||
if len(missing) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, rom := range missing {
|
||||
if err := DownloadAndCache(rom, host); err != nil {
|
||||
logger.Debug("Failed to download artwork", "rom", rom.Name, "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+214
-289
@@ -1,177 +1,95 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"grout/constants"
|
||||
"grout/internal/fileutil"
|
||||
"grout/internal/imageutil"
|
||||
"grout/romm"
|
||||
"image/png"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
|
||||
)
|
||||
|
||||
func GetArtworkCachePath(fsSlug string, romID int) string {
|
||||
return filepath.Join(GetArtworkCacheDir(), fsSlug, strconv.Itoa(romID)+".png")
|
||||
func GetArtworkCachePath(platformFSSlug string, romID int) string {
|
||||
return filepath.Join(GetArtworkCacheDir(), platformFSSlug, strconv.Itoa(romID)+".png")
|
||||
}
|
||||
|
||||
func (cm *CacheManager) IsArtworkCached(fsSlug string, romID int) bool {
|
||||
if cm == nil || !cm.initialized {
|
||||
// Fallback to file check
|
||||
return fileutil.FileExists(GetArtworkCachePath(fsSlug, romID))
|
||||
}
|
||||
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
var count int
|
||||
err := cm.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM artwork_metadata
|
||||
WHERE platform_fs_slug = ? AND rom_id = ?
|
||||
`, fsSlug, romID).Scan(&count)
|
||||
|
||||
if err != nil || count == 0 {
|
||||
// No metadata, but check if file exists (for legacy compatibility)
|
||||
return fileutil.FileExists(GetArtworkCachePath(fsSlug, romID))
|
||||
}
|
||||
|
||||
return fileutil.FileExists(GetArtworkCachePath(fsSlug, romID))
|
||||
func ArtworkExists(platformFSSlug string, romID int) bool {
|
||||
return fileutil.FileExists(GetArtworkCachePath(platformFSSlug, romID))
|
||||
}
|
||||
|
||||
func (cm *CacheManager) GetArtworkPath(fsSlug string, romID int) string {
|
||||
return GetArtworkCachePath(fsSlug, romID)
|
||||
}
|
||||
|
||||
func (cm *CacheManager) MarkArtworkCached(fsSlug string, romID int, filePath string) error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
var size int64
|
||||
if info, err := os.Stat(filePath); err == nil {
|
||||
size = info.Size()
|
||||
}
|
||||
|
||||
_, err := cm.db.Exec(`
|
||||
INSERT OR REPLACE INTO artwork_metadata
|
||||
(platform_fs_slug, rom_id, file_path, file_size_bytes, cached_at, validated_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
`, fsSlug, romID, filePath, size)
|
||||
|
||||
if err != nil {
|
||||
logger.Debug("Failed to mark artwork cached", "fsSlug", fsSlug, "romID", romID, "error", err)
|
||||
return newCacheError("save", "artwork", strconv.Itoa(romID), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cm *CacheManager) RemoveArtworkMetadata(fsSlug string, romID int) error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
_, err := cm.db.Exec(`
|
||||
DELETE FROM artwork_metadata
|
||||
WHERE platform_fs_slug = ? AND rom_id = ?
|
||||
`, fsSlug, romID)
|
||||
|
||||
if err != nil {
|
||||
return newCacheError("delete", "artwork", strconv.Itoa(romID), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cm *CacheManager) ValidateArtworkCache() (int, error) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return 0, ErrNotInitialized
|
||||
}
|
||||
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
cm.mu.RLock()
|
||||
rows, err := cm.db.Query(`
|
||||
SELECT platform_fs_slug, rom_id, file_path FROM artwork_metadata
|
||||
`)
|
||||
cm.mu.RUnlock()
|
||||
|
||||
if err != nil {
|
||||
return 0, newCacheError("validate", "artwork", "", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
type toRemove struct {
|
||||
fsSlug string
|
||||
romID int
|
||||
path string
|
||||
}
|
||||
|
||||
var removeList []toRemove
|
||||
|
||||
for rows.Next() {
|
||||
var fsSlug string
|
||||
var romID int
|
||||
var filePath string
|
||||
|
||||
if err := rows.Scan(&fsSlug, &romID, &filePath); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !fileutil.FileExists(filePath) {
|
||||
removeList = append(removeList, toRemove{fsSlug, romID, filePath})
|
||||
continue
|
||||
}
|
||||
|
||||
if !isValidPNG(filePath) {
|
||||
removeList = append(removeList, toRemove{fsSlug, romID, filePath})
|
||||
}
|
||||
}
|
||||
|
||||
cm.mu.Lock()
|
||||
for _, item := range removeList {
|
||||
cm.db.Exec(`
|
||||
DELETE FROM artwork_metadata
|
||||
WHERE platform_fs_slug = ? AND rom_id = ?
|
||||
`, item.fsSlug, item.romID)
|
||||
|
||||
os.Remove(item.path)
|
||||
}
|
||||
cm.mu.Unlock()
|
||||
|
||||
if len(removeList) > 0 {
|
||||
logger.Debug("Removed invalid artwork entries", "count", len(removeList))
|
||||
}
|
||||
|
||||
return len(removeList), nil
|
||||
}
|
||||
|
||||
func (cm *CacheManager) GetArtworkCount() int {
|
||||
if cm == nil || !cm.initialized {
|
||||
return 0
|
||||
}
|
||||
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
var count int
|
||||
cm.db.QueryRow(`SELECT COUNT(*) FROM artwork_metadata`).Scan(&count)
|
||||
return count
|
||||
}
|
||||
|
||||
func EnsureArtworkCacheDir(fsSlug string) error {
|
||||
dir := filepath.Join(GetArtworkCacheDir(), fsSlug)
|
||||
func EnsureArtworkCacheDir(platformFSSlug string) error {
|
||||
dir := filepath.Join(GetArtworkCacheDir(), platformFSSlug)
|
||||
return os.MkdirAll(dir, 0755)
|
||||
}
|
||||
|
||||
func (cm *Manager) ValidateArtworkCache() (int, error) {
|
||||
logger := gaba.GetLogger()
|
||||
cacheDir := GetArtworkCacheDir()
|
||||
removed := 0
|
||||
|
||||
platformDirs, err := os.ReadDir(cacheDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
for _, platformDir := range platformDirs {
|
||||
if !platformDir.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
platformPath := filepath.Join(cacheDir, platformDir.Name())
|
||||
files, err := os.ReadDir(platformPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() || filepath.Ext(file.Name()) != ".png" {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(platformPath, file.Name())
|
||||
if !isValidPNG(filePath) {
|
||||
os.Remove(filePath)
|
||||
removed++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if removed > 0 {
|
||||
logger.Debug("Removed invalid artwork files", "count", removed)
|
||||
}
|
||||
|
||||
return removed, nil
|
||||
}
|
||||
|
||||
func RunArtworkValidation() {
|
||||
if cm := GetCacheManager(); cm != nil {
|
||||
go func() {
|
||||
removed, err := cm.ValidateArtworkCache()
|
||||
if err != nil {
|
||||
gaba.GetLogger().Debug("Failed to validate artwork cache", "error", err)
|
||||
return
|
||||
}
|
||||
if removed > 0 {
|
||||
gaba.GetLogger().Debug("Removed invalid artwork files", "count", removed)
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
func isValidPNG(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
@@ -183,165 +101,172 @@ func isValidPNG(path string) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (cm *CacheManager) ScanAndIndexArtwork() (int, error) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return 0, ErrNotInitialized
|
||||
func GetRomsWithArtwork(roms []romm.Rom) []romm.Rom {
|
||||
var withArtwork []romm.Rom
|
||||
for _, rom := range roms {
|
||||
if HasArtworkURL(rom) {
|
||||
withArtwork = append(withArtwork, rom)
|
||||
}
|
||||
}
|
||||
return withArtwork
|
||||
}
|
||||
|
||||
func GetMissingArtwork(roms []romm.Rom) []romm.Rom {
|
||||
var missing []romm.Rom
|
||||
for _, rom := range roms {
|
||||
if !HasArtworkURL(rom) {
|
||||
continue
|
||||
}
|
||||
if !ArtworkExists(rom.PlatformFSSlug, rom.ID) {
|
||||
missing = append(missing, rom)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
func HasArtworkURL(rom romm.Rom) bool {
|
||||
return rom.PathCoverSmall != "" || rom.PathCoverLarge != "" || rom.URLCover != ""
|
||||
}
|
||||
|
||||
func GetArtworkCoverPath(rom romm.Rom) string {
|
||||
if rom.PathCoverSmall != "" {
|
||||
return rom.PathCoverSmall
|
||||
}
|
||||
if rom.PathCoverLarge != "" {
|
||||
return rom.PathCoverLarge
|
||||
}
|
||||
return rom.URLCover
|
||||
}
|
||||
|
||||
func DownloadAndCacheArtwork(rom romm.Rom, host romm.Host) error {
|
||||
logger := gaba.GetLogger()
|
||||
cacheDir := GetArtworkCacheDir()
|
||||
|
||||
platformDirs, err := os.ReadDir(cacheDir)
|
||||
coverPath := GetArtworkCoverPath(rom)
|
||||
if coverPath == "" {
|
||||
return nil // No artwork available
|
||||
}
|
||||
|
||||
if err := EnsureArtworkCacheDir(rom.PlatformFSSlug); err != nil {
|
||||
return fmt.Errorf("failed to create cache directory: %w", err)
|
||||
}
|
||||
|
||||
cachePath := GetArtworkCachePath(rom.PlatformFSSlug, rom.ID)
|
||||
|
||||
artURL := host.URL() + coverPath
|
||||
artURL = strings.ReplaceAll(artURL, " ", "%20")
|
||||
|
||||
req, err := http.NewRequest("GET", artURL, nil)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, newCacheError("scan", "artwork", "", err)
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", host.BasicAuthHeader())
|
||||
|
||||
client := &http.Client{Timeout: constants.DefaultClientTimeout}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to download artwork: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
indexed := 0
|
||||
for _, platformDir := range platformDirs {
|
||||
if !platformDir.IsDir() {
|
||||
continue
|
||||
}
|
||||
outFile, err := os.Create(cachePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create cache file: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
fsSlug := platformDir.Name()
|
||||
platformPath := filepath.Join(cacheDir, fsSlug)
|
||||
if _, err = io.Copy(outFile, resp.Body); err != nil {
|
||||
os.Remove(cachePath)
|
||||
return fmt.Errorf("failed to write cache file: %w", err)
|
||||
}
|
||||
outFile.Close()
|
||||
|
||||
files, err := os.ReadDir(platformPath)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file.IsDir() || filepath.Ext(file.Name()) != ".png" {
|
||||
continue
|
||||
}
|
||||
|
||||
name := file.Name()
|
||||
romIDStr := name[:len(name)-4] // Remove ".png"
|
||||
romID, err := strconv.Atoi(romIDStr)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(platformPath, file.Name())
|
||||
|
||||
cm.mu.RLock()
|
||||
var count int
|
||||
cm.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM artwork_metadata
|
||||
WHERE platform_fs_slug = ? AND rom_id = ?
|
||||
`, fsSlug, romID).Scan(&count)
|
||||
cm.mu.RUnlock()
|
||||
|
||||
if count == 0 {
|
||||
cm.MarkArtworkCached(fsSlug, romID, filePath)
|
||||
indexed++
|
||||
}
|
||||
}
|
||||
if err := imageutil.ProcessArtImage(cachePath); err != nil {
|
||||
logger.Warn("Failed to process artwork image", "path", cachePath, "error", err)
|
||||
os.Remove(cachePath)
|
||||
return fmt.Errorf("failed to process artwork: %w", err)
|
||||
}
|
||||
|
||||
if indexed > 0 {
|
||||
logger.Debug("Indexed existing artwork files", "count", indexed)
|
||||
file, err := os.Open(cachePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open processed artwork: %w", err)
|
||||
}
|
||||
_, err = png.DecodeConfig(file)
|
||||
file.Close()
|
||||
if err != nil {
|
||||
os.Remove(cachePath)
|
||||
return fmt.Errorf("processed artwork is not a valid PNG: %w", err)
|
||||
}
|
||||
|
||||
return indexed, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cm *CacheManager) GetAllArtworkMetadata() ([]struct {
|
||||
FSSlug string
|
||||
RomID int
|
||||
Path string
|
||||
}, error) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return nil, ErrNotInitialized
|
||||
func SyncArtworkInBackground(host romm.Host, games []romm.Rom) {
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
missing := GetMissingArtwork(games)
|
||||
if len(missing) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
rows, err := cm.db.Query(`
|
||||
SELECT platform_fs_slug, rom_id, file_path FROM artwork_metadata
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, newCacheError("get", "artwork", "", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []struct {
|
||||
FSSlug string
|
||||
RomID int
|
||||
Path string
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var fsSlug string
|
||||
var romID int
|
||||
var path string
|
||||
|
||||
if err := rows.Scan(&fsSlug, &romID, &path); err != nil {
|
||||
continue
|
||||
for _, rom := range missing {
|
||||
if err := DownloadAndCacheArtwork(rom, host); err != nil {
|
||||
logger.Debug("Failed to download artwork", "rom", rom.Name, "error", err)
|
||||
}
|
||||
|
||||
results = append(results, struct {
|
||||
FSSlug string
|
||||
RomID int
|
||||
Path string
|
||||
}{fsSlug, romID, path})
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (cm *CacheManager) HasArtworkByFilename(fsSlug string) bool {
|
||||
if cm == nil || !cm.initialized {
|
||||
func CheckRemoteArtworkLastModified(url string, authHeader string) (time.Time, error) {
|
||||
req, err := http.NewRequest("HEAD", url, nil)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
if authHeader != "" {
|
||||
req.Header.Set("Authorization", authHeader)
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * constants.DefaultHTTPTimeout}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return time.Time{}, fmt.Errorf("bad status: %s", resp.Status)
|
||||
}
|
||||
|
||||
lastModified := resp.Header.Get("Last-Modified")
|
||||
if lastModified == "" {
|
||||
return time.Time{}, nil
|
||||
}
|
||||
|
||||
return http.ParseTime(lastModified)
|
||||
}
|
||||
|
||||
func ArtworkNeedsUpdate(rom romm.Rom, host romm.Host) bool {
|
||||
cachePath := GetArtworkCachePath(rom.PlatformFSSlug, rom.ID)
|
||||
|
||||
localInfo, err := os.Stat(cachePath)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
coverPath := GetArtworkCoverPath(rom)
|
||||
if coverPath == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
artURL := host.URL() + coverPath
|
||||
artURL = strings.ReplaceAll(artURL, " ", "%20")
|
||||
|
||||
var count int
|
||||
err := cm.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM artwork_metadata WHERE platform_fs_slug = ?
|
||||
`, fsSlug).Scan(&count)
|
||||
|
||||
if err != nil {
|
||||
// Fallback to checking directory
|
||||
dir := filepath.Join(GetArtworkCacheDir(), fsSlug)
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return len(entries) > 0
|
||||
remoteModTime, err := CheckRemoteArtworkLastModified(artURL, host.BasicAuthHeader())
|
||||
if err != nil || remoteModTime.IsZero() {
|
||||
return false // On error or no Last-Modified header, skip re-download
|
||||
}
|
||||
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func GetGameByIDForArtwork(gameID int) (struct {
|
||||
PlatformFSSlug string
|
||||
}, error) {
|
||||
cm := GetCacheManager()
|
||||
if cm == nil {
|
||||
return struct{ PlatformFSSlug string }{}, ErrNotInitialized
|
||||
}
|
||||
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
var platformFSSlug string
|
||||
err := cm.db.QueryRow(`
|
||||
SELECT platform_fs_slug FROM games WHERE id = ?
|
||||
`, gameID).Scan(&platformFSSlug)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return struct{ PlatformFSSlug string }{}, ErrCacheMiss
|
||||
}
|
||||
if err != nil {
|
||||
return struct{ PlatformFSSlug string }{}, err
|
||||
}
|
||||
|
||||
return struct{ PlatformFSSlug string }{platformFSSlug}, nil
|
||||
return remoteModTime.After(localInfo.ModTime())
|
||||
}
|
||||
|
||||
Vendored
+3
-74
@@ -1,15 +1,13 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"grout/romm"
|
||||
|
||||
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
|
||||
)
|
||||
|
||||
// GetCollections retrieves all cached collections
|
||||
func (cm *CacheManager) GetCollections() ([]romm.Collection, error) {
|
||||
func (cm *Manager) GetCollections() ([]romm.Collection, error) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return nil, ErrNotInitialized
|
||||
}
|
||||
@@ -56,8 +54,7 @@ func (cm *CacheManager) GetCollections() ([]romm.Collection, error) {
|
||||
return collections, nil
|
||||
}
|
||||
|
||||
// GetCollectionsByType retrieves collections of a specific type
|
||||
func (cm *CacheManager) GetCollectionsByType(collType string) ([]romm.Collection, error) {
|
||||
func (cm *Manager) GetCollectionsByType(collType string) ([]romm.Collection, error) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return nil, ErrNotInitialized
|
||||
}
|
||||
@@ -104,8 +101,7 @@ func (cm *CacheManager) GetCollectionsByType(collType string) ([]romm.Collection
|
||||
return collections, nil
|
||||
}
|
||||
|
||||
// SaveCollections saves collection metadata to cache
|
||||
func (cm *CacheManager) SaveCollections(collections []romm.Collection) error {
|
||||
func (cm *Manager) SaveCollections(collections []romm.Collection) error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
@@ -176,70 +172,3 @@ func (cm *CacheManager) SaveCollections(collections []romm.Collection) error {
|
||||
logger.Debug("Saved collections to cache", "count", len(collections))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCollection retrieves a single collection by its identifier
|
||||
func (cm *CacheManager) GetCollection(collection romm.Collection) (romm.Collection, error) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return romm.Collection{}, ErrNotInitialized
|
||||
}
|
||||
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
var dataJSON string
|
||||
var err error
|
||||
|
||||
if collection.IsVirtual {
|
||||
err = cm.db.QueryRow(`
|
||||
SELECT data_json FROM collections WHERE virtual_id = ?
|
||||
`, collection.VirtualID).Scan(&dataJSON)
|
||||
} else {
|
||||
collType := "regular"
|
||||
if collection.IsSmart {
|
||||
collType = "smart"
|
||||
}
|
||||
err = cm.db.QueryRow(`
|
||||
SELECT data_json FROM collections WHERE romm_id = ? AND type = ?
|
||||
`, collection.ID, collType).Scan(&dataJSON)
|
||||
}
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
cm.stats.recordMiss()
|
||||
return romm.Collection{}, ErrCacheMiss
|
||||
}
|
||||
if err != nil {
|
||||
cm.stats.recordError()
|
||||
return romm.Collection{}, newCacheError("get", "collections", collection.Name, err)
|
||||
}
|
||||
|
||||
var result romm.Collection
|
||||
if err := json.Unmarshal([]byte(dataJSON), &result); err != nil {
|
||||
cm.stats.recordError()
|
||||
return romm.Collection{}, newCacheError("get", "collections", collection.Name, err)
|
||||
}
|
||||
|
||||
cm.stats.recordHit()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// HasCollectionGames checks if games are cached for a collection
|
||||
func (cm *CacheManager) HasCollectionGames(collection romm.Collection) bool {
|
||||
if cm == nil || !cm.initialized {
|
||||
return false
|
||||
}
|
||||
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
collectionID, err := cm.getCollectionInternalID(collection)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var count int
|
||||
err = cm.db.QueryRow(`
|
||||
SELECT COUNT(*) FROM game_collections WHERE collection_id = ?
|
||||
`, collectionID).Scan(&count)
|
||||
|
||||
return err == nil && count > 0
|
||||
}
|
||||
|
||||
Vendored
-2
@@ -2,8 +2,6 @@ package cache
|
||||
|
||||
import "time"
|
||||
|
||||
// Config defines the configuration interface needed by the cache package.
|
||||
// This interface is implemented by utils.Config.
|
||||
type Config interface {
|
||||
GetApiTimeout() time.Duration
|
||||
GetShowCollections() bool
|
||||
|
||||
Vendored
+5
-8
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Sentinel errors for cache operations
|
||||
var (
|
||||
ErrNotInitialized = errors.New("cache manager not initialized")
|
||||
ErrCacheMiss = errors.New("cache miss")
|
||||
@@ -13,15 +12,14 @@ var (
|
||||
ErrInvalidCacheKey = errors.New("invalid cache key")
|
||||
)
|
||||
|
||||
// CacheError provides detailed error information for cache operations
|
||||
type CacheError struct {
|
||||
type Error struct {
|
||||
Op string // Operation name: "get", "save", "delete", etc.
|
||||
Key string // Cache key if applicable
|
||||
CacheType string // "platform", "collection", "rom_id", "artwork"
|
||||
Err error // Underlying error
|
||||
}
|
||||
|
||||
func (e *CacheError) Error() string {
|
||||
func (e *Error) Error() string {
|
||||
if e.Key != "" {
|
||||
return fmt.Sprintf("cache %s [%s:%s]: %v", e.Op, e.CacheType, e.Key, e.Err)
|
||||
}
|
||||
@@ -31,13 +29,12 @@ func (e *CacheError) Error() string {
|
||||
return fmt.Sprintf("cache %s: %v", e.Op, e.Err)
|
||||
}
|
||||
|
||||
func (e *CacheError) Unwrap() error {
|
||||
func (e *Error) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// newCacheError creates a new CacheError
|
||||
func newCacheError(op, cacheType, key string, err error) *CacheError {
|
||||
return &CacheError{
|
||||
func newCacheError(op, cacheType, key string, err error) *Error {
|
||||
return &Error{
|
||||
Op: op,
|
||||
Key: key,
|
||||
CacheType: cacheType,
|
||||
|
||||
Vendored
+18
-124
@@ -11,7 +11,6 @@ import (
|
||||
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
|
||||
)
|
||||
|
||||
// Cache key types (kept for compatibility)
|
||||
type Type string
|
||||
|
||||
const (
|
||||
@@ -21,17 +20,14 @@ const (
|
||||
VirtualCollection Type = "virtual_collection"
|
||||
)
|
||||
|
||||
// GetCacheKey generates a cache key (kept for compatibility with existing code)
|
||||
func GetCacheKey(cacheType Type, id string) string {
|
||||
return string(cacheType) + "_" + id
|
||||
}
|
||||
|
||||
// GetPlatformCacheKey generates a platform cache key
|
||||
func GetPlatformCacheKey(platformID int) string {
|
||||
return GetCacheKey(Platform, strconv.Itoa(platformID))
|
||||
}
|
||||
|
||||
// GetCollectionCacheKey generates a collection cache key
|
||||
func GetCollectionCacheKey(collection romm.Collection) string {
|
||||
if collection.IsVirtual {
|
||||
return GetCacheKey(VirtualCollection, collection.VirtualID)
|
||||
@@ -42,8 +38,7 @@ func GetCollectionCacheKey(collection romm.Collection) string {
|
||||
return GetCacheKey(Collection, strconv.Itoa(collection.ID))
|
||||
}
|
||||
|
||||
// GetPlatformGames retrieves all games for a platform from cache
|
||||
func (cm *CacheManager) GetPlatformGames(platformID int) ([]romm.Rom, error) {
|
||||
func (cm *Manager) GetPlatformGames(platformID int) ([]romm.Rom, error) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return nil, ErrNotInitialized
|
||||
}
|
||||
@@ -90,8 +85,7 @@ func (cm *CacheManager) GetPlatformGames(platformID int) ([]romm.Rom, error) {
|
||||
return games, nil
|
||||
}
|
||||
|
||||
// SavePlatformGames saves games for a platform to cache
|
||||
func (cm *CacheManager) SavePlatformGames(platformID int, games []romm.Rom) error {
|
||||
func (cm *Manager) SavePlatformGames(platformID int, games []romm.Rom) error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
@@ -107,15 +101,14 @@ func (cm *CacheManager) SavePlatformGames(platformID int, games []romm.Rom) erro
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Delete existing games for this platform
|
||||
_, err = tx.Exec(`DELETE FROM games WHERE platform_id = ?`, platformID)
|
||||
if err != nil {
|
||||
return newCacheError("save", "games", GetPlatformCacheKey(platformID), err)
|
||||
}
|
||||
|
||||
stmt, err := tx.Prepare(`
|
||||
INSERT INTO games (id, platform_id, platform_fs_slug, name, fs_name, data_json, updated_at, cached_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
INSERT INTO games (id, platform_id, platform_fs_slug, name, fs_name, fs_name_no_ext, crc_hash, md5_hash, sha1_hash, data_json, updated_at, cached_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
if err != nil {
|
||||
return newCacheError("save", "games", GetPlatformCacheKey(platformID), err)
|
||||
@@ -135,6 +128,10 @@ func (cm *CacheManager) SavePlatformGames(platformID int, games []romm.Rom) erro
|
||||
game.PlatformFSSlug,
|
||||
game.Name,
|
||||
game.FsName,
|
||||
game.FsNameNoExt,
|
||||
game.CrcHash,
|
||||
game.Md5Hash,
|
||||
game.Sha1Hash,
|
||||
string(dataJSON),
|
||||
game.UpdatedAt,
|
||||
now,
|
||||
@@ -152,8 +149,7 @@ func (cm *CacheManager) SavePlatformGames(platformID int, games []romm.Rom) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCollectionGames retrieves all games for a collection from cache
|
||||
func (cm *CacheManager) GetCollectionGames(collection romm.Collection) ([]romm.Rom, error) {
|
||||
func (cm *Manager) GetCollectionGames(collection romm.Collection) ([]romm.Rom, error) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return nil, ErrNotInitialized
|
||||
}
|
||||
@@ -161,7 +157,6 @@ func (cm *CacheManager) GetCollectionGames(collection romm.Collection) ([]romm.R
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
// Get the collection's internal ID
|
||||
collectionID, err := cm.getCollectionInternalID(collection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -209,9 +204,7 @@ func (cm *CacheManager) GetCollectionGames(collection romm.Collection) ([]romm.R
|
||||
return games, nil
|
||||
}
|
||||
|
||||
// SaveCollectionGames saves game-collection mappings to cache
|
||||
// Games should already exist from platform fetching; this only creates the mappings
|
||||
func (cm *CacheManager) SaveCollectionGames(collection romm.Collection, games []romm.Rom) error {
|
||||
func (cm *Manager) SaveCollectionGames(collection romm.Collection, games []romm.Rom) error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
@@ -277,8 +270,7 @@ func (cm *CacheManager) SaveCollectionGames(collection romm.Collection, games []
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCollectionInternalIDLocked gets collection ID without acquiring lock (caller must hold lock)
|
||||
func (cm *CacheManager) getCollectionInternalIDLocked(collection romm.Collection) (int64, error) {
|
||||
func (cm *Manager) getCollectionInternalID(collection romm.Collection) (int64, error) {
|
||||
var id int64
|
||||
var err error
|
||||
|
||||
@@ -304,9 +296,11 @@ func (cm *CacheManager) getCollectionInternalIDLocked(collection romm.Collection
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// SaveAllCollectionMappings saves all game-collection mappings in a single transaction
|
||||
// Uses ROMIDs from the collection response - no additional API calls needed
|
||||
func (cm *CacheManager) SaveAllCollectionMappings(collections []romm.Collection) error {
|
||||
func (cm *Manager) getCollectionInternalIDLocked(collection romm.Collection) (int64, error) {
|
||||
return cm.getCollectionInternalID(collection)
|
||||
}
|
||||
|
||||
func (cm *Manager) SaveAllCollectionMappings(collections []romm.Collection) error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
@@ -402,8 +396,7 @@ func (cm *CacheManager) SaveAllCollectionMappings(collections []romm.Collection)
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetGamesByIDs retrieves multiple games by their IDs efficiently
|
||||
func (cm *CacheManager) GetGamesByIDs(gameIDs []int) ([]romm.Rom, error) {
|
||||
func (cm *Manager) GetGamesByIDs(gameIDs []int) ([]romm.Rom, error) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return nil, ErrNotInitialized
|
||||
}
|
||||
@@ -462,55 +455,7 @@ func (cm *CacheManager) GetGamesByIDs(gameIDs []int) ([]romm.Rom, error) {
|
||||
return games, nil
|
||||
}
|
||||
|
||||
// GetGameByID retrieves a single game by ID
|
||||
func (cm *CacheManager) GetGameByID(gameID int) (romm.Rom, error) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return romm.Rom{}, ErrNotInitialized
|
||||
}
|
||||
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
var dataJSON string
|
||||
err := cm.db.QueryRow(`
|
||||
SELECT data_json FROM games WHERE id = ?
|
||||
`, gameID).Scan(&dataJSON)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
cm.stats.recordMiss()
|
||||
return romm.Rom{}, ErrCacheMiss
|
||||
}
|
||||
if err != nil {
|
||||
cm.stats.recordError()
|
||||
return romm.Rom{}, newCacheError("get", "games", strconv.Itoa(gameID), err)
|
||||
}
|
||||
|
||||
var game romm.Rom
|
||||
if err := json.Unmarshal([]byte(dataJSON), &game); err != nil {
|
||||
cm.stats.recordError()
|
||||
return romm.Rom{}, newCacheError("get", "games", strconv.Itoa(gameID), err)
|
||||
}
|
||||
|
||||
cm.stats.recordHit()
|
||||
return game, nil
|
||||
}
|
||||
|
||||
// HasPlatformGames checks if games are cached for a platform
|
||||
func (cm *CacheManager) HasPlatformGames(platformID int) bool {
|
||||
if cm == nil || !cm.initialized {
|
||||
return false
|
||||
}
|
||||
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
var count int
|
||||
err := cm.db.QueryRow(`SELECT COUNT(*) FROM games WHERE platform_id = ?`, platformID).Scan(&count)
|
||||
return err == nil && count > 0
|
||||
}
|
||||
|
||||
// GetCachedGameIDs returns a map of all game IDs in the cache for fast lookup
|
||||
func (cm *CacheManager) GetCachedGameIDs() map[int]bool {
|
||||
func (cm *Manager) GetCachedGameIDs() map[int]bool {
|
||||
if cm == nil || !cm.initialized {
|
||||
return nil
|
||||
}
|
||||
@@ -534,54 +479,3 @@ func (cm *CacheManager) GetCachedGameIDs() map[int]bool {
|
||||
|
||||
return gameIDs
|
||||
}
|
||||
|
||||
// Helper function to get collection internal ID
|
||||
func (cm *CacheManager) getCollectionInternalID(collection romm.Collection) (int64, error) {
|
||||
var id int64
|
||||
var err error
|
||||
|
||||
if collection.IsVirtual {
|
||||
err = cm.db.QueryRow(`SELECT id FROM collections WHERE virtual_id = ?`, collection.VirtualID).Scan(&id)
|
||||
} else {
|
||||
collType := "regular"
|
||||
if collection.IsSmart {
|
||||
collType = "smart"
|
||||
}
|
||||
err = cm.db.QueryRow(`SELECT id FROM collections WHERE romm_id = ? AND type = ?`, collection.ID, collType).Scan(&id)
|
||||
}
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
cm.stats.recordMiss()
|
||||
return 0, ErrCacheMiss
|
||||
}
|
||||
if err != nil {
|
||||
cm.stats.recordError()
|
||||
return 0, newCacheError("get", "collections", GetCollectionCacheKey(collection), err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// ClearGamesCache clears the games cache (compatibility wrapper)
|
||||
func ClearGamesCache() error {
|
||||
cm := GetCacheManager()
|
||||
if cm == nil {
|
||||
return nil
|
||||
}
|
||||
return cm.ClearGames()
|
||||
}
|
||||
|
||||
// HasGamesCache checks if games cache has data (compatibility wrapper)
|
||||
func HasGamesCache() bool {
|
||||
cm := GetCacheManager()
|
||||
if cm == nil {
|
||||
return false
|
||||
}
|
||||
return cm.HasCache()
|
||||
}
|
||||
|
||||
// GetGamesCacheDir returns the cache directory (kept for compatibility)
|
||||
// Note: This no longer reflects actual storage location but is kept for compatibility
|
||||
func GetGamesCacheDir() string {
|
||||
return GetArtworkCacheDir() // Just return something valid
|
||||
}
|
||||
|
||||
Vendored
+49
-106
@@ -14,8 +14,7 @@ import (
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
// CacheManager is the unified cache interface for all caching operations
|
||||
type CacheManager struct {
|
||||
type Manager struct {
|
||||
db *sql.DB
|
||||
dbPath string
|
||||
mu sync.RWMutex
|
||||
@@ -23,11 +22,9 @@ type CacheManager struct {
|
||||
config Config
|
||||
initialized bool
|
||||
|
||||
// Stats tracking
|
||||
stats *CacheStats
|
||||
}
|
||||
|
||||
// CacheStats tracks cache performance metrics
|
||||
type CacheStats struct {
|
||||
mu sync.Mutex
|
||||
Hits int64
|
||||
@@ -57,19 +54,15 @@ func (s *CacheStats) recordError() {
|
||||
}
|
||||
|
||||
var (
|
||||
cacheManager *CacheManager
|
||||
cacheManager *Manager
|
||||
cacheManagerOnce sync.Once
|
||||
cacheManagerErr error
|
||||
)
|
||||
|
||||
// GetCacheManager returns the singleton cache manager instance
|
||||
// Returns nil if not yet initialized
|
||||
func GetCacheManager() *CacheManager {
|
||||
func GetCacheManager() *Manager {
|
||||
return cacheManager
|
||||
}
|
||||
|
||||
// InitCacheManager initializes the singleton cache manager
|
||||
// This should be called once at application startup
|
||||
func InitCacheManager(host romm.Host, config Config) error {
|
||||
cacheManagerOnce.Do(func() {
|
||||
cacheManager, cacheManagerErr = newCacheManager(host, config)
|
||||
@@ -77,38 +70,32 @@ func InitCacheManager(host romm.Host, config Config) error {
|
||||
return cacheManagerErr
|
||||
}
|
||||
|
||||
// newCacheManager creates a new CacheManager instance
|
||||
func newCacheManager(host romm.Host, config Config) (*CacheManager, error) {
|
||||
func newCacheManager(host romm.Host, config Config) (*Manager, error) {
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
dbPath := getCacheDBPath()
|
||||
|
||||
// Ensure cache directory exists
|
||||
cacheDir := filepath.Dir(dbPath)
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
return nil, newCacheError("init", "", "", err)
|
||||
}
|
||||
|
||||
// Clean up old JSON cache directories if they exist
|
||||
cleanupLegacyCache()
|
||||
|
||||
// Open SQLite database with WAL mode for better concurrent read performance
|
||||
db, err := sql.Open("sqlite", dbPath+"?_pragma=journal_mode(WAL)&_pragma=synchronous(NORMAL)&_pragma=busy_timeout(5000)")
|
||||
if err != nil {
|
||||
return nil, newCacheError("init", "", "", err)
|
||||
}
|
||||
|
||||
// SQLite is single-writer, limit connections
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
|
||||
// Create tables
|
||||
if err := createTables(db); err != nil {
|
||||
db.Close()
|
||||
return nil, newCacheError("init", "", "", err)
|
||||
}
|
||||
|
||||
cm := &CacheManager{
|
||||
cm := &Manager{
|
||||
db: db,
|
||||
dbPath: dbPath,
|
||||
host: host,
|
||||
@@ -121,8 +108,7 @@ func newCacheManager(host romm.Host, config Config) (*CacheManager, error) {
|
||||
return cm, nil
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (cm *CacheManager) Close() error {
|
||||
func (cm *Manager) Close() error {
|
||||
if cm == nil || cm.db == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -134,8 +120,7 @@ func (cm *CacheManager) Close() error {
|
||||
return cm.db.Close()
|
||||
}
|
||||
|
||||
// IsFirstRun returns true if the cache has no games (needs population)
|
||||
func (cm *CacheManager) IsFirstRun() bool {
|
||||
func (cm *Manager) IsFirstRun() bool {
|
||||
if cm == nil || !cm.initialized {
|
||||
return true
|
||||
}
|
||||
@@ -152,8 +137,7 @@ func (cm *CacheManager) IsFirstRun() bool {
|
||||
return count == 0
|
||||
}
|
||||
|
||||
// HasCache returns true if the cache database exists and has data
|
||||
func (cm *CacheManager) HasCache() bool {
|
||||
func (cm *Manager) HasCache() bool {
|
||||
if cm == nil || !cm.initialized {
|
||||
return false
|
||||
}
|
||||
@@ -166,8 +150,7 @@ func (cm *CacheManager) HasCache() bool {
|
||||
return err == nil && count > 0
|
||||
}
|
||||
|
||||
// Clear removes all cached data but keeps the database structure
|
||||
func (cm *CacheManager) Clear() error {
|
||||
func (cm *Manager) Clear() error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
@@ -177,7 +160,7 @@ func (cm *CacheManager) Clear() error {
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
tables := []string{"games", "game_collections", "collections", "platforms", "rom_id_cache", "artwork_metadata", "bios_availability"}
|
||||
tables := []string{"games", "game_collections", "collections", "platforms", "bios_availability"}
|
||||
|
||||
tx, err := cm.db.Begin()
|
||||
if err != nil {
|
||||
@@ -195,7 +178,6 @@ func (cm *CacheManager) Clear() error {
|
||||
return newCacheError("clear", "", "", err)
|
||||
}
|
||||
|
||||
// Also clear artwork files from disk
|
||||
artworkDir := GetArtworkCacheDir()
|
||||
if fileutil.FileExists(artworkDir) {
|
||||
os.RemoveAll(artworkDir)
|
||||
@@ -205,8 +187,7 @@ func (cm *CacheManager) Clear() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearGames removes only the games cache
|
||||
func (cm *CacheManager) ClearGames() error {
|
||||
func (cm *Manager) ClearGames() error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
@@ -231,30 +212,7 @@ func (cm *CacheManager) ClearGames() error {
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// ClearArtwork removes artwork metadata and files
|
||||
func (cm *CacheManager) ClearArtwork() error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
if _, err := cm.db.Exec("DELETE FROM artwork_metadata"); err != nil {
|
||||
return newCacheError("clear_artwork", "", "", err)
|
||||
}
|
||||
|
||||
// Also clear artwork files from disk
|
||||
artworkDir := GetArtworkCacheDir()
|
||||
if fileutil.FileExists(artworkDir) {
|
||||
os.RemoveAll(artworkDir)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearCollections removes collections and their game mappings
|
||||
func (cm *CacheManager) ClearCollections() error {
|
||||
func (cm *Manager) ClearCollections() error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
@@ -279,8 +237,7 @@ func (cm *CacheManager) ClearCollections() error {
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
// HasCollections returns true if collections are cached
|
||||
func (cm *CacheManager) HasCollections() bool {
|
||||
func (cm *Manager) HasCollections() bool {
|
||||
if cm == nil || !cm.initialized {
|
||||
return false
|
||||
}
|
||||
@@ -293,15 +250,12 @@ func (cm *CacheManager) HasCollections() bool {
|
||||
return err == nil && count > 0
|
||||
}
|
||||
|
||||
// Cache refresh timestamp keys
|
||||
const (
|
||||
MetaKeyGamesRefreshedAt = "games_refreshed_at"
|
||||
MetaKeyCollectionsRefreshedAt = "collections_refreshed_at"
|
||||
MetaKeyArtworkRefreshedAt = "artwork_refreshed_at"
|
||||
)
|
||||
|
||||
// SetMetadata stores a key-value pair in the cache metadata table
|
||||
func (cm *CacheManager) SetMetadata(key, value string) error {
|
||||
func (cm *Manager) SetMetadata(key, value string) error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
@@ -320,8 +274,7 @@ func (cm *CacheManager) SetMetadata(key, value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMetadata retrieves a value from the cache metadata table
|
||||
func (cm *CacheManager) GetMetadata(key string) (string, error) {
|
||||
func (cm *Manager) GetMetadata(key string) (string, error) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return "", ErrNotInitialized
|
||||
}
|
||||
@@ -338,8 +291,7 @@ func (cm *CacheManager) GetMetadata(key string) (string, error) {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// GetLastRefreshTime returns the last refresh time for a cache type
|
||||
func (cm *CacheManager) GetLastRefreshTime(key string) (time.Time, error) {
|
||||
func (cm *Manager) GetLastRefreshTime(key string) (time.Time, error) {
|
||||
value, err := cm.GetMetadata(key)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
@@ -348,16 +300,14 @@ func (cm *CacheManager) GetLastRefreshTime(key string) (time.Time, error) {
|
||||
return time.Parse(time.RFC3339, value)
|
||||
}
|
||||
|
||||
// RecordRefreshTime records the current time as the last refresh for a cache type
|
||||
func (cm *CacheManager) RecordRefreshTime(key string) error {
|
||||
func (cm *Manager) RecordRefreshTime(key string) error {
|
||||
return cm.SetMetadata(key, time.Now().Format(time.RFC3339))
|
||||
}
|
||||
|
||||
// GetAllRefreshTimes returns the last refresh times for all cache types
|
||||
func (cm *CacheManager) GetAllRefreshTimes() map[string]time.Time {
|
||||
func (cm *Manager) GetAllRefreshTimes() map[string]time.Time {
|
||||
result := make(map[string]time.Time)
|
||||
|
||||
keys := []string{MetaKeyGamesRefreshedAt, MetaKeyCollectionsRefreshedAt, MetaKeyArtworkRefreshedAt}
|
||||
keys := []string{MetaKeyGamesRefreshedAt, MetaKeyCollectionsRefreshedAt}
|
||||
for _, key := range keys {
|
||||
if t, err := cm.GetLastRefreshTime(key); err == nil {
|
||||
result[key] = t
|
||||
@@ -367,36 +317,7 @@ func (cm *CacheManager) GetAllRefreshTimes() map[string]time.Time {
|
||||
return result
|
||||
}
|
||||
|
||||
// GetStats returns current cache statistics
|
||||
func (cm *CacheManager) GetStats() CacheStats {
|
||||
if cm == nil || cm.stats == nil {
|
||||
return CacheStats{}
|
||||
}
|
||||
|
||||
cm.stats.mu.Lock()
|
||||
defer cm.stats.mu.Unlock()
|
||||
|
||||
return CacheStats{
|
||||
Hits: cm.stats.Hits,
|
||||
Misses: cm.stats.Misses,
|
||||
Errors: cm.stats.Errors,
|
||||
LastAccess: cm.stats.LastAccess,
|
||||
}
|
||||
}
|
||||
|
||||
// SetHost updates the host configuration (used after re-login)
|
||||
func (cm *CacheManager) SetHost(host romm.Host) {
|
||||
if cm == nil {
|
||||
return
|
||||
}
|
||||
|
||||
cm.mu.Lock()
|
||||
cm.host = host
|
||||
cm.mu.Unlock()
|
||||
}
|
||||
|
||||
// PopulateFullCacheWithProgress populates the entire cache with progress reporting
|
||||
func (cm *CacheManager) PopulateFullCacheWithProgress(platforms []romm.Platform, progress *atomic.Float64) error {
|
||||
func (cm *Manager) PopulateFullCacheWithProgress(platforms []romm.Platform, progress *atomic.Float64) error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
@@ -404,9 +325,6 @@ func (cm *CacheManager) PopulateFullCacheWithProgress(platforms []romm.Platform,
|
||||
return cm.populateCache(platforms, progress)
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
// getCacheDBPath returns the path to the SQLite database file
|
||||
func getCacheDBPath() string {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
@@ -415,7 +333,6 @@ func getCacheDBPath() string {
|
||||
return filepath.Join(wd, ".cache", "grout.db")
|
||||
}
|
||||
|
||||
// GetArtworkCacheDir returns the path to the artwork cache directory
|
||||
func GetArtworkCacheDir() string {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
@@ -424,7 +341,35 @@ func GetArtworkCacheDir() string {
|
||||
return filepath.Join(wd, ".cache", "artwork")
|
||||
}
|
||||
|
||||
// cleanupLegacyCache removes old JSON cache directories
|
||||
func GetCacheDir() string {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return filepath.Join(os.TempDir(), ".cache")
|
||||
}
|
||||
return filepath.Join(wd, ".cache")
|
||||
}
|
||||
|
||||
func DeleteCacheFolder() error {
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
if cacheManager != nil {
|
||||
cacheManager.Close()
|
||||
cacheManager = nil
|
||||
}
|
||||
|
||||
cacheManagerOnce = sync.Once{}
|
||||
cacheManagerErr = nil
|
||||
|
||||
cacheDir := GetCacheDir()
|
||||
if err := os.RemoveAll(cacheDir); err != nil {
|
||||
logger.Error("Failed to delete cache folder", "path", cacheDir, "error", err)
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Info("Cache folder deleted", "path", cacheDir)
|
||||
return nil
|
||||
}
|
||||
|
||||
func cleanupLegacyCache() {
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
@@ -433,7 +378,6 @@ func cleanupLegacyCache() {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove old games JSON cache
|
||||
gamesDir := filepath.Join(wd, ".cache", "games")
|
||||
if fileutil.FileExists(gamesDir) {
|
||||
if err := os.RemoveAll(gamesDir); err != nil {
|
||||
@@ -443,7 +387,6 @@ func cleanupLegacyCache() {
|
||||
}
|
||||
}
|
||||
|
||||
// Remove old ROM ID JSON cache
|
||||
romsDir := filepath.Join(wd, ".cache", "roms")
|
||||
if fileutil.FileExists(romsDir) {
|
||||
if err := os.RemoveAll(romsDir); err != nil {
|
||||
|
||||
Vendored
+7
-43
@@ -9,8 +9,7 @@ import (
|
||||
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
|
||||
)
|
||||
|
||||
// GetPlatforms retrieves all cached platforms
|
||||
func (cm *CacheManager) GetPlatforms() ([]romm.Platform, error) {
|
||||
func (cm *Manager) GetPlatforms() ([]romm.Platform, error) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return nil, ErrNotInitialized
|
||||
}
|
||||
@@ -57,8 +56,7 @@ func (cm *CacheManager) GetPlatforms() ([]romm.Platform, error) {
|
||||
return platforms, nil
|
||||
}
|
||||
|
||||
// SavePlatforms saves platform data to the cache
|
||||
func (cm *CacheManager) SavePlatforms(platforms []romm.Platform) error {
|
||||
func (cm *Manager) SavePlatforms(platforms []romm.Platform) error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
@@ -76,8 +74,8 @@ func (cm *CacheManager) SavePlatforms(platforms []romm.Platform) error {
|
||||
|
||||
stmt, err := tx.Prepare(`
|
||||
INSERT OR REPLACE INTO platforms
|
||||
(id, slug, fs_slug, name, custom_name, rom_count, has_bios, data_json, updated_at, cached_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
(id, slug, fs_slug, name, api_name, custom_name, rom_count, has_bios, data_json, updated_at, cached_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`)
|
||||
if err != nil {
|
||||
return newCacheError("save", "platforms", "", err)
|
||||
@@ -101,6 +99,7 @@ func (cm *CacheManager) SavePlatforms(platforms []romm.Platform) error {
|
||||
p.Slug,
|
||||
p.FSSlug,
|
||||
p.Name,
|
||||
p.ApiName,
|
||||
p.CustomName,
|
||||
p.ROMCount,
|
||||
hasBIOS,
|
||||
@@ -121,41 +120,7 @@ func (cm *CacheManager) SavePlatforms(platforms []romm.Platform) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPlatformByID retrieves a single platform by ID
|
||||
func (cm *CacheManager) GetPlatformByID(platformID int) (romm.Platform, error) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return romm.Platform{}, ErrNotInitialized
|
||||
}
|
||||
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
var dataJSON string
|
||||
err := cm.db.QueryRow(`
|
||||
SELECT data_json FROM platforms WHERE id = ?
|
||||
`, platformID).Scan(&dataJSON)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
cm.stats.recordMiss()
|
||||
return romm.Platform{}, ErrCacheMiss
|
||||
}
|
||||
if err != nil {
|
||||
cm.stats.recordError()
|
||||
return romm.Platform{}, newCacheError("get", "platforms", "", err)
|
||||
}
|
||||
|
||||
var platform romm.Platform
|
||||
if err := json.Unmarshal([]byte(dataJSON), &platform); err != nil {
|
||||
cm.stats.recordError()
|
||||
return romm.Platform{}, newCacheError("get", "platforms", "", err)
|
||||
}
|
||||
|
||||
cm.stats.recordHit()
|
||||
return platform, nil
|
||||
}
|
||||
|
||||
// HasBIOS returns whether a platform has BIOS files available
|
||||
func (cm *CacheManager) HasBIOS(platformID int) (bool, bool) {
|
||||
func (cm *Manager) HasBIOS(platformID int) (bool, bool) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return false, false
|
||||
}
|
||||
@@ -178,8 +143,7 @@ func (cm *CacheManager) HasBIOS(platformID int) (bool, bool) {
|
||||
return hasBIOS == 1, true
|
||||
}
|
||||
|
||||
// SetBIOSAvailability sets whether a platform has BIOS files
|
||||
func (cm *CacheManager) SetBIOSAvailability(platformID int, hasBIOS bool) error {
|
||||
func (cm *Manager) SetBIOSAvailability(platformID int, hasBIOS bool) error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
|
||||
Vendored
+19
-204
@@ -8,19 +8,12 @@ import (
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
// Configuration constants for API pagination
|
||||
const (
|
||||
// DefaultRomPageSize is the number of ROMs to fetch per API call
|
||||
// Kept small for better progress feedback and to avoid timeouts
|
||||
// Increase this when the bulk API becomes more performant
|
||||
DefaultRomPageSize = 200
|
||||
|
||||
// MaxConcurrentPlatformFetches limits parallel platform API calls
|
||||
DefaultRomPageSize = 200
|
||||
MaxConcurrentPlatformFetches = 5
|
||||
)
|
||||
|
||||
// populateCache populates the entire cache with platform and collection data
|
||||
func (cm *CacheManager) populateCache(platforms []romm.Platform, progress *atomic.Float64) error {
|
||||
func (cm *Manager) populateCache(platforms []romm.Platform, progress *atomic.Float64) error {
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
if len(platforms) == 0 {
|
||||
@@ -30,22 +23,18 @@ func (cm *CacheManager) populateCache(platforms []romm.Platform, progress *atomi
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save platforms first
|
||||
if err := cm.SavePlatforms(platforms); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Calculate total expected games for granular progress tracking
|
||||
// Use 90% for games, reserve 10% for collections
|
||||
totalExpectedGames := int64(0)
|
||||
for _, p := range platforms {
|
||||
totalExpectedGames += int64(p.ROMCount)
|
||||
}
|
||||
if totalExpectedGames == 0 {
|
||||
totalExpectedGames = int64(len(platforms)) // Fallback to platform count
|
||||
totalExpectedGames = int64(len(platforms))
|
||||
}
|
||||
|
||||
// Track progress based on games fetched
|
||||
gamesFetched := &atomic.Int64{}
|
||||
updateProgress := func(count int) {
|
||||
if progress != nil {
|
||||
@@ -59,7 +48,6 @@ func (cm *CacheManager) populateCache(platforms []romm.Platform, progress *atomi
|
||||
}
|
||||
}
|
||||
|
||||
// Use bounded concurrency for platform fetches
|
||||
sem := make(chan struct{}, MaxConcurrentPlatformFetches)
|
||||
var wg sync.WaitGroup
|
||||
var firstErr error
|
||||
@@ -69,8 +57,8 @@ func (cm *CacheManager) populateCache(platforms []romm.Platform, progress *atomi
|
||||
wg.Add(1)
|
||||
go func(p romm.Platform) {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{} // Acquire semaphore
|
||||
defer func() { <-sem }() // Release semaphore
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
|
||||
if err := cm.fetchAndCachePlatformGamesWithProgress(p, updateProgress); err != nil {
|
||||
logger.Error("Failed to cache platform", "platform", p.Name, "error", err)
|
||||
@@ -83,7 +71,6 @@ func (cm *CacheManager) populateCache(platforms []romm.Platform, progress *atomi
|
||||
}(platform)
|
||||
}
|
||||
|
||||
// Also fetch BIOS availability in parallel
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -92,16 +79,12 @@ func (cm *CacheManager) populateCache(platforms []romm.Platform, progress *atomi
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Record games refresh time
|
||||
if firstErr == nil {
|
||||
cm.RecordRefreshTime(MetaKeyGamesRefreshedAt)
|
||||
}
|
||||
|
||||
// Fetch collections after platforms (they may depend on game data)
|
||||
// This uses the remaining 10% of progress (90% -> 100%)
|
||||
cm.fetchAndCacheCollectionsWithProgress(progress)
|
||||
|
||||
// Record collections refresh time
|
||||
cm.RecordRefreshTime(MetaKeyCollectionsRefreshedAt)
|
||||
|
||||
if progress != nil {
|
||||
@@ -112,13 +95,11 @@ func (cm *CacheManager) populateCache(platforms []romm.Platform, progress *atomi
|
||||
return firstErr
|
||||
}
|
||||
|
||||
// fetchAndCachePlatformGames fetches all games for a platform using paginated API calls
|
||||
func (cm *CacheManager) fetchAndCachePlatformGames(platform romm.Platform) error {
|
||||
func (cm *Manager) fetchAndCachePlatformGames(platform romm.Platform) error {
|
||||
return cm.fetchAndCachePlatformGamesWithProgress(platform, nil)
|
||||
}
|
||||
|
||||
// fetchAndCachePlatformGamesWithProgress fetches all games with progress callback per batch
|
||||
func (cm *CacheManager) fetchAndCachePlatformGamesWithProgress(platform romm.Platform, onProgress func(count int)) error {
|
||||
func (cm *Manager) fetchAndCachePlatformGamesWithProgress(platform romm.Platform, onProgress func(count int)) error {
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
client := romm.NewClientFromHost(cm.host, cm.config.GetApiTimeout())
|
||||
@@ -126,7 +107,6 @@ func (cm *CacheManager) fetchAndCachePlatformGamesWithProgress(platform romm.Pla
|
||||
var allGames []romm.Rom
|
||||
offset := 0
|
||||
expectedTotal := 0
|
||||
requestCount := 0
|
||||
|
||||
for {
|
||||
opt := romm.GetRomsQuery{
|
||||
@@ -143,72 +123,33 @@ func (cm *CacheManager) fetchAndCachePlatformGamesWithProgress(platform romm.Pla
|
||||
"error", err)
|
||||
return err
|
||||
}
|
||||
requestCount++
|
||||
|
||||
// Capture expected total from first request
|
||||
if offset == 0 {
|
||||
expectedTotal = res.Total
|
||||
}
|
||||
|
||||
logger.Debug("Fetched games batch",
|
||||
"platform", platform.Name,
|
||||
"offset", offset,
|
||||
"received", len(res.Items),
|
||||
"total", res.Total,
|
||||
"expectedTotal", expectedTotal,
|
||||
"accumulated", len(allGames)+len(res.Items))
|
||||
|
||||
allGames = append(allGames, res.Items...)
|
||||
|
||||
// Report progress after each batch
|
||||
if onProgress != nil && len(res.Items) > 0 {
|
||||
onProgress(len(res.Items))
|
||||
}
|
||||
|
||||
// Check if we've fetched all games:
|
||||
// 1. We have at least as many as expected
|
||||
// 2. OR we received an empty batch (no more items)
|
||||
// 3. OR we received fewer items than requested (last batch)
|
||||
// Terminate when: got all expected, empty batch, or partial page (last batch)
|
||||
if len(allGames) >= expectedTotal || len(res.Items) == 0 || len(res.Items) < DefaultRomPageSize {
|
||||
break
|
||||
}
|
||||
|
||||
offset += len(res.Items)
|
||||
|
||||
// Safety limit to prevent infinite loops
|
||||
if requestCount > 1000 {
|
||||
logger.Warn("Hit request limit while fetching games",
|
||||
"platform", platform.Name,
|
||||
"accumulated", len(allGames))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Fetched all games for platform",
|
||||
logger.Info("Cached platform games",
|
||||
"platform", platform.Name,
|
||||
"expected", expectedTotal,
|
||||
"actual", len(allGames))
|
||||
"count", len(allGames))
|
||||
|
||||
// Save to cache (this acquires its own lock)
|
||||
if err := cm.SavePlatformGames(platform.ID, allGames); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debug("Fetched and cached platform games",
|
||||
"platform", platform.Name,
|
||||
"count", len(allGames),
|
||||
"requests", requestCount)
|
||||
|
||||
return nil
|
||||
return cm.SavePlatformGames(platform.ID, allGames)
|
||||
}
|
||||
|
||||
// fetchAndCacheCollections fetches and caches all collection types
|
||||
func (cm *CacheManager) fetchAndCacheCollections() {
|
||||
cm.fetchAndCacheCollectionsWithProgress(nil)
|
||||
}
|
||||
|
||||
// fetchAndCacheCollectionsWithProgress fetches and caches all collection types with progress tracking
|
||||
func (cm *CacheManager) fetchAndCacheCollectionsWithProgress(progress *atomic.Float64) {
|
||||
func (cm *Manager) fetchAndCacheCollectionsWithProgress(progress *atomic.Float64) {
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
client := romm.NewClientFromHost(cm.host, cm.config.GetApiTimeout())
|
||||
@@ -217,9 +158,6 @@ func (cm *CacheManager) fetchAndCacheCollectionsWithProgress(progress *atomic.Fl
|
||||
var mu sync.Mutex
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Fetch regular collections
|
||||
// Always fetch all collection types regardless of display settings
|
||||
// so data is ready when user enables them
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -233,7 +171,6 @@ func (cm *CacheManager) fetchAndCacheCollectionsWithProgress(progress *atomic.Fl
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// Fetch smart collections
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -250,7 +187,6 @@ func (cm *CacheManager) fetchAndCacheCollectionsWithProgress(progress *atomic.Fl
|
||||
mu.Unlock()
|
||||
}()
|
||||
|
||||
// Fetch virtual collections
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
@@ -268,7 +204,7 @@ func (cm *CacheManager) fetchAndCacheCollectionsWithProgress(progress *atomic.Fl
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Update progress to 92% after fetching collection metadata
|
||||
// Update progress to 92% after fetching collection metadata, arbitrary I know
|
||||
if progress != nil {
|
||||
progress.Store(0.92)
|
||||
}
|
||||
@@ -277,7 +213,6 @@ func (cm *CacheManager) fetchAndCacheCollectionsWithProgress(progress *atomic.Fl
|
||||
return
|
||||
}
|
||||
|
||||
// Save collection metadata
|
||||
if err := cm.SaveCollections(allCollections); err != nil {
|
||||
logger.Error("Failed to save collections", "error", err)
|
||||
}
|
||||
@@ -286,8 +221,6 @@ func (cm *CacheManager) fetchAndCacheCollectionsWithProgress(progress *atomic.Fl
|
||||
progress.Store(0.94)
|
||||
}
|
||||
|
||||
// Save game-collection mappings using ROMs already in collection response
|
||||
// No need to fetch games again - they're already included!
|
||||
if err := cm.SaveAllCollectionMappings(allCollections); err != nil {
|
||||
logger.Error("Failed to save collection mappings", "error", err)
|
||||
}
|
||||
@@ -299,84 +232,7 @@ func (cm *CacheManager) fetchAndCacheCollectionsWithProgress(progress *atomic.Fl
|
||||
logger.Debug("Cached collections", "count", len(allCollections))
|
||||
}
|
||||
|
||||
// fetchCollectionGames fetches all games for a collection
|
||||
func (cm *CacheManager) fetchCollectionGames(collection romm.Collection) ([]romm.Rom, error) {
|
||||
logger := gaba.GetLogger()
|
||||
client := romm.NewClientFromHost(cm.host, cm.config.GetApiTimeout())
|
||||
|
||||
var allGames []romm.Rom
|
||||
offset := 0
|
||||
expectedTotal := 0
|
||||
requestCount := 0
|
||||
|
||||
for {
|
||||
opt := romm.GetRomsQuery{
|
||||
Offset: offset,
|
||||
Limit: DefaultRomPageSize,
|
||||
}
|
||||
|
||||
if collection.IsVirtual {
|
||||
opt.VirtualCollectionID = collection.VirtualID
|
||||
} else if collection.IsSmart {
|
||||
opt.SmartCollectionID = collection.ID
|
||||
} else {
|
||||
opt.CollectionID = collection.ID
|
||||
}
|
||||
|
||||
res, err := client.GetRoms(opt)
|
||||
if err != nil {
|
||||
logger.Error("Failed to fetch collection games",
|
||||
"collection", collection.Name,
|
||||
"offset", offset,
|
||||
"error", err)
|
||||
return nil, err
|
||||
}
|
||||
requestCount++
|
||||
|
||||
// Capture expected total from first request
|
||||
if offset == 0 {
|
||||
expectedTotal = res.Total
|
||||
}
|
||||
|
||||
logger.Debug("Fetched collection games batch",
|
||||
"collection", collection.Name,
|
||||
"offset", offset,
|
||||
"received", len(res.Items),
|
||||
"total", res.Total,
|
||||
"expectedTotal", expectedTotal,
|
||||
"accumulated", len(allGames)+len(res.Items))
|
||||
|
||||
allGames = append(allGames, res.Items...)
|
||||
|
||||
// Check if we've fetched all games:
|
||||
// 1. We have at least as many as expected
|
||||
// 2. OR we received an empty batch (no more items)
|
||||
// 3. OR we received fewer items than requested (last batch)
|
||||
if len(allGames) >= expectedTotal || len(res.Items) == 0 || len(res.Items) < DefaultRomPageSize {
|
||||
break
|
||||
}
|
||||
|
||||
offset += len(res.Items)
|
||||
|
||||
// Safety limit to prevent infinite loops
|
||||
if requestCount > 1000 {
|
||||
logger.Warn("Hit request limit while fetching collection games",
|
||||
"collection", collection.Name,
|
||||
"accumulated", len(allGames))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logger.Debug("Fetched all games for collection",
|
||||
"collection", collection.Name,
|
||||
"expected", expectedTotal,
|
||||
"actual", len(allGames))
|
||||
|
||||
return allGames, nil
|
||||
}
|
||||
|
||||
// fetchBIOSAvailability fetches BIOS availability for all platforms
|
||||
func (cm *CacheManager) fetchBIOSAvailability(platforms []romm.Platform) {
|
||||
func (cm *Manager) fetchBIOSAvailability(platforms []romm.Platform) {
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
client := romm.NewClientFromHost(cm.host, cm.config.GetApiTimeout())
|
||||
@@ -406,9 +262,7 @@ func (cm *CacheManager) fetchBIOSAvailability(platforms []romm.Platform) {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// RefreshPlatformGames fetches and updates games for a single platform
|
||||
// Useful for refreshing a specific platform without full cache rebuild
|
||||
func (cm *CacheManager) RefreshPlatformGames(platform romm.Platform) error {
|
||||
func (cm *Manager) RefreshPlatformGames(platform romm.Platform) error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
@@ -416,9 +270,7 @@ func (cm *CacheManager) RefreshPlatformGames(platform romm.Platform) error {
|
||||
return cm.fetchAndCachePlatformGames(platform)
|
||||
}
|
||||
|
||||
// RefreshPlatformGamesWithProgress fetches and updates games for a single platform with progress tracking
|
||||
// Progress is reported as 0.0 to 1.0 based on games fetched vs expected total
|
||||
func (cm *CacheManager) RefreshPlatformGamesWithProgress(platform romm.Platform, progress *atomic.Float64) error {
|
||||
func (cm *Manager) RefreshPlatformGamesWithProgress(platform romm.Platform, progress *atomic.Float64) error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
@@ -429,7 +281,6 @@ func (cm *CacheManager) RefreshPlatformGamesWithProgress(platform romm.Platform,
|
||||
var allGames []romm.Rom
|
||||
offset := 0
|
||||
expectedTotal := 0
|
||||
requestCount := 0
|
||||
|
||||
for {
|
||||
opt := romm.GetRomsQuery{
|
||||
@@ -446,24 +297,13 @@ func (cm *CacheManager) RefreshPlatformGamesWithProgress(platform romm.Platform,
|
||||
"error", err)
|
||||
return err
|
||||
}
|
||||
requestCount++
|
||||
|
||||
// Capture expected total from first request
|
||||
if offset == 0 {
|
||||
expectedTotal = res.Total
|
||||
}
|
||||
|
||||
logger.Debug("Fetched games batch",
|
||||
"platform", platform.Name,
|
||||
"offset", offset,
|
||||
"received", len(res.Items),
|
||||
"total", res.Total,
|
||||
"expectedTotal", expectedTotal,
|
||||
"accumulated", len(allGames)+len(res.Items))
|
||||
|
||||
allGames = append(allGames, res.Items...)
|
||||
|
||||
// Update progress after each batch
|
||||
if progress != nil && expectedTotal > 0 {
|
||||
pct := float64(len(allGames)) / float64(expectedTotal)
|
||||
if pct > 1.0 {
|
||||
@@ -472,50 +312,25 @@ func (cm *CacheManager) RefreshPlatformGamesWithProgress(platform romm.Platform,
|
||||
progress.Store(pct)
|
||||
}
|
||||
|
||||
// Check if we've fetched all games
|
||||
// Terminate when: got all expected, empty batch, or partial page (last batch)
|
||||
if len(allGames) >= expectedTotal || len(res.Items) == 0 || len(res.Items) < DefaultRomPageSize {
|
||||
break
|
||||
}
|
||||
|
||||
offset += len(res.Items)
|
||||
|
||||
// Safety limit to prevent infinite loops
|
||||
if requestCount > 1000 {
|
||||
logger.Warn("Hit request limit while fetching games",
|
||||
"platform", platform.Name,
|
||||
"accumulated", len(allGames))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Fetched all games for platform",
|
||||
logger.Info("Refreshed platform games",
|
||||
"platform", platform.Name,
|
||||
"expected", expectedTotal,
|
||||
"actual", len(allGames))
|
||||
"count", len(allGames))
|
||||
|
||||
// Save to cache
|
||||
if err := cm.SavePlatformGames(platform.ID, allGames); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Ensure progress is at 100% when done
|
||||
if progress != nil {
|
||||
progress.Store(1.0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshCollectionGames fetches and updates games for a single collection
|
||||
func (cm *CacheManager) RefreshCollectionGames(collection romm.Collection) error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
|
||||
games, err := cm.fetchCollectionGames(collection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return cm.SaveCollectionGames(collection, games)
|
||||
}
|
||||
|
||||
Vendored
+37
-100
@@ -2,20 +2,12 @@ package cache
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"grout/internal/stringutil"
|
||||
|
||||
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
|
||||
)
|
||||
|
||||
// stripExtension removes the file extension from a filename
|
||||
func stripExtension(filename string) string {
|
||||
return strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||
}
|
||||
|
||||
// GetRomIDByFilename looks up a ROM ID by platform and filename
|
||||
// Returns (romID, romName, found)
|
||||
func (cm *CacheManager) GetRomIDByFilename(fsSlug, filename string) (int, string, bool) {
|
||||
func (cm *Manager) GetRomIDByFilename(fsSlug, filename string) (int, string, bool) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return 0, "", false
|
||||
}
|
||||
@@ -23,13 +15,13 @@ func (cm *CacheManager) GetRomIDByFilename(fsSlug, filename string) (int, string
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
key := stripExtension(filename)
|
||||
key := stringutil.StripExtension(filename)
|
||||
|
||||
var romID int
|
||||
var romName string
|
||||
err := cm.db.QueryRow(`
|
||||
SELECT rom_id, rom_name FROM rom_id_cache
|
||||
WHERE platform_fs_slug = ? AND filename_key = ?
|
||||
SELECT id, name FROM games
|
||||
WHERE platform_fs_slug = ? AND fs_name_no_ext = ?
|
||||
`, fsSlug, key).Scan(&romID, &romName)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
@@ -38,6 +30,7 @@ func (cm *CacheManager) GetRomIDByFilename(fsSlug, filename string) (int, string
|
||||
}
|
||||
if err != nil {
|
||||
cm.stats.recordError()
|
||||
gaba.GetLogger().Debug("ROM lookup error", "fsSlug", fsSlug, "filename", filename, "error", err)
|
||||
return 0, "", false
|
||||
}
|
||||
|
||||
@@ -45,68 +38,45 @@ func (cm *CacheManager) GetRomIDByFilename(fsSlug, filename string) (int, string
|
||||
return romID, romName, true
|
||||
}
|
||||
|
||||
// StoreRomID stores a ROM ID mapping for a platform and filename
|
||||
func (cm *CacheManager) StoreRomID(fsSlug, filename string, romID int, romName string) error {
|
||||
func (cm *Manager) GetRomByHash(md5, sha1, crc string) (int, string, bool) {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
key := stripExtension(filename)
|
||||
|
||||
_, err := cm.db.Exec(`
|
||||
INSERT OR REPLACE INTO rom_id_cache
|
||||
(platform_fs_slug, filename_key, rom_id, rom_name, cached_at)
|
||||
VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP)
|
||||
`, fsSlug, key, romID, romName)
|
||||
|
||||
if err != nil {
|
||||
logger.Debug("Failed to store ROM ID", "fsSlug", fsSlug, "filename", filename, "error", err)
|
||||
return newCacheError("save", "rom_id", key, err)
|
||||
}
|
||||
|
||||
logger.Debug("Stored ROM ID mapping", "fsSlug", fsSlug, "filename", filename, "romID", romID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearRomIDCache clears all ROM ID mappings
|
||||
func (cm *CacheManager) ClearRomIDCache() error {
|
||||
if cm == nil || !cm.initialized {
|
||||
return ErrNotInitialized
|
||||
}
|
||||
|
||||
cm.mu.Lock()
|
||||
defer cm.mu.Unlock()
|
||||
|
||||
_, err := cm.db.Exec(`DELETE FROM rom_id_cache`)
|
||||
if err != nil {
|
||||
return newCacheError("clear", "rom_id", "", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetRomIDCacheCount returns the number of cached ROM ID mappings
|
||||
func (cm *CacheManager) GetRomIDCacheCount() int {
|
||||
if cm == nil || !cm.initialized {
|
||||
return 0
|
||||
return 0, "", false
|
||||
}
|
||||
|
||||
cm.mu.RLock()
|
||||
defer cm.mu.RUnlock()
|
||||
|
||||
var count int
|
||||
cm.db.QueryRow(`SELECT COUNT(*) FROM rom_id_cache`).Scan(&count)
|
||||
return count
|
||||
var romID int
|
||||
var romName string
|
||||
|
||||
if md5 != "" {
|
||||
err := cm.db.QueryRow(`SELECT id, name FROM games WHERE md5_hash = ?`, md5).Scan(&romID, &romName)
|
||||
if err == nil {
|
||||
cm.stats.recordHit()
|
||||
return romID, romName, true
|
||||
}
|
||||
}
|
||||
|
||||
if sha1 != "" {
|
||||
err := cm.db.QueryRow(`SELECT id, name FROM games WHERE sha1_hash = ?`, sha1).Scan(&romID, &romName)
|
||||
if err == nil {
|
||||
cm.stats.recordHit()
|
||||
return romID, romName, true
|
||||
}
|
||||
}
|
||||
|
||||
if crc != "" {
|
||||
err := cm.db.QueryRow(`SELECT id, name FROM games WHERE crc_hash = ?`, crc).Scan(&romID, &romName)
|
||||
if err == nil {
|
||||
cm.stats.recordHit()
|
||||
return romID, romName, true
|
||||
}
|
||||
}
|
||||
|
||||
cm.stats.recordMiss()
|
||||
return 0, "", false
|
||||
}
|
||||
|
||||
// Compatibility functions for existing code
|
||||
|
||||
// GetCachedRomIDByFilename looks up a ROM ID (compatibility wrapper)
|
||||
func GetCachedRomIDByFilename(fsSlug, filename string) (int, string, bool) {
|
||||
cm := GetCacheManager()
|
||||
if cm == nil {
|
||||
@@ -114,36 +84,3 @@ func GetCachedRomIDByFilename(fsSlug, filename string) (int, string, bool) {
|
||||
}
|
||||
return cm.GetRomIDByFilename(fsSlug, filename)
|
||||
}
|
||||
|
||||
// StoreRomID stores a ROM ID mapping (compatibility wrapper)
|
||||
func StoreRomID(fsSlug, filename string, romID int, romName string) {
|
||||
cm := GetCacheManager()
|
||||
if cm == nil {
|
||||
return
|
||||
}
|
||||
_ = cm.StoreRomID(fsSlug, filename, romID, romName)
|
||||
}
|
||||
|
||||
// ClearRomCache clears the ROM ID cache (compatibility wrapper)
|
||||
func ClearRomCache() error {
|
||||
cm := GetCacheManager()
|
||||
if cm == nil {
|
||||
return nil
|
||||
}
|
||||
return cm.ClearRomIDCache()
|
||||
}
|
||||
|
||||
// HasRomCache checks if ROM cache has data (compatibility wrapper)
|
||||
func HasRomCache() bool {
|
||||
cm := GetCacheManager()
|
||||
if cm == nil {
|
||||
return false
|
||||
}
|
||||
return cm.GetRomIDCacheCount() > 0
|
||||
}
|
||||
|
||||
// GetRomCacheDir returns a placeholder path (compatibility wrapper)
|
||||
// Note: ROM cache is now in SQLite, this is kept for compatibility
|
||||
func GetRomCacheDir() string {
|
||||
return GetArtworkCacheDir()
|
||||
}
|
||||
|
||||
Vendored
+25
-53
@@ -6,16 +6,13 @@ import (
|
||||
|
||||
const schemaVersion = 1
|
||||
|
||||
// createTables creates all required database tables
|
||||
func createTables(db *sql.DB) error {
|
||||
// Create tables in a transaction
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Cache metadata table - stores schema version and global state
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS cache_metadata (
|
||||
key TEXT PRIMARY KEY,
|
||||
@@ -27,13 +24,13 @@ func createTables(db *sql.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Platforms table - cached platform data
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS platforms (
|
||||
id INTEGER PRIMARY KEY,
|
||||
slug TEXT NOT NULL,
|
||||
fs_slug TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
api_name TEXT DEFAULT '',
|
||||
custom_name TEXT DEFAULT '',
|
||||
rom_count INTEGER DEFAULT 0,
|
||||
has_bios INTEGER DEFAULT 0,
|
||||
@@ -51,7 +48,6 @@ func createTables(db *sql.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Collections table - cached collection data
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS collections (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -76,8 +72,6 @@ func createTables(db *sql.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Games table - cached ROM/game data
|
||||
// Stores full JSON to preserve all fields from romm.Rom
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS games (
|
||||
id INTEGER PRIMARY KEY,
|
||||
@@ -85,6 +79,10 @@ func createTables(db *sql.DB) error {
|
||||
platform_fs_slug TEXT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
fs_name TEXT DEFAULT '',
|
||||
fs_name_no_ext TEXT DEFAULT '',
|
||||
crc_hash TEXT DEFAULT '',
|
||||
md5_hash TEXT DEFAULT '',
|
||||
sha1_hash TEXT DEFAULT '',
|
||||
data_json TEXT NOT NULL,
|
||||
updated_at DATETIME,
|
||||
cached_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
@@ -104,7 +102,26 @@ func createTables(db *sql.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Game-Collection many-to-many relationship
|
||||
_, err = tx.Exec(`CREATE INDEX IF NOT EXISTS idx_games_fs_lookup ON games(platform_fs_slug, fs_name_no_ext)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`CREATE INDEX IF NOT EXISTS idx_games_md5 ON games(md5_hash) WHERE md5_hash != ''`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`CREATE INDEX IF NOT EXISTS idx_games_sha1 ON games(sha1_hash) WHERE sha1_hash != ''`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`CREATE INDEX IF NOT EXISTS idx_games_crc ON games(crc_hash) WHERE crc_hash != ''`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS game_collections (
|
||||
game_id INTEGER NOT NULL,
|
||||
@@ -116,50 +133,6 @@ func createTables(db *sql.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ROM ID cache - maps filenames to ROM IDs for save sync
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS rom_id_cache (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
platform_fs_slug TEXT NOT NULL,
|
||||
filename_key TEXT NOT NULL,
|
||||
rom_id INTEGER NOT NULL,
|
||||
rom_name TEXT NOT NULL,
|
||||
cached_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(platform_fs_slug, filename_key)
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`CREATE INDEX IF NOT EXISTS idx_rom_id_cache_lookup ON rom_id_cache(platform_fs_slug, filename_key)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Artwork metadata - tracks artwork files on disk
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS artwork_metadata (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
platform_fs_slug TEXT NOT NULL,
|
||||
rom_id INTEGER NOT NULL,
|
||||
file_path TEXT NOT NULL,
|
||||
file_size_bytes INTEGER DEFAULT 0,
|
||||
cached_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
validated_at DATETIME,
|
||||
UNIQUE(platform_fs_slug, rom_id)
|
||||
)
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = tx.Exec(`CREATE INDEX IF NOT EXISTS idx_artwork_platform_rom ON artwork_metadata(platform_fs_slug, rom_id)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// BIOS availability per platform
|
||||
_, err = tx.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS bios_availability (
|
||||
platform_id INTEGER PRIMARY KEY,
|
||||
@@ -171,7 +144,6 @@ func createTables(db *sql.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store schema version
|
||||
_, err = tx.Exec(`
|
||||
INSERT OR REPLACE INTO cache_metadata (key, value, updated_at)
|
||||
VALUES ('schema_version', ?, CURRENT_TIMESTAMP)
|
||||
|
||||
@@ -15,7 +15,6 @@ const (
|
||||
ExitCodeCollectionsSettings gaba.ExitCode = 107
|
||||
ExitCodeAdvancedSettings gaba.ExitCode = 108
|
||||
ExitCodeBackToAdvanced gaba.ExitCode = 109
|
||||
ExitCodeClearCache gaba.ExitCode = 110
|
||||
ExitCodeRefreshCache gaba.ExitCode = 111
|
||||
ExitCodeSaveSyncSettings gaba.ExitCode = 112
|
||||
ExitCodeGameOptions gaba.ExitCode = 113
|
||||
|
||||
+6
-7
@@ -280,6 +280,9 @@ See [General Settings](#general-settings) below.
|
||||
**Collections** - Opens a sub-menu for configuring collection display options.
|
||||
See [Collections Settings](#collections-settings) below.
|
||||
|
||||
**Directory Mappings** – Change which device directories are mapped to which RomM platforms. This takes you back to
|
||||
the platform mapping screen that appeared during setup.
|
||||
|
||||
**Save Sync** - Controls save synchronization behavior:
|
||||
|
||||
- **Off** – Save sync is completely disabled
|
||||
@@ -350,15 +353,11 @@ This sub-menu contains all collection-related configuration:
|
||||
|
||||
This sub-menu contains advanced configuration and system settings:
|
||||
|
||||
**Directory Mappings** – Change which device directories are mapped to which RomM platforms. This takes you back to
|
||||
the platform mapping screen that appeared during setup.
|
||||
|
||||
**Cache Artwork** - Pre-cache artwork for all games across all mapped platforms. Grout scans your platforms, identifies
|
||||
**Preload Artwork** - Pre-cache artwork for all games across all mapped platforms. Grout scans your platforms, identifies
|
||||
games without cached artwork, and downloads cover art from RomM. Useful for pre-caching after adding new games.
|
||||
|
||||
**Clear Cache** - Clears cached artwork and/or games cache from your device. This will free up storage space but
|
||||
artwork will need to be re-downloaded as you browse. You'll be prompted to select which caches to clear. Only visible
|
||||
when there is cached data to clear.
|
||||
**Refresh Cache** - Re-sync cached data from RomM. Select which caches to refresh: Games Cache (platform and ROM data)
|
||||
or Collections Cache. Shows when each cache was last refreshed.
|
||||
|
||||
**Download Timeout** – How long Grout waits for a single ROM to download before giving up. Useful for large files or
|
||||
slow connections. Options range from 15 to 120 minutes.
|
||||
|
||||
@@ -144,21 +144,13 @@ other = "Auswählen"
|
||||
hash = "sha1-c7f73bb54d928922c3838bb789ee9fb8a5b1eb37"
|
||||
other = "Einstellungen"
|
||||
|
||||
[cache_artwork]
|
||||
hash = "sha1-1cb834b429143507648837792190c647a34f64ab"
|
||||
other = "Artwork-Cache"
|
||||
|
||||
[cache_artwork_metadata]
|
||||
other = "Artwork-Metadaten"
|
||||
[cache_collections]
|
||||
other = "Sammlungs-Cache"
|
||||
|
||||
[cache_games]
|
||||
hash = "sha1-1e7dbd606b8bad53986d06fd58c9d73dcad67d64"
|
||||
other = "Spiele-Cache"
|
||||
|
||||
[clear_cache_title]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "Cache leeren"
|
||||
|
||||
[collection_platform_no_mapped]
|
||||
hash = "sha1-423fa43d1088dbfa790bd41466ee1193947902cd"
|
||||
other = "Keine Plattformen mit zugeordneten Spielen in\n{{.Name}}"
|
||||
@@ -551,10 +543,6 @@ other = "API-Zeitüberschreitung"
|
||||
hash = "sha1-0eb3d5ab393c2db23feb4e143d5c1987ff52685a"
|
||||
other = "Box-Art"
|
||||
|
||||
[settings_clear_cache]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "Cache leeren"
|
||||
|
||||
[settings_collection_view]
|
||||
hash = "sha1-b5c401da5ed3ead09ad836091786ea351f1a4218"
|
||||
other = "Sammlungsansicht"
|
||||
|
||||
@@ -35,10 +35,8 @@ button_save_sync = "Sync"
|
||||
button_search = "Search"
|
||||
button_select = "Select"
|
||||
button_settings = "Settings"
|
||||
cache_artwork = "Artwork Cache"
|
||||
cache_artwork_metadata = "Artwork Metadata"
|
||||
cache_collections = "Collections Cache"
|
||||
cache_games = "Games Cache"
|
||||
clear_cache_title = "Clear Cache"
|
||||
collection_platform_no_mapped = "No platforms with mapped games in\n{{.Name}}"
|
||||
collection_platform_title = "{{.Name}} - Platforms"
|
||||
collection_view_platform = "Platform"
|
||||
@@ -138,7 +136,6 @@ save_sync_uploaded = "Uploaded"
|
||||
settings_advanced = "Advanced"
|
||||
settings_api_timeout = "API Timeout"
|
||||
settings_box_art = "Box Art"
|
||||
settings_clear_cache = "Clear Cache"
|
||||
settings_collection_view = "Collection View"
|
||||
settings_collections = "Collections"
|
||||
settings_download_art = "Download Art"
|
||||
|
||||
@@ -145,22 +145,13 @@ other = "Seleccionar"
|
||||
hash = "sha1-c7f73bb54d928922c3838bb789ee9fb8a5b1eb37"
|
||||
other = "Configuración"
|
||||
|
||||
[cache_artwork]
|
||||
hash = "sha1-1cb834b429143507648837792190c647a34f64ab"
|
||||
other = "Caché de Artwork"
|
||||
[cache_collections]
|
||||
other = "Caché de Colecciones"
|
||||
|
||||
[cache_games]
|
||||
hash = "sha1-1e7dbd606b8bad53986d06fd58c9d73dcad67d64"
|
||||
other = "Caché de Juegos"
|
||||
|
||||
[cache_artwork_metadata]
|
||||
hash = "sha1-447a12c1a94ffbcefafc528ff6de40d16b247a7d"
|
||||
other = "Metadatos de Artwork"
|
||||
|
||||
[clear_cache_title]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "Borrar Caché"
|
||||
|
||||
[collection_platform_no_mapped]
|
||||
hash = "sha1-423fa43d1088dbfa790bd41466ee1193947902cd"
|
||||
other = "No hay plataformas con juegos mapeados en\n{{.Name}}"
|
||||
@@ -553,10 +544,6 @@ other = "Tiempo de Espera de API"
|
||||
hash = "sha1-0eb3d5ab393c2db23feb4e143d5c1987ff52685a"
|
||||
other = "Arte de Caja"
|
||||
|
||||
[settings_clear_cache]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "Borrar Caché"
|
||||
|
||||
[settings_collection_view]
|
||||
hash = "sha1-b5c401da5ed3ead09ad836091786ea351f1a4218"
|
||||
other = "Vista de Colección"
|
||||
|
||||
@@ -145,22 +145,13 @@ other = "Sélectionner"
|
||||
hash = "sha1-c7f73bb54d928922c3838bb789ee9fb8a5b1eb37"
|
||||
other = "Paramètres"
|
||||
|
||||
[cache_artwork]
|
||||
hash = "sha1-1cb834b429143507648837792190c647a34f64ab"
|
||||
other = "Cache des Artworks"
|
||||
[cache_collections]
|
||||
other = "Cache des Collections"
|
||||
|
||||
[cache_games]
|
||||
hash = "sha1-1e7dbd606b8bad53986d06fd58c9d73dcad67d64"
|
||||
other = "Cache des Jeux"
|
||||
|
||||
[cache_artwork_metadata]
|
||||
hash = "sha1-447a12c1a94ffbcefafc528ff6de40d16b247a7d"
|
||||
other = "Métadonnées des illustrations"
|
||||
|
||||
[clear_cache_title]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "Vider le Cache"
|
||||
|
||||
[collection_platform_no_mapped]
|
||||
hash = "sha1-423fa43d1088dbfa790bd41466ee1193947902cd"
|
||||
other = "Aucune plateforme ne possède de jeux associés dans in\n{{.Name}}"
|
||||
@@ -553,10 +544,6 @@ other = "Délai d'attente de l'API"
|
||||
hash = "sha1-0eb3d5ab393c2db23feb4e143d5c1987ff52685a"
|
||||
other = "Illustrations"
|
||||
|
||||
[settings_clear_cache]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "Vider le Cache"
|
||||
|
||||
[settings_collection_view]
|
||||
hash = "sha1-b5c401da5ed3ead09ad836091786ea351f1a4218"
|
||||
other = "Vue de Collection"
|
||||
|
||||
@@ -145,22 +145,13 @@ other = "Seleziona"
|
||||
hash = "sha1-c7f73bb54d928922c3838bb789ee9fb8a5b1eb37"
|
||||
other = "Impostazioni"
|
||||
|
||||
[cache_artwork]
|
||||
hash = "sha1-1cb834b429143507648837792190c647a34f64ab"
|
||||
other = "Cache Artwork"
|
||||
[cache_collections]
|
||||
other = "Cache Collezioni"
|
||||
|
||||
[cache_games]
|
||||
hash = "sha1-1e7dbd606b8bad53986d06fd58c9d73dcad67d64"
|
||||
other = "Cache Giochi"
|
||||
|
||||
[cache_artwork_metadata]
|
||||
hash = "sha1-447a12c1a94ffbcefafc528ff6de40d16b247a7d"
|
||||
other = "Metadati Artwork"
|
||||
|
||||
[clear_cache_title]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "Svuota Cache"
|
||||
|
||||
[collection_platform_no_mapped]
|
||||
hash = "sha1-423fa43d1088dbfa790bd41466ee1193947902cd"
|
||||
other = "Nessuna piattaforma con giochi mappati in\n{{.Name}}"
|
||||
@@ -553,10 +544,6 @@ other = "Timeout API"
|
||||
hash = "sha1-0eb3d5ab393c2db23feb4e143d5c1987ff52685a"
|
||||
other = "Copertina"
|
||||
|
||||
[settings_clear_cache]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "Svuota Cache"
|
||||
|
||||
[settings_collection_view]
|
||||
hash = "sha1-b5c401da5ed3ead09ad836091786ea351f1a4218"
|
||||
other = "Vista Collezione"
|
||||
|
||||
@@ -145,22 +145,13 @@ other = "選択"
|
||||
hash = "sha1-c7f73bb54d928922c3838bb789ee9fb8a5b1eb37"
|
||||
other = "設定"
|
||||
|
||||
[cache_artwork]
|
||||
hash = "sha1-1cb834b429143507648837792190c647a34f64ab"
|
||||
other = "アートワークキャッシュ"
|
||||
[cache_collections]
|
||||
other = "コレクションキャッシュ"
|
||||
|
||||
[cache_games]
|
||||
hash = "sha1-1e7dbd606b8bad53986d06fd58c9d73dcad67d64"
|
||||
other = "ゲームキャッシュ"
|
||||
|
||||
[cache_artwork_metadata]
|
||||
hash = "sha1-447a12c1a94ffbcefafc528ff6de40d16b247a7d"
|
||||
other = "アートワークメタデータ"
|
||||
|
||||
[clear_cache_title]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "キャッシュをクリア"
|
||||
|
||||
[collection_platform_no_mapped]
|
||||
hash = "sha1-423fa43d1088dbfa790bd41466ee1193947902cd"
|
||||
other = "{{.Name}}にマッピングされたゲームのある\nプラットフォームがありません"
|
||||
@@ -553,10 +544,6 @@ other = "APIタイムアウト"
|
||||
hash = "sha1-0eb3d5ab393c2db23feb4e143d5c1987ff52685a"
|
||||
other = "ボックスアート"
|
||||
|
||||
[settings_clear_cache]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "キャッシュをクリア"
|
||||
|
||||
[settings_collection_view]
|
||||
hash = "sha1-b5c401da5ed3ead09ad836091786ea351f1a4218"
|
||||
other = "コレクション表示"
|
||||
|
||||
@@ -145,22 +145,13 @@ other = "Selecionar"
|
||||
hash = "sha1-c7f73bb54d928922c3838bb789ee9fb8a5b1eb37"
|
||||
other = "Configurações"
|
||||
|
||||
[cache_artwork]
|
||||
hash = "sha1-1cb834b429143507648837792190c647a34f64ab"
|
||||
other = "Cache de Artwork"
|
||||
[cache_collections]
|
||||
other = "Cache de Coleções"
|
||||
|
||||
[cache_games]
|
||||
hash = "sha1-1e7dbd606b8bad53986d06fd58c9d73dcad67d64"
|
||||
other = "Cache de Jogos"
|
||||
|
||||
[cache_artwork_metadata]
|
||||
hash = "sha1-447a12c1a94ffbcefafc528ff6de40d16b247a7d"
|
||||
other = "Metadados de Artwork"
|
||||
|
||||
[clear_cache_title]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "Limpar Cache"
|
||||
|
||||
[collection_platform_no_mapped]
|
||||
hash = "sha1-423fa43d1088dbfa790bd41466ee1193947902cd"
|
||||
other = "Nenhuma plataforma com jogos mapeados em\n{{.Name}}"
|
||||
@@ -553,10 +544,6 @@ other = "Timeout da API"
|
||||
hash = "sha1-0eb3d5ab393c2db23feb4e143d5c1987ff52685a"
|
||||
other = "Capa"
|
||||
|
||||
[settings_clear_cache]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "Limpar Cache"
|
||||
|
||||
[settings_collection_view]
|
||||
hash = "sha1-b5c401da5ed3ead09ad836091786ea351f1a4218"
|
||||
other = "Visualização de Coleção"
|
||||
|
||||
@@ -145,22 +145,13 @@ other = "Выбрать"
|
||||
hash = "sha1-c7f73bb54d928922c3838bb789ee9fb8a5b1eb37"
|
||||
other = "Настройки"
|
||||
|
||||
[cache_artwork]
|
||||
hash = "sha1-1cb834b429143507648837792190c647a34f64ab"
|
||||
other = "Кэш обложек"
|
||||
[cache_collections]
|
||||
other = "Кэш коллекций"
|
||||
|
||||
[cache_games]
|
||||
hash = "sha1-1e7dbd606b8bad53986d06fd58c9d73dcad67d64"
|
||||
other = "Кэш игр"
|
||||
|
||||
[cache_artwork_metadata]
|
||||
hash = "sha1-447a12c1a94ffbcefafc528ff6de40d16b247a7d"
|
||||
other = "Метаданные обложек"
|
||||
|
||||
[clear_cache_title]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "Очистить кэш"
|
||||
|
||||
[collection_platform_no_mapped]
|
||||
hash = "sha1-423fa43d1088dbfa790bd41466ee1193947902cd"
|
||||
other = "Нет платформ с сопоставленными играми в\n{{.Name}}"
|
||||
@@ -553,10 +544,6 @@ other = "Тайм-аут API"
|
||||
hash = "sha1-0eb3d5ab393c2db23feb4e143d5c1987ff52685a"
|
||||
other = "Обложка"
|
||||
|
||||
[settings_clear_cache]
|
||||
hash = "sha1-522175083f0bf50b4584d1dd8dbed60ae22535be"
|
||||
other = "Очистить кэш"
|
||||
|
||||
[settings_collection_view]
|
||||
hash = "sha1-b5c401da5ed3ead09ad836091786ea351f1a4218"
|
||||
other = "Вид коллекции"
|
||||
|
||||
+4
-1
@@ -10,6 +10,7 @@ type Platform struct {
|
||||
Slug string `json:"slug"`
|
||||
FSSlug string `json:"fs_slug"`
|
||||
Name string `json:"name"`
|
||||
ApiName string `json:"-"` // Original name from API (not serialized, set by DisambiguatePlatformNames)
|
||||
CustomName string `json:"custom_name"`
|
||||
ShortName string `json:"short_name"`
|
||||
LogoPath string `json:"logo_path"`
|
||||
@@ -48,10 +49,12 @@ func (c *Client) GetPlatform(id int) (Platform, error) {
|
||||
// DisambiguatePlatformNames sets each platform's Name field to its display name
|
||||
// (preferring CustomName if set), and appends the FSSlug when multiple platforms
|
||||
// share the same display name (e.g., "Arcade" becomes "Arcade (fbneo)")
|
||||
// The original API name is preserved in ApiName before modification.
|
||||
func DisambiguatePlatformNames(platforms []Platform) {
|
||||
// First pass: set Name to DisplayName and count occurrences
|
||||
// First pass: save original API name, set Name to DisplayName, and count occurrences
|
||||
nameCounts := make(map[string]int)
|
||||
for i := range platforms {
|
||||
platforms[i].ApiName = platforms[i].Name // Preserve original API name
|
||||
platforms[i].Name = platforms[i].DisplayName()
|
||||
nameCounts[platforms[i].Name]++
|
||||
}
|
||||
|
||||
+24
-59
@@ -5,7 +5,6 @@ import (
|
||||
"grout/cache"
|
||||
"grout/internal"
|
||||
"grout/internal/fileutil"
|
||||
"grout/internal/stringutil"
|
||||
"grout/romm"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -192,25 +191,16 @@ func (s *SaveSync) upload(host romm.Host, config *internal.Config) (string, erro
|
||||
return s.Local.Path, nil
|
||||
}
|
||||
|
||||
// lookupRomID looks up a ROM ID by filename, first checking cache then the provided ROM map
|
||||
func lookupRomID(romFile *LocalRomFile, romsByFilename map[string]romm.Rom) (int, string) {
|
||||
// lookupRomID looks up a ROM ID by filename from the cache
|
||||
func lookupRomID(romFile *LocalRomFile) (int, string) {
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
// Check cache first
|
||||
// Look up from the games cache
|
||||
if romID, romName, found := cache.GetCachedRomIDByFilename(romFile.FSSlug, romFile.FileName); found {
|
||||
logger.Debug("ROM lookup from cache", "fsSlug", romFile.FSSlug, "file", romFile.FileName, "romID", romID, "name", romName)
|
||||
return romID, romName
|
||||
}
|
||||
|
||||
// Look up in the ROM map by filename (without extension)
|
||||
key := stringutil.StripExtension(romFile.FileName)
|
||||
if rom, found := romsByFilename[key]; found {
|
||||
// Cache the result for next time
|
||||
cache.StoreRomID(romFile.FSSlug, romFile.FileName, rom.ID, rom.Name)
|
||||
logger.Debug("ROM lookup from RomM", "fsSlug", romFile.FSSlug, "file", romFile.FileName, "romID", rom.ID, "name", rom.Name)
|
||||
return rom.ID, rom.Name
|
||||
}
|
||||
|
||||
logger.Debug("No ROM found for file", "fsSlug", romFile.FSSlug, "file", romFile.FileName)
|
||||
return 0, ""
|
||||
}
|
||||
@@ -228,11 +218,21 @@ func FindSaveSyncsFromScan(host romm.Host, config *internal.Config, scanLocal Lo
|
||||
|
||||
logger.Debug("FindSaveSyncs: Scanned local ROMs", "platformCount", len(scanLocal))
|
||||
|
||||
// Get all platforms to build fsSlug -> platformID map
|
||||
platforms, err := rc.GetPlatforms()
|
||||
if err != nil {
|
||||
logger.Error("FindSaveSyncs: Could not retrieve platforms", "error", err)
|
||||
return []SaveSync{}, nil, err
|
||||
// Get platforms from cache or API to build fsSlug -> platformID map
|
||||
cm := cache.GetCacheManager()
|
||||
var platforms []romm.Platform
|
||||
var err error
|
||||
|
||||
if cm != nil {
|
||||
platforms, err = cm.GetPlatforms()
|
||||
}
|
||||
if err != nil || len(platforms) == 0 {
|
||||
// Fall back to API if cache miss
|
||||
platforms, err = rc.GetPlatforms()
|
||||
if err != nil {
|
||||
logger.Error("FindSaveSyncs: Could not retrieve platforms", "error", err)
|
||||
return []SaveSync{}, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
fsSlugToPlatformID := make(map[string]int)
|
||||
@@ -240,11 +240,10 @@ func FindSaveSyncsFromScan(host romm.Host, config *internal.Config, scanLocal Lo
|
||||
fsSlugToPlatformID[p.FSSlug] = p.ID
|
||||
}
|
||||
|
||||
// Fetch saves and ROMs per platform in parallel
|
||||
// Fetch saves per platform in parallel (saves are not cached - always fresh from API)
|
||||
type platformFetchResult struct {
|
||||
fsSlug string
|
||||
saves []romm.Save
|
||||
roms map[string]romm.Rom
|
||||
hasError bool
|
||||
}
|
||||
|
||||
@@ -264,10 +263,9 @@ func FindSaveSyncsFromScan(host romm.Host, config *internal.Config, scanLocal Lo
|
||||
|
||||
result := platformFetchResult{
|
||||
fsSlug: fsSlug,
|
||||
roms: make(map[string]romm.Rom),
|
||||
}
|
||||
|
||||
// Fetch saves for this platform
|
||||
// Fetch saves for this platform (always from API - saves need to be fresh)
|
||||
platformSaves, err := rc.GetSaves(romm.SaveQuery{PlatformID: platformID})
|
||||
if err != nil {
|
||||
logger.Warn("FindSaveSyncs: Could not retrieve saves for platform", "fsSlug", fsSlug, "error", err)
|
||||
@@ -278,31 +276,6 @@ func FindSaveSyncsFromScan(host romm.Host, config *internal.Config, scanLocal Lo
|
||||
result.saves = platformSaves
|
||||
logger.Debug("FindSaveSyncs: Retrieved saves for platform", "fsSlug", fsSlug, "count", len(platformSaves))
|
||||
|
||||
// Fetch all ROMs for this platform to build filename map
|
||||
offset := 0
|
||||
for {
|
||||
romsPage, err := rc.GetRoms(romm.GetRomsQuery{
|
||||
PlatformID: platformID,
|
||||
Offset: offset,
|
||||
Limit: 100,
|
||||
})
|
||||
if err != nil {
|
||||
logger.Warn("FindSaveSyncs: Could not retrieve ROMs for platform", "fsSlug", fsSlug, "error", err)
|
||||
break
|
||||
}
|
||||
|
||||
for _, rom := range romsPage.Items {
|
||||
key := stringutil.StripExtension(rom.FsNameNoExt)
|
||||
result.roms[key] = rom
|
||||
}
|
||||
|
||||
if len(romsPage.Items) < 100 || len(result.roms) >= romsPage.Total {
|
||||
break
|
||||
}
|
||||
offset += len(romsPage.Items)
|
||||
}
|
||||
logger.Debug("FindSaveSyncs: Built ROM filename map", "fsSlug", fsSlug, "count", len(result.roms))
|
||||
|
||||
resultChan <- result
|
||||
}(fsSlug, platformID)
|
||||
}
|
||||
@@ -312,10 +285,8 @@ func FindSaveSyncsFromScan(host romm.Host, config *internal.Config, scanLocal Lo
|
||||
close(resultChan)
|
||||
}()
|
||||
|
||||
// Collect results
|
||||
// Collect saves by ROM ID
|
||||
savesByRomID := make(map[int][]romm.Save)
|
||||
romsByFilename := make(map[string]map[string]romm.Rom)
|
||||
|
||||
for result := range resultChan {
|
||||
if result.hasError {
|
||||
continue
|
||||
@@ -324,18 +295,11 @@ func FindSaveSyncsFromScan(host romm.Host, config *internal.Config, scanLocal Lo
|
||||
for _, s := range result.saves {
|
||||
savesByRomID[s.RomID] = append(savesByRomID[s.RomID], s)
|
||||
}
|
||||
|
||||
romsByFilename[result.fsSlug] = result.roms
|
||||
}
|
||||
|
||||
// Match local ROMs to remote ROMs by filename
|
||||
// Match local ROMs to cached ROMs by filename
|
||||
var unmatched []UnmatchedSave
|
||||
for fsSlug, localRoms := range scanLocal {
|
||||
platformRoms := romsByFilename[fsSlug]
|
||||
if platformRoms == nil {
|
||||
platformRoms = make(map[string]romm.Rom)
|
||||
}
|
||||
|
||||
for idx := range localRoms {
|
||||
romFile := &scanLocal[fsSlug][idx]
|
||||
|
||||
@@ -344,7 +308,8 @@ func FindSaveSyncsFromScan(host romm.Host, config *internal.Config, scanLocal Lo
|
||||
continue
|
||||
}
|
||||
|
||||
romID, romName := lookupRomID(romFile, platformRoms)
|
||||
// Look up ROM ID from the games cache
|
||||
romID, romName := lookupRomID(romFile)
|
||||
|
||||
if romID == 0 {
|
||||
if romFile.SaveFile != nil {
|
||||
|
||||
@@ -2,8 +2,6 @@ package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"grout/artwork"
|
||||
"grout/cache"
|
||||
"grout/constants"
|
||||
"grout/internal"
|
||||
"grout/romm"
|
||||
@@ -22,8 +20,6 @@ type AdvancedSettingsInput struct {
|
||||
}
|
||||
|
||||
type AdvancedSettingsOutput struct {
|
||||
EditMappingsClicked bool
|
||||
ClearCacheClicked bool
|
||||
RefreshCacheClicked bool
|
||||
SyncArtworkClicked bool
|
||||
LastSelectedIndex int
|
||||
@@ -74,16 +70,6 @@ func (s *AdvancedSettingsScreen) Draw(input AdvancedSettingsInput) (ScreenResult
|
||||
if result.Action == gaba.ListActionSelected {
|
||||
selectedText := items[result.Selected].Item.Text
|
||||
|
||||
if selectedText == i18n.Localize(&goi18n.Message{ID: "settings_edit_mappings", Other: "Directory Mappings"}, nil) {
|
||||
output.EditMappingsClicked = true
|
||||
return withCode(output, constants.ExitCodeEditMappings), nil
|
||||
}
|
||||
|
||||
if selectedText == i18n.Localize(&goi18n.Message{ID: "settings_clear_cache", Other: "Clear Cache"}, nil) {
|
||||
output.ClearCacheClicked = true
|
||||
return withCode(output, constants.ExitCodeClearCache), nil
|
||||
}
|
||||
|
||||
if selectedText == i18n.Localize(&goi18n.Message{ID: "settings_refresh_cache", Other: "Refresh Cache"}, nil) {
|
||||
output.RefreshCacheClicked = true
|
||||
return withCode(output, constants.ExitCodeRefreshCache), nil
|
||||
@@ -108,21 +94,10 @@ func (s *AdvancedSettingsScreen) Draw(input AdvancedSettingsInput) (ScreenResult
|
||||
|
||||
func (s *AdvancedSettingsScreen) buildMenuItems(config *internal.Config) []gaba.ItemWithOptions {
|
||||
return []gaba.ItemWithOptions{
|
||||
{
|
||||
Item: gaba.MenuItem{Text: i18n.Localize(&goi18n.Message{ID: "settings_edit_mappings", Other: "Directory Mappings"}, nil)},
|
||||
Options: []gaba.Option{{Type: gaba.OptionTypeClickable}},
|
||||
},
|
||||
{
|
||||
Item: gaba.MenuItem{Text: i18n.Localize(&goi18n.Message{ID: "settings_sync_artwork", Other: "Preload Artwork"}, nil)},
|
||||
Options: []gaba.Option{{Type: gaba.OptionTypeClickable}},
|
||||
},
|
||||
{
|
||||
Item: gaba.MenuItem{Text: i18n.Localize(&goi18n.Message{ID: "settings_clear_cache", Other: "Clear Cache"}, nil)},
|
||||
Options: []gaba.Option{{Type: gaba.OptionTypeClickable}},
|
||||
Visible: func() bool {
|
||||
return artwork.HasCache() || cache.HasGamesCache()
|
||||
},
|
||||
},
|
||||
{
|
||||
Item: gaba.MenuItem{Text: i18n.Localize(&goi18n.Message{ID: "settings_refresh_cache", Other: "Refresh Cache"}, nil)},
|
||||
Options: []gaba.Option{{Type: gaba.OptionTypeClickable}},
|
||||
|
||||
+4
-5
@@ -2,7 +2,6 @@ package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"grout/artwork"
|
||||
"grout/cache"
|
||||
"grout/internal"
|
||||
"grout/internal/imageutil"
|
||||
@@ -106,7 +105,7 @@ func (s *ArtworkSyncScreen) draw(input ArtworkSyncInput) {
|
||||
}
|
||||
|
||||
// Get all ROMs with artwork URLs (download everything)
|
||||
withArtwork := artwork.GetWithArtwork(roms)
|
||||
withArtwork := cache.GetRomsWithArtwork(roms)
|
||||
allWithArtwork = append(allWithArtwork, withArtwork...)
|
||||
return nil, nil
|
||||
},
|
||||
@@ -128,16 +127,16 @@ func (s *ArtworkSyncScreen) draw(input ArtworkSyncInput) {
|
||||
|
||||
baseURL := input.Host.URL()
|
||||
for _, rom := range allWithArtwork {
|
||||
coverPath := artwork.GetCoverPath(rom)
|
||||
coverPath := cache.GetArtworkCoverPath(rom)
|
||||
if coverPath == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
downloadURL := strings.ReplaceAll(baseURL+coverPath, " ", "%20")
|
||||
cachePath := artwork.GetCachePath(rom.PlatformFSSlug, rom.ID)
|
||||
cachePath := cache.GetArtworkCachePath(rom.PlatformFSSlug, rom.ID)
|
||||
|
||||
// Ensure directory exists
|
||||
artwork.EnsureCacheDir(rom.PlatformFSSlug)
|
||||
cache.EnsureArtworkCacheDir(rom.PlatformFSSlug)
|
||||
|
||||
downloads = append(downloads, gaba.Download{
|
||||
URL: downloadURL,
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"grout/artwork"
|
||||
"grout/cache"
|
||||
"time"
|
||||
|
||||
gaba "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool"
|
||||
icons "github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/constants"
|
||||
"github.com/BrandonKowalski/gabagool/v2/pkg/gabagool/i18n"
|
||||
goi18n "github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
)
|
||||
|
||||
type ClearCacheOutput struct {
|
||||
ClearedCount int
|
||||
}
|
||||
|
||||
type ClearCacheScreen struct{}
|
||||
|
||||
func NewClearCacheScreen() *ClearCacheScreen {
|
||||
return &ClearCacheScreen{}
|
||||
}
|
||||
|
||||
type cacheItem struct {
|
||||
name string
|
||||
hasCache bool
|
||||
clear func() error
|
||||
}
|
||||
|
||||
func (s *ClearCacheScreen) Draw() (ScreenResult[ClearCacheOutput], error) {
|
||||
output := ClearCacheOutput{}
|
||||
|
||||
caches := []cacheItem{
|
||||
{
|
||||
name: i18n.Localize(&goi18n.Message{ID: "cache_artwork", Other: "Artwork Cache"}, nil),
|
||||
hasCache: artwork.HasCache(),
|
||||
clear: artwork.ClearCache,
|
||||
},
|
||||
{
|
||||
name: i18n.Localize(&goi18n.Message{ID: "cache_games", Other: "Games Cache"}, nil),
|
||||
hasCache: cache.HasGamesCache(),
|
||||
clear: cache.ClearGamesCache,
|
||||
},
|
||||
}
|
||||
|
||||
// Build menu items for caches that exist
|
||||
items := make([]gaba.MenuItem, 0)
|
||||
availableCaches := make([]cacheItem, 0)
|
||||
for _, cache := range caches {
|
||||
if cache.hasCache {
|
||||
items = append(items, gaba.MenuItem{Text: cache.name})
|
||||
availableCaches = append(availableCaches, cache)
|
||||
}
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
// No caches to clear
|
||||
return back(output), nil
|
||||
}
|
||||
|
||||
options := gaba.DefaultListOptions(
|
||||
i18n.Localize(&goi18n.Message{ID: "clear_cache_title", Other: "Clear Cache"}, nil),
|
||||
items,
|
||||
)
|
||||
options.FooterHelpItems = []gaba.FooterHelpItem{
|
||||
FooterCancel(),
|
||||
{ButtonName: icons.Start, HelpText: i18n.Localize(&goi18n.Message{ID: "button_confirm", Other: "Confirm"}, nil), IsConfirmButton: true},
|
||||
}
|
||||
options.StartInMultiSelectMode = true
|
||||
options.StatusBar = StatusBar()
|
||||
options.SmallTitle = true
|
||||
|
||||
result, err := gaba.List(options)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gaba.ErrCancelled) {
|
||||
return back(output), nil
|
||||
}
|
||||
return withCode(output, gaba.ExitCodeError), err
|
||||
}
|
||||
|
||||
for _, idx := range result.Selected {
|
||||
if idx >= 0 && idx < len(availableCaches) {
|
||||
cache := availableCaches[idx]
|
||||
if err := cache.clear(); err != nil {
|
||||
gaba.GetLogger().Error("Failed to clear cache", "cache", cache.name, "error", err)
|
||||
} else {
|
||||
output.ClearedCount++
|
||||
gaba.GetLogger().Info("Cleared cache", "cache", cache.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if output.ClearedCount > 0 {
|
||||
gaba.ProcessMessage(
|
||||
i18n.Localize(&goi18n.Message{ID: "cache_cleared", Other: "Cache cleared!"}, nil),
|
||||
gaba.ProcessMessageOptions{},
|
||||
func() (interface{}, error) {
|
||||
time.Sleep(time.Second * 1)
|
||||
return nil, nil
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
return success(output), nil
|
||||
}
|
||||
@@ -3,7 +3,6 @@ package ui
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"grout/cache"
|
||||
"grout/cfw"
|
||||
"grout/cfw/muos"
|
||||
"grout/constants"
|
||||
@@ -267,18 +266,6 @@ func (s *DownloadScreen) draw(input downloadInput) (ScreenResult[downloadOutput]
|
||||
return d.DisplayName == g.Name
|
||||
}) {
|
||||
downloadedGames = append(downloadedGames, g)
|
||||
|
||||
// Cache ROM filename for future save sync lookups
|
||||
var cacheFilename string
|
||||
if g.HasMultipleFiles {
|
||||
cacheFilename = g.FsNameNoExt
|
||||
} else if len(g.Files) > 0 {
|
||||
cacheFilename = g.Files[0].FileName
|
||||
}
|
||||
if cacheFilename != "" {
|
||||
cache.StoreRomID(g.PlatformFSSlug, cacheFilename, g.ID, g.Name)
|
||||
logger.Debug("Cached ROM filename", "name", g.Name, "filename", cacheFilename, "id", g.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-6
@@ -3,7 +3,7 @@ package ui
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"grout/artwork"
|
||||
"grout/cache"
|
||||
"grout/internal"
|
||||
"grout/internal/imageutil"
|
||||
"grout/internal/stringutil"
|
||||
@@ -205,8 +205,8 @@ func (s *GameDetailsScreen) getCoverImagePath(host romm.Host, game romm.Rom) str
|
||||
logger := gaba.GetLogger()
|
||||
|
||||
// First, check if artwork is in the cache
|
||||
if artwork.Exists(game.PlatformFSSlug, game.ID) {
|
||||
cachePath := artwork.GetCachePath(game.PlatformFSSlug, game.ID)
|
||||
if cache.ArtworkExists(game.PlatformFSSlug, game.ID) {
|
||||
cachePath := cache.GetArtworkCachePath(game.PlatformFSSlug, game.ID)
|
||||
logger.Debug("Using cached artwork for game details", "game", game.Name)
|
||||
return cachePath
|
||||
}
|
||||
@@ -228,11 +228,10 @@ func (s *GameDetailsScreen) getCoverImagePath(host romm.Host, game romm.Rom) str
|
||||
|
||||
// Cache the artwork for future use and return cache path
|
||||
if imageData != nil {
|
||||
if err := artwork.EnsureCacheDir(game.PlatformFSSlug); err == nil {
|
||||
cachePath := artwork.GetCachePath(game.PlatformFSSlug, game.ID)
|
||||
if err := cache.EnsureArtworkCacheDir(game.PlatformFSSlug); err == nil {
|
||||
cachePath := cache.GetArtworkCachePath(game.PlatformFSSlug, game.ID)
|
||||
if err := os.WriteFile(cachePath, imageData, 0644); err == nil {
|
||||
imageutil.ProcessArtImage(cachePath)
|
||||
artwork.MarkCached(game.PlatformFSSlug, game.ID)
|
||||
return cachePath
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -3,7 +3,6 @@ package ui
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"grout/artwork"
|
||||
"grout/cache"
|
||||
"grout/constants"
|
||||
"grout/internal"
|
||||
@@ -75,7 +74,7 @@ func (s *GameListScreen) Draw(input GameListInput) (ScreenResult[GameListOutput]
|
||||
hasBIOS = loaded.hasBIOS
|
||||
|
||||
if input.Config.ShowBoxArt {
|
||||
go artwork.SyncInBackground(input.Host, games)
|
||||
go cache.SyncArtworkInBackground(input.Host, games)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +172,7 @@ func (s *GameListScreen) Draw(input GameListInput) (ScreenResult[GameListOutput]
|
||||
for i, game := range displayGames {
|
||||
imageFilename := ""
|
||||
if input.Config.ShowBoxArt {
|
||||
imageFilename = artwork.GetCachePath(game.PlatformFSSlug, game.ID)
|
||||
imageFilename = cache.GetArtworkCachePath(game.PlatformFSSlug, game.ID)
|
||||
}
|
||||
menuItems[i] = gaba.MenuItem{
|
||||
Text: game.DisplayName,
|
||||
|
||||
@@ -18,7 +18,6 @@ type RefreshCacheType int
|
||||
const (
|
||||
RefreshCacheGames RefreshCacheType = iota
|
||||
RefreshCacheCollections
|
||||
RefreshCacheArtwork
|
||||
)
|
||||
|
||||
type RefreshCacheOutput struct {
|
||||
@@ -65,13 +64,6 @@ func (s *RefreshCacheScreen) Draw() (ScreenResult[RefreshCacheOutput], error) {
|
||||
metaKey: cache.MetaKeyCollectionsRefreshedAt,
|
||||
lastRefresh: refreshTimes[cache.MetaKeyCollectionsRefreshedAt],
|
||||
},
|
||||
{
|
||||
name: i18n.Localize(&goi18n.Message{ID: "cache_artwork_metadata", Other: "Artwork Metadata"}, nil),
|
||||
cacheType: RefreshCacheArtwork,
|
||||
hasCache: true, // Artwork can always be refreshed
|
||||
metaKey: cache.MetaKeyArtworkRefreshedAt,
|
||||
lastRefresh: refreshTimes[cache.MetaKeyArtworkRefreshedAt],
|
||||
},
|
||||
}
|
||||
|
||||
// Build menu items for caches that exist
|
||||
|
||||
@@ -31,6 +31,7 @@ type SettingsOutput struct {
|
||||
GeneralSettingsClicked bool
|
||||
InfoClicked bool
|
||||
CollectionsSettingsClicked bool
|
||||
DirectoryMappingsClicked bool
|
||||
AdvancedSettingsClicked bool
|
||||
SaveSyncSettingsClicked bool
|
||||
CheckUpdatesClicked bool
|
||||
@@ -49,6 +50,7 @@ type SettingType string
|
||||
const (
|
||||
SettingGeneralSettings SettingType = "general_settings"
|
||||
SettingCollectionsSettings SettingType = "collections_settings"
|
||||
SettingDirectoryMappings SettingType = "directory_mappings"
|
||||
SettingSaveSync SettingType = "save_sync"
|
||||
SettingSaveSyncSettings SettingType = "save_sync_settings"
|
||||
SettingAdvancedSettings SettingType = "advanced_settings"
|
||||
@@ -59,6 +61,7 @@ const (
|
||||
var settingsOrder = []SettingType{
|
||||
SettingGeneralSettings,
|
||||
SettingCollectionsSettings,
|
||||
SettingDirectoryMappings,
|
||||
SettingSaveSync,
|
||||
SettingSaveSyncSettings,
|
||||
SettingAdvancedSettings,
|
||||
@@ -119,6 +122,11 @@ func (s *SettingsScreen) Draw(input SettingsInput) (ScreenResult[SettingsOutput]
|
||||
return withCode(output, constants.ExitCodeCollectionsSettings), nil
|
||||
}
|
||||
|
||||
if selectedText == i18n.Localize(&goi18n.Message{ID: "settings_edit_mappings", Other: "Directory Mappings"}, nil) {
|
||||
output.DirectoryMappingsClicked = true
|
||||
return withCode(output, constants.ExitCodeEditMappings), nil
|
||||
}
|
||||
|
||||
if selectedText == i18n.Localize(&goi18n.Message{ID: "settings_advanced", Other: "Advanced"}, nil) {
|
||||
output.AdvancedSettingsClicked = true
|
||||
return withCode(output, constants.ExitCodeAdvancedSettings), nil
|
||||
@@ -161,6 +169,12 @@ func (s *SettingsScreen) buildMenuItem(settingType SettingType, config *internal
|
||||
Options: []gaba.Option{{Type: gaba.OptionTypeClickable}},
|
||||
}
|
||||
|
||||
case SettingDirectoryMappings:
|
||||
return gaba.ItemWithOptions{
|
||||
Item: gaba.MenuItem{Text: i18n.Localize(&goi18n.Message{ID: "settings_edit_mappings", Other: "Directory Mappings"}, nil)},
|
||||
Options: []gaba.Option{{Type: gaba.OptionTypeClickable}},
|
||||
}
|
||||
|
||||
case SettingSaveSync:
|
||||
return gaba.ItemWithOptions{
|
||||
Item: gaba.MenuItem{Text: i18n.Localize(&goi18n.Message{ID: "settings_save_sync", Other: "Save Sync"}, nil)},
|
||||
|
||||
Reference in New Issue
Block a user