Skip to content

Commit

Permalink
echo.Context copier to limit race issues
Browse files Browse the repository at this point in the history
  • Loading branch information
damongolding committed Feb 6, 2025
1 parent 2569d0d commit ae35cb1
Show file tree
Hide file tree
Showing 11 changed files with 135 additions and 88 deletions.
4 changes: 2 additions & 2 deletions frontend/public/assets/css/kiosk.css
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ form {
opacity: 0.3;
padding: 0 0.5rem;
}
.frame--layout-splitview .asset--metadata--desciption, .frame--layout-splitview-landscape .asset--metadata--desciption {
.frame--layout-splitview .asset--metadata--description, .frame--layout-splitview-landscape .asset--metadata--description {
max-width: 50%;
}
.frame--layout-splitview:nth-child(1) .asset--metadata {
Expand Down Expand Up @@ -544,7 +544,7 @@ form {
padding: 0.5rem !important;
max-width: 50vw;
}
.asset--metadata--desciption {
.asset--metadata--description {
max-width: 100%;
}
.asset--metadata--location span {
Expand Down
2 changes: 1 addition & 1 deletion frontend/public/assets/js/kiosk.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions frontend/src/css/image.css
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@

.frame--layout-splitview,
.frame--layout-splitview-landscape {
.asset--metadata--desciption {
.asset--metadata--description {
max-width: 50%;
}
}
Expand Down Expand Up @@ -178,7 +178,7 @@
max-width: 50vw;
}

.asset--metadata--desciption {
.asset--metadata--description {
max-width: 100%;
}

Expand Down
23 changes: 16 additions & 7 deletions frontend/src/ts/polling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,14 @@ class PollingController {
this.triggerNewAsset();
};

// Function to clear timeout when video starts playing
handlePlayStart = (playTimeout: number) => {
clearTimeout(playTimeout);
this.video?.removeEventListener("playing", () =>
this.handlePlayStart(playTimeout),
);
};

/**
* Handles video playback
* @param id - The ID of the video element to handle
Expand All @@ -213,14 +221,14 @@ class PollingController {
}
}, 5000); // 5 seconds timeout

// Function to clear timeout when video starts playing
const handlePlayStart = () => {
clearTimeout(playTimeout);
this.video?.removeEventListener("playing", handlePlayStart);
};

// Add listener for when video starts playing
this.video.addEventListener("playing", handlePlayStart, { once: true });
this.video.addEventListener(
"playing",
() => this.handlePlayStart(playTimeout),
{
once: true,
},
);

this.progressBarElement?.classList.remove("progress--bar-paused");
this.menuElement?.classList.add("navigation-hidden");
Expand Down Expand Up @@ -268,6 +276,7 @@ class PollingController {
private videoCleanup = () => {
this.video?.removeEventListener("ended", this.videoEndedHandler);
this.video?.removeEventListener("error", this.handleVideoError);
this.video?.removeEventListener("playing", () => this.handlePlayStart);

this.progressBarElement?.classList.add("progress--bar-paused");

Expand Down
23 changes: 23 additions & 0 deletions internal/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package common
import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"os/signal"
Expand All @@ -14,6 +15,7 @@ import (
"github.com/damongolding/immich-kiosk/internal/config"
"github.com/damongolding/immich-kiosk/internal/immich"
"github.com/damongolding/immich-kiosk/internal/utils"
"github.com/labstack/echo/v4"
)

var (
Expand Down Expand Up @@ -109,3 +111,24 @@ type ViewData struct {
CustomCss []byte // CustomCss contains custom CSS styling as bytes
config.Config // Config contains the instance configuration
}

// ContextCopy stores a copy of key HTTP context information including URL and headers
type ContextCopy struct {
URL url.URL // The request URL
RequestHeader http.Header // Headers from the incoming request
ResponseHeader http.Header // Headers for the outgoing response
}

// CopyContext creates a copy of essential context data from an echo.Context
// This allows preserving context information without maintaining a reference to the original context
// Returns a ContextCopy containing the URL and header information
func CopyContext(c echo.Context) ContextCopy {

ctxCopy := ContextCopy{
URL: *c.Request().URL,
RequestHeader: c.Request().Header.Clone(),
ResponseHeader: c.Response().Header().Clone(),
}

return ctxCopy
}
17 changes: 9 additions & 8 deletions internal/routes/routes_asset.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"github.com/charmbracelet/log"
"github.com/labstack/echo/v4"

"github.com/damongolding/immich-kiosk/internal/common"
"github.com/damongolding/immich-kiosk/internal/config"
"github.com/damongolding/immich-kiosk/internal/immich"
imageComponent "github.com/damongolding/immich-kiosk/internal/templates/components/image"
Expand All @@ -14,7 +15,7 @@ import (
"github.com/damongolding/immich-kiosk/internal/webhooks"
)

// NewAsset returns an echo.HandlerFunc that handles requests for new images.
// NewAsset returns an echo.HandlerFunc that handles requests for new assets.
// It manages image processing, caching, and prefetching based on the configuration.
func NewAsset(baseConfig *config.Config) echo.HandlerFunc {
return func(c echo.Context) error {
Expand Down Expand Up @@ -45,31 +46,31 @@ func NewAsset(baseConfig *config.Config) echo.HandlerFunc {
return c.NoContent(http.StatusNoContent)
}

requestCtx := common.CopyContext(c)

// get and use prefetch data (if found)
if requestConfig.Kiosk.PreFetch {
if cachedViewData := fromCache(c.Request().URL.String(), deviceID); cachedViewData != nil {
requestEchoCtx := c
go assetPreFetch(requestData, requestEchoCtx)
if cachedViewData := fromCache(requestCtx.URL.String(), deviceID); cachedViewData != nil {
go assetPreFetch(requestData, requestCtx)
go webhooks.Trigger(requestData, KioskVersion, webhooks.NewAsset, cachedViewData[0])

return renderCachedViewData(c, cachedViewData, &requestConfig, requestID, deviceID)
}
log.Debug(requestID, "deviceID", deviceID, "cache miss for new image")
}

viewData, err := generateViewData(requestConfig, c, deviceID, false)
viewData, err := generateViewData(requestConfig, requestCtx, deviceID, false)
if err != nil {
return RenderError(c, err, "retrieving image")
}

if requestConfig.Kiosk.PreFetch {
requestEchoCtx := c
go assetPreFetch(requestData, requestEchoCtx)
go assetPreFetch(requestData, requestCtx)
}

go webhooks.Trigger(requestData, KioskVersion, webhooks.NewAsset, viewData)

if requestConfig.ExperimentalAlbumVideo && viewData.Assets[0].ImmichAsset.Type == immich.VideoType {
if len(viewData.Assets) > 0 && requestConfig.ExperimentalAlbumVideo && viewData.Assets[0].ImmichAsset.Type == immich.VideoType {
return Render(c, http.StatusOK, videoComponent.Video(viewData))
}

Expand Down
20 changes: 10 additions & 10 deletions internal/routes/routes_asset_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,9 +347,9 @@ func DrawFaceOnImage(img image.Image, i *immich.ImmichAsset) image.Image {

// processViewImageData handles the entire process of preparing page data including image processing.
// It returns the ImageData and an error if any step fails.
func processViewImageData(imageOrientation immich.ImageOrientation, requestConfig config.Config, c echo.Context, isPrefetch bool) (common.ViewImageData, error) {
requestID := utils.ColorizeRequestId(c.Response().Header().Get(echo.HeaderXRequestID))
deviceID := c.Request().Header.Get("kiosk-device-id")
func processViewImageData(imageOrientation immich.ImageOrientation, requestConfig config.Config, c common.ContextCopy, isPrefetch bool) (common.ViewImageData, error) {
requestID := utils.ColorizeRequestId(c.ResponseHeader.Get(echo.HeaderXRequestID))
deviceID := c.RequestHeader.Get("kiosk-device-id")

// if multiple users are given via the url pick a random one
if len(requestConfig.User) > 0 {
Expand All @@ -374,7 +374,7 @@ func processViewImageData(imageOrientation immich.ImageOrientation, requestConfi
allowedAssetTypes = immich.AllAssetTypes
}

img, err := processAsset(&immichAsset, allowedAssetTypes, requestConfig, requestID, deviceID, c.Request().URL.String(), isPrefetch)
img, err := processAsset(&immichAsset, allowedAssetTypes, requestConfig, requestID, deviceID, c.URL.String(), isPrefetch)
if err != nil {
return common.ViewImageData{}, fmt.Errorf("selecting image: %w", err)
}
Expand Down Expand Up @@ -414,25 +414,25 @@ func processViewImageData(imageOrientation immich.ImageOrientation, requestConfi
}

// ProcessViewImageData processes view data for an image without orientation constraints
func ProcessViewImageData(requestConfig config.Config, c echo.Context, isPrefetch bool) (common.ViewImageData, error) {
func ProcessViewImageData(requestConfig config.Config, c common.ContextCopy, isPrefetch bool) (common.ViewImageData, error) {
return processViewImageData("", requestConfig, c, isPrefetch)
}

// ProcessViewImageDataWithRatio processes view data for an image with the specified orientation
func ProcessViewImageDataWithRatio(imageOrientation immich.ImageOrientation, requestConfig config.Config, c echo.Context, isPrefetch bool) (common.ViewImageData, error) {
func ProcessViewImageDataWithRatio(imageOrientation immich.ImageOrientation, requestConfig config.Config, c common.ContextCopy, isPrefetch bool) (common.ViewImageData, error) {
return processViewImageData(imageOrientation, requestConfig, c, isPrefetch)
}

// assetToCache stores view data in the cache and triggers prefetch webhooks
func assetToCache(viewDataToAdd common.ViewData, requestConfig *config.Config, deviceID string, requestData *common.RouteRequestData, c echo.Context) {
func assetToCache(viewDataToAdd common.ViewData, requestConfig *config.Config, deviceID string, requestData *common.RouteRequestData, c common.ContextCopy) {

cache.AssetToCache(viewDataToAdd, requestConfig, deviceID, c.Request().URL.String())
cache.AssetToCache(viewDataToAdd, requestConfig, deviceID, c.URL.String())

go webhooks.Trigger(requestData, KioskVersion, webhooks.PrefetchAsset, viewDataToAdd)
}

// assetPreFetch handles prefetching assets for the current request
func assetPreFetch(requestData *common.RouteRequestData, c echo.Context) {
func assetPreFetch(requestData *common.RouteRequestData, c common.ContextCopy) {

requestConfig := requestData.RequestConfig
requestID := requestData.RequestID
Expand Down Expand Up @@ -489,7 +489,7 @@ func renderCachedViewData(c echo.Context, cachedViewData []common.ViewData, requ
}

// generateViewData generates page data for the current request.
func generateViewData(requestConfig config.Config, c echo.Context, deviceID string, isPrefetch bool) (common.ViewData, error) {
func generateViewData(requestConfig config.Config, c common.ContextCopy, deviceID string, isPrefetch bool) (common.ViewData, error) {

const maxImageRetrievalAttepmts = 3

Expand Down
Loading

0 comments on commit ae35cb1

Please sign in to comment.