fix: Use better default media download filenames (#1949)
* Closes #1938 [skip ci]
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { downloadMedia, downloadURL } from '../utils/download';
|
import { downloadMedia, downloadURL } from '../utils/download';
|
||||||
import { generateScreenshotTitle } from '../utils/screenshot';
|
import { generateScreenshotFilename } from '../utils/screenshot';
|
||||||
import { CardDownloadAPI } from './types';
|
import { CardDownloadAPI } from './types';
|
||||||
|
|
||||||
export class DownloadManager {
|
export class DownloadManager {
|
||||||
@@ -35,7 +35,7 @@ export class DownloadManager {
|
|||||||
.get()
|
.get()
|
||||||
?.mediaPlayerController?.getScreenshotURL();
|
?.mediaPlayerController?.getScreenshotURL();
|
||||||
if (url) {
|
if (url) {
|
||||||
downloadURL(url, generateScreenshotTitle(this._api.getViewManager().getView()));
|
downloadURL(url, generateScreenshotFilename(this._api.getViewManager().getView()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-1
@@ -1,3 +1,4 @@
|
|||||||
|
import { format } from 'date-fns';
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
import { localize } from '../localize/localize';
|
import { localize } from '../localize/localize';
|
||||||
import { AdvancedCameraCardError, ExtendedHomeAssistant } from '../types';
|
import { AdvancedCameraCardError, ExtendedHomeAssistant } from '../types';
|
||||||
@@ -50,5 +51,20 @@ export const downloadMedia = async (
|
|||||||
finalURL = response;
|
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`)}` : '')
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,16 +26,16 @@ const screenshotElement = (
|
|||||||
return canvas.toDataURL('image/jpeg');
|
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')) {
|
if (view?.is('live') || view?.is('image')) {
|
||||||
return `${view.view}-${view.camera}-${format(
|
return `${view.view}_${view.camera}_${format(
|
||||||
new Date(),
|
new Date(),
|
||||||
`yyyy-MM-dd-HH-mm-ss`,
|
`yyyy-MM-dd-HH-mm-ss`,
|
||||||
)}.jpg`;
|
)}.jpg`;
|
||||||
} else if (view?.isViewerView()) {
|
} else if (view?.isViewerView()) {
|
||||||
const media = view.queryResults?.getSelectedResult();
|
const media = view.queryResults?.getSelectedResult();
|
||||||
const id = media?.getID() ?? null;
|
const id = media?.getID() ?? null;
|
||||||
return `${view.view}-${view.camera}${id ? `-${id}` : ''}.jpg`;
|
return `${view.view}_${view.camera}${id ? `_${id}` : ''}.jpg`;
|
||||||
}
|
}
|
||||||
return 'screenshot.jpg';
|
return 'screenshot.jpg';
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 { mock } from 'vitest-mock-extended';
|
||||||
import { downloadMedia, downloadURL } from '../../src/utils/download';
|
import { downloadMedia, downloadURL } from '../../src/utils/download';
|
||||||
import { homeAssistantSignPath } from '../../src/utils/ha';
|
import { homeAssistantSignPath } from '../../src/utils/ha';
|
||||||
import { ViewMedia } from '../../src/view/media';
|
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');
|
vi.mock('../../src/utils/ha');
|
||||||
|
|
||||||
const media = new ViewMedia('clip', 'camera-1');
|
const media = new ViewMedia('clip', 'camera.office');
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
describe('downloadURL', () => {
|
describe('downloadURL', () => {
|
||||||
@@ -60,9 +65,9 @@ describe('downloadURL', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('downloadMedia', () => {
|
describe('downloadMedia', () => {
|
||||||
afterEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.restoreAllMocks();
|
||||||
global.window.location = mock<Location>();
|
global.window.location = mock<Location>({ origin: 'https://foo' });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error when no media', () => {
|
it('should throw error when no media', () => {
|
||||||
@@ -107,11 +112,67 @@ describe('downloadMedia', () => {
|
|||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
vi.mocked(cameraManager).getMediaDownloadPath.mockResolvedValue({
|
vi.mocked(cameraManager).getMediaDownloadPath.mockResolvedValue({
|
||||||
sign: false,
|
sign: false,
|
||||||
endpoint: 'https://foo/',
|
endpoint: 'https://another/',
|
||||||
});
|
});
|
||||||
const windowSpy = vi.spyOn(window, 'open').mockReturnValue(null);
|
const windowSpy = vi.spyOn(window, 'open').mockReturnValue(null);
|
||||||
|
|
||||||
await downloadMedia(createHASS(), cameraManager, media);
|
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',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import {
|
import {
|
||||||
generateScreenshotTitle,
|
generateScreenshotFilename,
|
||||||
screenshotImage,
|
screenshotImage,
|
||||||
screenshotVideo,
|
screenshotVideo,
|
||||||
} from '../../src/utils/screenshot';
|
} from '../../src/utils/screenshot';
|
||||||
@@ -79,19 +79,19 @@ describe('generateScreenshotTitle', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get title without view', () => {
|
it('should get title without view', () => {
|
||||||
expect(generateScreenshotTitle()).toBe('screenshot.jpg');
|
expect(generateScreenshotFilename()).toBe('screenshot.jpg');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get title for live view', () => {
|
it('should get title for live view', () => {
|
||||||
expect(generateScreenshotTitle(new View({ view: 'live', camera: 'camera-1' }))).toBe(
|
expect(
|
||||||
'live-camera-1-2023-06-13-21-54-01.jpg',
|
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', () => {
|
it('should get title for image view', () => {
|
||||||
expect(
|
expect(
|
||||||
generateScreenshotTitle(new View({ view: 'image', camera: 'camera-1' })),
|
generateScreenshotFilename(new View({ view: 'image', camera: 'camera-1' })),
|
||||||
).toBe('image-camera-1-2023-06-13-21-54-01.jpg');
|
).toBe('image_camera-1_2023-06-13-21-54-01.jpg');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get title for media viewer view with id', () => {
|
it('should get title for media viewer view with id', () => {
|
||||||
@@ -106,7 +106,7 @@ describe('generateScreenshotTitle', () => {
|
|||||||
queryResults: new MediaQueriesResults({ results: [media], selectedIndex: 0 }),
|
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', () => {
|
it('should get title for media viewer view without id', () => {
|
||||||
@@ -121,6 +121,6 @@ describe('generateScreenshotTitle', () => {
|
|||||||
queryResults: new MediaQueriesResults({ results: [media], selectedIndex: 0 }),
|
queryResults: new MediaQueriesResults({ results: [media], selectedIndex: 0 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(generateScreenshotTitle(view)).toBe('media-camera-1.jpg');
|
expect(generateScreenshotFilename(view)).toBe('media_camera-1.jpg');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user