diff --git a/src/components/VErrorSection/VErrorImage.vue b/src/components/VErrorSection/VErrorImage.vue index 062da485ea..a477a5d2ee 100644 --- a/src/components/VErrorSection/VErrorImage.vue +++ b/src/components/VErrorSection/VErrorImage.vue @@ -51,6 +51,7 @@ export default defineComponent({ let image = errorItem.image const errorImage: ErrorImage = { ...image, + originalTitle: image.title, src: require(`~/assets/error_images/${image.file}.jpg`), alt: `error-images.${image.id}`, license: image.license as License, diff --git a/src/models/media.ts b/src/models/media.ts index 368d0d759a..9b9739d183 100644 --- a/src/models/media.ts +++ b/src/models/media.ts @@ -12,6 +12,7 @@ export interface Tag { export interface Media { id: string title: string + originalTitle: string creator?: string creator_url?: string diff --git a/src/utils/attribution-html.ts b/src/utils/attribution-html.ts index 00435f6efe..718578187e 100644 --- a/src/utils/attribution-html.ts +++ b/src/utils/attribution-html.ts @@ -114,7 +114,7 @@ const extLink = (href: string, text: string) => */ export type AttributableMedia = Pick< Media, - | 'title' + | 'originalTitle' | 'foreign_landing_url' | 'creator' | 'creator_url' @@ -162,10 +162,10 @@ export const getAttribution = ( /* Title */ - let title = mediaItem.title || tFn('generic-title') + let title = mediaItem.originalTitle || tFn('generic-title') if (!isPlaintext && mediaItem.foreign_landing_url) title = extLink(mediaItem.foreign_landing_url, title) - if (mediaItem.title) title = tFn('actual-title', { title }) + if (mediaItem.originalTitle) title = tFn('actual-title', { title }) /* License */ diff --git a/src/utils/decode-media-data.ts b/src/utils/decode-media-data.ts index 75a24e3361..a65fd3d2d9 100644 --- a/src/utils/decode-media-data.ts +++ b/src/utils/decode-media-data.ts @@ -1,14 +1,85 @@ -import { title } from 'case' +import { title as titleCase } from 'case' import { decodeData as decodeString } from '~/utils/decode-data' import type { Media, Tag } from '~/models/media' +import type { MediaType } from '~/constants/media' +import { AUDIO, IMAGE, MODEL_3D, VIDEO } from '~/constants/media' /** * This interface is a subset of `Media` that types dictionaries sent by the API * being decoded in the `decodeMediaData` function. */ -interface ApiMedia extends Omit { +interface ApiMedia + extends Omit { title?: string + originalTitle?: string +} + +const mediaTypeExtensions: Record = { + [IMAGE]: ['jpg', 'jpeg', 'png', 'gif', 'svg'], + [AUDIO]: ['mp3', 'wav', 'ogg', 'flac', 'aac', 'aiff', 'mp32'], + [VIDEO]: ['mp4', 'webm', 'mkv', 'avi', 'mov', 'wmv', 'flv', 'mpg', 'mpeg'], + [MODEL_3D]: ['fbx', 'obj', 'stl', 'dae', '3ds', 'blend', 'max', 'obj', 'ply'], +} + +const matchers = [/jpe?g$/i, /tiff?$/i, /mp32?$/i] + +/** + * Compares the filetypes, taking into account different versions of the same + * filetype. For example, `.jpg` and `.jpeg` are considered the same filetype. + * @param extension - the extension of the file. + * @param filetype - the type of the file. + */ +const isFiletypeMatching = (extension: string, filetype?: string) => { + if (filetype === extension) { + return true + } + if (!filetype) return false + return matchers.some((matcher) => + Boolean(filetype.match(matcher) && extension.match(matcher)) + ) +} +const extractPartAfterLastDot = (str?: string) => { + if (!str) { + return '' + } + const parts = str.split('.') + return parts.length ? parts[parts.length - 1].toLowerCase() : '' +} + +/** + * Strip the extension from title if it matches the filetype of the media. + * Since not all media records return filetype, we also try to guess the filetype + * from the url extension. + */ +const stripExtension = ( + title: string, + mediaType: MediaType, + media: ApiMedia +) => { + const filetype = media.filetype ?? extractPartAfterLastDot(media.url) + const titleParts = title.split('.') + if ( + mediaTypeExtensions[mediaType].includes(filetype) && + isFiletypeMatching(extractPartAfterLastDot(title), filetype) + ) { + titleParts.pop() + } + return titleParts.join('.') +} +/** + * Corrects the encoding of the media title, or uses the media type as the title. + * If the title has a file extension that matches media filetype, it will be stripped. + */ +const mediaTitle = ( + media: ApiMedia, + mediaType: MediaType +): { title: string; originalTitle: string } => { + const originalTitle = decodeString(media.title) || titleCase(mediaType) + return { + originalTitle, + title: stripExtension(originalTitle, mediaType, media), + } } /** @@ -25,8 +96,8 @@ export const decodeMediaData = ( ): T => ({ ...media, + ...mediaTitle(media, mediaType), frontendMediaType: mediaType, - title: decodeString(media.title) || title(mediaType), creator: decodeString(media.creator), // TODO: remove `?? []` tags: (media.tags ?? ([] as Tag[])).map((tag) => ({ diff --git a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-2xl-linux.png b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-2xl-linux.png index c242659ba7..6e1f1dbb92 100644 Binary files a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-2xl-linux.png and b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-2xl-linux.png differ diff --git a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-lg-linux.png b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-lg-linux.png index a3658eebf8..3c5d66d3fc 100644 Binary files a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-lg-linux.png and b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-lg-linux.png differ diff --git a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-md-linux.png b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-md-linux.png index b12dd89708..2c81b894a1 100644 Binary files a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-md-linux.png and b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-md-linux.png differ diff --git a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-xl-linux.png b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-xl-linux.png index 30828d9541..5c5c03cc78 100644 Binary files a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-xl-linux.png and b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-desktop-xl-linux.png differ diff --git a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-desktop-UA-sm-linux.png b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-desktop-UA-sm-linux.png index d928d6b270..8bff3b7966 100644 Binary files a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-desktop-UA-sm-linux.png and b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-desktop-UA-sm-linux.png differ diff --git a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-desktop-UA-xs-linux.png b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-desktop-UA-xs-linux.png index 6c233f3c71..bbc4869927 100644 Binary files a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-desktop-UA-xs-linux.png and b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-desktop-UA-xs-linux.png differ diff --git a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-mobile-UA-sm-linux.png b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-mobile-UA-sm-linux.png index 6094e8d92b..f8e03b7ad2 100644 Binary files a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-mobile-UA-sm-linux.png and b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-mobile-UA-sm-linux.png differ diff --git a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-mobile-UA-xs-linux.png b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-mobile-UA-xs-linux.png index 015de121af..7bff6efe9c 100644 Binary files a/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-mobile-UA-xs-linux.png and b/test/playwright/visual-regression/components/audio-results.spec.ts-snapshots/audio-results-narrow-viewport-mobile-UA-xs-linux.png differ diff --git a/test/unit/specs/utils/attribution-html.spec.ts b/test/unit/specs/utils/attribution-html.spec.ts index 0c3fbcb310..01da747a72 100644 --- a/test/unit/specs/utils/attribution-html.spec.ts +++ b/test/unit/specs/utils/attribution-html.spec.ts @@ -11,7 +11,7 @@ const i18n = new Vuei18n({ }) const mediaItem: AttributableMedia = { - title: 'Title', + originalTitle: 'Title', foreign_landing_url: 'https://foreign.landing/url', creator: 'Creator', creator_url: 'https://creator/url', @@ -44,7 +44,7 @@ describe('getAttribution', () => { ) it('uses generic title if not known', () => { - const mediaItemNoTitle = { ...mediaItem, title: '' } + const mediaItemNoTitle = { ...mediaItem, originalTitle: '' } const attrText = getAttribution(mediaItemNoTitle, i18n, { isPlaintext: true, }) diff --git a/test/unit/specs/utils/decode-image-data.spec.js b/test/unit/specs/utils/decode-image-data.spec.js index 697f0e0a0c..ef28fab23b 100644 --- a/test/unit/specs/utils/decode-image-data.spec.js +++ b/test/unit/specs/utils/decode-image-data.spec.js @@ -1,16 +1,36 @@ import { decodeMediaData } from '~/utils/decode-media-data' import { IMAGE } from '~/constants/media' +const requiredFields = { + id: 'id', + url: 'https://example.com/image.jpg', + foreign_landing_url: 'https://example.com', + license: 'by', + license_version: '4.0', + attribution: 'Attribution', + + category: null, + provider: 'provider', + + detail_url: 'url', + related_url: 'url', + + tags: [], +} + describe('decodeImageData', () => { - it('returns empty string for empty string', () => { + it('decodes symbols correctly', () => { const data = { + ...requiredFields, creator: 'S\\xe3', title: 'S\\xe9', tags: [{ name: 'ma\\xdf' }], } const expected = { + ...requiredFields, title: 'Sé', + originalTitle: 'Sé', creator: 'Sã', tags: [{ name: 'maß' }], frontendMediaType: IMAGE, @@ -18,4 +38,63 @@ describe('decodeImageData', () => { expect(decodeMediaData(data, IMAGE)).toEqual(expected) }) + + it('strips the extension if the same as media filetype', () => { + const data = { + ...requiredFields, + creator: 'Creator', + title: 'Image.JPEG', + filetype: 'jpg', + } + + const expected = { + ...requiredFields, + title: 'Image', + originalTitle: 'Image.JPEG', + creator: 'Creator', + filetype: 'jpg', + frontendMediaType: IMAGE, + } + + expect(decodeMediaData(data, IMAGE)).toEqual(expected) + }) + + it('strips the extension if the same as url extension', () => { + const data = { + ...requiredFields, + url: 'https://example.com/image.jpg', + creator: 'Creator', + title: 'Image.JPG', + } + + const expected = { + ...requiredFields, + title: 'Image', + originalTitle: 'Image.JPG', + creator: 'Creator', + frontendMediaType: IMAGE, + } + + expect(decodeMediaData(data, IMAGE)).toEqual(expected) + }) + + it('does not strip the extension if different from filetype in url extension', () => { + const data = { + ...requiredFields, + url: 'https://example.com/image.png', + creator: 'Creator', + title: 'Image.JPG', + } + + const expected = { + ...requiredFields, + url: 'https://example.com/image.png', + title: 'Image.JPG', + originalTitle: 'Image.JPG', + creator: 'Creator', + frontendMediaType: IMAGE, + } + + expect(decodeMediaData(data, IMAGE)).toEqual(expected) + }) })