Add user history and resume videos
This commit is contained in:
		
							parent
							
								
									a585824160
								
							
						
					
					
						commit
						6e46de095d
					
				
					 41 changed files with 649 additions and 122 deletions
				
			
		| 
						 | 
					@ -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))
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										36
									
								
								server/controllers/api/videos/watching.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								server/controllers/api/videos/watching.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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'
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										8
									
								
								server/middlewares/validators/videos/index.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								server/middlewares/validators/videos/index.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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')
 | 
				
			||||||
							
								
								
									
										28
									
								
								server/middlewares/validators/videos/video-watch.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								server/middlewares/validators/videos/video-watch.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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')
 | 
				
			||||||
							
								
								
									
										55
									
								
								server/models/account/user-video-history.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								server/models/account/user-video-history.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										79
									
								
								server/tests/api/check-params/videos-history.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								server/tests/api/check-params/videos-history.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										128
									
								
								server/tests/api/videos/videos-history.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								server/tests/api/videos/videos-history.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										14
									
								
								server/tests/utils/videos/video-history.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								server/tests/utils/videos/video-history.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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'
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										3
									
								
								shared/models/users/user-watching-video.model.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								shared/models/users/user-watching-video.model.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue