Skip to content
This repository has been archived by the owner on Feb 22, 2023. It is now read-only.

Make MediaService generic #868

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions src/constants/license.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,45 @@
export const CC_LICENSES = [
export const CC_LICENSES = /** @type {const} */ ([
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sarayourfriend , can you give me a reference I can read about the type {const} declaration? I couldn't find anything searching for typescript const type.

The reason I'm asking this is the messages I see in my IDEs.
WebStorm is trying to find something in the node_modules, I guess?

Screen Shot 2022-02-15 at 3 14 24 PM

VS Code just shows it as unresolved, even when I set language mode to TypeScript (I don't know what other information can I get about VS Code TS hints)

Screen Shot 2022-02-15 at 3 14 45 PM

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the JSDoc version of this feature: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions

The TypeScript JSDoc documentation for it is here: https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#casts (the second example in that section)

I'm not sure why TS isn't picking it up. In VSCode have you tried running the "Restart TS Service" command? I have to do that anytime tsconfig.json changes.

For WebStorm it might be the same. You will probably also have to enable the TS language server using the instructions described here: https://www.jetbrains.com/help/webstorm/typescript-support.html#ws_ts_use_ts_service_checkbox

'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<CCLicense>[number]} LicensePart */
Comment on lines +10 to +15
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just me being kind of silly with TS string types. We could just as easily (and probably should just) use a type directly as 'by' | 'sa' | 'nd' | 'nc' 😝

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to agree with your comment 😆 I mean it looks very interesting, but it would probably take me years to understand what it means :)


/** @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,
...NON_CC_LICENSES,
...DEPRECATED_LICENSES,
]

/** @typedef {typeof ALL_LICENSES[number]} License */

export const MARKS = /** @type {const} */ (['cc0', 'pdm'])

/** @typedef {typeof MARKS[number]} Mark */

/** @type {Record<LicensePart | Mark, string>} */
export const LICENSE_ICON_MAPPING = {
by: 'by',
nc: 'nc',
Expand Down
20 changes: 11 additions & 9 deletions src/constants/media.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.<MediaType, SupportStatus>} */
/** @type {Record<MediaType, SupportStatus>} */
export const contentStatus = {
[ALL_MEDIA]: statuses.SUPPORTED,
[IMAGE]: statuses.SUPPORTED,
Expand Down
39 changes: 24 additions & 15 deletions src/data/media-service.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<import('../store/types').MediaDetail[]|{}>} data
* @returns {import('../store/types').MediaResult<import('../store/types').MediaStoreResult>}
* @param {import('../store/types').MediaResult<T[]>} data
* @returns {import('../store/types').MediaStoreResult<T>}
*/
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<string, import('../store/types').DetailFromMediaType<T>>} */ ({})),
}
}

/**
* Search for media items by keyword.
* @param {Object} params
* @return {Promise<{data: any}>}
* @param {import('../store/types').ApiQueryParams} params
* @return {Promise<import('axios').AxiosResponse<import('../store/types').MediaResult<T[]>>>}
*/
search(params) {
return ApiService.query(this.mediaType, params)
Expand All @@ -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<import('axios').AxiosResponse<import('../store/types').MediaResult<T>>>}
*/
getMediaDetail(params) {
if (!params.id) {
Expand All @@ -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<import('axios').AxiosResponse<import('../store/types').MediaResult<T[]>>>}
*/
getRelatedMedia(params) {
if (!params.id) {
Expand Down
4 changes: 2 additions & 2 deletions src/store/media.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
/**
Expand Down
78 changes: 36 additions & 42 deletions src/store/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
/**
* The search result object
*/
export interface MediaResult<T> {

type FrontendMediaType = MediaDetail['frontendMediaType']
export interface MediaResult<T extends FrontendMediaType | FrontendMediaType[] | Record<unknown, FrontendMediaType>> {
result_count: number
page_count: number
page_size: number
results: T
results:
T extends FrontendMediaType
? DetailFromMediaType<T>
: T extends Array<infer P>
? DetailFromMediaType<P>[]
: T extends Record<infer K, infer P>
? Record<K, DetailFromMediaType<P>>
: never
}

export type Query = {
Expand Down Expand Up @@ -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<FrontendMediaType extends string> {
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 MediaDetail['frontendMediaType']> = T extends 'image' ? ImageDetail : T extends 'audio' ? AudioDetail : never

export interface FilterItem {
code: string
Expand Down Expand Up @@ -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<T extends FrontendMediaType> extends MediaResult<Record<MediaDetail['id'], T>> {}

export interface MediaState {
results: {
audio: MediaStoreResult
image: MediaStoreResult
audio: MediaStoreResult<AudioDetail>
image: MediaStoreResult<ImageDetail>
}
fetchState: {
audio: FetchState
image: FetchState
}
audio: Object | AudioDetail
image: Object | ImageDetail
audio: AudioDetail
image: ImageDetail
}

export interface MediaFetchState {
Expand Down
6 changes: 3 additions & 3 deletions src/utils/decode-data.js
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
6 changes: 6 additions & 0 deletions src/utils/decode-media-data.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
4 changes: 4 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down