From 1cc14ecacd3491cca11c59a074852b78d847b329 Mon Sep 17 00:00:00 2001 From: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com> Date: Tue, 15 Feb 2022 06:21:34 -0500 Subject: [PATCH] Make MediaService generic --- src/constants/license.js | 31 ++++++++++++-- src/constants/media.js | 20 +++++---- src/data/media-service.js | 39 ++++++++++------- src/store/media.js | 4 +- src/store/types.d.ts | 78 ++++++++++++++++------------------ src/utils/decode-data.js | 6 +-- src/utils/decode-media-data.js | 6 +++ tsconfig.json | 4 ++ 8 files changed, 113 insertions(+), 75 deletions(-) diff --git a/src/constants/license.js b/src/constants/license.js index 287229a2d5..ac593e8d7c 100644 --- a/src/constants/license.js +++ b/src/constants/license.js @@ -1,15 +1,31 @@ -export const CC_LICENSES = [ +export const CC_LICENSES = /** @type {const} */ ([ 'by', 'by-sa', 'by-nd', 'by-nc', 'by-nc-sa', 'by-nc-nd', -] +]) + +/** + * @template T + * @typedef {T extends `${infer P}-${infer PP}-${infer PPP}` ? [P, PP, PPP] : T extends `${infer P}-${infer PP}` ? [P, PP] : [T]} PartitionLicense + */ + +/** @typedef {PartitionLicense[number]} LicensePart */ + +/** @typedef {typeof CC_LICENSES[number]} CCLicense */ + +export const NON_CC_LICENSES = /** @type {const} */ (['cc0', 'pdm']) -export const NON_CC_LICENSES = ['cc0', 'pdm'] +/** @typedef {typeof NON_CC_LICENSES[number]} NonCCLicense */ -export const DEPRECATED_LICENSES = ['nc-sampling+', 'sampling+'] +export const DEPRECATED_LICENSES = /** @type {const} */ ([ + 'nc-sampling+', + 'sampling+', +]) + +/** @typedef {typeof DEPRECATED_LICENSES[number]} DeprecatedLicense */ export const ALL_LICENSES = [ ...CC_LICENSES, @@ -17,6 +33,13 @@ export const ALL_LICENSES = [ ...DEPRECATED_LICENSES, ] +/** @typedef {typeof ALL_LICENSES[number]} License */ + +export const MARKS = /** @type {const} */ (['cc0', 'pdm']) + +/** @typedef {typeof MARKS[number]} Mark */ + +/** @type {Record} */ export const LICENSE_ICON_MAPPING = { by: 'by', nc: 'nc', diff --git a/src/constants/media.js b/src/constants/media.js index 78dc7f0bdf..8126e7fe76 100644 --- a/src/constants/media.js +++ b/src/constants/media.js @@ -8,25 +8,27 @@ export const ALL_MEDIA = 'all' /** * Media types that the API supports. * These types also support custom filters. - * @type {MediaType[]} */ -export const supportedMediaTypes = [IMAGE, AUDIO] +export const supportedMediaTypes = /** @type {const} */ ([IMAGE, AUDIO]) /** * The types of content that users can search. `All` is also an option here. - * @type {MediaType[]} */ -export const supportedContentTypes = [ALL_MEDIA, IMAGE, AUDIO] +export const supportedContentTypes = /** @type {const} */ ([ + ALL_MEDIA, + IMAGE, + AUDIO, +]) -/** @typedef {'supported'|'beta'|'additional'} SupportStatus */ -/** @type {{SUPPORTED: SupportStatus, ADDITIONAL: SupportStatus, BETA: SupportStatus}}*/ -export const statuses = { +export const statuses = /** @type {const} */ ({ SUPPORTED: 'supported', BETA: 'beta', ADDITIONAL: 'additional', -} +}) + +/** @typedef {typeof statuses[keyof typeof statuses]} SupportStatus */ -/** @type {Object.} */ +/** @type {Record} */ export const contentStatus = { [ALL_MEDIA]: statuses.SUPPORTED, [IMAGE]: statuses.SUPPORTED, diff --git a/src/data/media-service.js b/src/data/media-service.js index a8fbe1848d..f0ab721863 100644 --- a/src/data/media-service.js +++ b/src/data/media-service.js @@ -2,28 +2,39 @@ import ApiService from '~/data/api-service' import decodeMediaData from '~/utils/decode-media-data' +/** + * @template {import('../store/types').FrontendMediaType} [T=any] + */ class MediaService { + /** + * @param {T} mediaType + */ constructor(mediaType) { + /** @type {T} */ this.mediaType = mediaType } + /** * Decodes the text data to avoid encoding problems. * Also, converts the results from an array of media objects into an object with * media id as keys. - * @param {import('../store/types').MediaResult} data - * @returns {import('../store/types').MediaResult} + * @param {import('../store/types').MediaResult} data + * @returns {import('../store/types').MediaStoreResult} */ transformResults(data) { - data.results = data.results.reduce((acc, item) => { - acc[item.id] = decodeMediaData(item, this.mediaType) - return acc - }, {}) - return data + return { + ...data, + results: data.results.reduce((acc, item) => { + acc[item.id] = decodeMediaData(item, this.mediaType) + return acc + }, /** @type {Record>} */ ({})), + } } + /** * Search for media items by keyword. - * @param {Object} params - * @return {Promise<{data: any}>} + * @param {import('../store/types').ApiQueryParams} params + * @return {Promise>>} */ search(params) { return ApiService.query(this.mediaType, params) @@ -32,9 +43,8 @@ class MediaService { /** * Retrieve media details by its id. * SSR-called - * @param {object} params - * @param {string} params.id - * @return {Promise<{data: any}>} + * @param {{ id: string }} params + * @return {Promise>>} */ getMediaDetail(params) { if (!params.id) { @@ -48,9 +58,8 @@ class MediaService { /** * Retrieve related media - * @param {object} params - * @param {string} params.id - * @return {Promise<{data: any}>} + * @param {{ id: string }} params + * @return {Promise>>} */ getRelatedMedia(params) { if (!params.id) { diff --git a/src/store/media.js b/src/store/media.js index cf9d8d5393..8f9674ca34 100644 --- a/src/store/media.js +++ b/src/store/media.js @@ -68,10 +68,10 @@ export const state = () => ({ image: {}, }) -export const mediaServices = { +export const mediaServices = /** @type {const} */ ({ [AUDIO]: new MediaService(AUDIO), [IMAGE]: new MediaService(IMAGE), -} +}) export const createActions = (services = mediaServices) => ({ /** diff --git a/src/store/types.d.ts b/src/store/types.d.ts index 0e6593253d..a0f1bd3c7d 100644 --- a/src/store/types.d.ts +++ b/src/store/types.d.ts @@ -1,11 +1,20 @@ /** * The search result object */ -export interface MediaResult { + +type FrontendMediaType = MediaDetail['frontendMediaType'] +export interface MediaResult> { result_count: number page_count: number page_size: number - results: T + results: + T extends FrontendMediaType + ? DetailFromMediaType + : T extends Array + ? DetailFromMediaType

[] + : T extends Record + ? Record> + : never } export type Query = { @@ -37,58 +46,48 @@ export type ApiQueryParams = { mature?: string } -/** - * Audio Properties returned by the API - */ -export type AudioDetail = { +export interface Tag { + name: string + provider: [string] +} + +export interface BaseMediaDetail { id: string foreign_landing_url: string creator?: string creator_url?: string url: string - license: string + title?: string + license: import('../constants/license').License | import('../constants/license').Mark license_version: string license_url: string provider: string source?: string - tags?: any + tags?: Tag[] attribution: string + detail_url: string + related_url: string + thumbnail?: string + frontendMediaType: FrontendMediaType +} + +export interface AudioDetail extends BaseMediaDetail<'audio'> { audio_set?: any genres?: any duration?: number bit_rate?: number sample_rate?: number alt_files?: any - detail_url: string - related_url: string filetype?: string - frontendMediaType?: 'audio' } -/** - * Image Properties returned by the API - */ -export type ImageDetail = { - id: string - title?: string - creator?: string - creator_url?: string - tags?: { name: string; provider: [string] }[] - url: string - thumbnail?: string - provider: string - source?: string - license: string - license_version: string - license_url: string - foreign_landing_url: string - detail_url: string - related_url: string +export interface ImageDetail extends BaseMediaDetail<'image'> { fields_matched?: string[] - frontendMediaType?: 'image' } -export const MediaDetail = AudioDetail | ImageDetail +export type MediaDetail = ImageDetail | AudioDetail + +export type DetailFromMediaType = T extends 'image' ? ImageDetail : T extends 'audio' ? AudioDetail : never export interface FilterItem { code: string @@ -131,24 +130,19 @@ export interface ActiveMediaState { status: 'ejected' | 'playing' | 'paused' // 'ejected' means player is closed } -export interface MediaStoreResult { - count: number - page?: number - pageCount: number - items: { [key: SupportedMediaType]: MediaDetail } -} +export interface MediaStoreResult extends MediaResult> {} export interface MediaState { results: { - audio: MediaStoreResult - image: MediaStoreResult + audio: MediaStoreResult + image: MediaStoreResult } fetchState: { audio: FetchState image: FetchState } - audio: Object | AudioDetail - image: Object | ImageDetail + audio: AudioDetail + image: ImageDetail } export interface MediaFetchState { diff --git a/src/utils/decode-data.js b/src/utils/decode-data.js index 66f9eb2625..dbe4a3b5fd 100644 --- a/src/utils/decode-data.js +++ b/src/utils/decode-data.js @@ -1,17 +1,17 @@ /** * decodes some edge cases of Unicode characters with an extra \ * See test cases for some examples - * @param {string} data + * @param {string} [data] */ export default function decodeData(data) { if (data) { try { const regexASCII = /\\x([\d\w]{2})/gi - const ascii = data.replace(regexASCII, (match, grp) => + const ascii = data.replace(regexASCII, (_, grp) => String.fromCharCode(parseInt(grp, 16)) ) const regexUni = /\\u([\d\w]{4})/gi - const uni = ascii.replace(regexUni, (match, grp) => + const uni = ascii.replace(regexUni, (_, grp) => String.fromCharCode(parseInt(grp, 16)) ) const res = decodeURI(uni) diff --git a/src/utils/decode-media-data.js b/src/utils/decode-media-data.js index 556152aeda..d92b879622 100644 --- a/src/utils/decode-media-data.js +++ b/src/utils/decode-media-data.js @@ -1,6 +1,12 @@ import decodeData from '~/utils/decode-data' import { IMAGE } from '~/constants/media' +/** + * @template {import('../store/types').MediaDetail} T + * @param {T} media + * @param {import('../constants/media').MediaType} mediaType + * @return {T} + */ export default function decodeMediaData(media, mediaType = IMAGE) { return { ...media, diff --git a/tsconfig.json b/tsconfig.json index 33381b6ae9..1a9b29107c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -39,10 +39,14 @@ "src/composables/use-browser-detection.js", "src/composables/use-media-query.js", "src/composables/window.js", + "src/constants/license.js", "src/constants/screens.js", "src/data/api-service.js", + "src/data/media-service.js", "src/data/usage-data-service.js", "src/store/user.js", + "src/utils/decode-data.js", + "src/utils/decode-media-data.js", "src/utils/key-codes.js", "src/utils/local.js", "src/utils/warn.js"