Add model cache for video
When fetching only immutable attributes
This commit is contained in:
parent
e436baf0b0
commit
7eba5e1fa8
|
@ -37,7 +37,7 @@ import { buildDislikeActivity } from '../../lib/activitypub/send/send-dislike'
|
||||||
import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
|
import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
|
||||||
import { VideoPlaylistModel } from '../../models/video/video-playlist'
|
import { VideoPlaylistModel } from '../../models/video/video-playlist'
|
||||||
import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
|
import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
|
||||||
import { MAccountId, MActorId, MVideo, MVideoAPWithoutCaption } from '@server/typings/models'
|
import { MAccountId, MActorId, MVideo, MVideoAPWithoutCaption, MVideoId } from '@server/typings/models'
|
||||||
|
|
||||||
const activityPubClientRouter = express.Router()
|
const activityPubClientRouter = express.Router()
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ activityPubClientRouter.get('/videos/watch/:id/activity',
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get('/videos/watch/:id/announces',
|
activityPubClientRouter.get('/videos/watch/:id/announces',
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
asyncMiddleware(videosCustomGetValidator('only-video')),
|
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
|
||||||
asyncMiddleware(videoAnnouncesController)
|
asyncMiddleware(videoAnnouncesController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
|
activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
|
||||||
|
@ -95,17 +95,17 @@ activityPubClientRouter.get('/videos/watch/:id/announces/:actorId',
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get('/videos/watch/:id/likes',
|
activityPubClientRouter.get('/videos/watch/:id/likes',
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
asyncMiddleware(videosCustomGetValidator('only-video')),
|
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
|
||||||
asyncMiddleware(videoLikesController)
|
asyncMiddleware(videoLikesController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get('/videos/watch/:id/dislikes',
|
activityPubClientRouter.get('/videos/watch/:id/dislikes',
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
asyncMiddleware(videosCustomGetValidator('only-video')),
|
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
|
||||||
asyncMiddleware(videoDislikesController)
|
asyncMiddleware(videoDislikesController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get('/videos/watch/:id/comments',
|
activityPubClientRouter.get('/videos/watch/:id/comments',
|
||||||
executeIfActivityPub,
|
executeIfActivityPub,
|
||||||
asyncMiddleware(videosCustomGetValidator('only-video')),
|
asyncMiddleware(videosCustomGetValidator('only-immutable-attributes')),
|
||||||
asyncMiddleware(videoCommentsController)
|
asyncMiddleware(videoCommentsController)
|
||||||
)
|
)
|
||||||
activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId',
|
activityPubClientRouter.get('/videos/watch/:videoId/comments/:commentId',
|
||||||
|
@ -238,7 +238,7 @@ async function videoAnnounceController (req: express.Request, res: express.Respo
|
||||||
}
|
}
|
||||||
|
|
||||||
async function videoAnnouncesController (req: express.Request, res: express.Response) {
|
async function videoAnnouncesController (req: express.Request, res: express.Response) {
|
||||||
const video = res.locals.onlyVideo
|
const video = res.locals.onlyImmutableVideo
|
||||||
|
|
||||||
const handler = async (start: number, count: number) => {
|
const handler = async (start: number, count: number) => {
|
||||||
const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count)
|
const result = await VideoShareModel.listAndCountByVideoId(video.id, start, count)
|
||||||
|
@ -253,21 +253,21 @@ async function videoAnnouncesController (req: express.Request, res: express.Resp
|
||||||
}
|
}
|
||||||
|
|
||||||
async function videoLikesController (req: express.Request, res: express.Response) {
|
async function videoLikesController (req: express.Request, res: express.Response) {
|
||||||
const video = res.locals.onlyVideo
|
const video = res.locals.onlyImmutableVideo
|
||||||
const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video))
|
const json = await videoRates(req, 'like', video, getVideoLikesActivityPubUrl(video))
|
||||||
|
|
||||||
return activityPubResponse(activityPubContextify(json), res)
|
return activityPubResponse(activityPubContextify(json), res)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function videoDislikesController (req: express.Request, res: express.Response) {
|
async function videoDislikesController (req: express.Request, res: express.Response) {
|
||||||
const video = res.locals.onlyVideo
|
const video = res.locals.onlyImmutableVideo
|
||||||
const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video))
|
const json = await videoRates(req, 'dislike', video, getVideoDislikesActivityPubUrl(video))
|
||||||
|
|
||||||
return activityPubResponse(activityPubContextify(json), res)
|
return activityPubResponse(activityPubContextify(json), res)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function videoCommentsController (req: express.Request, res: express.Response) {
|
async function videoCommentsController (req: express.Request, res: express.Response) {
|
||||||
const video = res.locals.onlyVideo
|
const video = res.locals.onlyImmutableVideo
|
||||||
|
|
||||||
const handler = async (start: number, count: number) => {
|
const handler = async (start: number, count: number) => {
|
||||||
const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count)
|
const result = await VideoCommentModel.listAndCountByVideoId(video.id, start, count)
|
||||||
|
@ -386,7 +386,7 @@ async function actorPlaylists (req: express.Request, account: MAccountId) {
|
||||||
return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
|
return activityPubCollectionPagination(WEBSERVER.URL + req.path, handler, req.query.page)
|
||||||
}
|
}
|
||||||
|
|
||||||
function videoRates (req: express.Request, rateType: VideoRateType, video: MVideo, url: string) {
|
function videoRates (req: express.Request, rateType: VideoRateType, video: MVideoId, url: string) {
|
||||||
const handler = async (start: number, count: number) => {
|
const handler = async (start: number, count: number) => {
|
||||||
const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
|
const result = await AccountVideoRateModel.listAndCountAccountUrlsByVideoId(rateType, video.id, start, count)
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -2,7 +2,16 @@ import { Response } from 'express'
|
||||||
import { fetchVideo, VideoFetchType } from '../video'
|
import { fetchVideo, VideoFetchType } from '../video'
|
||||||
import { UserRight } from '../../../shared/models/users'
|
import { UserRight } from '../../../shared/models/users'
|
||||||
import { VideoChannelModel } from '../../models/video/video-channel'
|
import { VideoChannelModel } from '../../models/video/video-channel'
|
||||||
import { MUser, MUserAccountId, MVideoAccountLight, MVideoFullLight, MVideoThumbnail, MVideoWithRights } from '@server/typings/models'
|
import {
|
||||||
|
MUser,
|
||||||
|
MUserAccountId,
|
||||||
|
MVideoAccountLight,
|
||||||
|
MVideoFullLight,
|
||||||
|
MVideoIdThumbnail,
|
||||||
|
MVideoImmutable,
|
||||||
|
MVideoThumbnail,
|
||||||
|
MVideoWithRights
|
||||||
|
} from '@server/typings/models'
|
||||||
|
|
||||||
async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') {
|
async function doesVideoExist (id: number | string, res: Response, fetchType: VideoFetchType = 'all') {
|
||||||
const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
||||||
|
@ -22,8 +31,12 @@ async function doesVideoExist (id: number | string, res: Response, fetchType: Vi
|
||||||
res.locals.videoAll = video as MVideoFullLight
|
res.locals.videoAll = video as MVideoFullLight
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'only-immutable-attributes':
|
||||||
|
res.locals.onlyImmutableVideo = video as MVideoImmutable
|
||||||
|
break
|
||||||
|
|
||||||
case 'id':
|
case 'id':
|
||||||
res.locals.videoId = video
|
res.locals.videoId = video as MVideoIdThumbnail
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'only-video':
|
case 'only-video':
|
||||||
|
|
|
@ -5,13 +5,15 @@ import {
|
||||||
MVideoFullLight,
|
MVideoFullLight,
|
||||||
MVideoIdThumbnail,
|
MVideoIdThumbnail,
|
||||||
MVideoThumbnail,
|
MVideoThumbnail,
|
||||||
MVideoWithRights
|
MVideoWithRights,
|
||||||
|
MVideoImmutable
|
||||||
} from '@server/typings/models'
|
} from '@server/typings/models'
|
||||||
import { Response } from 'express'
|
import { Response } from 'express'
|
||||||
|
|
||||||
type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none'
|
type VideoFetchType = 'all' | 'only-video' | 'only-video-with-rights' | 'id' | 'none' | 'only-immutable-attributes'
|
||||||
|
|
||||||
function fetchVideo (id: number | string, fetchType: 'all', userId?: number): Bluebird<MVideoFullLight>
|
function fetchVideo (id: number | string, fetchType: 'all', userId?: number): Bluebird<MVideoFullLight>
|
||||||
|
function fetchVideo (id: number | string, fetchType: 'only-immutable-attributes'): Bluebird<MVideoImmutable>
|
||||||
function fetchVideo (id: number | string, fetchType: 'only-video', userId?: number): Bluebird<MVideoThumbnail>
|
function fetchVideo (id: number | string, fetchType: 'only-video', userId?: number): Bluebird<MVideoThumbnail>
|
||||||
function fetchVideo (id: number | string, fetchType: 'only-video-with-rights', userId?: number): Bluebird<MVideoWithRights>
|
function fetchVideo (id: number | string, fetchType: 'only-video-with-rights', userId?: number): Bluebird<MVideoWithRights>
|
||||||
function fetchVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Bluebird<MVideoIdThumbnail>
|
function fetchVideo (id: number | string, fetchType: 'id' | 'none', userId?: number): Bluebird<MVideoIdThumbnail>
|
||||||
|
@ -19,14 +21,16 @@ function fetchVideo (
|
||||||
id: number | string,
|
id: number | string,
|
||||||
fetchType: VideoFetchType,
|
fetchType: VideoFetchType,
|
||||||
userId?: number
|
userId?: number
|
||||||
): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail>
|
): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable>
|
||||||
function fetchVideo (
|
function fetchVideo (
|
||||||
id: number | string,
|
id: number | string,
|
||||||
fetchType: VideoFetchType,
|
fetchType: VideoFetchType,
|
||||||
userId?: number
|
userId?: number
|
||||||
): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail> {
|
): Bluebird<MVideoFullLight | MVideoThumbnail | MVideoWithRights | MVideoIdThumbnail | MVideoImmutable> {
|
||||||
if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
|
if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
|
||||||
|
|
||||||
|
if (fetchType === 'only-immutable-attributes') return VideoModel.loadImmutableAttributes(id)
|
||||||
|
|
||||||
if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id)
|
if (fetchType === 'only-video-with-rights') return VideoModel.loadWithRights(id)
|
||||||
|
|
||||||
if (fetchType === 'only-video') return VideoModel.load(id)
|
if (fetchType === 'only-video') return VideoModel.load(id)
|
||||||
|
|
|
@ -147,7 +147,10 @@ async function checkVideoFollowConstraints (req: express.Request, res: express.R
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const videosCustomGetValidator = (fetchType: 'all' | 'only-video' | 'only-video-with-rights', authenticateInQuery = false) => {
|
const videosCustomGetValidator = (
|
||||||
|
fetchType: 'all' | 'only-video' | 'only-video-with-rights' | 'only-immutable-attributes',
|
||||||
|
authenticateInQuery = false
|
||||||
|
) => {
|
||||||
return [
|
return [
|
||||||
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,10 @@ type ModelCacheType =
|
||||||
'local-account-name'
|
'local-account-name'
|
||||||
| 'local-actor-name'
|
| 'local-actor-name'
|
||||||
| 'local-actor-url'
|
| 'local-actor-url'
|
||||||
|
| 'video-immutable'
|
||||||
|
|
||||||
|
type DeleteKey =
|
||||||
|
'video'
|
||||||
|
|
||||||
class ModelCache {
|
class ModelCache {
|
||||||
|
|
||||||
|
@ -14,7 +18,14 @@ class ModelCache {
|
||||||
private readonly localCache: { [id in ModelCacheType]: Map<string, any> } = {
|
private readonly localCache: { [id in ModelCacheType]: Map<string, any> } = {
|
||||||
'local-account-name': new Map(),
|
'local-account-name': new Map(),
|
||||||
'local-actor-name': new Map(),
|
'local-actor-name': new Map(),
|
||||||
'local-actor-url': new Map()
|
'local-actor-url': new Map(),
|
||||||
|
'video-immutable': new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly deleteIds: {
|
||||||
|
[deleteKey in DeleteKey]: Map<number, { cacheType: ModelCacheType, key: string }[]>
|
||||||
|
} = {
|
||||||
|
video: new Map()
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor () {
|
private constructor () {
|
||||||
|
@ -29,8 +40,9 @@ class ModelCache {
|
||||||
key: string
|
key: string
|
||||||
fun: () => Bluebird<T>
|
fun: () => Bluebird<T>
|
||||||
whitelist?: () => boolean
|
whitelist?: () => boolean
|
||||||
|
deleteKey?: DeleteKey
|
||||||
}) {
|
}) {
|
||||||
const { cacheType, key, fun, whitelist } = options
|
const { cacheType, key, fun, whitelist, deleteKey } = options
|
||||||
|
|
||||||
if (whitelist && whitelist() !== true) return fun()
|
if (whitelist && whitelist() !== true) return fun()
|
||||||
|
|
||||||
|
@ -42,11 +54,34 @@ class ModelCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
return fun().then(m => {
|
return fun().then(m => {
|
||||||
|
if (!m) return m
|
||||||
|
|
||||||
if (!whitelist || whitelist()) cache.set(key, m)
|
if (!whitelist || whitelist()) cache.set(key, m)
|
||||||
|
|
||||||
|
if (deleteKey) {
|
||||||
|
const map = this.deleteIds[deleteKey]
|
||||||
|
if (!map.has(m.id)) map.set(m.id, [])
|
||||||
|
|
||||||
|
const a = map.get(m.id)
|
||||||
|
a.push({ cacheType, key })
|
||||||
|
}
|
||||||
|
|
||||||
return m
|
return m
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invalidateCache (deleteKey: DeleteKey, modelId: number) {
|
||||||
|
const map = this.deleteIds[deleteKey]
|
||||||
|
|
||||||
|
if (!map.has(modelId)) return
|
||||||
|
|
||||||
|
for (const toDelete of map.get(modelId)) {
|
||||||
|
logger.debug('Removing %s -> %d of model cache %s -> %s.', deleteKey, modelId, toDelete.cacheType, toDelete.key)
|
||||||
|
this.localCache[toDelete.cacheType].delete(toDelete.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
map.delete(modelId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|
|
@ -120,7 +120,7 @@ import {
|
||||||
MVideoFormattableDetails,
|
MVideoFormattableDetails,
|
||||||
MVideoForUser,
|
MVideoForUser,
|
||||||
MVideoFullLight,
|
MVideoFullLight,
|
||||||
MVideoIdThumbnail,
|
MVideoIdThumbnail, MVideoImmutable,
|
||||||
MVideoThumbnail,
|
MVideoThumbnail,
|
||||||
MVideoThumbnailBlacklist,
|
MVideoThumbnailBlacklist,
|
||||||
MVideoWithAllFiles,
|
MVideoWithAllFiles,
|
||||||
|
@ -132,6 +132,7 @@ import { MThumbnail } from '../../typings/models/video/thumbnail'
|
||||||
import { VideoFile } from '@shared/models/videos/video-file.model'
|
import { VideoFile } from '@shared/models/videos/video-file.model'
|
||||||
import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
|
import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
|
||||||
import validator from 'validator'
|
import validator from 'validator'
|
||||||
|
import { ModelCache } from '@server/models/model-cache'
|
||||||
|
|
||||||
export enum ScopeNames {
|
export enum ScopeNames {
|
||||||
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
|
AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
|
||||||
|
@ -1074,6 +1075,11 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@BeforeDestroy
|
||||||
|
static invalidateCache (instance: VideoModel) {
|
||||||
|
ModelCache.Instance.invalidateCache('video', instance.id)
|
||||||
|
}
|
||||||
|
|
||||||
static listLocal (): Bluebird<MVideoWithAllFiles[]> {
|
static listLocal (): Bluebird<MVideoWithAllFiles[]> {
|
||||||
const query = {
|
const query = {
|
||||||
where: {
|
where: {
|
||||||
|
@ -1468,6 +1474,28 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
]).findOne(options)
|
]).findOne(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static loadImmutableAttributes (id: number | string, t?: Transaction): Bluebird<MVideoImmutable> {
|
||||||
|
const fun = () => {
|
||||||
|
const where = buildWhereIdOrUUID(id)
|
||||||
|
const options = {
|
||||||
|
attributes: [
|
||||||
|
'id', 'url', 'uuid'
|
||||||
|
],
|
||||||
|
where,
|
||||||
|
transaction: t
|
||||||
|
}
|
||||||
|
|
||||||
|
return VideoModel.unscoped().findOne(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ModelCache.Instance.doCache({
|
||||||
|
cacheType: 'video-immutable',
|
||||||
|
key: '' + id,
|
||||||
|
deleteKey: 'video',
|
||||||
|
fun
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
|
static loadWithRights (id: number | string, t?: Transaction): Bluebird<MVideoWithRights> {
|
||||||
const where = buildWhereIdOrUUID(id)
|
const where = buildWhereIdOrUUID(id)
|
||||||
const options = {
|
const options = {
|
||||||
|
|
|
@ -21,7 +21,7 @@ import {
|
||||||
} from './models'
|
} from './models'
|
||||||
import { MVideoPlaylistFull, MVideoPlaylistFullSummary } from './models/video/video-playlist'
|
import { MVideoPlaylistFull, MVideoPlaylistFullSummary } from './models/video/video-playlist'
|
||||||
import { MVideoImportDefault } from '@server/typings/models/video/video-import'
|
import { MVideoImportDefault } from '@server/typings/models/video/video-import'
|
||||||
import { MAccountBlocklist, MActorUrl, MStreamingPlaylist, MVideoFile } from '@server/typings/models'
|
import { MAccountBlocklist, MActorUrl, MStreamingPlaylist, MVideoFile, MVideoImmutable } from '@server/typings/models'
|
||||||
import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/typings/models/video/video-playlist-element'
|
import { MVideoPlaylistElement, MVideoPlaylistElementVideoUrlPlaylistPrivacy } from '@server/typings/models/video/video-playlist-element'
|
||||||
import { MAccountVideoRateAccountVideo } from '@server/typings/models/video/video-rate'
|
import { MAccountVideoRateAccountVideo } from '@server/typings/models/video/video-rate'
|
||||||
import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership'
|
import { MVideoChangeOwnershipFull } from './models/video/video-change-ownership'
|
||||||
|
@ -35,6 +35,7 @@ declare module 'express' {
|
||||||
|
|
||||||
locals: {
|
locals: {
|
||||||
videoAll?: MVideoFullLight
|
videoAll?: MVideoFullLight
|
||||||
|
onlyImmutableVideo?: MVideoImmutable
|
||||||
onlyVideo?: MVideoThumbnail
|
onlyVideo?: MVideoThumbnail
|
||||||
onlyVideoWithRights?: MVideoWithRights
|
onlyVideoWithRights?: MVideoWithRights
|
||||||
videoId?: MVideoIdThumbnail
|
videoId?: MVideoIdThumbnail
|
||||||
|
|
|
@ -37,6 +37,7 @@ export type MVideoId = Pick<MVideo, 'id'>
|
||||||
export type MVideoUrl = Pick<MVideo, 'url'>
|
export type MVideoUrl = Pick<MVideo, 'url'>
|
||||||
export type MVideoUUID = Pick<MVideo, 'uuid'>
|
export type MVideoUUID = Pick<MVideo, 'uuid'>
|
||||||
|
|
||||||
|
export type MVideoImmutable = Pick<MVideo, 'id' | 'url' | 'uuid'>
|
||||||
export type MVideoIdUrl = MVideoId & MVideoUrl
|
export type MVideoIdUrl = MVideoId & MVideoUrl
|
||||||
export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'>
|
export type MVideoFeed = Pick<MVideo, 'name' | 'uuid'>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue