diff --git a/ui/app/pages/browser/[...slug].vue b/ui/app/pages/browser/[...slug].vue index 6e7180d3..44b68dbd 100644 --- a/ui/app/pages/browser/[...slug].vue +++ b/ui/app/pages/browser/[...slug].vue @@ -332,7 +332,7 @@ watch(() => socket.isConnected, async () => { }) const handleClick = (item: FileItem): void => { - if ('video' === item.content_type) { + if (true === ['video','audio'].includes(item.content_type)) { model_item.value = { "type": 'video', "filename": item.path, diff --git a/ui/app/utils/index.ts b/ui/app/utils/index.ts index cbd3b431..a419cf56 100644 --- a/ui/app/utils/index.ts +++ b/ui/app/utils/index.ts @@ -179,7 +179,6 @@ const makePagination = (current: number, last: number, delta: number = 5): Array /** * Safely encode a path string for use in a URL. - * Will manually encode '#' and ensure valid URI segments. * * @param item - The input path string. * @returns The URL-encoded path. @@ -189,14 +188,37 @@ const encodePath = (item: string): string => { return item } - item = item.replace(/#/g, '%23') + return item.split('/').map(segment => { + try { + const decoded = decodeURIComponent(segment) + const reEncoded = encodeURIComponent(decoded) - try { - return item.split('/').map(decodeURIComponent).map(encodeURIComponent).join('/') - } catch (e) { - console.error('Error encoding path:', e, item) - return item - } + if (reEncoded === segment) { + return segment + } + } catch { + // Decoding failed, segment has invalid encoding + } + + const placeholders: string[] = [] + const _PREFIX = `_YTP${Math.random().toString(36).substring(2, 8).toUpperCase()}_` + const _SUFFIX = `_YTP${Math.random().toString(36).substring(2, 8).toUpperCase()}_` + + let processed = segment.replace(/%[0-9A-Fa-f]{2}/g, match => { + const index = placeholders.length + placeholders.push(match) + return `${_PREFIX}${index}${_SUFFIX}` + }) + + processed = encodeURIComponent(processed) + + const placeholderRegex = new RegExp(`${_PREFIX.replace(/_/g, '_')}(\\d+)${_SUFFIX.replace(/_/g, '_')}`, 'g') + processed = processed.replace(placeholderRegex, (_match, index: string) => { + return placeholders[parseInt(index)] || '' + }) + + return processed + }).join('/') } /** diff --git a/ui/tests/utils/index.test.ts b/ui/tests/utils/index.test.ts index 458e254e..689ab462 100644 --- a/ui/tests/utils/index.test.ts +++ b/ui/tests/utils/index.test.ts @@ -279,6 +279,47 @@ describe('string manipulation helpers', () => { expect(utils.encodePath('folder#1/video name.mp4')).toBe('folder%231/video%20name.mp4') }) + it('encodePath handles % character correctly', () => { + // This is the edge case reported in the bug + expect(utils.encodePath('How to enjoy Shin Ramyun 100%.opus')).toBe('How%20to%20enjoy%20Shin%20Ramyun%20100%25.opus') + }) + + it('encodePath handles multiple special characters', () => { + expect(utils.encodePath('100% complete [HD] #1.mp4')).toBe('100%25%20complete%20%5BHD%5D%20%231.mp4') + }) + + it('encodePath handles paths with % character', () => { + expect(utils.encodePath('folder/How to enjoy Shin Ramyun 100%.opus')).toBe('folder/How%20to%20enjoy%20Shin%20Ramyun%20100%25.opus') + }) + + it('encodePath handles already encoded strings', () => { + expect(utils.encodePath('How%20to%20enjoy%20Shin%20Ramyun%20100%25.opus')).toBe('How%20to%20enjoy%20Shin%20Ramyun%20100%25.opus') + }) + + it('encodePath handles mixed encoded and unencoded', () => { + expect(utils.encodePath('folder/file%20name 100%.mp4')).toBe('folder/file%20name%20100%25.mp4') + }) + + it('encodePath handles special characters &, =, ?', () => { + expect(utils.encodePath('query?param=value&key=100%.mp4')).toBe('query%3Fparam%3Dvalue%26key%3D100%25.mp4') + }) + + it('encodePath handles empty string', () => { + expect(utils.encodePath('')).toBe('') + }) + + it('encodePath handles simple filename', () => { + expect(utils.encodePath('video.mp4')).toBe('video.mp4') + }) + + it('encodePath handles unicode characters', () => { + expect(utils.encodePath('视频文件.mp4')).toBe('%E8%A7%86%E9%A2%91%E6%96%87%E4%BB%B6.mp4') + }) + + it('encodePath handles parentheses', () => { + expect(utils.encodePath('video (1080p).mp4')).toBe('video%20(1080p).mp4') + }) + it('removeANSIColors strips escape codes', () => { const sample = '\u001b[31mError\u001b[0m' expect(utils.removeANSIColors(sample)).toBe('Error')