Skip to content

Commit b09bd9e

Browse files
authored
fix: preserve asset subfolder in the assets lightbox (#14689)
## Summary Inspecting a video saved under a subfolder shows an empty player, because the assets lightbox builds its media URL from a subfolder the sidebar discarded. ## Changes - **What**: `AssetsSidebarTab` built every gallery `ResultItemImpl` with `subfolder: ''` and overrode only the `url` getter. Images resolve through `preview_url` and were unaffected; `ResultVideo` uses `vhsAdvancedPreviewUrl`, which is rebuilt from `urlParams` — i.e. from the discarded subfolder. Extracted `getAssetSubfolder` (reads `preview_url`, falls back to `user_metadata`, mirroring how `getAssetType` resolves the type) and used it when building gallery items and in `getAssetUrl`. Measured against a local backend — same file, same name, only the address differs: | Request | Response | | --- | --- | | `viewvideo?filename=clip.webm&type=output&subfolder=` (file at output root) | 200, 910017 b | | `viewvideo?filename=clip.webm&type=output&subfolder=sub` (file in `sub/`) | 200, 910017 b | | `viewvideo?filename=clip.webm&type=output&subfolder=` (file in `sub/`) | **204, 0 b** | The endpoint handles subfolders correctly; only the URL the lightbox constructed failed. This also matches the 204 independently reported in #7192, and explains why the same videos play from Job History (which builds result items from real queue data) and why the bug only appears with a `filename_prefix` containing a directory. ## Review Focus `getAssetSubfolder` prefers `preview_url` over `user_metadata` because `preview_url` is already the trusted source for the `url` getter and for `getAssetType`. `getAssetUrl` previously read `user_metadata` only; that path is preserved as the fallback, so its behaviour is unchanged when `preview_url` carries no subfolder. Not addressed here, to keep this focused: the same constructor hard-codes `type: 'output'`, which is wrong for imported (input) videos. `ResultItem['type']` is a narrow union while `getAssetType` returns `string`, so that needs its own change. Every case reported in #7192 is an output. Fixes #7192
1 parent 3e1b0d2 commit b09bd9e

3 files changed

Lines changed: 105 additions & 4 deletions

File tree

src/components/sidebar/tabs/AssetsSidebarTab.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,10 @@ import type { OutputAssetMetadata } from '@/platform/assets/schemas/assetMetadat
229229
import { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'
230230
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
231231
import { getAssetDisplayName } from '@/platform/assets/utils/assetMetadataUtils'
232-
import { getAssetUrl } from '@/platform/assets/utils/assetUrlUtil'
232+
import {
233+
getAssetSubfolder,
234+
getAssetUrl
235+
} from '@/platform/assets/utils/assetUrlUtil'
233236
import type { MediaKind } from '@/platform/assets/schemas/mediaAssetSchema'
234237
import { resolveOutputAssetItems } from '@/platform/assets/utils/outputAssetUtil'
235238
import { isCloud } from '@/platform/distribution/types'
@@ -457,7 +460,7 @@ const galleryItems = computed(() => {
457460
const mediaType = getMediaTypeFromFilename(asset.name)
458461
const resultItem = new ResultItemImpl({
459462
filename: asset.name,
460-
subfolder: '',
463+
subfolder: getAssetSubfolder(asset),
461464
type: 'output',
462465
nodeId: '0',
463466
mediaType: mediaType === 'image' ? 'images' : mediaType
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest'
2+
3+
import type { AssetItem } from '@/platform/assets/schemas/assetSchema'
4+
import {
5+
getAssetSubfolder,
6+
getAssetUrl
7+
} from '@/platform/assets/utils/assetUrlUtil'
8+
9+
const mockApiURL = vi.hoisted(() =>
10+
vi.fn((path: string) => `http://localhost:8188/api${path}`)
11+
)
12+
13+
vi.mock('@/scripts/api', () => ({
14+
api: { apiURL: mockApiURL }
15+
}))
16+
17+
function createAsset(overrides: Partial<AssetItem> = {}): AssetItem {
18+
return {
19+
id: 'asset-1',
20+
name: 'clip.webm',
21+
tags: ['output'],
22+
...overrides
23+
} as AssetItem
24+
}
25+
26+
describe('getAssetSubfolder', () => {
27+
beforeEach(() => vi.clearAllMocks())
28+
29+
it('reads the subfolder from preview_url', () => {
30+
const asset = createAsset({
31+
preview_url: '/api/view?filename=clip.webm&type=output&subfolder=vid/2026'
32+
})
33+
34+
expect(getAssetSubfolder(asset)).toBe('vid/2026')
35+
})
36+
37+
it('falls back to user_metadata when preview_url carries no subfolder', () => {
38+
const asset = createAsset({
39+
preview_url: '/api/view?filename=clip.webm&type=output',
40+
user_metadata: { subfolder: 'vid/2026' }
41+
})
42+
43+
expect(getAssetSubfolder(asset)).toBe('vid/2026')
44+
})
45+
46+
it('returns an empty string for an asset at the type root', () => {
47+
expect(getAssetSubfolder(createAsset())).toBe('')
48+
expect(
49+
getAssetSubfolder(
50+
createAsset({ user_metadata: { subfolder: undefined } })
51+
)
52+
).toBe('')
53+
})
54+
})
55+
56+
describe('getAssetUrl', () => {
57+
beforeEach(() => vi.clearAllMocks())
58+
59+
it('includes the subfolder carried by preview_url', () => {
60+
const asset = createAsset({
61+
preview_url: '/api/view?filename=clip.webm&type=output&subfolder=vid/2026'
62+
})
63+
64+
expect(getAssetUrl(asset)).toContain('subfolder=vid%2F2026')
65+
})
66+
67+
it('includes the subfolder taken from the user_metadata fallback', () => {
68+
const asset = createAsset({
69+
preview_url: '/api/view?filename=clip.webm&type=output',
70+
user_metadata: { subfolder: 'vid/2026' }
71+
})
72+
73+
expect(getAssetUrl(asset)).toContain('subfolder=vid%2F2026')
74+
})
75+
76+
it('omits the subfolder param for an asset at the type root', () => {
77+
expect(getAssetUrl(createAsset())).not.toContain('subfolder')
78+
})
79+
})

src/platform/assets/utils/assetUrlUtil.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,31 @@ export function getAssetUrl(
2323
defaultType: 'input' | 'output' = 'output'
2424
): string {
2525
const assetType = getAssetType(asset, defaultType)
26-
const subfolder = asset.user_metadata?.subfolder
26+
const subfolder = getAssetSubfolder(asset)
2727
const params = new URLSearchParams()
2828
params.set('filename', asset.name)
2929
params.set('type', assetType)
30-
if (typeof subfolder === 'string' && subfolder) {
30+
if (subfolder) {
3131
params.set('subfolder', subfolder)
3232
}
3333
return api.apiURL(`/view?${params}`)
3434
}
35+
36+
/**
37+
* Get the subfolder an asset lives in, relative to its type root
38+
*
39+
* Reads `preview_url` first and falls back to `user_metadata`, mirroring how
40+
* {@link getAssetType} resolves the type.
41+
*
42+
* @param asset The asset to get the subfolder for
43+
* @returns The subfolder, or an empty string when the asset is at the root
44+
*/
45+
export function getAssetSubfolder(asset: AssetItem): string {
46+
const previewSubfolder = new URLSearchParams(
47+
(asset.preview_url ?? '').split('?')[1] ?? ''
48+
).get('subfolder')
49+
if (previewSubfolder) return previewSubfolder
50+
51+
const { subfolder } = asset.user_metadata ?? {}
52+
return typeof subfolder === 'string' ? subfolder : ''
53+
}

0 commit comments

Comments
 (0)