Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

api recursion #267

Merged
merged 5 commits into from
Jan 15, 2025
Merged
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
131 changes: 81 additions & 50 deletions internal/immich/immich_album.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func (i *ImmichAsset) albums(requestID, deviceID string, shared bool) (ImmichAlb

u, err := url.Parse(requestConfig.ImmichUrl)
if err != nil {
log.Fatal(err)
return immichApiFail(albums, err, nil, "")
}

apiUrl := url.URL{
Expand Down Expand Up @@ -73,7 +73,7 @@ func (i *ImmichAsset) albumAssets(albumID, requestID, deviceID string) (ImmichAl

u, err := url.Parse(requestConfig.ImmichUrl)
if err != nil {
log.Fatal(err)
return immichApiFail(album, err, nil, "")
}

apiUrl := url.URL{
Expand Down Expand Up @@ -111,6 +111,14 @@ func countAssetsInAlbums(albums ImmichAlbums) int {
}

// AlbumImageCount retrieves the number of images in a specific album from Immich.
// Parameters:
// - albumID: ID of the album to count images from, can be a special keyword
// - requestID: ID used for tracking API call
// - deviceID: ID of the device making the request
//
// Returns:
// - int: Number of images in the album
// - error: Any error encountered during the request
func (i *ImmichAsset) AlbumImageCount(albumID string, requestID, deviceID string) (int, error) {
switch albumID {
case kiosk.AlbumKeywordAll:
Expand Down Expand Up @@ -143,74 +151,97 @@ func (i *ImmichAsset) AlbumImageCount(albumID string, requestID, deviceID string
}
}

// ImageFromAlbumRandom retrieve random image within a specified album from Immich
// ImageFromAlbum retrieves and returns an image from an album in the Immich server.
// It handles retrying failed requests, caching of album assets, and filtering of images based on type and status.
// The returned image is set into the ImmichAsset receiver.
//
// Parameters:
// - albumID: The ID of the album to get an image from
// - albumAssetsOrder: The order to return assets (Rand for random, Asc for ascending)
// - requestID: ID used to track the API request chain
// - deviceID: ID of the device making the request
// - isPrefetch: Whether this is a prefetch request for caching
//
// Returns:
// - error: Any error encountered during the image retrieval process, including when no viable images are found
// after maximum retry attempts
func (i *ImmichAsset) ImageFromAlbum(albumID string, albumAssetsOrder ImmichAssetOrder, requestID, deviceID string, isPrefetch bool) error {

album, apiUrl, err := i.albumAssets(albumID, requestID, deviceID)
if err != nil {
return err
}
for retries := 0; retries < MaxRetries; retries++ {

apiCacheKey := cache.ApiCacheKey(apiUrl, deviceID)
album, apiUrl, err := i.albumAssets(albumID, requestID, deviceID)
if err != nil {
return err
}

if len(album.Assets) == 0 {
log.Debug(requestID+" No images left in cache. Refreshing and trying again for album", albumID)
cache.Delete(apiCacheKey)
apiCacheKey := cache.ApiCacheKey(apiUrl, deviceID)

album, _, retryErr := i.albumAssets(albumID, requestID, deviceID)
if retryErr != nil || len(album.Assets) == 0 {
return fmt.Errorf("no assets found for album %s after refresh", albumID)
}
if len(album.Assets) == 0 {
log.Debug(requestID+" No images left in cache. Refreshing and trying again for album", albumID)
cache.Delete(apiCacheKey)

return i.ImageFromAlbum(albumID, albumAssetsOrder, requestID, deviceID, isPrefetch)
}
album, _, retryErr := i.albumAssets(albumID, requestID, deviceID)
if retryErr != nil || len(album.Assets) == 0 {
return fmt.Errorf("no assets found for album %s after refresh", albumID)
}

switch albumAssetsOrder {
case Rand:
rand.Shuffle(len(album.Assets), func(i, j int) {
album.Assets[i], album.Assets[j] = album.Assets[j], album.Assets[i]
})
case Asc:
if !album.AssetsOrdered {
slices.Reverse(album.Assets)
album.AssetsOrdered = true
continue
}
}

for assetIndex, asset := range album.Assets {
// We only want images and that are not trashed or archived (unless wanted by user)
if asset.Type != ImageType || asset.IsTrashed || (asset.IsArchived && !requestConfig.ShowArchived) || !i.ratioCheck(&asset) {
continue
switch albumAssetsOrder {
case Rand:
rand.Shuffle(len(album.Assets), func(i, j int) {
album.Assets[i], album.Assets[j] = album.Assets[j], album.Assets[i]
})
case Asc:
if !album.AssetsOrdered {
slices.Reverse(album.Assets)
album.AssetsOrdered = true
}
}

if requestConfig.Kiosk.Cache {
// Remove the current image from the slice
assetsToCache := album
assetsToCache.Assets = append(album.Assets[:assetIndex], album.Assets[assetIndex+1:]...)
jsonBytes, err := json.Marshal(assetsToCache)
if err != nil {
log.Error("Failed to marshal assetsToCache", "error", err)
return err
for assetIndex, asset := range album.Assets {

// We only want images and that are not trashed or archived (unless wanted by user)
isInvalidType := asset.Type != ImageType
isTrashed := asset.IsTrashed
isArchived := asset.IsArchived && !requestConfig.ShowArchived
isInvalidRatio := !i.ratioCheck(&asset)

if isInvalidType || isTrashed || isArchived || isInvalidRatio {
continue
}

// replace with cache minus used asset
err = cache.Replace(apiCacheKey, jsonBytes)
if err != nil {
log.Debug("Failed to update device cache for album", "albumID", albumID, "deviceID", deviceID)
if requestConfig.Kiosk.Cache {
// Remove the current image from the slice
assetsToCache := album
assetsToCache.Assets = append(album.Assets[:assetIndex], album.Assets[assetIndex+1:]...)
jsonBytes, err := json.Marshal(assetsToCache)
if err != nil {
log.Error("Failed to marshal assetsToCache", "error", err)
return err
}

// replace with cache minus used asset
err = cache.Replace(apiCacheKey, jsonBytes)
if err != nil {
log.Debug("Failed to update device cache for album", "albumID", albumID, "deviceID", deviceID)
}

}

}
*i = asset

*i = asset
i.KioskSourceName = album.AlbumName

i.KioskSourceName = album.AlbumName
return nil
}

return nil
log.Debug(requestID + " No viable images left in cache. Refreshing and trying again")
cache.Delete(apiCacheKey)
}

log.Debug(requestID + " No viable images left in cache. Refreshing and trying again")
cache.Delete(apiCacheKey)
return i.ImageFromAlbum(albumID, albumAssetsOrder, requestID, deviceID, isPrefetch)
return fmt.Errorf("No images found for '%s'. Max retries reached.", albumID)
}

// selectRandomAlbum selects a random album from the given list of albums, excluding specific albums.
Expand Down
177 changes: 91 additions & 86 deletions internal/immich/immich_date.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,22 @@ import (
"github.com/google/go-querystring/query"
)

// RandomImageInDateRange gets a random image from the Immich server within the specified date range.
// dateRange should be in the format "YYYY-MM-DD_to_YYYY-MM-DD"
// requestID is used for tracing the request through logs
// deviceID identifies the kiosk device making the request
// isPrefetch indicates if this is a prefetch request
// RandomImageInDateRange retrieves a random image from the Immich API within the specified date range.
// Parameters:
// - dateRange: A string in the format "YYYY-MM-DD_to_YYYY-MM-DD" or using "today" for current date
// - requestID: Unique identifier for tracking the request
// - deviceID: ID of the requesting device
// - isPrefetch: Whether this is a prefetch request
//
// The function handles:
// - Date range parsing and validation
// - Making API requests with retries
// - Caching of results
// - Filtering images based on type/status
// - Ratio checking of images
//
damongolding marked this conversation as resolved.
Show resolved Hide resolved
// Returns an error if no valid images are found after max retries
func (i *ImmichAsset) RandomImageInDateRange(dateRange, requestID, deviceID string, isPrefetch bool) error {
return i.randomImageInDateRange(dateRange, requestID, deviceID, isPrefetch, 0)
}

// randomImageInDateRange is the internal implementation of RandomImageInDateRange that includes retry logic
// It makes requests to the Immich API to get random images within the date range and handles caching
// retries tracks the number of retry attempts made
func (i *ImmichAsset) randomImageInDateRange(dateRange, requestID, deviceID string, isPrefetch bool, retries int) error {

if retries >= MaxRetries {
return fmt.Errorf("No images found for '%s'. Max retries reached.", dateRange)
}

if !strings.Contains(dateRange, "_to_") {
return fmt.Errorf("Invalid date range format. Expected 'YYYY-MM-DD_to_YYYY-MM-DD', got '%s'", dateRange)
Expand Down Expand Up @@ -73,96 +72,102 @@ func (i *ImmichAsset) randomImageInDateRange(dateRange, requestID, deviceID stri
log.Debug(requestID+" Getting Random image", "from", dateStartHuman, "to", dateEndHuman)
}

var immichAssets []ImmichAsset
for retries := 0; retries < MaxRetries; retries++ {

u, err := url.Parse(requestConfig.ImmichUrl)
if err != nil {
return fmt.Errorf("parsing url: %w", err)
}
var immichAssets []ImmichAsset

requestBody := ImmichSearchRandomBody{
Type: string(ImageType),
TakenAfter: dateStart.Format(time.RFC3339),
TakenBefore: dateEnd.Format(time.RFC3339),
WithExif: true,
WithPeople: true,
Size: requestConfig.Kiosk.FetchedAssetsSize,
}

if requestConfig.ShowArchived {
requestBody.WithArchived = true
}
u, err := url.Parse(requestConfig.ImmichUrl)
if err != nil {
return fmt.Errorf("parsing url: %w", err)
}

// convert body to queries so url is unique and can be cached
queries, _ := query.Values(requestBody)
requestBody := ImmichSearchRandomBody{
Type: string(ImageType),
TakenAfter: dateStart.Format(time.RFC3339),
TakenBefore: dateEnd.Format(time.RFC3339),
WithExif: true,
WithPeople: true,
Size: requestConfig.Kiosk.FetchedAssetsSize,
}

apiUrl := url.URL{
Scheme: u.Scheme,
Host: u.Host,
Path: "api/search/random",
RawQuery: fmt.Sprintf("kiosk=%x", sha256.Sum256([]byte(queries.Encode()))),
}
if requestConfig.ShowArchived {
requestBody.WithArchived = true
}

jsonBody, err := json.Marshal(requestBody)
if err != nil {
return fmt.Errorf("marshaling request body: %w", err)
}
// convert body to queries so url is unique and can be cached
queries, _ := query.Values(requestBody)

immichApiCall := immichApiCallDecorator(i.immichApiCall, requestID, deviceID, immichAssets)
apiBody, err := immichApiCall("POST", apiUrl.String(), jsonBody)
if err != nil {
_, _, err = immichApiFail(immichAssets, err, apiBody, apiUrl.String())
return err
}
apiUrl := url.URL{
Scheme: u.Scheme,
Host: u.Host,
Path: "api/search/random",
RawQuery: fmt.Sprintf("kiosk=%x", sha256.Sum256([]byte(queries.Encode()))),
}

err = json.Unmarshal(apiBody, &immichAssets)
if err != nil {
_, _, err = immichApiFail(immichAssets, err, apiBody, apiUrl.String())
return err
}
jsonBody, err := json.Marshal(requestBody)
if err != nil {
return fmt.Errorf("marshaling request body: %w", err)
}

apiCacheKey := cache.ApiCacheKey(apiUrl.String(), deviceID)
immichApiCall := immichApiCallDecorator(i.immichApiCall, requestID, deviceID, immichAssets)
apiBody, err := immichApiCall("POST", apiUrl.String(), jsonBody)
if err != nil {
_, _, err = immichApiFail(immichAssets, err, apiBody, apiUrl.String())
return err
}

if len(immichAssets) == 0 {
log.Debug(requestID + " No images left in cache. Refreshing and trying again")
cache.Delete(apiCacheKey)
retries++
return i.randomImageInDateRange(dateRange, requestID, deviceID, isPrefetch, retries)
}
err = json.Unmarshal(apiBody, &immichAssets)
if err != nil {
_, _, err = immichApiFail(immichAssets, err, apiBody, apiUrl.String())
return err
}

log.Info("Date range res", "items", len(immichAssets))
apiCacheKey := cache.ApiCacheKey(apiUrl.String(), deviceID)

for immichAssetIndex, img := range immichAssets {
// We only want images and that are not trashed or archived (unless wanted by user)
if img.Type != ImageType || img.IsTrashed || (img.IsArchived && !requestConfig.ShowArchived) || !i.ratioCheck(&img) {
if len(immichAssets) == 0 {
log.Debug(requestID + " No images left in cache. Refreshing and trying again")
cache.Delete(apiCacheKey)
continue
}

if requestConfig.Kiosk.Cache {
// Remove the current image from the slice
immichAssetsToCache := append(immichAssets[:immichAssetIndex], immichAssets[immichAssetIndex+1:]...)
jsonBytes, err := json.Marshal(immichAssetsToCache)
if err != nil {
log.Error("Failed to marshal immichAssetsToCache", "error", err)
return err
for immichAssetIndex, img := range immichAssets {

// We only want images and that are not trashed or archived (unless wanted by user)
isInvalidType := img.Type != ImageType
isTrashed := img.IsTrashed
isArchived := img.IsArchived && !requestConfig.ShowArchived
isInvalidRatio := !i.ratioCheck(&img)

if isInvalidType || isTrashed || isArchived || isInvalidRatio {
continue
}

// replace cache with used image(s) removed
err = cache.Replace(apiCacheKey, jsonBytes)
if err != nil {
log.Debug("Failed to update cache", "error", err, "url", apiUrl.String())
if requestConfig.Kiosk.Cache {
// Remove the current image from the slice
immichAssetsToCache := append(immichAssets[:immichAssetIndex], immichAssets[immichAssetIndex+1:]...)
jsonBytes, err := json.Marshal(immichAssetsToCache)
if err != nil {
log.Error("Failed to marshal immichAssetsToCache", "error", err)
return err
}

// replace cache with used image(s) removed
err = cache.Replace(apiCacheKey, jsonBytes)
if err != nil {
log.Debug("Failed to update cache", "error", err, "url", apiUrl.String())
}
}
}

img.KioskSourceName = fmt.Sprintf("%s to %s", dateStartHuman, dateEndHuman)
img.KioskSourceName = fmt.Sprintf("%s to %s", dateStartHuman, dateEndHuman)

*i = img

*i = img
return nil
}

return nil
log.Debug(requestID + " No viable images left in cache. Refreshing and trying again")
cache.Delete(apiCacheKey)
}

log.Debug(requestID + " No viable images left in cache. Refreshing and trying again")
cache.Delete(apiCacheKey)
retries++
return i.randomImageInDateRange(dateRange, requestID, deviceID, isPrefetch, retries)
return fmt.Errorf("No images found for '%s'. Max retries reached.", dateRange)
}
Loading
Loading