initial commit
This commit is contained in:
commit
dd01f3b3b6
25
cmd/steamimmich.go
Normal file
25
cmd/steamimmich.go
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
steamimmich "git.kapelle.org/niklas/steam-immich/internal"
|
||||||
|
"github.com/alexflint/go-arg"
|
||||||
|
)
|
||||||
|
|
||||||
|
type args struct {
|
||||||
|
BaseURL string `arg:"--base-url,required,env:BASE_URL" placeholder:"https://demo.immich.app"`
|
||||||
|
APIKey string `arg:"--api-key,required,env:API_KEY" placeholder:"API_KEY"`
|
||||||
|
UserdataDir string `arg:"--steam-userdata-dir,env:USERDATA_DIR" default:"$HOME/.var/app/com.valvesoftware.Steam/.local/share/Steam/userdata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var args args
|
||||||
|
arg.MustParse(&args)
|
||||||
|
|
||||||
|
steamimmich.Run(steamimmich.Config{
|
||||||
|
APIKey: args.APIKey,
|
||||||
|
BaseURL: args.BaseURL,
|
||||||
|
UserdataDir: os.ExpandEnv(args.UserdataDir),
|
||||||
|
})
|
||||||
|
}
|
||||||
8
go.mod
Normal file
8
go.mod
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
module git.kapelle.org/niklas/steam-immich
|
||||||
|
|
||||||
|
go 1.24.6
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/alexflint/go-arg v1.6.0 // indirect
|
||||||
|
github.com/alexflint/go-scalar v1.2.0 // indirect
|
||||||
|
)
|
||||||
7
go.sum
Normal file
7
go.sum
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
github.com/alexflint/go-arg v1.6.0 h1:wPP9TwTPO54fUVQl4nZoxbFfKCcy5E6HBCumj1XVRSo=
|
||||||
|
github.com/alexflint/go-arg v1.6.0/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
|
||||||
|
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
|
||||||
|
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
113
internal/immich.go
Normal file
113
internal/immich.go
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
package steamimmich
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ImmichAPIUploadResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImmichAPIAssetPutRequest struct {
|
||||||
|
Ids []string `json:"ids"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadToImmich(filePath, appID, baseURL, apiKey, deviceID string) (*ImmichAPIUploadResponse, error) {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("open file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
fileInfo, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("stat file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create multipart form
|
||||||
|
var body bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&body)
|
||||||
|
|
||||||
|
// Attach the image file
|
||||||
|
part, err := writer.CreateFormFile("assetData", filepath.Base(filePath))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create form file: %w", err)
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(part, file); err != nil {
|
||||||
|
return nil, fmt.Errorf("copy file data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
modTime := fileInfo.ModTime().UTC().Format(time.RFC3339)
|
||||||
|
createTime := modTime // If you don't have a separate created time
|
||||||
|
|
||||||
|
// Required metadata fields
|
||||||
|
writer.WriteField("deviceAssetId", fmt.Sprintf("%s-%s", appID, filepath.Base(filePath)))
|
||||||
|
writer.WriteField("deviceId", deviceID)
|
||||||
|
writer.WriteField("fileCreatedAt", createTime)
|
||||||
|
writer.WriteField("fileModifiedAt", modTime)
|
||||||
|
writer.WriteField("isFavorite", "false")
|
||||||
|
|
||||||
|
writer.Close()
|
||||||
|
|
||||||
|
// Build request
|
||||||
|
req, err := http.NewRequest("POST", fmt.Sprintf("%s/api/assets", baseURL), &body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("create request: %w", err)
|
||||||
|
}
|
||||||
|
req.Header.Set("x-api-key", apiKey)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 60 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("POST to Immich: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
respBody, _ := io.ReadAll(resp.Body)
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
return nil, fmt.Errorf("Immich upload failed (%d): %s", resp.StatusCode, string(respBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
var result ImmichAPIUploadResponse
|
||||||
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||||
|
return nil, fmt.Errorf("Failed to parse immich response: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setAssetMetadata(assets []string, description, baseURL, apiKey string) error {
|
||||||
|
body, _ := json.Marshal(ImmichAPIAssetPutRequest{
|
||||||
|
Ids: assets,
|
||||||
|
Description: description,
|
||||||
|
})
|
||||||
|
|
||||||
|
req, err := http.NewRequest("PUT", fmt.Sprintf("%s/api/assets", baseURL), bytes.NewBuffer(body))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("x-api-key", apiKey)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
_, err = http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
52
internal/screenshots.go
Normal file
52
internal/screenshots.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package steamimmich
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func listScreenshots(steamUserdataDir string) (map[string][]string, error) {
|
||||||
|
if _, err := os.Stat(steamUserdataDir); os.IsNotExist(err) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pattern: userdata/<steamid>/760/remote/<appid>/screenshots
|
||||||
|
reAppDir := regexp.MustCompile(`.*/760/remote/([0-9]+)/screenshots$`)
|
||||||
|
|
||||||
|
screenshotFiles := make(map[string][]string)
|
||||||
|
|
||||||
|
err := filepath.WalkDir(steamUserdataDir, func(path string, d os.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if reAppDir.MatchString(path) {
|
||||||
|
appid := reAppDir.FindStringSubmatch(path)[1]
|
||||||
|
files, _ := os.ReadDir(path)
|
||||||
|
for _, f := range files {
|
||||||
|
if f.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := strings.ToLower(f.Name())
|
||||||
|
if strings.HasSuffix(name, ".jpg") ||
|
||||||
|
strings.HasSuffix(name, ".jpeg") ||
|
||||||
|
strings.HasSuffix(name, ".png") {
|
||||||
|
fullPath := filepath.Join(path, f.Name())
|
||||||
|
screenshotFiles[appid] = append(screenshotFiles[appid], fullPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return screenshotFiles, nil
|
||||||
|
}
|
||||||
55
internal/steam.go
Normal file
55
internal/steam.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package steamimmich
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
const UNKOWN_GAME_GAME = "Unkown"
|
||||||
|
|
||||||
|
type SteamAPIResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Data struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getGameName(appid string, cache map[string]string) string {
|
||||||
|
if name, ok := cache[appid]; ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://store.steampowered.com/api/appdetails?appids=%s", appid)
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error fetching app name for %s: %v\n", appid, err)
|
||||||
|
cache[appid] = UNKOWN_GAME_GAME
|
||||||
|
return cache[appid]
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading response for %s: %v\n", appid, err)
|
||||||
|
cache[appid] = UNKOWN_GAME_GAME
|
||||||
|
return cache[appid]
|
||||||
|
}
|
||||||
|
|
||||||
|
var result map[string]SteamAPIResponse
|
||||||
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
|
fmt.Printf("Error parsing JSON for %s: %v\n", appid, err)
|
||||||
|
cache[appid] = UNKOWN_GAME_GAME
|
||||||
|
return cache[appid]
|
||||||
|
}
|
||||||
|
|
||||||
|
if entry, ok := result[appid]; ok && entry.Success {
|
||||||
|
name := entry.Data.Name
|
||||||
|
cache[appid] = name
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
cache[appid] = UNKOWN_GAME_GAME
|
||||||
|
return cache[appid]
|
||||||
|
}
|
||||||
55
internal/steamimmich.go
Normal file
55
internal/steamimmich.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package steamimmich
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
BaseURL string
|
||||||
|
APIKey string
|
||||||
|
UserdataDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Run(config Config) {
|
||||||
|
screenshotFiles, err := listScreenshots(config.UserdataDir)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Failed to scan for screenshots")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(screenshotFiles) == 0 {
|
||||||
|
fmt.Println("No screenshots found.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gameNameCache := make(map[string]string)
|
||||||
|
|
||||||
|
for appid, files := range screenshotFiles {
|
||||||
|
gameName := getGameName(appid, gameNameCache)
|
||||||
|
fmt.Printf("AppID: %s → %s\n", appid, gameName)
|
||||||
|
|
||||||
|
assetsInGame := make([]string, 0)
|
||||||
|
|
||||||
|
for _, f := range files {
|
||||||
|
fmt.Printf(" %s\n", f)
|
||||||
|
|
||||||
|
res, err := uploadToImmich(f, appid, config.BaseURL, config.APIKey, "steam-screenshot-test")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to upload to immich: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Done. ID: %s", res.ID)
|
||||||
|
|
||||||
|
assetsInGame = append(assetsInGame, res.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
setAssetMetadata(assetsInGame, fmt.Sprintf("Game: %s", gameName), config.BaseURL, config.APIKey)
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user