Compare commits
2 Commits
34f55eccf0
...
239373d5a3
| Author | SHA1 | Date | |
|---|---|---|---|
|
239373d5a3
|
|||
|
0e47fb6983
|
@@ -1,7 +1,7 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
@@ -17,13 +17,18 @@ type args struct {
|
|||||||
DeviceID string `arg:"--device-id" default:"steam-immich" help:"Device ID of the uploads"`
|
DeviceID string `arg:"--device-id" default:"steam-immich" help:"Device ID of the uploads"`
|
||||||
Album string `arg:"--album" help:"UUID of a album to upload to"`
|
Album string `arg:"--album" help:"UUID of a album to upload to"`
|
||||||
CacheFile string `arg:"--cache" default:"$XDG_CACHE_HOME/steam-immich.json" help:"Location of the cache file"`
|
CacheFile string `arg:"--cache" default:"$XDG_CACHE_HOME/steam-immich.json" help:"Location of the cache file"`
|
||||||
Revalidate bool `args:"--revalidate" default:"false" help:"Only revalidate and fix the cache"`
|
Revalidate bool `arg:"--revalidate" help:"Only revalidate and fix the cache"`
|
||||||
|
Verbose bool `arg:"-v,--verbose" help:"Toggle verbose logging"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run() {
|
func Run() {
|
||||||
var args args
|
var args args
|
||||||
arg.MustParse(&args)
|
arg.MustParse(&args)
|
||||||
|
|
||||||
|
if args.Verbose {
|
||||||
|
slog.SetLogLoggerLevel(slog.LevelDebug)
|
||||||
|
}
|
||||||
|
|
||||||
var steamUserdataDir = ""
|
var steamUserdataDir = ""
|
||||||
|
|
||||||
if args.UserdataDir != "" {
|
if args.UserdataDir != "" {
|
||||||
@@ -33,12 +38,10 @@ func Run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if steamUserdataDir == "" {
|
if steamUserdataDir == "" {
|
||||||
log.Fatal("Can not find steam userdata dir. Please set with --steam-userdata-dir")
|
slog.Error("Can not find steam userdata dir. Please set with --steam-userdata-dir")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disable timestamp in log
|
|
||||||
log.SetFlags(0)
|
|
||||||
|
|
||||||
config := steamimmich.Config{
|
config := steamimmich.Config{
|
||||||
APIKey: args.APIKey,
|
APIKey: args.APIKey,
|
||||||
BaseURL: args.BaseURL,
|
BaseURL: args.BaseURL,
|
||||||
@@ -49,9 +52,11 @@ func Run() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if args.Revalidate {
|
if args.Revalidate {
|
||||||
steamimmich.Revalidate(config)
|
statusCode := steamimmich.Revalidate(config)
|
||||||
|
os.Exit(statusCode)
|
||||||
} else {
|
} else {
|
||||||
steamimmich.Run(config)
|
statusCode := steamimmich.Run(config)
|
||||||
|
os.Exit(statusCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func hashFiles(assets []AssetState) (map[int]string, error) {
|
func hashFiles(assets []assetState) (map[int]string, error) {
|
||||||
results := make(map[int]string)
|
results := make(map[int]string)
|
||||||
|
|
||||||
for i, p := range assets {
|
for i, p := range assets {
|
||||||
|
|||||||
@@ -41,18 +41,18 @@ type assetBulkUploadCheckResult struct {
|
|||||||
Reason string // can be "duplicate" or "unsupportedFormat" (i don't know what they mean by this)
|
Reason string // can be "duplicate" or "unsupportedFormat" (i don't know what they mean by this)
|
||||||
}
|
}
|
||||||
|
|
||||||
type APIKeyTransport struct {
|
type apiKeyTransport struct {
|
||||||
APIKey string
|
APIKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *APIKeyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (t *apiKeyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
req = req.Clone(req.Context())
|
req = req.Clone(req.Context())
|
||||||
req.Header.Add("x-api-key", t.APIKey)
|
req.Header.Add("x-api-key", t.APIKey)
|
||||||
return http.DefaultTransport.RoundTrip(req)
|
return http.DefaultTransport.RoundTrip(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newImmichHttpClient(apiKey string) http.Client {
|
func newImmichHttpClient(apiKey string) http.Client {
|
||||||
transport := APIKeyTransport{
|
transport := apiKeyTransport{
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,21 +7,21 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AssetState struct {
|
type assetState struct {
|
||||||
FilePath string
|
FilePath string
|
||||||
ImmichID string
|
ImmichID string
|
||||||
}
|
}
|
||||||
|
|
||||||
type LocalState struct {
|
type localState struct {
|
||||||
Assets []AssetState
|
Assets []assetState
|
||||||
GameIDs map[string]string
|
GameIDs map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadLocalState(filename string) (*LocalState, error) {
|
func loadLocalState(filename string) (*localState, error) {
|
||||||
file, err := os.Open(filename)
|
file, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
return &LocalState{Assets: []AssetState{}, GameIDs: map[string]string{}}, nil
|
return &localState{Assets: []assetState{}, GameIDs: map[string]string{}}, nil
|
||||||
}
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -31,7 +31,7 @@ func loadLocalState(filename string) (*LocalState, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var data LocalState
|
var data localState
|
||||||
err = json.Unmarshal(content, &data)
|
err = json.Unmarshal(content, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -40,7 +40,7 @@ func loadLocalState(filename string) (*LocalState, error) {
|
|||||||
return &data, nil
|
return &data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveLocalState(filename string, state LocalState) error {
|
func saveLocalState(filename string, state localState) error {
|
||||||
content, err := json.Marshal(state)
|
content, err := json.Marshal(state)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
const UNKOWN_GAME_GAME = "Unkown"
|
const UNKOWN_GAME_GAME = "Unkown"
|
||||||
|
|
||||||
type SteamAPIResponse struct {
|
type steamAPIResponse struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
Data struct {
|
Data struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -24,7 +24,7 @@ func getGameName(appID string, cache map[string]string) string {
|
|||||||
|
|
||||||
name, err := fetchGameName(appID)
|
name, err := fetchGameName(appID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("failed to fetch name for AppID: %s. Setting to Unknown", appID)
|
slog.Warn("Failed to fetch name for AppID. Setting to Unknown", "appid", appID)
|
||||||
cache[appID] = UNKOWN_GAME_GAME
|
cache[appID] = UNKOWN_GAME_GAME
|
||||||
return UNKOWN_GAME_GAME
|
return UNKOWN_GAME_GAME
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ func fetchGameName(appID string) (string, error) {
|
|||||||
return "", fmt.Errorf("reading response for %s: %s", appID, err)
|
return "", fmt.Errorf("reading response for %s: %s", appID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var result map[string]SteamAPIResponse
|
var result map[string]steamAPIResponse
|
||||||
if err := json.Unmarshal(body, &result); err != nil {
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
return "", fmt.Errorf("parsing response for %s: %s", appID, err)
|
return "", fmt.Errorf("parsing response for %s: %s", appID, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package steamimmich
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log/slog"
|
||||||
"slices"
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -17,24 +17,25 @@ type Config struct {
|
|||||||
CacheFile string
|
CacheFile string
|
||||||
}
|
}
|
||||||
|
|
||||||
func Run(config Config) {
|
func Run(config Config) int {
|
||||||
screenshotFiles, err := listScreenshots(config.UserdataDir)
|
screenshotFiles, err := listScreenshots(config.UserdataDir)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to scan for screenshots: %s", err)
|
slog.Error("Failed to scan for screenshots", "err", err)
|
||||||
return
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(screenshotFiles) == 0 {
|
if len(screenshotFiles) == 0 {
|
||||||
log.Println("No screenshots found.")
|
slog.Info("No screenshots found")
|
||||||
return
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
immichClient := newImmichHttpClient(config.APIKey)
|
immichClient := newImmichHttpClient(config.APIKey)
|
||||||
|
|
||||||
localState, err := loadLocalState(config.CacheFile)
|
localState, err := loadLocalState(config.CacheFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load local state: %s", err)
|
slog.Error("Failed to load local cache", "err", err)
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
for appid, files := range screenshotFiles {
|
for appid, files := range screenshotFiles {
|
||||||
@@ -43,65 +44,70 @@ func Run(config Config) {
|
|||||||
|
|
||||||
for _, filepath := range files {
|
for _, filepath := range files {
|
||||||
|
|
||||||
idx := slices.IndexFunc(localState.Assets, func(e AssetState) bool { return e.FilePath == filepath })
|
idx := slices.IndexFunc(localState.Assets, func(e assetState) bool { return e.FilePath == filepath })
|
||||||
|
|
||||||
if idx != -1 {
|
if idx != -1 {
|
||||||
log.Printf("Asset already uploaded: %s", filepath)
|
slog.Debug("Asset already uploaded", "path", filepath)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := uploadToImmich(filepath, appid, config.BaseURL, config.DeiveID, &immichClient)
|
res, err := uploadToImmich(filepath, appid, config.BaseURL, config.DeiveID, &immichClient)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to upload to immich: %s", err)
|
slog.Error("Failed to upload to immich", "err", err)
|
||||||
return
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Assest uploaded: %s (%s) %s with ID: %s", gameName, appid, filepath, res.ID)
|
slog.Info("Assets uploaded", "path", filepath, "id", res.ID)
|
||||||
|
|
||||||
localState.Assets = append(localState.Assets, AssetState{FilePath: filepath, ImmichID: res.ID})
|
localState.Assets = append(localState.Assets, assetState{FilePath: filepath, ImmichID: res.ID})
|
||||||
|
|
||||||
assetsInGame = append(assetsInGame, res.ID)
|
assetsInGame = append(assetsInGame, res.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := setAssetMetadata(assetsInGame, fmt.Sprintf("Game: %s", gameName), config.BaseURL, immichClient)
|
err := setAssetMetadata(assetsInGame, fmt.Sprintf("Game: %s", gameName), config.BaseURL, immichClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to set asset metadata: %s", err)
|
slog.Error("Failed to set assets metadata", "err", err)
|
||||||
return
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.Album != "" {
|
if config.Album != "" {
|
||||||
err = addAssetsToAlbum(assetsInGame, config.Album, config.BaseURL, immichClient)
|
err = addAssetsToAlbum(assetsInGame, config.Album, config.BaseURL, immichClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to add assets to album: %s", err)
|
slog.Error("Failed to add assets to album", "err", err)
|
||||||
return
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Added %d assets to album %s", len(assetsInGame), config.Album)
|
if len(assetsInGame) > 0 {
|
||||||
|
slog.Info("Added assets to album", "count", len(assetsInGame))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Finished uploading screenshots")
|
slog.Info("Finished uploading screenshots")
|
||||||
err = saveLocalState(config.CacheFile, *localState)
|
err = saveLocalState(config.CacheFile, *localState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to save local cache: %s", err)
|
slog.Error("Failed to save local cache", "err", err)
|
||||||
return
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func Revalidate(config Config) {
|
func Revalidate(config Config) int {
|
||||||
immichClient := newImmichHttpClient(config.APIKey)
|
immichClient := newImmichHttpClient(config.APIKey)
|
||||||
|
|
||||||
localState, err := loadLocalState(config.CacheFile)
|
localState, err := loadLocalState(config.CacheFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to load local state: %s", err)
|
slog.Error("Failed to load local cache", "err", err)
|
||||||
return
|
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
hashes, err := hashFiles(localState.Assets)
|
hashes, err := hashFiles(localState.Assets)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to generate hashes: %s", err)
|
slog.Error("Failed to generate hashes", "err", err)
|
||||||
return
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
idxToRemoveFromCache := []int{}
|
idxToRemoveFromCache := []int{}
|
||||||
@@ -125,20 +131,21 @@ func Revalidate(config Config) {
|
|||||||
|
|
||||||
results, err := bulkCheckAssets(idsToCheck, config.BaseURL, immichClient)
|
results, err := bulkCheckAssets(idsToCheck, config.BaseURL, immichClient)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Failed to bulk check hashes: %s", err)
|
slog.Error("Failed to bulk check hashes", "err", err)
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, result := range results {
|
for _, result := range results {
|
||||||
i, err := strconv.Atoi(result.Id)
|
i, err := strconv.Atoi(result.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("What ?: %s", err)
|
slog.Error("What ? This should never happen", "err", err)
|
||||||
return
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
asset := localState.Assets[i]
|
asset := localState.Assets[i]
|
||||||
|
|
||||||
if asset.ImmichID != result.AssetId {
|
if asset.ImmichID != result.AssetId {
|
||||||
log.Printf("Asset %s had the wrong Immich ID. New %s", asset.FilePath, result.AssetId)
|
slog.Info("Asset has the wrong Immich ID", "path", asset.FilePath, "id", result.AssetId)
|
||||||
localState.Assets[i].ImmichID = result.AssetId
|
localState.Assets[i].ImmichID = result.AssetId
|
||||||
updated += 1
|
updated += 1
|
||||||
}
|
}
|
||||||
@@ -159,13 +166,15 @@ func Revalidate(config Config) {
|
|||||||
|
|
||||||
saveLocalState(config.CacheFile, *localState)
|
saveLocalState(config.CacheFile, *localState)
|
||||||
|
|
||||||
log.Printf("Revalidated %d assets", len(results))
|
slog.Info("Revalidated assets", "count", len(localState.Assets))
|
||||||
log.Printf("Assets present on Immich: %d", rejected)
|
slog.Info("Assets present on immich", "count", rejected)
|
||||||
log.Printf("Wrong metadata on cache: %d", updated)
|
slog.Info("Wrong metadata on cache", "count", updated)
|
||||||
log.Printf("Missing on Immich: %d", missingFromImmich)
|
slog.Info("Missing on Immich", "count", missingFromImmich)
|
||||||
log.Printf("Missing on local storage: %d", missingFile)
|
slog.Info("Missing on local storage", "count", missingFile)
|
||||||
|
|
||||||
if (updated + missingFromImmich + missingFromImmich) > 0 {
|
if (updated + missingFromImmich + missingFromImmich) > 0 {
|
||||||
log.Printf("Fixed cache. Run the normal upload command again. If error persists try removing cache file at: %s", config.CacheFile)
|
slog.Info("Fixed cache. Run the normal upload command again. If error persists try removing cache file", "path", config.CacheFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user