Add user history and resume videos
This commit is contained in:
parent
a585824160
commit
6e46de095d
|
@ -2,9 +2,11 @@
|
||||||
[routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name"
|
[routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name"
|
||||||
class="video-thumbnail"
|
class="video-thumbnail"
|
||||||
>
|
>
|
||||||
<img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" />
|
<img alt="" [attr.aria-labelledby]="video.name" [attr.src]="getImageUrl()" [ngClass]="{ 'blur-filter': nsfw }" />
|
||||||
|
|
||||||
<div class="video-thumbnail-overlay">
|
<div class="video-thumbnail-overlay">{{ video.durationLabel }}</div>
|
||||||
{{ video.durationLabel }}
|
|
||||||
</div>
|
<div class="progress-bar" *ngIf="video.userHistory?.currentTime">
|
||||||
|
<div [ngStyle]="{ 'width.%': getProgressPercent() }"></div>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -29,6 +29,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
height: 3px;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
top: -3px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.20);
|
||||||
|
|
||||||
|
div {
|
||||||
|
height: 100%;
|
||||||
|
background-color: var(--mainColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.video-thumbnail-overlay {
|
.video-thumbnail-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 5px;
|
right: 5px;
|
||||||
|
|
|
@ -22,4 +22,12 @@ export class VideoThumbnailComponent {
|
||||||
|
|
||||||
return this.video.thumbnailUrl
|
return this.video.thumbnailUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProgressPercent () {
|
||||||
|
if (!this.video.userHistory) return 0
|
||||||
|
|
||||||
|
const currentTime = this.video.userHistory.currentTime
|
||||||
|
|
||||||
|
return (currentTime / this.video.duration) * 100
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,10 @@ export class Video implements VideoServerModel {
|
||||||
avatar: Avatar
|
avatar: Avatar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userHistory?: {
|
||||||
|
currentTime: number
|
||||||
|
}
|
||||||
|
|
||||||
static buildClientUrl (videoUUID: string) {
|
static buildClientUrl (videoUUID: string) {
|
||||||
return '/videos/watch/' + videoUUID
|
return '/videos/watch/' + videoUUID
|
||||||
}
|
}
|
||||||
|
@ -116,6 +120,8 @@ export class Video implements VideoServerModel {
|
||||||
|
|
||||||
this.blacklisted = hash.blacklisted
|
this.blacklisted = hash.blacklisted
|
||||||
this.blacklistedReason = hash.blacklistedReason
|
this.blacklistedReason = hash.blacklistedReason
|
||||||
|
|
||||||
|
this.userHistory = hash.userHistory
|
||||||
}
|
}
|
||||||
|
|
||||||
isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
|
isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
|
||||||
|
|
|
@ -58,6 +58,10 @@ export class VideoService implements VideosProvider {
|
||||||
return VideoService.BASE_VIDEO_URL + uuid + '/views'
|
return VideoService.BASE_VIDEO_URL + uuid + '/views'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUserWatchingVideoUrl (uuid: string) {
|
||||||
|
return VideoService.BASE_VIDEO_URL + uuid + '/watching'
|
||||||
|
}
|
||||||
|
|
||||||
getVideo (uuid: string): Observable<VideoDetails> {
|
getVideo (uuid: string): Observable<VideoDetails> {
|
||||||
return this.serverService.localeObservable
|
return this.serverService.localeObservable
|
||||||
.pipe(
|
.pipe(
|
||||||
|
|
|
@ -369,7 +369,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], startTime = 0) {
|
private async onVideoFetched (video: VideoDetails, videoCaptions: VideoCaption[], startTimeFromUrl: number) {
|
||||||
this.video = video
|
this.video = video
|
||||||
|
|
||||||
// Re init attributes
|
// Re init attributes
|
||||||
|
@ -377,6 +377,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
this.completeDescriptionShown = false
|
this.completeDescriptionShown = false
|
||||||
this.remoteServerDown = false
|
this.remoteServerDown = false
|
||||||
|
|
||||||
|
let startTime = startTimeFromUrl || (this.video.userHistory ? this.video.userHistory.currentTime : 0)
|
||||||
|
// Don't start the video if we are at the end
|
||||||
|
if (this.video.duration - startTime <= 1) startTime = 0
|
||||||
|
|
||||||
if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) {
|
if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) {
|
||||||
const res = await this.confirmService.confirm(
|
const res = await this.confirmService.confirm(
|
||||||
this.i18n('This video contains mature or explicit content. Are you sure you want to watch it?'),
|
this.i18n('This video contains mature or explicit content. Are you sure you want to watch it?'),
|
||||||
|
@ -414,7 +418,12 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
poster: this.video.previewUrl,
|
poster: this.video.previewUrl,
|
||||||
startTime,
|
startTime,
|
||||||
theaterMode: true,
|
theaterMode: true,
|
||||||
language: this.localeId
|
language: this.localeId,
|
||||||
|
|
||||||
|
userWatching: this.user ? {
|
||||||
|
url: this.videoService.getUserWatchingVideoUrl(this.video.uuid),
|
||||||
|
authorizationHeader: this.authService.getRequestHeaderValue()
|
||||||
|
} : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.videojsLocaleLoaded === false) {
|
if (this.videojsLocaleLoaded === false) {
|
||||||
|
|
|
@ -10,7 +10,7 @@ import './webtorrent-info-button'
|
||||||
import './peertube-videojs-plugin'
|
import './peertube-videojs-plugin'
|
||||||
import './peertube-load-progress-bar'
|
import './peertube-load-progress-bar'
|
||||||
import './theater-button'
|
import './theater-button'
|
||||||
import { VideoJSCaption, videojsUntyped } from './peertube-videojs-typings'
|
import { UserWatching, VideoJSCaption, videojsUntyped } from './peertube-videojs-typings'
|
||||||
import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils'
|
import { buildVideoEmbed, buildVideoLink, copyToClipboard } from './utils'
|
||||||
import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n'
|
import { getCompleteLocale, getShortLocale, is18nLocale, isDefaultLocale } from '../../../../shared/models/i18n/i18n'
|
||||||
|
|
||||||
|
@ -34,10 +34,13 @@ function getVideojsOptions (options: {
|
||||||
startTime: number | string
|
startTime: number | string
|
||||||
theaterMode: boolean,
|
theaterMode: boolean,
|
||||||
videoCaptions: VideoJSCaption[],
|
videoCaptions: VideoJSCaption[],
|
||||||
|
|
||||||
language?: string,
|
language?: string,
|
||||||
controls?: boolean,
|
controls?: boolean,
|
||||||
muted?: boolean,
|
muted?: boolean,
|
||||||
loop?: boolean
|
loop?: boolean
|
||||||
|
|
||||||
|
userWatching?: UserWatching
|
||||||
}) {
|
}) {
|
||||||
const videojsOptions = {
|
const videojsOptions = {
|
||||||
// We don't use text track settings for now
|
// We don't use text track settings for now
|
||||||
|
@ -57,7 +60,8 @@ function getVideojsOptions (options: {
|
||||||
playerElement: options.playerElement,
|
playerElement: options.playerElement,
|
||||||
videoViewUrl: options.videoViewUrl,
|
videoViewUrl: options.videoViewUrl,
|
||||||
videoDuration: options.videoDuration,
|
videoDuration: options.videoDuration,
|
||||||
startTime: options.startTime
|
startTime: options.startTime,
|
||||||
|
userWatching: options.userWatching
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
controlBar: {
|
controlBar: {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import * as WebTorrent from 'webtorrent'
|
||||||
import { VideoFile } from '../../../../shared/models/videos/video.model'
|
import { VideoFile } from '../../../../shared/models/videos/video.model'
|
||||||
import { renderVideo } from './video-renderer'
|
import { renderVideo } from './video-renderer'
|
||||||
import './settings-menu-button'
|
import './settings-menu-button'
|
||||||
import { PeertubePluginOptions, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
|
import { PeertubePluginOptions, UserWatching, VideoJSCaption, VideoJSComponentInterface, videojsUntyped } from './peertube-videojs-typings'
|
||||||
import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils'
|
import { isMobile, timeToInt, videoFileMaxByResolution, videoFileMinByResolution } from './utils'
|
||||||
import * as CacheChunkStore from 'cache-chunk-store'
|
import * as CacheChunkStore from 'cache-chunk-store'
|
||||||
import { PeertubeChunkStore } from './peertube-chunk-store'
|
import { PeertubeChunkStore } from './peertube-chunk-store'
|
||||||
|
@ -32,7 +32,8 @@ class PeerTubePlugin extends Plugin {
|
||||||
AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it
|
AUTO_QUALITY_THRESHOLD_PERCENT: 30, // Bandwidth should be 30% more important than a resolution bitrate to change to it
|
||||||
AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check
|
AUTO_QUALITY_OBSERVATION_TIME: 10000, // Wait 10 seconds after having change the resolution before another check
|
||||||
AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds
|
AUTO_QUALITY_HIGHER_RESOLUTION_DELAY: 5000, // Buffering higher resolution during 5 seconds
|
||||||
BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5 // Last 5 seconds to build average bandwidth
|
BANDWIDTH_AVERAGE_NUMBER_OF_VALUES: 5, // Last 5 seconds to build average bandwidth
|
||||||
|
USER_WATCHING_VIDEO_INTERVAL: 5000 // Every 5 seconds, notify the user is watching the video
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly webtorrent = new WebTorrent({
|
private readonly webtorrent = new WebTorrent({
|
||||||
|
@ -67,6 +68,7 @@ class PeerTubePlugin extends Plugin {
|
||||||
private videoViewInterval
|
private videoViewInterval
|
||||||
private torrentInfoInterval
|
private torrentInfoInterval
|
||||||
private autoQualityInterval
|
private autoQualityInterval
|
||||||
|
private userWatchingVideoInterval
|
||||||
private addTorrentDelay
|
private addTorrentDelay
|
||||||
private qualityObservationTimer
|
private qualityObservationTimer
|
||||||
private runAutoQualitySchedulerTimer
|
private runAutoQualitySchedulerTimer
|
||||||
|
@ -100,6 +102,8 @@ class PeerTubePlugin extends Plugin {
|
||||||
this.runTorrentInfoScheduler()
|
this.runTorrentInfoScheduler()
|
||||||
this.runViewAdd()
|
this.runViewAdd()
|
||||||
|
|
||||||
|
if (options.userWatching) this.runUserWatchVideo(options.userWatching)
|
||||||
|
|
||||||
this.player.one('play', () => {
|
this.player.one('play', () => {
|
||||||
// Don't run immediately scheduler, wait some seconds the TCP connections are made
|
// Don't run immediately scheduler, wait some seconds the TCP connections are made
|
||||||
this.runAutoQualitySchedulerTimer = setTimeout(() => this.runAutoQualityScheduler(), this.CONSTANTS.AUTO_QUALITY_SCHEDULER)
|
this.runAutoQualitySchedulerTimer = setTimeout(() => this.runAutoQualityScheduler(), this.CONSTANTS.AUTO_QUALITY_SCHEDULER)
|
||||||
|
@ -121,6 +125,8 @@ class PeerTubePlugin extends Plugin {
|
||||||
clearInterval(this.torrentInfoInterval)
|
clearInterval(this.torrentInfoInterval)
|
||||||
clearInterval(this.autoQualityInterval)
|
clearInterval(this.autoQualityInterval)
|
||||||
|
|
||||||
|
if (this.userWatchingVideoInterval) clearInterval(this.userWatchingVideoInterval)
|
||||||
|
|
||||||
// Don't need to destroy renderer, video player will be destroyed
|
// Don't need to destroy renderer, video player will be destroyed
|
||||||
this.flushVideoFile(this.currentVideoFile, false)
|
this.flushVideoFile(this.currentVideoFile, false)
|
||||||
|
|
||||||
|
@ -524,6 +530,21 @@ class PeerTubePlugin extends Plugin {
|
||||||
}, 1000)
|
}, 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private runUserWatchVideo (options: UserWatching) {
|
||||||
|
let lastCurrentTime = 0
|
||||||
|
|
||||||
|
this.userWatchingVideoInterval = setInterval(() => {
|
||||||
|
const currentTime = Math.floor(this.player.currentTime())
|
||||||
|
|
||||||
|
if (currentTime - lastCurrentTime >= 1) {
|
||||||
|
lastCurrentTime = currentTime
|
||||||
|
|
||||||
|
this.notifyUserIsWatching(currentTime, options.url, options.authorizationHeader)
|
||||||
|
.catch(err => console.error('Cannot notify user is watching.', err))
|
||||||
|
}
|
||||||
|
}, this.CONSTANTS.USER_WATCHING_VIDEO_INTERVAL)
|
||||||
|
}
|
||||||
|
|
||||||
private clearVideoViewInterval () {
|
private clearVideoViewInterval () {
|
||||||
if (this.videoViewInterval !== undefined) {
|
if (this.videoViewInterval !== undefined) {
|
||||||
clearInterval(this.videoViewInterval)
|
clearInterval(this.videoViewInterval)
|
||||||
|
@ -537,6 +558,15 @@ class PeerTubePlugin extends Plugin {
|
||||||
return fetch(this.videoViewUrl, { method: 'POST' })
|
return fetch(this.videoViewUrl, { method: 'POST' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private notifyUserIsWatching (currentTime: number, url: string, authorizationHeader: string) {
|
||||||
|
const body = new URLSearchParams()
|
||||||
|
body.append('currentTime', currentTime.toString())
|
||||||
|
|
||||||
|
const headers = new Headers({ 'Authorization': authorizationHeader })
|
||||||
|
|
||||||
|
return fetch(url, { method: 'PUT', body, headers })
|
||||||
|
}
|
||||||
|
|
||||||
private fallbackToHttp (done?: Function, play = true) {
|
private fallbackToHttp (done?: Function, play = true) {
|
||||||
this.disableAutoResolution(true)
|
this.disableAutoResolution(true)
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,11 @@ type VideoJSCaption = {
|
||||||
src: string
|
src: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserWatching = {
|
||||||
|
url: string,
|
||||||
|
authorizationHeader: string
|
||||||
|
}
|
||||||
|
|
||||||
type PeertubePluginOptions = {
|
type PeertubePluginOptions = {
|
||||||
videoFiles: VideoFile[]
|
videoFiles: VideoFile[]
|
||||||
playerElement: HTMLVideoElement
|
playerElement: HTMLVideoElement
|
||||||
|
@ -30,6 +35,8 @@ type PeertubePluginOptions = {
|
||||||
startTime: number | string
|
startTime: number | string
|
||||||
autoplay: boolean,
|
autoplay: boolean,
|
||||||
videoCaptions: VideoJSCaption[]
|
videoCaptions: VideoJSCaption[]
|
||||||
|
|
||||||
|
userWatching?: UserWatching
|
||||||
}
|
}
|
||||||
|
|
||||||
// videojs typings don't have some method we need
|
// videojs typings don't have some method we need
|
||||||
|
@ -39,5 +46,6 @@ export {
|
||||||
VideoJSComponentInterface,
|
VideoJSComponentInterface,
|
||||||
PeertubePluginOptions,
|
PeertubePluginOptions,
|
||||||
videojsUntyped,
|
videojsUntyped,
|
||||||
VideoJSCaption
|
VideoJSCaption,
|
||||||
|
UserWatching
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,7 @@ import {
|
||||||
localVideoChannelValidator,
|
localVideoChannelValidator,
|
||||||
videosCustomGetValidator
|
videosCustomGetValidator
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
import { videosGetValidator, videosShareValidator } from '../../middlewares/validators'
|
import { videoCommentGetValidator, videosGetValidator, videosShareValidator } from '../../middlewares/validators'
|
||||||
import { videoCommentGetValidator } from '../../middlewares/validators/video-comments'
|
|
||||||
import { AccountModel } from '../../models/account/account'
|
import { AccountModel } from '../../models/account/account'
|
||||||
import { ActorModel } from '../../models/activitypub/actor'
|
import { ActorModel } from '../../models/activitypub/actor'
|
||||||
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
|
import { ActorFollowModel } from '../../models/activitypub/actor-follow'
|
||||||
|
|
|
@ -117,7 +117,8 @@ function searchVideos (req: express.Request, res: express.Response) {
|
||||||
async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
|
async function searchVideosDB (query: VideosSearchQuery, res: express.Response) {
|
||||||
const options = Object.assign(query, {
|
const options = Object.assign(query, {
|
||||||
includeLocalVideos: true,
|
includeLocalVideos: true,
|
||||||
nsfw: buildNSFWFilter(res, query.nsfw)
|
nsfw: buildNSFWFilter(res, query.nsfw),
|
||||||
|
userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
||||||
})
|
})
|
||||||
const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
|
const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares'
|
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares'
|
||||||
import {
|
import { addVideoCaptionValidator, deleteVideoCaptionValidator, listVideoCaptionsValidator } from '../../../middlewares/validators'
|
||||||
addVideoCaptionValidator,
|
|
||||||
deleteVideoCaptionValidator,
|
|
||||||
listVideoCaptionsValidator
|
|
||||||
} from '../../../middlewares/validators/video-captions'
|
|
||||||
import { createReqFiles } from '../../../helpers/express-utils'
|
import { createReqFiles } from '../../../helpers/express-utils'
|
||||||
import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers'
|
import { CONFIG, sequelizeTypescript, VIDEO_CAPTIONS_MIMETYPE_EXT } from '../../../initializers'
|
||||||
import { getFormattedObjects } from '../../../helpers/utils'
|
import { getFormattedObjects } from '../../../helpers/utils'
|
||||||
|
|
|
@ -13,14 +13,14 @@ import {
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
setDefaultSort
|
setDefaultSort
|
||||||
} from '../../../middlewares'
|
} from '../../../middlewares'
|
||||||
import { videoCommentThreadsSortValidator } from '../../../middlewares/validators'
|
|
||||||
import {
|
import {
|
||||||
addVideoCommentReplyValidator,
|
addVideoCommentReplyValidator,
|
||||||
addVideoCommentThreadValidator,
|
addVideoCommentThreadValidator,
|
||||||
listVideoCommentThreadsValidator,
|
listVideoCommentThreadsValidator,
|
||||||
listVideoThreadCommentsValidator,
|
listVideoThreadCommentsValidator,
|
||||||
removeVideoCommentValidator
|
removeVideoCommentValidator,
|
||||||
} from '../../../middlewares/validators/video-comments'
|
videoCommentThreadsSortValidator
|
||||||
|
} from '../../../middlewares/validators'
|
||||||
import { VideoModel } from '../../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
import { VideoCommentModel } from '../../../models/video/video-comment'
|
import { VideoCommentModel } from '../../../models/video/video-comment'
|
||||||
import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
|
import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
|
||||||
|
|
|
@ -57,6 +57,7 @@ import { videoCaptionsRouter } from './captions'
|
||||||
import { videoImportsRouter } from './import'
|
import { videoImportsRouter } from './import'
|
||||||
import { resetSequelizeInstance } from '../../../helpers/database-utils'
|
import { resetSequelizeInstance } from '../../../helpers/database-utils'
|
||||||
import { rename } from 'fs-extra'
|
import { rename } from 'fs-extra'
|
||||||
|
import { watchingRouter } from './watching'
|
||||||
|
|
||||||
const auditLogger = auditLoggerFactory('videos')
|
const auditLogger = auditLoggerFactory('videos')
|
||||||
const videosRouter = express.Router()
|
const videosRouter = express.Router()
|
||||||
|
@ -86,6 +87,7 @@ videosRouter.use('/', videoCommentRouter)
|
||||||
videosRouter.use('/', videoCaptionsRouter)
|
videosRouter.use('/', videoCaptionsRouter)
|
||||||
videosRouter.use('/', videoImportsRouter)
|
videosRouter.use('/', videoImportsRouter)
|
||||||
videosRouter.use('/', ownershipVideoRouter)
|
videosRouter.use('/', ownershipVideoRouter)
|
||||||
|
videosRouter.use('/', watchingRouter)
|
||||||
|
|
||||||
videosRouter.get('/categories', listVideoCategories)
|
videosRouter.get('/categories', listVideoCategories)
|
||||||
videosRouter.get('/licences', listVideoLicences)
|
videosRouter.get('/licences', listVideoLicences)
|
||||||
|
@ -119,6 +121,7 @@ videosRouter.get('/:id/description',
|
||||||
asyncMiddleware(getVideoDescription)
|
asyncMiddleware(getVideoDescription)
|
||||||
)
|
)
|
||||||
videosRouter.get('/:id',
|
videosRouter.get('/:id',
|
||||||
|
optionalAuthenticate,
|
||||||
asyncMiddleware(videosGetValidator),
|
asyncMiddleware(videosGetValidator),
|
||||||
getVideo
|
getVideo
|
||||||
)
|
)
|
||||||
|
@ -433,7 +436,8 @@ async function listVideos (req: express.Request, res: express.Response, next: ex
|
||||||
tagsAllOf: req.query.tagsAllOf,
|
tagsAllOf: req.query.tagsAllOf,
|
||||||
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
||||||
filter: req.query.filter as VideoFilter,
|
filter: req.query.filter as VideoFilter,
|
||||||
withFiles: false
|
withFiles: false,
|
||||||
|
userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
import * as express from 'express'
|
||||||
|
import { UserWatchingVideo } from '../../../../shared'
|
||||||
|
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares'
|
||||||
|
import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
|
||||||
|
import { UserModel } from '../../../models/account/user'
|
||||||
|
|
||||||
|
const watchingRouter = express.Router()
|
||||||
|
|
||||||
|
watchingRouter.put('/:videoId/watching',
|
||||||
|
authenticate,
|
||||||
|
asyncMiddleware(videoWatchingValidator),
|
||||||
|
asyncRetryTransactionMiddleware(userWatchVideo)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
watchingRouter
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function userWatchVideo (req: express.Request, res: express.Response) {
|
||||||
|
const user = res.locals.oauth.token.User as UserModel
|
||||||
|
|
||||||
|
const body: UserWatchingVideo = req.body
|
||||||
|
const { id: videoId } = res.locals.video as { id: number }
|
||||||
|
|
||||||
|
await UserVideoHistoryModel.upsert({
|
||||||
|
videoId,
|
||||||
|
userId: user.id,
|
||||||
|
currentTime: body.currentTime
|
||||||
|
})
|
||||||
|
|
||||||
|
return res.type('json').status(204).end()
|
||||||
|
}
|
|
@ -154,7 +154,9 @@ function checkUserCanManageVideo (user: UserModel, video: VideoModel, right: Use
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') {
|
async function isVideoExist (id: string, res: Response, fetchType: VideoFetchType = 'all') {
|
||||||
const video = await fetchVideo(id, fetchType)
|
const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
|
||||||
|
|
||||||
|
const video = await fetchVideo(id, fetchType, userId)
|
||||||
|
|
||||||
if (video === null) {
|
if (video === null) {
|
||||||
res.status(404)
|
res.status(404)
|
||||||
|
|
|
@ -2,8 +2,8 @@ import { VideoModel } from '../models/video/video'
|
||||||
|
|
||||||
type VideoFetchType = 'all' | 'only-video' | 'id' | 'none'
|
type VideoFetchType = 'all' | 'only-video' | 'id' | 'none'
|
||||||
|
|
||||||
function fetchVideo (id: number | string, fetchType: VideoFetchType) {
|
function fetchVideo (id: number | string, fetchType: VideoFetchType, userId?: number) {
|
||||||
if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id)
|
if (fetchType === 'all') return VideoModel.loadAndPopulateAccountAndServerAndTags(id, undefined, userId)
|
||||||
|
|
||||||
if (fetchType === 'only-video') return VideoModel.load(id)
|
if (fetchType === 'only-video') return VideoModel.load(id)
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ import { VideoImportModel } from '../models/video/video-import'
|
||||||
import { VideoViewModel } from '../models/video/video-views'
|
import { VideoViewModel } from '../models/video/video-views'
|
||||||
import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
|
import { VideoChangeOwnershipModel } from '../models/video/video-change-ownership'
|
||||||
import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
|
import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
|
||||||
|
import { UserVideoHistoryModel } from '../models/account/user-video-history'
|
||||||
|
|
||||||
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
require('pg').defaults.parseInt8 = true // Avoid BIGINT to be converted to string
|
||||||
|
|
||||||
|
@ -89,7 +90,8 @@ async function initDatabaseModels (silent: boolean) {
|
||||||
ScheduleVideoUpdateModel,
|
ScheduleVideoUpdateModel,
|
||||||
VideoImportModel,
|
VideoImportModel,
|
||||||
VideoViewModel,
|
VideoViewModel,
|
||||||
VideoRedundancyModel
|
VideoRedundancyModel,
|
||||||
|
UserVideoHistoryModel
|
||||||
])
|
])
|
||||||
|
|
||||||
// Check extensions exist in the database
|
// Check extensions exist in the database
|
||||||
|
|
|
@ -48,6 +48,8 @@ class Redis {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************* Forgot password *************/
|
||||||
|
|
||||||
async setResetPasswordVerificationString (userId: number) {
|
async setResetPasswordVerificationString (userId: number) {
|
||||||
const generatedString = await generateRandomString(32)
|
const generatedString = await generateRandomString(32)
|
||||||
|
|
||||||
|
@ -60,6 +62,8 @@ class Redis {
|
||||||
return this.getValue(this.generateResetPasswordKey(userId))
|
return this.getValue(this.generateResetPasswordKey(userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************* Email verification *************/
|
||||||
|
|
||||||
async setVerifyEmailVerificationString (userId: number) {
|
async setVerifyEmailVerificationString (userId: number) {
|
||||||
const generatedString = await generateRandomString(32)
|
const generatedString = await generateRandomString(32)
|
||||||
|
|
||||||
|
@ -72,16 +76,20 @@ class Redis {
|
||||||
return this.getValue(this.generateVerifyEmailKey(userId))
|
return this.getValue(this.generateVerifyEmailKey(userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************* Views per IP *************/
|
||||||
|
|
||||||
setIPVideoView (ip: string, videoUUID: string) {
|
setIPVideoView (ip: string, videoUUID: string) {
|
||||||
return this.setValue(this.buildViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
|
return this.setValue(this.generateViewKey(ip, videoUUID), '1', VIDEO_VIEW_LIFETIME)
|
||||||
}
|
}
|
||||||
|
|
||||||
async isVideoIPViewExists (ip: string, videoUUID: string) {
|
async isVideoIPViewExists (ip: string, videoUUID: string) {
|
||||||
return this.exists(this.buildViewKey(ip, videoUUID))
|
return this.exists(this.generateViewKey(ip, videoUUID))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************* API cache *************/
|
||||||
|
|
||||||
async getCachedRoute (req: express.Request) {
|
async getCachedRoute (req: express.Request) {
|
||||||
const cached = await this.getObject(this.buildCachedRouteKey(req))
|
const cached = await this.getObject(this.generateCachedRouteKey(req))
|
||||||
|
|
||||||
return cached as CachedRoute
|
return cached as CachedRoute
|
||||||
}
|
}
|
||||||
|
@ -94,9 +102,11 @@ class Redis {
|
||||||
(statusCode) ? { statusCode: statusCode.toString() } : null
|
(statusCode) ? { statusCode: statusCode.toString() } : null
|
||||||
)
|
)
|
||||||
|
|
||||||
return this.setObject(this.buildCachedRouteKey(req), cached, lifetime)
|
return this.setObject(this.generateCachedRouteKey(req), cached, lifetime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/************* Video views *************/
|
||||||
|
|
||||||
addVideoView (videoId: number) {
|
addVideoView (videoId: number) {
|
||||||
const keyIncr = this.generateVideoViewKey(videoId)
|
const keyIncr = this.generateVideoViewKey(videoId)
|
||||||
const keySet = this.generateVideosViewKey()
|
const keySet = this.generateVideosViewKey()
|
||||||
|
@ -131,33 +141,37 @@ class Redis {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
generateVideosViewKey (hour?: number) {
|
/************* Keys generation *************/
|
||||||
|
|
||||||
|
generateCachedRouteKey (req: express.Request) {
|
||||||
|
return req.method + '-' + req.originalUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateVideosViewKey (hour?: number) {
|
||||||
if (!hour) hour = new Date().getHours()
|
if (!hour) hour = new Date().getHours()
|
||||||
|
|
||||||
return `videos-view-h${hour}`
|
return `videos-view-h${hour}`
|
||||||
}
|
}
|
||||||
|
|
||||||
generateVideoViewKey (videoId: number, hour?: number) {
|
private generateVideoViewKey (videoId: number, hour?: number) {
|
||||||
if (!hour) hour = new Date().getHours()
|
if (!hour) hour = new Date().getHours()
|
||||||
|
|
||||||
return `video-view-${videoId}-h${hour}`
|
return `video-view-${videoId}-h${hour}`
|
||||||
}
|
}
|
||||||
|
|
||||||
generateResetPasswordKey (userId: number) {
|
private generateResetPasswordKey (userId: number) {
|
||||||
return 'reset-password-' + userId
|
return 'reset-password-' + userId
|
||||||
}
|
}
|
||||||
|
|
||||||
generateVerifyEmailKey (userId: number) {
|
private generateVerifyEmailKey (userId: number) {
|
||||||
return 'verify-email-' + userId
|
return 'verify-email-' + userId
|
||||||
}
|
}
|
||||||
|
|
||||||
buildViewKey (ip: string, videoUUID: string) {
|
private generateViewKey (ip: string, videoUUID: string) {
|
||||||
return videoUUID + '-' + ip
|
return videoUUID + '-' + ip
|
||||||
}
|
}
|
||||||
|
|
||||||
buildCachedRouteKey (req: express.Request) {
|
/************* Redis helpers *************/
|
||||||
return req.method + '-' + req.originalUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
private getValue (key: string) {
|
private getValue (key: string) {
|
||||||
return new Promise<string>((res, rej) => {
|
return new Promise<string>((res, rej) => {
|
||||||
|
@ -197,6 +211,12 @@ class Redis {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private deleteFieldInHash (key: string, field: string) {
|
||||||
|
return new Promise<void>((res, rej) => {
|
||||||
|
this.client.hdel(this.prefix + key, field, err => err ? rej(err) : res())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private setValue (key: string, value: string, expirationMilliseconds: number) {
|
private setValue (key: string, value: string, expirationMilliseconds: number) {
|
||||||
return new Promise<void>((res, rej) => {
|
return new Promise<void>((res, rej) => {
|
||||||
this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => {
|
this.client.set(this.prefix + key, value, 'PX', expirationMilliseconds, (err, ok) => {
|
||||||
|
@ -235,6 +255,16 @@ class Redis {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private setValueInHash (key: string, field: string, value: string) {
|
||||||
|
return new Promise<void>((res, rej) => {
|
||||||
|
this.client.hset(this.prefix + key, field, value, (err) => {
|
||||||
|
if (err) return rej(err)
|
||||||
|
|
||||||
|
return res()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private increment (key: string) {
|
private increment (key: string) {
|
||||||
return new Promise<number>((res, rej) => {
|
return new Promise<number>((res, rej) => {
|
||||||
this.client.incr(this.prefix + key, (err, value) => {
|
this.client.incr(this.prefix + key, (err, value) => {
|
||||||
|
|
|
@ -8,7 +8,7 @@ const lock = new AsyncLock({ timeout: 5000 })
|
||||||
|
|
||||||
function cacheRoute (lifetimeArg: string | number) {
|
function cacheRoute (lifetimeArg: string | number) {
|
||||||
return async function (req: express.Request, res: express.Response, next: express.NextFunction) {
|
return async function (req: express.Request, res: express.Response, next: express.NextFunction) {
|
||||||
const redisKey = Redis.Instance.buildCachedRouteKey(req)
|
const redisKey = Redis.Instance.generateCachedRouteKey(req)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await lock.acquire(redisKey, async (done) => {
|
await lock.acquire(redisKey, async (done) => {
|
||||||
|
|
|
@ -8,9 +8,5 @@ export * from './sort'
|
||||||
export * from './users'
|
export * from './users'
|
||||||
export * from './user-subscriptions'
|
export * from './user-subscriptions'
|
||||||
export * from './videos'
|
export * from './videos'
|
||||||
export * from './video-abuses'
|
|
||||||
export * from './video-blacklist'
|
|
||||||
export * from './video-channels'
|
|
||||||
export * from './webfinger'
|
export * from './webfinger'
|
||||||
export * from './search'
|
export * from './search'
|
||||||
export * from './video-imports'
|
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
export * from './video-abuses'
|
||||||
|
export * from './video-blacklist'
|
||||||
|
export * from './video-captions'
|
||||||
|
export * from './video-channels'
|
||||||
|
export * from './video-comments'
|
||||||
|
export * from './video-imports'
|
||||||
|
export * from './video-watch'
|
||||||
|
export * from './videos'
|
|
@ -1,16 +1,16 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import 'express-validator'
|
import 'express-validator'
|
||||||
import { body, param } from 'express-validator/check'
|
import { body, param } from 'express-validator/check'
|
||||||
import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc'
|
import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
|
||||||
import { isVideoExist } from '../../helpers/custom-validators/videos'
|
import { isVideoExist } from '../../../helpers/custom-validators/videos'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from '../utils'
|
||||||
import {
|
import {
|
||||||
isVideoAbuseExist,
|
isVideoAbuseExist,
|
||||||
isVideoAbuseModerationCommentValid,
|
isVideoAbuseModerationCommentValid,
|
||||||
isVideoAbuseReasonValid,
|
isVideoAbuseReasonValid,
|
||||||
isVideoAbuseStateValid
|
isVideoAbuseStateValid
|
||||||
} from '../../helpers/custom-validators/video-abuses'
|
} from '../../../helpers/custom-validators/video-abuses'
|
||||||
|
|
||||||
const videoAbuseReportValidator = [
|
const videoAbuseReportValidator = [
|
||||||
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
|
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
|
|
@ -1,10 +1,10 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { body, param } from 'express-validator/check'
|
import { body, param } from 'express-validator/check'
|
||||||
import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
|
import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
|
||||||
import { isVideoExist } from '../../helpers/custom-validators/videos'
|
import { isVideoExist } from '../../../helpers/custom-validators/videos'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from '../utils'
|
||||||
import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
|
import { isVideoBlacklistExist, isVideoBlacklistReasonValid } from '../../../helpers/custom-validators/video-blacklist'
|
||||||
|
|
||||||
const videosBlacklistRemoveValidator = [
|
const videosBlacklistRemoveValidator = [
|
||||||
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
|
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
|
|
@ -1,13 +1,13 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from '../utils'
|
||||||
import { checkUserCanManageVideo, isVideoExist } from '../../helpers/custom-validators/videos'
|
import { checkUserCanManageVideo, isVideoExist } from '../../../helpers/custom-validators/videos'
|
||||||
import { isIdOrUUIDValid } from '../../helpers/custom-validators/misc'
|
import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
|
||||||
import { body, param } from 'express-validator/check'
|
import { body, param } from 'express-validator/check'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
import { CONSTRAINTS_FIELDS } from '../../../initializers'
|
||||||
import { UserRight } from '../../../shared'
|
import { UserRight } from '../../../../shared'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
|
import { isVideoCaptionExist, isVideoCaptionFile, isVideoCaptionLanguageValid } from '../../../helpers/custom-validators/video-captions'
|
||||||
import { cleanUpReqFiles } from '../../helpers/express-utils'
|
import { cleanUpReqFiles } from '../../../helpers/express-utils'
|
||||||
|
|
||||||
const addVideoCaptionValidator = [
|
const addVideoCaptionValidator = [
|
||||||
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
|
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'),
|
|
@ -1,20 +1,20 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { body, param } from 'express-validator/check'
|
import { body, param } from 'express-validator/check'
|
||||||
import { UserRight } from '../../../shared'
|
import { UserRight } from '../../../../shared'
|
||||||
import { isAccountNameWithHostExist } from '../../helpers/custom-validators/accounts'
|
import { isAccountNameWithHostExist } from '../../../helpers/custom-validators/accounts'
|
||||||
import {
|
import {
|
||||||
isLocalVideoChannelNameExist,
|
isLocalVideoChannelNameExist,
|
||||||
isVideoChannelDescriptionValid,
|
isVideoChannelDescriptionValid,
|
||||||
isVideoChannelNameValid,
|
isVideoChannelNameValid,
|
||||||
isVideoChannelNameWithHostExist,
|
isVideoChannelNameWithHostExist,
|
||||||
isVideoChannelSupportValid
|
isVideoChannelSupportValid
|
||||||
} from '../../helpers/custom-validators/video-channels'
|
} from '../../../helpers/custom-validators/video-channels'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { UserModel } from '../../models/account/user'
|
import { UserModel } from '../../../models/account/user'
|
||||||
import { VideoChannelModel } from '../../models/video/video-channel'
|
import { VideoChannelModel } from '../../../models/video/video-channel'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from '../utils'
|
||||||
import { isActorPreferredUsernameValid } from '../../helpers/custom-validators/activitypub/actor'
|
import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor'
|
||||||
import { ActorModel } from '../../models/activitypub/actor'
|
import { ActorModel } from '../../../models/activitypub/actor'
|
||||||
|
|
||||||
const listVideoAccountChannelsValidator = [
|
const listVideoAccountChannelsValidator = [
|
||||||
param('accountName').exists().withMessage('Should have a valid account name'),
|
param('accountName').exists().withMessage('Should have a valid account name'),
|
|
@ -1,14 +1,14 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { body, param } from 'express-validator/check'
|
import { body, param } from 'express-validator/check'
|
||||||
import { UserRight } from '../../../shared'
|
import { UserRight } from '../../../../shared'
|
||||||
import { isIdOrUUIDValid, isIdValid } from '../../helpers/custom-validators/misc'
|
import { isIdOrUUIDValid, isIdValid } from '../../../helpers/custom-validators/misc'
|
||||||
import { isValidVideoCommentText } from '../../helpers/custom-validators/video-comments'
|
import { isValidVideoCommentText } from '../../../helpers/custom-validators/video-comments'
|
||||||
import { isVideoExist } from '../../helpers/custom-validators/videos'
|
import { isVideoExist } from '../../../helpers/custom-validators/videos'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { UserModel } from '../../models/account/user'
|
import { UserModel } from '../../../models/account/user'
|
||||||
import { VideoModel } from '../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
import { VideoCommentModel } from '../../models/video/video-comment'
|
import { VideoCommentModel } from '../../../models/video/video-comment'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from '../utils'
|
||||||
|
|
||||||
const listVideoCommentThreadsValidator = [
|
const listVideoCommentThreadsValidator = [
|
||||||
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
|
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'),
|
|
@ -1,14 +1,14 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { body } from 'express-validator/check'
|
import { body } from 'express-validator/check'
|
||||||
import { isIdValid } from '../../helpers/custom-validators/misc'
|
import { isIdValid } from '../../../helpers/custom-validators/misc'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from '../utils'
|
||||||
import { getCommonVideoAttributes } from './videos'
|
import { getCommonVideoAttributes } from './videos'
|
||||||
import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../helpers/custom-validators/video-imports'
|
import { isVideoImportTargetUrlValid, isVideoImportTorrentFile } from '../../../helpers/custom-validators/video-imports'
|
||||||
import { cleanUpReqFiles } from '../../helpers/express-utils'
|
import { cleanUpReqFiles } from '../../../helpers/express-utils'
|
||||||
import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../helpers/custom-validators/videos'
|
import { isVideoChannelOfAccountExist, isVideoMagnetUriValid, isVideoNameValid } from '../../../helpers/custom-validators/videos'
|
||||||
import { CONFIG } from '../../initializers/constants'
|
import { CONFIG } from '../../../initializers/constants'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
import { CONSTRAINTS_FIELDS } from '../../../initializers'
|
||||||
|
|
||||||
const videoImportAddValidator = getCommonVideoAttributes().concat([
|
const videoImportAddValidator = getCommonVideoAttributes().concat([
|
||||||
body('channelId')
|
body('channelId')
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { body, param } from 'express-validator/check'
|
||||||
|
import * as express from 'express'
|
||||||
|
import { isIdOrUUIDValid } from '../../../helpers/custom-validators/misc'
|
||||||
|
import { isVideoExist } from '../../../helpers/custom-validators/videos'
|
||||||
|
import { areValidationErrors } from '../utils'
|
||||||
|
import { logger } from '../../../helpers/logger'
|
||||||
|
|
||||||
|
const videoWatchingValidator = [
|
||||||
|
param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'),
|
||||||
|
body('currentTime')
|
||||||
|
.toInt()
|
||||||
|
.isInt().withMessage('Should have correct current time'),
|
||||||
|
|
||||||
|
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
|
logger.debug('Checking videoWatching parameters', { parameters: req.body })
|
||||||
|
|
||||||
|
if (areValidationErrors(req, res)) return
|
||||||
|
if (!await isVideoExist(req.params.videoId, res, 'id')) return
|
||||||
|
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
videoWatchingValidator
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import 'express-validator'
|
import 'express-validator'
|
||||||
import { body, param, ValidationChain } from 'express-validator/check'
|
import { body, param, ValidationChain } from 'express-validator/check'
|
||||||
import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../shared'
|
import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
|
||||||
import {
|
import {
|
||||||
isBooleanValid,
|
isBooleanValid,
|
||||||
isDateValid,
|
isDateValid,
|
||||||
|
@ -10,7 +10,7 @@ import {
|
||||||
isUUIDValid,
|
isUUIDValid,
|
||||||
toIntOrNull,
|
toIntOrNull,
|
||||||
toValueOrNull
|
toValueOrNull
|
||||||
} from '../../helpers/custom-validators/misc'
|
} from '../../../helpers/custom-validators/misc'
|
||||||
import {
|
import {
|
||||||
checkUserCanManageVideo,
|
checkUserCanManageVideo,
|
||||||
isScheduleVideoUpdatePrivacyValid,
|
isScheduleVideoUpdatePrivacyValid,
|
||||||
|
@ -27,21 +27,21 @@ import {
|
||||||
isVideoRatingTypeValid,
|
isVideoRatingTypeValid,
|
||||||
isVideoSupportValid,
|
isVideoSupportValid,
|
||||||
isVideoTagsValid
|
isVideoTagsValid
|
||||||
} from '../../helpers/custom-validators/videos'
|
} from '../../../helpers/custom-validators/videos'
|
||||||
import { getDurationFromVideoFile } from '../../helpers/ffmpeg-utils'
|
import { getDurationFromVideoFile } from '../../../helpers/ffmpeg-utils'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../../helpers/logger'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../initializers'
|
import { CONSTRAINTS_FIELDS } from '../../../initializers'
|
||||||
import { VideoShareModel } from '../../models/video/video-share'
|
import { VideoShareModel } from '../../../models/video/video-share'
|
||||||
import { authenticate } from '../oauth'
|
import { authenticate } from '../../oauth'
|
||||||
import { areValidationErrors } from './utils'
|
import { areValidationErrors } from '../utils'
|
||||||
import { cleanUpReqFiles } from '../../helpers/express-utils'
|
import { cleanUpReqFiles } from '../../../helpers/express-utils'
|
||||||
import { VideoModel } from '../../models/video/video'
|
import { VideoModel } from '../../../models/video/video'
|
||||||
import { UserModel } from '../../models/account/user'
|
import { UserModel } from '../../../models/account/user'
|
||||||
import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../helpers/custom-validators/video-ownership'
|
import { checkUserCanTerminateOwnershipChange, doesChangeVideoOwnershipExist } from '../../../helpers/custom-validators/video-ownership'
|
||||||
import { VideoChangeOwnershipAccept } from '../../../shared/models/videos/video-change-ownership-accept.model'
|
import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
|
||||||
import { VideoChangeOwnershipModel } from '../../models/video/video-change-ownership'
|
import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
|
||||||
import { AccountModel } from '../../models/account/account'
|
import { AccountModel } from '../../../models/account/account'
|
||||||
import { VideoFetchType } from '../../helpers/video'
|
import { VideoFetchType } from '../../../helpers/video'
|
||||||
|
|
||||||
const videosAddValidator = getCommonVideoAttributes().concat([
|
const videosAddValidator = getCommonVideoAttributes().concat([
|
||||||
body('videofile')
|
body('videofile')
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Min, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
|
import { VideoModel } from '../video/video'
|
||||||
|
import { UserModel } from './user'
|
||||||
|
|
||||||
|
@Table({
|
||||||
|
tableName: 'userVideoHistory',
|
||||||
|
indexes: [
|
||||||
|
{
|
||||||
|
fields: [ 'userId', 'videoId' ],
|
||||||
|
unique: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [ 'userId' ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [ 'videoId' ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class UserVideoHistoryModel extends Model<UserVideoHistoryModel> {
|
||||||
|
@CreatedAt
|
||||||
|
createdAt: Date
|
||||||
|
|
||||||
|
@UpdatedAt
|
||||||
|
updatedAt: Date
|
||||||
|
|
||||||
|
@AllowNull(false)
|
||||||
|
@IsInt
|
||||||
|
@Column
|
||||||
|
currentTime: number
|
||||||
|
|
||||||
|
@ForeignKey(() => VideoModel)
|
||||||
|
@Column
|
||||||
|
videoId: number
|
||||||
|
|
||||||
|
@BelongsTo(() => VideoModel, {
|
||||||
|
foreignKey: {
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
Video: VideoModel
|
||||||
|
|
||||||
|
@ForeignKey(() => UserModel)
|
||||||
|
@Column
|
||||||
|
userId: number
|
||||||
|
|
||||||
|
@BelongsTo(() => UserModel, {
|
||||||
|
foreignKey: {
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
User: UserModel
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import {
|
||||||
getVideoLikesActivityPubUrl,
|
getVideoLikesActivityPubUrl,
|
||||||
getVideoSharesActivityPubUrl
|
getVideoSharesActivityPubUrl
|
||||||
} from '../../lib/activitypub'
|
} from '../../lib/activitypub'
|
||||||
|
import { isArray } from 'util'
|
||||||
|
|
||||||
export type VideoFormattingJSONOptions = {
|
export type VideoFormattingJSONOptions = {
|
||||||
completeDescription?: boolean
|
completeDescription?: boolean
|
||||||
|
@ -24,6 +25,8 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
|
||||||
const formattedAccount = video.VideoChannel.Account.toFormattedJSON()
|
const formattedAccount = video.VideoChannel.Account.toFormattedJSON()
|
||||||
const formattedVideoChannel = video.VideoChannel.toFormattedJSON()
|
const formattedVideoChannel = video.VideoChannel.toFormattedJSON()
|
||||||
|
|
||||||
|
const userHistory = isArray(video.UserVideoHistories) ? video.UserVideoHistories[0] : undefined
|
||||||
|
|
||||||
const videoObject: Video = {
|
const videoObject: Video = {
|
||||||
id: video.id,
|
id: video.id,
|
||||||
uuid: video.uuid,
|
uuid: video.uuid,
|
||||||
|
@ -74,7 +77,11 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
|
||||||
url: formattedVideoChannel.url,
|
url: formattedVideoChannel.url,
|
||||||
host: formattedVideoChannel.host,
|
host: formattedVideoChannel.host,
|
||||||
avatar: formattedVideoChannel.avatar
|
avatar: formattedVideoChannel.avatar
|
||||||
}
|
},
|
||||||
|
|
||||||
|
userHistory: userHistory ? {
|
||||||
|
currentTime: userHistory.currentTime
|
||||||
|
} : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options) {
|
if (options) {
|
||||||
|
|
|
@ -92,6 +92,8 @@ import {
|
||||||
videoModelToFormattedJSON
|
videoModelToFormattedJSON
|
||||||
} from './video-format-utils'
|
} from './video-format-utils'
|
||||||
import * as validator from 'validator'
|
import * as validator from 'validator'
|
||||||
|
import { UserVideoHistoryModel } from '../account/user-video-history'
|
||||||
|
|
||||||
|
|
||||||
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
|
// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
|
||||||
const indexes: Sequelize.DefineIndexesOptions[] = [
|
const indexes: Sequelize.DefineIndexesOptions[] = [
|
||||||
|
@ -127,7 +129,8 @@ export enum ScopeNames {
|
||||||
WITH_TAGS = 'WITH_TAGS',
|
WITH_TAGS = 'WITH_TAGS',
|
||||||
WITH_FILES = 'WITH_FILES',
|
WITH_FILES = 'WITH_FILES',
|
||||||
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
|
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
|
||||||
WITH_BLACKLISTED = 'WITH_BLACKLISTED'
|
WITH_BLACKLISTED = 'WITH_BLACKLISTED',
|
||||||
|
WITH_USER_HISTORY = 'WITH_USER_HISTORY'
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForAPIOptions = {
|
type ForAPIOptions = {
|
||||||
|
@ -464,6 +467,8 @@ type AvailableForListIDsOptions = {
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: () => VideoFileModel.unscoped(),
|
model: () => VideoFileModel.unscoped(),
|
||||||
|
// FIXME: typings
|
||||||
|
[ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
|
||||||
required: false,
|
required: false,
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
|
@ -482,6 +487,20 @@ type AvailableForListIDsOptions = {
|
||||||
required: false
|
required: false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
[ ScopeNames.WITH_USER_HISTORY ]: (userId: number) => {
|
||||||
|
return {
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
attributes: [ 'currentTime' ],
|
||||||
|
model: UserVideoHistoryModel.unscoped(),
|
||||||
|
required: false,
|
||||||
|
where: {
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@Table({
|
@Table({
|
||||||
|
@ -672,11 +691,19 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
name: 'videoId',
|
name: 'videoId',
|
||||||
allowNull: false
|
allowNull: false
|
||||||
},
|
},
|
||||||
onDelete: 'cascade',
|
onDelete: 'cascade'
|
||||||
hooks: true
|
|
||||||
})
|
})
|
||||||
VideoViews: VideoViewModel[]
|
VideoViews: VideoViewModel[]
|
||||||
|
|
||||||
|
@HasMany(() => UserVideoHistoryModel, {
|
||||||
|
foreignKey: {
|
||||||
|
name: 'videoId',
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
onDelete: 'cascade'
|
||||||
|
})
|
||||||
|
UserVideoHistories: UserVideoHistoryModel[]
|
||||||
|
|
||||||
@HasOne(() => ScheduleVideoUpdateModel, {
|
@HasOne(() => ScheduleVideoUpdateModel, {
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'videoId',
|
name: 'videoId',
|
||||||
|
@ -930,7 +957,8 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
accountId?: number,
|
accountId?: number,
|
||||||
videoChannelId?: number,
|
videoChannelId?: number,
|
||||||
actorId?: number
|
actorId?: number
|
||||||
trendingDays?: number
|
trendingDays?: number,
|
||||||
|
userId?: number
|
||||||
}, countVideos = true) {
|
}, countVideos = true) {
|
||||||
const query: IFindOptions<VideoModel> = {
|
const query: IFindOptions<VideoModel> = {
|
||||||
offset: options.start,
|
offset: options.start,
|
||||||
|
@ -961,6 +989,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
accountId: options.accountId,
|
accountId: options.accountId,
|
||||||
videoChannelId: options.videoChannelId,
|
videoChannelId: options.videoChannelId,
|
||||||
includeLocalVideos: options.includeLocalVideos,
|
includeLocalVideos: options.includeLocalVideos,
|
||||||
|
userId: options.userId,
|
||||||
trendingDays
|
trendingDays
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -983,6 +1012,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
tagsAllOf?: string[]
|
tagsAllOf?: string[]
|
||||||
durationMin?: number // seconds
|
durationMin?: number // seconds
|
||||||
durationMax?: number // seconds
|
durationMax?: number // seconds
|
||||||
|
userId?: number
|
||||||
}) {
|
}) {
|
||||||
const whereAnd = []
|
const whereAnd = []
|
||||||
|
|
||||||
|
@ -1058,7 +1088,8 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
licenceOneOf: options.licenceOneOf,
|
licenceOneOf: options.licenceOneOf,
|
||||||
languageOneOf: options.languageOneOf,
|
languageOneOf: options.languageOneOf,
|
||||||
tagsOneOf: options.tagsOneOf,
|
tagsOneOf: options.tagsOneOf,
|
||||||
tagsAllOf: options.tagsAllOf
|
tagsAllOf: options.tagsAllOf,
|
||||||
|
userId: options.userId
|
||||||
}
|
}
|
||||||
|
|
||||||
return VideoModel.getAvailableForApi(query, queryOptions)
|
return VideoModel.getAvailableForApi(query, queryOptions)
|
||||||
|
@ -1125,7 +1156,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
|
return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction) {
|
static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) {
|
||||||
const where = VideoModel.buildWhereIdOrUUID(id)
|
const where = VideoModel.buildWhereIdOrUUID(id)
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
|
@ -1134,14 +1165,20 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
transaction: t
|
transaction: t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scopes = [
|
||||||
|
ScopeNames.WITH_TAGS,
|
||||||
|
ScopeNames.WITH_BLACKLISTED,
|
||||||
|
ScopeNames.WITH_FILES,
|
||||||
|
ScopeNames.WITH_ACCOUNT_DETAILS,
|
||||||
|
ScopeNames.WITH_SCHEDULED_UPDATE
|
||||||
|
]
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings
|
||||||
|
}
|
||||||
|
|
||||||
return VideoModel
|
return VideoModel
|
||||||
.scope([
|
.scope(scopes)
|
||||||
ScopeNames.WITH_TAGS,
|
|
||||||
ScopeNames.WITH_BLACKLISTED,
|
|
||||||
ScopeNames.WITH_FILES,
|
|
||||||
ScopeNames.WITH_ACCOUNT_DETAILS,
|
|
||||||
ScopeNames.WITH_SCHEDULED_UPDATE
|
|
||||||
])
|
|
||||||
.findOne(options)
|
.findOne(options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1225,7 +1262,11 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions, countVideos = true) {
|
private static async getAvailableForApi (
|
||||||
|
query: IFindOptions<VideoModel>,
|
||||||
|
options: AvailableForListIDsOptions & { userId?: number},
|
||||||
|
countVideos = true
|
||||||
|
) {
|
||||||
const idsScope = {
|
const idsScope = {
|
||||||
method: [
|
method: [
|
||||||
ScopeNames.AVAILABLE_FOR_LIST_IDS, options
|
ScopeNames.AVAILABLE_FOR_LIST_IDS, options
|
||||||
|
@ -1249,8 +1290,15 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
|
|
||||||
if (ids.length === 0) return { data: [], total: count }
|
if (ids.length === 0) return { data: [], total: count }
|
||||||
|
|
||||||
const apiScope = {
|
// FIXME: typings
|
||||||
method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
|
const apiScope: any[] = [
|
||||||
|
{
|
||||||
|
method: [ ScopeNames.FOR_API, { ids, withFiles: options.withFiles } as ForAPIOptions ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (options.userId) {
|
||||||
|
apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.userId ] })
|
||||||
}
|
}
|
||||||
|
|
||||||
const secondQuery = {
|
const secondQuery = {
|
||||||
|
|
|
@ -15,3 +15,4 @@ import './video-channels'
|
||||||
import './video-comments'
|
import './video-comments'
|
||||||
import './video-imports'
|
import './video-imports'
|
||||||
import './videos'
|
import './videos'
|
||||||
|
import './videos-history'
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
/* tslint:disable:no-unused-expression */
|
||||||
|
|
||||||
|
import * as chai from 'chai'
|
||||||
|
import 'mocha'
|
||||||
|
import {
|
||||||
|
flushTests,
|
||||||
|
killallServers,
|
||||||
|
makePostBodyRequest,
|
||||||
|
makePutBodyRequest,
|
||||||
|
runServer,
|
||||||
|
ServerInfo,
|
||||||
|
setAccessTokensToServers,
|
||||||
|
uploadVideo
|
||||||
|
} from '../../utils'
|
||||||
|
|
||||||
|
const expect = chai.expect
|
||||||
|
|
||||||
|
describe('Test videos history API validator', function () {
|
||||||
|
let path: string
|
||||||
|
let server: ServerInfo
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
await flushTests()
|
||||||
|
|
||||||
|
server = await runServer(1)
|
||||||
|
|
||||||
|
await setAccessTokensToServers([ server ])
|
||||||
|
|
||||||
|
const res = await uploadVideo(server.url, server.accessToken, {})
|
||||||
|
const videoUUID = res.body.video.uuid
|
||||||
|
|
||||||
|
path = '/api/v1/videos/' + videoUUID + '/watching'
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('When notifying a user is watching a video', function () {
|
||||||
|
|
||||||
|
it('Should fail with an unauthenticated user', async function () {
|
||||||
|
const fields = { currentTime: 5 }
|
||||||
|
await makePutBodyRequest({ url: server.url, path, fields, statusCodeExpected: 401 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an incorrect video id', async function () {
|
||||||
|
const fields = { currentTime: 5 }
|
||||||
|
const path = '/api/v1/videos/blabla/watching'
|
||||||
|
await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 400 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with an unknown video', async function () {
|
||||||
|
const fields = { currentTime: 5 }
|
||||||
|
const path = '/api/v1/videos/d91fff41-c24d-4508-8e13-3bd5902c3b02/watching'
|
||||||
|
|
||||||
|
await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 404 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should fail with a bad current time', async function () {
|
||||||
|
const fields = { currentTime: 'hello' }
|
||||||
|
await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 400 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should succeed with the correct parameters', async function () {
|
||||||
|
const fields = { currentTime: 5 }
|
||||||
|
|
||||||
|
await makePutBodyRequest({ url: server.url, path, fields, token: server.accessToken, statusCodeExpected: 204 })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
killallServers([ server ])
|
||||||
|
|
||||||
|
// Keep the logs if the test failed
|
||||||
|
if (this['ok']) {
|
||||||
|
await flushTests()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -14,4 +14,5 @@ import './video-nsfw'
|
||||||
import './video-privacy'
|
import './video-privacy'
|
||||||
import './video-schedule-update'
|
import './video-schedule-update'
|
||||||
import './video-transcoder'
|
import './video-transcoder'
|
||||||
|
import './videos-history'
|
||||||
import './videos-overview'
|
import './videos-overview'
|
||||||
|
|
|
@ -0,0 +1,128 @@
|
||||||
|
/* tslint:disable:no-unused-expression */
|
||||||
|
|
||||||
|
import * as chai from 'chai'
|
||||||
|
import 'mocha'
|
||||||
|
import {
|
||||||
|
flushTests,
|
||||||
|
getVideosListWithToken,
|
||||||
|
getVideoWithToken,
|
||||||
|
killallServers, makePutBodyRequest,
|
||||||
|
runServer, searchVideoWithToken,
|
||||||
|
ServerInfo,
|
||||||
|
setAccessTokensToServers,
|
||||||
|
uploadVideo
|
||||||
|
} from '../../utils'
|
||||||
|
import { Video, VideoDetails } from '../../../../shared/models/videos'
|
||||||
|
import { userWatchVideo } from '../../utils/videos/video-history'
|
||||||
|
|
||||||
|
const expect = chai.expect
|
||||||
|
|
||||||
|
describe('Test videos history', function () {
|
||||||
|
let server: ServerInfo = null
|
||||||
|
let video1UUID: string
|
||||||
|
let video2UUID: string
|
||||||
|
let video3UUID: string
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
this.timeout(30000)
|
||||||
|
|
||||||
|
await flushTests()
|
||||||
|
|
||||||
|
server = await runServer(1)
|
||||||
|
|
||||||
|
await setAccessTokensToServers([ server ])
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await uploadVideo(server.url, server.accessToken, { name: 'video 1' })
|
||||||
|
video1UUID = res.body.video.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await uploadVideo(server.url, server.accessToken, { name: 'video 2' })
|
||||||
|
video2UUID = res.body.video.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await uploadVideo(server.url, server.accessToken, { name: 'video 3' })
|
||||||
|
video3UUID = res.body.video.uuid
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should get videos, without watching history', async function () {
|
||||||
|
const res = await getVideosListWithToken(server.url, server.accessToken)
|
||||||
|
const videos: Video[] = res.body.data
|
||||||
|
|
||||||
|
for (const video of videos) {
|
||||||
|
const resDetail = await getVideoWithToken(server.url, server.accessToken, video.id)
|
||||||
|
const videoDetails: VideoDetails = resDetail.body
|
||||||
|
|
||||||
|
expect(video.userHistory).to.be.undefined
|
||||||
|
expect(videoDetails.userHistory).to.be.undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should watch the first and second video', async function () {
|
||||||
|
await userWatchVideo(server.url, server.accessToken, video1UUID, 3)
|
||||||
|
await userWatchVideo(server.url, server.accessToken, video2UUID, 8)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Should return the correct history when listing, searching and getting videos', async function () {
|
||||||
|
const videosOfVideos: Video[][] = []
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await getVideosListWithToken(server.url, server.accessToken)
|
||||||
|
videosOfVideos.push(res.body.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const res = await searchVideoWithToken(server.url, 'video', server.accessToken)
|
||||||
|
videosOfVideos.push(res.body.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const videos of videosOfVideos) {
|
||||||
|
const video1 = videos.find(v => v.uuid === video1UUID)
|
||||||
|
const video2 = videos.find(v => v.uuid === video2UUID)
|
||||||
|
const video3 = videos.find(v => v.uuid === video3UUID)
|
||||||
|
|
||||||
|
expect(video1.userHistory).to.not.be.undefined
|
||||||
|
expect(video1.userHistory.currentTime).to.equal(3)
|
||||||
|
|
||||||
|
expect(video2.userHistory).to.not.be.undefined
|
||||||
|
expect(video2.userHistory.currentTime).to.equal(8)
|
||||||
|
|
||||||
|
expect(video3.userHistory).to.be.undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const resDetail = await getVideoWithToken(server.url, server.accessToken, video1UUID)
|
||||||
|
const videoDetails: VideoDetails = resDetail.body
|
||||||
|
|
||||||
|
expect(videoDetails.userHistory).to.not.be.undefined
|
||||||
|
expect(videoDetails.userHistory.currentTime).to.equal(3)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const resDetail = await getVideoWithToken(server.url, server.accessToken, video2UUID)
|
||||||
|
const videoDetails: VideoDetails = resDetail.body
|
||||||
|
|
||||||
|
expect(videoDetails.userHistory).to.not.be.undefined
|
||||||
|
expect(videoDetails.userHistory.currentTime).to.equal(8)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const resDetail = await getVideoWithToken(server.url, server.accessToken, video3UUID)
|
||||||
|
const videoDetails: VideoDetails = resDetail.body
|
||||||
|
|
||||||
|
expect(videoDetails.userHistory).to.be.undefined
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
after(async function () {
|
||||||
|
killallServers([ server ])
|
||||||
|
|
||||||
|
// Keep the logs if the test failed
|
||||||
|
if (this['ok']) {
|
||||||
|
await flushTests()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { makePutBodyRequest } from '../requests/requests'
|
||||||
|
|
||||||
|
function userWatchVideo (url: string, token: string, videoId: number | string, currentTime: number) {
|
||||||
|
const path = '/api/v1/videos/' + videoId + '/watching'
|
||||||
|
const fields = { currentTime }
|
||||||
|
|
||||||
|
return makePutBodyRequest({ url, path, token, fields, statusCodeExpected: 204 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export {
|
||||||
|
userWatchVideo
|
||||||
|
}
|
|
@ -7,3 +7,4 @@ export * from './user-update-me.model'
|
||||||
export * from './user-right.enum'
|
export * from './user-right.enum'
|
||||||
export * from './user-role'
|
export * from './user-role'
|
||||||
export * from './user-video-quota.model'
|
export * from './user-video-quota.model'
|
||||||
|
export * from './user-watching-video.model'
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface UserWatchingVideo {
|
||||||
|
currentTime: number
|
||||||
|
}
|
|
@ -68,6 +68,10 @@ export interface Video {
|
||||||
|
|
||||||
account: AccountAttribute
|
account: AccountAttribute
|
||||||
channel: VideoChannelAttribute
|
channel: VideoChannelAttribute
|
||||||
|
|
||||||
|
userHistory?: {
|
||||||
|
currentTime: number
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoDetails extends Video {
|
export interface VideoDetails extends Video {
|
||||||
|
|
Loading…
Reference in New Issue