From 7daf580c7a539c88fdca2be56fc98eed7fc28087 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Fri, 7 Mar 2025 18:57:07 -0800 Subject: [PATCH] fix: Use better default media download filenames (#1949) * Closes #1938 [skip ci] --- src/card-controller/download-manager.ts | 4 +- src/utils/download.ts | 18 +++++- src/utils/screenshot.ts | 6 +- tests/utils/download.test.ts | 77 ++++++++++++++++++++++--- tests/utils/screenshot.test.ts | 18 +++--- 5 files changed, 100 insertions(+), 23 deletions(-) diff --git a/src/card-controller/download-manager.ts b/src/card-controller/download-manager.ts index 0fca446a..950a6a29 100644 --- a/src/card-controller/download-manager.ts +++ b/src/card-controller/download-manager.ts @@ -1,5 +1,5 @@ import { downloadMedia, downloadURL } from '../utils/download'; -import { generateScreenshotTitle } from '../utils/screenshot'; +import { generateScreenshotFilename } from '../utils/screenshot'; import { CardDownloadAPI } from './types'; export class DownloadManager { @@ -35,7 +35,7 @@ export class DownloadManager { .get() ?.mediaPlayerController?.getScreenshotURL(); if (url) { - downloadURL(url, generateScreenshotTitle(this._api.getViewManager().getView())); + downloadURL(url, generateScreenshotFilename(this._api.getViewManager().getView())); } } } diff --git a/src/utils/download.ts b/src/utils/download.ts index f42a3b59..04dc1ec7 100644 --- a/src/utils/download.ts +++ b/src/utils/download.ts @@ -1,3 +1,4 @@ +import { format } from 'date-fns'; import { CameraManager } from '../camera-manager/manager'; import { localize } from '../localize/localize'; import { AdvancedCameraCardError, ExtendedHomeAssistant } from '../types'; @@ -50,5 +51,20 @@ export const downloadMedia = async ( finalURL = response; } - downloadURL(finalURL); + downloadURL(finalURL, generateDownloadFilename(media)); +}; + +const generateDownloadFilename = (media: ViewMedia): string => { + const toFilename = (input: string): string => { + return input.toLowerCase().replaceAll(/(\.|\s)+/g, '-'); + }; + + const id = media.getID(); + const startTime = media.getStartTime(); + + return ( + toFilename(media.getCameraID()) + + (id ? `_${toFilename(id)}` : '') + + (startTime ? `_${format(startTime, `yyyy-MM-dd-HH-mm-ss`)}` : '') + ); }; diff --git a/src/utils/screenshot.ts b/src/utils/screenshot.ts index 048af561..dcd79019 100644 --- a/src/utils/screenshot.ts +++ b/src/utils/screenshot.ts @@ -26,16 +26,16 @@ const screenshotElement = ( return canvas.toDataURL('image/jpeg'); }; -export const generateScreenshotTitle = (view?: View | null): string => { +export const generateScreenshotFilename = (view?: View | null): string => { if (view?.is('live') || view?.is('image')) { - return `${view.view}-${view.camera}-${format( + return `${view.view}_${view.camera}_${format( new Date(), `yyyy-MM-dd-HH-mm-ss`, )}.jpg`; } else if (view?.isViewerView()) { const media = view.queryResults?.getSelectedResult(); const id = media?.getID() ?? null; - return `${view.view}-${view.camera}${id ? `-${id}` : ''}.jpg`; + return `${view.view}_${view.camera}${id ? `_${id}` : ''}.jpg`; } return 'screenshot.jpg'; }; diff --git a/tests/utils/download.test.ts b/tests/utils/download.test.ts index 1b4363d3..7ff58b69 100644 --- a/tests/utils/download.test.ts +++ b/tests/utils/download.test.ts @@ -1,13 +1,18 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; import { downloadMedia, downloadURL } from '../../src/utils/download'; import { homeAssistantSignPath } from '../../src/utils/ha'; import { ViewMedia } from '../../src/view/media'; -import { createCameraManager, createHASS } from '../test-utils'; +import { + createCameraManager, + createHASS, + createStore, + TestViewMedia, +} from '../test-utils'; vi.mock('../../src/utils/ha'); -const media = new ViewMedia('clip', 'camera-1'); +const media = new ViewMedia('clip', 'camera.office'); // @vitest-environment jsdom describe('downloadURL', () => { @@ -60,9 +65,9 @@ describe('downloadURL', () => { }); describe('downloadMedia', () => { - afterEach(() => { - vi.resetAllMocks(); - global.window.location = mock(); + beforeEach(() => { + vi.restoreAllMocks(); + global.window.location = mock({ origin: 'https://foo' }); }); it('should throw error when no media', () => { @@ -107,11 +112,67 @@ describe('downloadMedia', () => { const cameraManager = createCameraManager(); vi.mocked(cameraManager).getMediaDownloadPath.mockResolvedValue({ sign: false, - endpoint: 'https://foo/', + endpoint: 'https://another/', }); const windowSpy = vi.spyOn(window, 'open').mockReturnValue(null); await downloadMedia(createHASS(), cameraManager, media); - expect(windowSpy).toBeCalledWith('https://foo/', '_blank'); + expect(windowSpy).toBeCalledWith('https://another/', '_blank'); + }); + + describe('should generate useful download filenames', () => { + it('should generate filename with just camera ID', async () => { + const cameraManager = createCameraManager( + createStore([ + { + cameraID: 'camera.office', + }, + ]), + ); + vi.mocked(cameraManager).getMediaDownloadPath.mockResolvedValue({ + sign: false, + endpoint: 'https://foo/', + }); + + const link = document.createElement('a'); + link.click = vi.fn(); + link.setAttribute = vi.fn(); + vi.spyOn(document, 'createElement').mockReturnValue(link); + + await downloadMedia(createHASS(), cameraManager, media); + + expect(link.setAttribute).toBeCalledWith('download', 'camera-office'); + }); + + it('should generate filename with full details ID', async () => { + const cameraManager = createCameraManager( + createStore([ + { + cameraID: 'camera.office', + }, + ]), + ); + vi.mocked(cameraManager).getMediaDownloadPath.mockResolvedValue({ + sign: false, + endpoint: 'https://foo/', + }); + + const link = document.createElement('a'); + link.click = vi.fn(); + link.setAttribute = vi.fn(); + vi.spyOn(document, 'createElement').mockReturnValue(link); + + const media = new TestViewMedia({ + cameraID: 'camera.office', + id: 'clip-id', + startTime: new Date('2025-03-06T21:31:29Z'), + }); + await downloadMedia(createHASS(), cameraManager, media); + + expect(link.setAttribute).toBeCalledWith( + 'download', + 'camera-office_clip-id_2025-03-06-21-31-29', + ); + }); }); }); diff --git a/tests/utils/screenshot.test.ts b/tests/utils/screenshot.test.ts index 0bee00b6..12ab3f78 100644 --- a/tests/utils/screenshot.test.ts +++ b/tests/utils/screenshot.test.ts @@ -1,7 +1,7 @@ import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; import { mock } from 'vitest-mock-extended'; import { - generateScreenshotTitle, + generateScreenshotFilename, screenshotImage, screenshotVideo, } from '../../src/utils/screenshot'; @@ -79,19 +79,19 @@ describe('generateScreenshotTitle', () => { }); it('should get title without view', () => { - expect(generateScreenshotTitle()).toBe('screenshot.jpg'); + expect(generateScreenshotFilename()).toBe('screenshot.jpg'); }); it('should get title for live view', () => { - expect(generateScreenshotTitle(new View({ view: 'live', camera: 'camera-1' }))).toBe( - 'live-camera-1-2023-06-13-21-54-01.jpg', - ); + expect( + generateScreenshotFilename(new View({ view: 'live', camera: 'camera-1' })), + ).toBe('live_camera-1_2023-06-13-21-54-01.jpg'); }); it('should get title for image view', () => { expect( - generateScreenshotTitle(new View({ view: 'image', camera: 'camera-1' })), - ).toBe('image-camera-1-2023-06-13-21-54-01.jpg'); + generateScreenshotFilename(new View({ view: 'image', camera: 'camera-1' })), + ).toBe('image_camera-1_2023-06-13-21-54-01.jpg'); }); it('should get title for media viewer view with id', () => { @@ -106,7 +106,7 @@ describe('generateScreenshotTitle', () => { queryResults: new MediaQueriesResults({ results: [media], selectedIndex: 0 }), }); - expect(generateScreenshotTitle(view)).toBe('media-camera-1-id1.jpg'); + expect(generateScreenshotFilename(view)).toBe('media_camera-1_id1.jpg'); }); it('should get title for media viewer view without id', () => { @@ -121,6 +121,6 @@ describe('generateScreenshotTitle', () => { queryResults: new MediaQueriesResults({ results: [media], selectedIndex: 0 }), }); - expect(generateScreenshotTitle(view)).toBe('media-camera-1.jpg'); + expect(generateScreenshotFilename(view)).toBe('media_camera-1.jpg'); }); });