Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/upload/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ func (uc *UpCmd) manageAssetTags(ctx context.Context, a *assets.Asset) {
tags[i] = a.Tags[i].Name
}
for _, t := range a.Tags {
if uc.tagsCache.AddIDToCollection(t.Name, t, a.ID) {
if uc.tagsCache.AddIDToCollection(t.Value, t, a.ID) {
// Record tag event
uc.app.FileProcessor().Logger().Record(ctx, fileevent.ProcessedTagged, a.File, "tag", t.Value)
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
50 changes: 50 additions & 0 deletions internal/e2e/client/fromFolder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,54 @@ func Test_FromFolder(t *testing.T) {
fileevent.ProcessedTagged: 0,
}, false, a.FileProcessor())
})
t.Run("folder-as-tags", func(t *testing.T) {
adm, err := getUser("admin@immich.app")
if err != nil {
t.Fatalf("can't get admin user: %v", err)
}
// A fresh user for a new test
u1, err := createUser("minimal")
if err != nil {
t.Fatalf("can't create user: %v", err)
}

ctx := t.Context()
c, a := root.RootImmichGoCommand(ctx)
c.SetArgs([]string{
// "--concurrent-tasks=0", // for debugging
"upload", "from-folder",
"--server=" + ImmichURL,
"--api-key=" + u1.APIKey,
"--admin-api-key=" + adm.APIKey,
"--folder-as-tags=true",
"--pause-immich-jobs=false", // has to scan sidecars to get tags immediately
"--no-ui",
"--api-trace",
"--log-level=debug",
"DATA/fromFolder/folder-as-tags-test",
})
err = c.ExecuteContext(ctx)
if err != nil && a.Log().GetSLog() != nil {
a.Log().Error(err.Error())
}

if err != nil {
t.Error("Unexpected error", err)
return
}

e2eutils.CheckResults(t, map[fileevent.Code]int64{
fileevent.ProcessedUploadSuccess: 4,
fileevent.ProcessedAlbumAdded: 0,
fileevent.ProcessedTagged: 4,
}, false, a.FileProcessor())

// Map filenames to expected tags (derived from folder structure)
e2eutils.VerifyTagList(t, u1.Email, u1.Password, map[string][]string{
"telescopes_01.jpg": {"folder-as-tags-test/one/same"},
"telescopes_02.jpg": {"folder-as-tags-test/one/unique1"},
"telescopes_03.jpg": {"folder-as-tags-test/2/same"},
"telescopes_04.jpg": {"folder-as-tags-test/2/unique2"},
})
})
}
35 changes: 35 additions & 0 deletions internal/e2e/e2eUtils/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,38 @@ func CheckResults(t *testing.T, expectedResults map[fileevent.Code]int64, forced
}
return r
}

// VerifyTagList checks that all assets for the given user have the expected tags
func VerifyTagList(t *testing.T, email, password string, expectedTags map[string][]string) {
assets, err := GetAllAssets(email, password)
if err != nil {
t.Fatalf("failed to get assets: %v", err)
}

for filename, expectedTagList := range expectedTags {
asset, exists := assets[filename]
if !exists {
t.Errorf("expected asset not found: %s", filename)
continue
}
asset, err := GetAssetDetails(email, password, asset.ID)
if err != nil {
t.Errorf("failed to get details for asset %s: %v", filename, err)
continue
}

if len(asset.Tags) != len(expectedTagList) {
t.Errorf("asset %s: expected %d tags, got %d", filename, len(expectedTagList), len(asset.Tags))
}

TAGLIST:
for _, expectedTag := range expectedTagList {
for _, actualTag := range asset.Tags {
if actualTag.Value == expectedTag {
continue TAGLIST
}
}
t.Errorf("asset %s: expected tag not found: %s (got: %v)\n\n", filename, expectedTag, asset.Tags)
}
}
}
15 changes: 12 additions & 3 deletions internal/e2e/e2eUtils/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@ func getAPIURL() string {
}

func do(method string, url string, body any, token Token) (*http.Response, error) {
jsonBody, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("can't post %s: %w", url, err)
var jsonBody []byte
// Don't marshal nil into JSON "null" which some endpoints do not accept
if body != nil {
var err error
jsonBody, err = json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("can't post %s: %w", url, err)
}
}
req, err := http.NewRequest(method, url, bytes.NewReader(jsonBody))
if err != nil {
Expand All @@ -51,6 +56,10 @@ func do(method string, url string, body any, token Token) (*http.Response, error
return resp, nil
}

func get(url string, token Token) (*http.Response, error) {
return do(http.MethodGet, url, nil, token)
}

func post(url string, body any, token Token) (*http.Response, error) {
return do(http.MethodPost, url, body, token)
}
Expand Down
42 changes: 42 additions & 0 deletions internal/e2e/e2eUtils/getAllTags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package e2eutils

import (
"encoding/json"
"fmt"
)

// TagSimplified represents a tag returned from the server
type TagSimplified struct {
ID string `json:"id"`
Name string `json:"name"`
Value string `json:"value"`
}

// GetAllTags retrieves all tags for a user
// Returns a slice of tag values
func GetAllTags(email, password string) ([]string, error) {
// Login to get access token
token, err := UserLogin(email, password)
if err != nil {
return nil, fmt.Errorf("failed to login: %w", err)
}

resp, err := get(getAPIURL()+"/tags", token)
if err != nil {
return nil, fmt.Errorf("failed to get tags: %w", err)
}
defer resp.Body.Close()

var tags []TagSimplified
err = json.NewDecoder(resp.Body).Decode(&tags)
if err != nil {
return nil, fmt.Errorf("failed to decode tags response: %w", err)
}

tagValues := make([]string, len(tags))
for i, tag := range tags {
tagValues[i] = tag.Value
}

return tagValues, nil
}
73 changes: 52 additions & 21 deletions internal/e2e/e2eUtils/getAssets.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,37 @@ import (
"fmt"
)

type Tag struct {
ID string `json:"id"`
Name string `json:"name"`
Value string `json:"value"`
UpdatedAt string `json:"updatedAt"`
}

// Asset represents a simplified Immich asset returned from search
// update to include albums, exifInfo, owner, people if needed or use internal/assets
type Asset struct {
ID string `json:"id"`
DeviceAssetID string `json:"deviceAssetId"`
DeviceID string `json:"deviceId"`
Type string `json:"type"`
OriginalPath string `json:"originalPath"`
OriginalFileName string `json:"originalFileName"`
Resized bool `json:"resized"`
Thumbhash string `json:"thumbhash"`
FileCreatedAt string `json:"fileCreatedAt"`
FileModifiedAt string `json:"fileModifiedAt"`
LocalDateTime string `json:"localDateTime"`
UpdatedAt string `json:"updatedAt"`
IsFavorite bool `json:"isFavorite"`
IsArchived bool `json:"isArchived"`
IsTrashed bool `json:"isTrashed"`
Duration string `json:"duration"`
Checksum string `json:"checksum"`
LivePhotoVideoID string `json:"livePhotoVideoId"`
Tags []string `json:"tags"`
Rating int `json:"rating"`
Visibility string `json:"visibility"`
ID string `json:"id"`
DeviceAssetID string `json:"deviceAssetId"`
DeviceID string `json:"deviceId"`
Type string `json:"type"`
OriginalPath string `json:"originalPath"`
OriginalFileName string `json:"originalFileName"`
Resized bool `json:"resized"`
Thumbhash string `json:"thumbhash"`
FileCreatedAt string `json:"fileCreatedAt"`
FileModifiedAt string `json:"fileModifiedAt"`
LocalDateTime string `json:"localDateTime"`
UpdatedAt string `json:"updatedAt"`
IsFavorite bool `json:"isFavorite"`
IsArchived bool `json:"isArchived"`
IsTrashed bool `json:"isTrashed"`
Duration string `json:"duration"`
Checksum string `json:"checksum"`
LivePhotoVideoID string `json:"livePhotoVideoId"`
Tags []Tag `json:"tags"`
Rating int `json:"rating"`
Visibility string `json:"visibility"`
}

// SearchMetadataRequest represents the request body for /search/metadata
Expand Down Expand Up @@ -95,3 +103,26 @@ func GetAllAssets(email, password string) (map[string]*Asset, error) {

return assetsByName, nil
}

// GetAssetDetails complete information about a specific asset, like its tags, albums
func GetAssetDetails(email, password, assetID string) (*Asset, error) {
// Login to get access token
token, err := UserLogin(email, password)
if err != nil {
return nil, fmt.Errorf("failed to login: %w", err)
}

resp, err := get(getAPIURL()+"/assets/"+assetID, token)
if err != nil {
return nil, fmt.Errorf("failed to get asset details: %w", err)
}
defer resp.Body.Close()

var asset Asset
err = json.NewDecoder(resp.Body).Decode(&asset)
if err != nil {
return nil, fmt.Errorf("failed to decode asset details response: %w", err)
}

return &asset, nil
}