diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html index cb23bb522..ea5f61b18 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html @@ -18,10 +18,13 @@
This channel does not have videos.
- +
- + Show this channel diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss index 98931f0c2..7f7652460 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss @@ -23,6 +23,11 @@ height: 50px; } } + + my-video-miniature ::ng-deep my-video-actions-dropdown > my-action-dropdown { + // Fix our overflow + position: absolute; + } } diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html index 284694b7f..4de4e69da 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html @@ -14,10 +14,10 @@ class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" cdkDropList (cdkDropListDropped)="drop($event)" > -
+
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts index d5122aeba..6434b9e50 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts @@ -3,15 +3,13 @@ import { Notifier, ServerService } from '@app/core' import { AuthService } from '../../core/auth' import { ConfirmService } from '../../core/confirm' import { ComponentPagination } from '@app/shared/rest/component-pagination.model' -import { Video } from '@app/shared/video/video.model' -import { Subject, Subscription } from 'rxjs' +import { Subscription } from 'rxjs' import { ActivatedRoute } from '@angular/router' -import { VideoService } from '@app/shared/video/video.service' import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' import { I18n } from '@ngx-translate/i18n-polyfill' -import { CdkDragDrop, CdkDragMove } from '@angular/cdk/drag-drop' -import { throttleTime } from 'rxjs/operators' +import { CdkDragDrop } from '@angular/cdk/drag-drop' +import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model' @Component({ selector: 'my-account-video-playlist-elements', @@ -19,7 +17,7 @@ import { throttleTime } from 'rxjs/operators' styleUrls: [ './my-account-video-playlist-elements.component.scss' ] }) export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy { - videos: Video[] = [] + playlistElements: VideoPlaylistElement[] = [] playlist: VideoPlaylist pagination: ComponentPagination = { @@ -30,7 +28,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro private videoPlaylistId: string | number private paramsSub: Subscription - private dragMoveSubject = new Subject() constructor ( private authService: AuthService, @@ -39,7 +36,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro private confirmService: ConfirmService, private route: ActivatedRoute, private i18n: I18n, - private videoService: VideoService, private videoPlaylistService: VideoPlaylistService ) {} @@ -50,10 +46,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro this.loadPlaylistInfo() }) - - this.dragMoveSubject.asObservable() - .pipe(throttleTime(200)) - .subscribe(y => this.checkScroll(y)) } ngOnDestroy () { @@ -66,8 +58,8 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro if (previousIndex === newIndex) return - const oldPosition = this.videos[previousIndex].playlistElement.position - let insertAfter = this.videos[newIndex].playlistElement.position + const oldPosition = this.playlistElements[previousIndex].position + let insertAfter = this.playlistElements[newIndex].position if (oldPosition > insertAfter) insertAfter-- @@ -78,42 +70,16 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro err => this.notifier.error(err.message) ) - const video = this.videos[previousIndex] + const element = this.playlistElements[previousIndex] - this.videos.splice(previousIndex, 1) - this.videos.splice(newIndex, 0, video) + this.playlistElements.splice(previousIndex, 1) + this.playlistElements.splice(newIndex, 0, element) this.reorderClientPositions() } - onDragMove (event: CdkDragMove) { - this.dragMoveSubject.next(event.pointerPosition.y) - } - - checkScroll (pointerY: number) { - // FIXME: Uncomment when https://github.com/angular/material2/issues/14098 is fixed - // FIXME: Remove when https://github.com/angular/material2/issues/13588 is implemented - // if (pointerY < 150) { - // window.scrollBy({ - // left: 0, - // top: -20, - // behavior: 'smooth' - // }) - // - // return - // } - // - // if (window.innerHeight - pointerY <= 50) { - // window.scrollBy({ - // left: 0, - // top: 20, - // behavior: 'smooth' - // }) - // } - } - - onElementRemoved (video: Video) { - this.videos = this.videos.filter(v => v.id !== video.id) + onElementRemoved (element: VideoPlaylistElement) { + this.playlistElements = this.playlistElements.filter(v => v.id !== element.id) this.reorderClientPositions() } @@ -125,14 +91,14 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro this.loadElements() } - trackByFn (index: number, elem: Video) { + trackByFn (index: number, elem: VideoPlaylistElement) { return elem.id } private loadElements () { - this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination) + this.videoPlaylistService.getPlaylistVideos(this.videoPlaylistId, this.pagination) .subscribe(({ total, data }) => { - this.videos = this.videos.concat(data) + this.playlistElements = this.playlistElements.concat(data) this.pagination.totalItems = total }) } @@ -147,8 +113,8 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro private reorderClientPositions () { let i = 1 - for (const video of this.videos) { - video.playlistElement.position = i + for (const element of this.playlistElements) { + element.position = i i++ } } diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts index c6cff03a4..08ceb21bc 100644 --- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts +++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts @@ -37,6 +37,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, } displayOptions = false + private playlistElementId: number + constructor ( protected formValidatorService: FormValidatorService, private authService: AuthService, @@ -96,6 +98,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined, stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined }) + + this.playlistElementId = existingPlaylist ? existingPlaylist.playlistElementId : undefined } this.cd.markForCheck() @@ -177,7 +181,9 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, } private removeVideoFromPlaylist (playlist: PlaylistSummary) { - this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, this.video.id) + if (!this.playlistElementId) return + + this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, this.playlistElementId) .subscribe( () => { this.notifier.success(this.i18n('Video removed from {{name}}', { name: playlist.displayName })) diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html index ab5a78928..25d4783fb 100644 --- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html +++ b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html @@ -6,66 +6,82 @@
+
+
- {{ video.name }} + + {{ playlistElement.video.name }} - - + + - {{ formatTimestamp(video) }} + {{ formatTimestamp(playlistElement) }} + + + + Unavailable + Private + Deleted +
-
+
- - -
-
- - - + + -
- +
+
+ - + +
+ +
+ + + +
+ +
+ - -
- - + Delete from {{ playlist?.displayName }}
diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss index cb7072d7f..9f4061b02 100644 --- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss +++ b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss @@ -2,9 +2,21 @@ @import '_mixins'; @import '_miniature'; -my-video-thumbnail { - @include thumbnail-size-component(130px, 72px); +$thumbnail-width: 130px; +$thumbnail-height: 72px; +my-video-thumbnail { + @include thumbnail-size-component($thumbnail-width, $thumbnail-height); +} + +.fake-thumbnail { + width: $thumbnail-width; + height: $thumbnail-height; + background-color: #ececec; +} + +my-video-thumbnail, +.fake-thumbnail { display: flex; // Avoids an issue with line-height that adds space below the element margin-right: 10px; } @@ -31,6 +43,7 @@ my-video-thumbnail { a { @include disable-default-a-behaviour; + color: var(--mainForegroundColor); display: flex; min-width: 0; align-items: center; @@ -58,7 +71,6 @@ my-video-thumbnail { min-width: 0; a { - color: var(--mainForegroundColor); width: auto; &:hover { @@ -66,20 +78,20 @@ my-video-thumbnail { } } - .video-info-name { - font-size: 18px; - font-weight: $font-semibold; - display: inline-block; - - @include ellipsis; - } - .video-info-account, .video-info-timestamp { color: $grey-foreground-color; } } } + .video-info-name { + font-size: 18px; + font-weight: $font-semibold; + display: inline-block; + + @include ellipsis; + } + .more { justify-self: flex-end; margin-left: auto; diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts index 62cf6536d..a8e5a4885 100644 --- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts +++ b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' import { Video } from '@app/shared/video/video.model' -import { VideoPlaylistElementUpdate } from '@shared/models' +import { VideoPlaylistElementType, VideoPlaylistElementUpdate } from '@shared/models' import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core' import { ActivatedRoute } from '@angular/router' import { I18n } from '@ngx-translate/i18n-polyfill' @@ -9,6 +9,7 @@ import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist. import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' import { secondsToTime } from '../../../assets/player/utils' +import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model' @Component({ selector: 'my-video-playlist-element-miniature', @@ -20,14 +21,14 @@ export class VideoPlaylistElementMiniatureComponent { @ViewChild('moreDropdown', { static: false }) moreDropdown: NgbDropdown @Input() playlist: VideoPlaylist - @Input() video: Video + @Input() playlistElement: VideoPlaylistElement @Input() owned = false @Input() playing = false @Input() rowLink = false @Input() accountLink = true - @Input() position: number + @Input() position: number // Keep this property because we're in the OnPush change detection strategy - @Output() elementRemoved = new EventEmitter
-
+
diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.scss b/client/src/app/videos/+video-watch/video-watch-playlist.component.scss index eeb763bd9..4c24d6b05 100644 --- a/client/src/app/videos/+video-watch/video-watch-playlist.component.scss +++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.scss @@ -53,6 +53,11 @@ my-video-thumbnail { @include thumbnail-size-component(90px, 50px); } + + .fake-thumbnail { + width: 90px; + height: 50px; + } } } } diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts index 2fb0cb0e5..6e8d58cd8 100644 --- a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts +++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts @@ -1,11 +1,11 @@ import { Component, Input } from '@angular/core' import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' import { ComponentPagination } from '@app/shared/rest/component-pagination.model' -import { Video } from '@app/shared/video/video.model' import { VideoDetails, VideoPlaylistPrivacy } from '@shared/models' -import { VideoService } from '@app/shared/video/video.service' import { Router } from '@angular/router' import { AuthService } from '@app/core' +import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' +import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model' @Component({ selector: 'my-video-watch-playlist', @@ -16,7 +16,7 @@ export class VideoWatchPlaylistComponent { @Input() video: VideoDetails @Input() playlist: VideoPlaylist - playlistVideos: Video[] = [] + playlistElements: VideoPlaylistElement[] = [] playlistPagination: ComponentPagination = { currentPage: 1, itemsPerPage: 30, @@ -28,7 +28,7 @@ export class VideoWatchPlaylistComponent { constructor ( private auth: AuthService, - private videoService: VideoService, + private videoPlaylist: VideoPlaylistService, private router: Router ) {} @@ -40,8 +40,8 @@ export class VideoWatchPlaylistComponent { this.loadPlaylistElements(this.playlist,false) } - onElementRemoved (video: Video) { - this.playlistVideos = this.playlistVideos.filter(v => v.id !== video.id) + onElementRemoved (playlistElement: VideoPlaylistElement) { + this.playlistElements = this.playlistElements.filter(e => e.id !== playlistElement.id) this.playlistPagination.totalItems-- } @@ -65,12 +65,13 @@ export class VideoWatchPlaylistComponent { } loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) { - this.videoService.getPlaylistVideos(playlist.uuid, this.playlistPagination) + this.videoPlaylist.getPlaylistVideos(playlist.uuid, this.playlistPagination) .subscribe(({ total, data }) => { - this.playlistVideos = this.playlistVideos.concat(data) + this.playlistElements = this.playlistElements.concat(data) this.playlistPagination.totalItems = total - if (total === 0) { + const firstAvailableVideos = this.playlistElements.find(e => !!e.video) + if (!firstAvailableVideos) { this.noPlaylistVideos = true return } @@ -79,7 +80,7 @@ export class VideoWatchPlaylistComponent { if (redirectToFirst) { const extras = { - queryParams: { videoId: this.playlistVideos[ 0 ].uuid }, + queryParams: { videoId: firstAvailableVideos.video.uuid }, replaceUrl: true } this.router.navigate([], extras) @@ -88,11 +89,11 @@ export class VideoWatchPlaylistComponent { } updatePlaylistIndex (video: VideoDetails) { - if (this.playlistVideos.length === 0 || !video) return + if (this.playlistElements.length === 0 || !video) return - for (const playlistVideo of this.playlistVideos) { - if (playlistVideo.id === video.id) { - this.currentPlaylistPosition = playlistVideo.playlistElement.position + for (const playlistElement of this.playlistElements) { + if (playlistElement.video && playlistElement.video.id === video.id) { + this.currentPlaylistPosition = playlistElement.position return } } @@ -103,11 +104,17 @@ export class VideoWatchPlaylistComponent { navigateToNextPlaylistVideo () { if (this.currentPlaylistPosition < this.playlistPagination.totalItems) { - const next = this.playlistVideos.find(v => v.playlistElement.position === this.currentPlaylistPosition + 1) + const next = this.playlistElements.find(e => e.position === this.currentPlaylistPosition + 1) - const start = next.playlistElement.startTimestamp - const stop = next.playlistElement.stopTimestamp - this.router.navigate([],{ queryParams: { videoId: next.uuid, start, stop } }) + if (!next || !next.video) { + this.currentPlaylistPosition++ + this.navigateToNextPlaylistVideo() + return + } + + const start = next.startTimestamp + const stop = next.stopTimestamp + this.router.navigate([],{ queryParams: { videoId: next.video.uuid, start, stop } }) } } } diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index 0d499d47f..d7c7b7497 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts @@ -464,7 +464,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { } this.zone.runOutsideAngular(async () => { - this.player = await PeertubePlayerManager.initialize(mode, options) + this.player = await PeertubePlayerManager.initialize(mode, options, player => this.player = player) this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 083c621d2..6c8b13087 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts @@ -86,6 +86,7 @@ export class PeertubePlayerManager { private static videojsLocaleCache: { [ path: string ]: any } = {} private static playerElementClassName: string + private static onPlayerChange: (player: any) => void static getServerTranslations (serverUrl: string, locale: string) { const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) @@ -100,9 +101,10 @@ export class PeertubePlayerManager { }) } - static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) { + static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: any) => void) { let p2pMediaLoader: any + this.onPlayerChange = onPlayerChange this.playerElementClassName = options.common.playerElement.className if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin') @@ -171,6 +173,8 @@ export class PeertubePlayerManager { const player = this self.addContextMenu(mode, player, options.common.embedUrl) + + PeertubePlayerManager.onPlayerChange(player) }) } diff --git a/client/src/assets/player/videojs-components/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts index 78879a2ec..24b7e0c70 100644 --- a/client/src/assets/player/videojs-components/settings-menu-item.ts +++ b/client/src/assets/player/videojs-components/settings-menu-item.ts @@ -43,6 +43,9 @@ class SettingsMenuItem extends MenuItem { player.ready(() => { // Voodoo magic for IOS setTimeout(() => { + // Player was destroyed + if (!this.player_) return + this.build() // Update on rate change diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index cfe8e94b1..6ff3efef1 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts @@ -212,7 +212,7 @@ export class PeerTubeEmbed { }) } - this.player = await PeertubePlayerManager.initialize(this.mode, options) + this.player = await PeertubePlayerManager.initialize(this.mode, options, player => this.player = player) this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) window[ 'videojsPlayer' ] = this.player diff --git a/server/controllers/api/users/my-video-playlists.ts b/server/controllers/api/users/my-video-playlists.ts index 15e92f4f3..735a3cbee 100644 --- a/server/controllers/api/users/my-video-playlists.ts +++ b/server/controllers/api/users/my-video-playlists.ts @@ -35,6 +35,7 @@ async function doVideosInPlaylistExist (req: express.Request, res: express.Respo for (const result of results) { for (const element of result.VideoPlaylistElements) { existObject[element.videoId].push({ + playlistElementId: element.id, playlistId: result.id, startTimestamp: element.startTimestamp, stopTimestamp: element.stopTimestamp diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index 62490e63b..540120cca 100644 --- a/server/controllers/api/video-playlist.ts +++ b/server/controllers/api/video-playlist.ts @@ -4,14 +4,13 @@ import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, - commonVideosFiltersValidator, optionalAuthenticate, paginationValidator, setDefaultPagination, setDefaultSort } from '../../middlewares' import { videoPlaylistsSortValidator } from '../../middlewares/validators' -import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' +import { buildNSFWFilter, createReqFiles } from '../../helpers/express-utils' import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants' import { logger } from '../../helpers/logger' import { resetSequelizeInstance } from '../../helpers/database-utils' @@ -32,7 +31,6 @@ import { join } from 'path' import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' import { getVideoPlaylistActivityPubUrl, getVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' import { VideoPlaylistUpdate } from '../../../shared/models/videos/playlist/video-playlist-update.model' -import { VideoModel } from '../../models/video/video' import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' @@ -88,7 +86,6 @@ videoPlaylistRouter.get('/:playlistId/videos', paginationValidator, setDefaultPagination, optionalAuthenticate, - commonVideosFiltersValidator, asyncMiddleware(getVideoPlaylistVideos) ) @@ -104,13 +101,13 @@ videoPlaylistRouter.post('/:playlistId/videos/reorder', asyncRetryTransactionMiddleware(reorderVideosPlaylist) ) -videoPlaylistRouter.put('/:playlistId/videos/:videoId', +videoPlaylistRouter.put('/:playlistId/videos/:playlistElementId', authenticate, asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator), asyncRetryTransactionMiddleware(updateVideoPlaylistElement) ) -videoPlaylistRouter.delete('/:playlistId/videos/:videoId', +videoPlaylistRouter.delete('/:playlistId/videos/:playlistElementId', authenticate, asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator), asyncRetryTransactionMiddleware(removeVideoFromPlaylist) @@ -426,26 +423,20 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons async function getVideoPlaylistVideos (req: express.Request, res: express.Response) { const videoPlaylistInstance = res.locals.videoPlaylist - const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined + const user = res.locals.oauth ? res.locals.oauth.token.User : undefined + const server = await getServerActor() - const resultList = await VideoModel.listForApi({ - followerActorId, + const resultList = await VideoPlaylistElementModel.listForApi({ start: req.query.start, count: req.query.count, - sort: 'VideoPlaylistElements.position', - includeLocalVideos: true, - categoryOneOf: req.query.categoryOneOf, - licenceOneOf: req.query.licenceOneOf, - languageOneOf: req.query.languageOneOf, - tagsOneOf: req.query.tagsOneOf, - tagsAllOf: req.query.tagsAllOf, - filter: req.query.filter, - nsfw: buildNSFWFilter(res, req.query.nsfw), - withFiles: false, videoPlaylistId: videoPlaylistInstance.id, - user: res.locals.oauth ? res.locals.oauth.token.User : undefined + serverAccount: server.Account, + user }) - const additionalAttributes = { playlistInfo: true } - return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes })) + const options = { + displayNSFW: buildNSFWFilter(res, req.query.nsfw), + accountId: user ? user.Account.id : undefined + } + return res.json(getFormattedObjects(resultList.data, resultList.total, options)) } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 5fe7d416c..8ab7c6bbd 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' // --------------------------------------------------------------------------- -const LAST_MIGRATION_VERSION = 405 +const LAST_MIGRATION_VERSION = 410 // --------------------------------------------------------------------------- diff --git a/server/initializers/migrations/0410-video-playlist-element.ts b/server/initializers/migrations/0410-video-playlist-element.ts new file mode 100644 index 000000000..f536632a2 --- /dev/null +++ b/server/initializers/migrations/0410-video-playlist-element.ts @@ -0,0 +1,39 @@ +import * as Sequelize from 'sequelize' + +async function up (utils: { + transaction: Sequelize.Transaction, + queryInterface: Sequelize.QueryInterface, + sequelize: Sequelize.Sequelize, + db: any +}): Promise { + { + const data = { + type: Sequelize.INTEGER, + allowNull: true, + defaultValue: null + } + + await utils.queryInterface.changeColumn('videoPlaylistElement', 'videoId', data) + } + + await utils.queryInterface.removeConstraint('videoPlaylistElement', 'videoPlaylistElement_videoId_fkey') + + await utils.queryInterface.addConstraint('videoPlaylistElement', [ 'videoId' ], { + type: 'foreign key', + references: { + table: 'video', + field: 'id' + }, + onDelete: 'set null', + onUpdate: 'CASCADE' + }) +} + +function down (options) { + throw new Error('Not implemented.') +} + +export { + up, + down +} diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index 2e9c8aa33..5823795be 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/server/middlewares/validators/videos/video-playlists.ts @@ -207,8 +207,8 @@ const videoPlaylistsAddVideoValidator = [ const videoPlaylistsUpdateOrRemoveVideoValidator = [ param('playlistId') .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), - param('videoId') - .custom(isIdOrUUIDValid).withMessage('Should have an video id/uuid'), + param('playlistElementId') + .custom(isIdValid).withMessage('Should have an element id/uuid'), body('startTimestamp') .optional() .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'), @@ -222,12 +222,10 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [ if (areValidationErrors(req, res)) return if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return - if (!await doesVideoExist(req.params.videoId, res, 'id')) return const videoPlaylist = res.locals.videoPlaylist - const video = res.locals.video - const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndVideo(videoPlaylist.id, video.id) + const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId) if (!videoPlaylistElement) { res.status(404) .json({ error: 'Video playlist element not found' }) diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index 59f586b54..85af9e378 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts @@ -9,7 +9,7 @@ import { ActorModel } from '../activitypub/actor' import { getSort, throwIfNotValid } from '../utils' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { AccountVideoRate } from '../../../shared' -import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from '../video/video-channel' +import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' /* Account rates per video. @@ -109,7 +109,7 @@ export class AccountVideoRateModel extends Model { required: true, include: [ { - model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, true] }), + model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), required: true } ] diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 09cada096..28014946f 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts @@ -27,12 +27,19 @@ import { UserModel } from './user' import { AvatarModel } from '../avatar/avatar' import { VideoPlaylistModel } from '../video/video-playlist' import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' -import { Op, Transaction, WhereOptions } from 'sequelize' +import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequelize' +import { AccountBlocklistModel } from './account-blocklist' +import { ServerBlocklistModel } from '../server/server-blocklist' export enum ScopeNames { SUMMARY = 'SUMMARY' } +export type SummaryOptions = { + whereActor?: WhereOptions + withAccountBlockerIds?: number[] +} + @DefaultScope(() => ({ include: [ { @@ -42,8 +49,16 @@ export enum ScopeNames { ] })) @Scopes(() => ({ - [ ScopeNames.SUMMARY ]: (whereActor?: WhereOptions) => { - return { + [ ScopeNames.SUMMARY ]: (options: SummaryOptions = {}) => { + const whereActor = options.whereActor || undefined + + const serverInclude: IncludeOptions = { + attributes: [ 'host' ], + model: ServerModel.unscoped(), + required: false + } + + const query: FindOptions = { attributes: [ 'id', 'name' ], include: [ { @@ -52,11 +67,8 @@ export enum ScopeNames { required: true, where: whereActor, include: [ - { - attributes: [ 'host' ], - model: ServerModel.unscoped(), - required: false - }, + serverInclude, + { model: AvatarModel.unscoped(), required: false @@ -65,6 +77,35 @@ export enum ScopeNames { } ] } + + if (options.withAccountBlockerIds) { + query.include.push({ + attributes: [ 'id' ], + model: AccountBlocklistModel.unscoped(), + as: 'BlockedAccounts', + required: false, + where: { + accountId: { + [Op.in]: options.withAccountBlockerIds + } + } + }) + + serverInclude.include = [ + { + attributes: [ 'id' ], + model: ServerBlocklistModel.unscoped(), + required: false, + where: { + accountId: { + [Op.in]: options.withAccountBlockerIds + } + } + } + ] + } + + return query } })) @Table({ @@ -163,6 +204,16 @@ export class AccountModel extends Model { }) VideoComments: VideoCommentModel[] + @HasMany(() => AccountBlocklistModel, { + foreignKey: { + name: 'targetAccountId', + allowNull: false + }, + as: 'BlockedAccounts', + onDelete: 'CASCADE' + }) + BlockedAccounts: AccountBlocklistModel[] + @BeforeDestroy static async sendDeleteIfOwned (instance: AccountModel, options) { if (!instance.Actor) { @@ -343,4 +394,8 @@ export class AccountModel extends Model { getDisplayName () { return this.name } + + isBlocked () { + return this.BlockedAccounts && this.BlockedAccounts.length !== 0 + } } diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 92c01f642..5138b0f76 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts @@ -67,7 +67,6 @@ export class ServerBlocklistModel extends Model { @BelongsTo(() => ServerModel, { foreignKey: { - name: 'targetServerId', allowNull: false }, onDelete: 'CASCADE' diff --git a/server/models/server/server.ts b/server/models/server/server.ts index 300d70938..1d211f1e0 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts @@ -2,6 +2,8 @@ import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, Updat import { isHostValid } from '../../helpers/custom-validators/servers' import { ActorModel } from '../activitypub/actor' import { throwIfNotValid } from '../utils' +import { AccountBlocklistModel } from '../account/account-blocklist' +import { ServerBlocklistModel } from './server-blocklist' @Table({ tableName: 'server', @@ -40,6 +42,14 @@ export class ServerModel extends Model { }) Actors: ActorModel[] + @HasMany(() => ServerBlocklistModel, { + foreignKey: { + allowNull: false + }, + onDelete: 'CASCADE' + }) + BlockedByAccounts: ServerBlocklistModel[] + static loadByHost (host: string) { const query = { where: { @@ -50,6 +60,10 @@ export class ServerModel extends Model { return ServerModel.findOne(query) } + isBlocked () { + return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0 + } + toFormattedJSON () { return { host: this.host diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index baef1d6ce..22d949da0 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts @@ -1,7 +1,7 @@ import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { getSortOnModel, SortType, throwIfNotValid } from '../utils' import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' -import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' +import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' import { CONSTRAINTS_FIELDS } from '../../initializers/constants' @@ -71,7 +71,7 @@ export class VideoBlacklistModel extends Model { required: true, include: [ { - model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }), + model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), required: true }, { diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index b0b261c88..6241a75a3 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts @@ -24,7 +24,7 @@ import { isVideoChannelSupportValid } from '../../helpers/custom-validators/video-channels' import { sendDeleteActor } from '../../lib/activitypub/send' -import { AccountModel, ScopeNames as AccountModelScopeNames } from '../account/account' +import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' import { VideoModel } from './video' @@ -58,6 +58,11 @@ type AvailableForListOptions = { actorId: number } +export type SummaryOptions = { + withAccount?: boolean // Default: false + withAccountBlockerIds?: number[] +} + @DefaultScope(() => ({ include: [ { @@ -67,7 +72,7 @@ type AvailableForListOptions = { ] })) @Scopes(() => ({ - [ScopeNames.SUMMARY]: (withAccount = false) => { + [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { const base: FindOptions = { attributes: [ 'name', 'description', 'id', 'actorId' ], include: [ @@ -90,9 +95,11 @@ type AvailableForListOptions = { ] } - if (withAccount === true) { + if (options.withAccount === true) { base.include.push({ - model: AccountModel.scope(AccountModelScopeNames.SUMMARY), + model: AccountModel.scope({ + method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ] + }), required: true }) } diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index b947eb16f..284539def 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts @@ -26,7 +26,6 @@ export type VideoFormattingJSONOptions = { waitTranscoding?: boolean, scheduledUpdate?: boolean, blacklistInfo?: boolean - playlistInfo?: boolean } } function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video { @@ -98,17 +97,6 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting videoObject.blacklisted = !!video.VideoBlacklist videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null } - - if (options.additionalAttributes.playlistInfo === true) { - // We filtered on a specific videoId/videoPlaylistId, that is unique - const playlistElement = video.VideoPlaylistElements[0] - - videoObject.playlistElement = { - position: playlistElement.position, - startTimestamp: playlistElement.startTimestamp, - stopTimestamp: playlistElement.stopTimestamp - } - } } return videoObject diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index eeb3d6bbd..bed6f8eaf 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts @@ -13,14 +13,18 @@ import { Table, UpdatedAt } from 'sequelize-typescript' -import { VideoModel } from './video' +import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' import { VideoPlaylistModel } from './video-playlist' import { getSort, throwIfNotValid } from '../utils' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { CONSTRAINTS_FIELDS } from '../../initializers/constants' import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' import * as validator from 'validator' -import { AggregateOptions, Op, Sequelize, Transaction } from 'sequelize' +import { AggregateOptions, Op, ScopeOptions, Sequelize, Transaction } from 'sequelize' +import { UserModel } from '../account/user' +import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model' +import { AccountModel } from '../account/account' +import { VideoPrivacy } from '../../../shared/models/videos' @Table({ tableName: 'videoPlaylistElement', @@ -90,9 +94,9 @@ export class VideoPlaylistElementModel extends Model @BelongsTo(() => VideoModel, { foreignKey: { - allowNull: false + allowNull: true }, - onDelete: 'CASCADE' + onDelete: 'set null' }) Video: VideoModel @@ -107,6 +111,57 @@ export class VideoPlaylistElementModel extends Model return VideoPlaylistElementModel.destroy(query) } + static listForApi (options: { + start: number, + count: number, + videoPlaylistId: number, + serverAccount: AccountModel, + user?: UserModel + }) { + const accountIds = [ options.serverAccount.id ] + const videoScope: (ScopeOptions | string)[] = [ + VideoScopeNames.WITH_BLACKLISTED + ] + + if (options.user) { + accountIds.push(options.user.Account.id) + videoScope.push({ method: [ VideoScopeNames.WITH_USER_HISTORY, options.user.id ] }) + } + + const forApiOptions: ForAPIOptions = { withAccountBlockerIds: accountIds } + videoScope.push({ + method: [ + VideoScopeNames.FOR_API, forApiOptions + ] + }) + + const findQuery = { + offset: options.start, + limit: options.count, + order: getSort('position'), + where: { + videoPlaylistId: options.videoPlaylistId + }, + include: [ + { + model: VideoModel.scope(videoScope), + required: false + } + ] + } + + const countQuery = { + where: { + videoPlaylistId: options.videoPlaylistId + } + } + + return Promise.all([ + VideoPlaylistElementModel.count(countQuery), + VideoPlaylistElementModel.findAll(findQuery) + ]).then(([ total, data ]) => ({ total, data })) + } + static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) { const query = { where: { @@ -118,6 +173,10 @@ export class VideoPlaylistElementModel extends Model return VideoPlaylistElementModel.findOne(query) } + static loadById (playlistElementId: number) { + return VideoPlaylistElementModel.findByPk(playlistElementId) + } + static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) { const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId } const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId } @@ -213,6 +272,42 @@ export class VideoPlaylistElementModel extends Model return VideoPlaylistElementModel.increment({ position: by }, query) } + getType (displayNSFW?: boolean, accountId?: number) { + const video = this.Video + + if (!video) return VideoPlaylistElementType.DELETED + + // Owned video, don't filter it + if (accountId && video.VideoChannel.Account.id === accountId) return VideoPlaylistElementType.REGULAR + + if (video.privacy === VideoPrivacy.PRIVATE) return VideoPlaylistElementType.PRIVATE + + if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE + if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE + + return VideoPlaylistElementType.REGULAR + } + + getVideoElement (displayNSFW?: boolean, accountId?: number) { + if (!this.Video) return null + if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null + + return this.Video.toFormattedJSON() + } + + toFormattedJSON (options: { displayNSFW?: boolean, accountId?: number } = {}): VideoPlaylistElement { + return { + id: this.id, + position: this.position, + startTimestamp: this.startTimestamp, + stopTimestamp: this.stopTimestamp, + + type: this.getType(options.displayNSFW, options.accountId), + + video: this.getVideoElement(options.displayNSFW, options.accountId) + } + } + toActivityPubObject (): PlaylistElementObject { const base: PlaylistElementObject = { id: this.url, diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 63b4a0715..61ff78bd2 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts @@ -33,7 +33,7 @@ import { WEBSERVER } from '../../initializers/constants' import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' -import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' +import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' import { join } from 'path' import { VideoPlaylistElementModel } from './video-playlist-element' @@ -115,7 +115,7 @@ type AvailableForListOptions = { [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => { // Only list local playlists OR playlists that are on an instance followed by actorId const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) - const actorWhere = { + const whereActor = { [ Op.or ]: [ { serverId: null @@ -159,7 +159,7 @@ type AvailableForListOptions = { } const accountScope = { - method: [ AccountScopeNames.SUMMARY, actorWhere ] + method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ] } return { @@ -341,7 +341,7 @@ export class VideoPlaylistModel extends Model { }, include: [ { - attributes: [ 'videoId', 'startTimestamp', 'stopTimestamp' ], + attributes: [ 'id', 'videoId', 'startTimestamp', 'stopTimestamp' ], model: VideoPlaylistElementModel.unscoped(), where: { videoId: { diff --git a/server/models/video/video.ts b/server/models/video/video.ts index c7f2658ed..05d625fc1 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -91,7 +91,7 @@ import { } from '../utils' import { TagModel } from './tag' import { VideoAbuseModel } from './video-abuse' -import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' +import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' import { VideoCommentModel } from './video-comment' import { VideoFileModel } from './video-file' import { VideoShareModel } from './video-share' @@ -190,26 +190,29 @@ export enum ScopeNames { WITH_FILES = 'WITH_FILES', WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', WITH_BLACKLISTED = 'WITH_BLACKLISTED', + WITH_BLOCKLIST = 'WITH_BLOCKLIST', WITH_USER_HISTORY = 'WITH_USER_HISTORY', WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', WITH_USER_ID = 'WITH_USER_ID', WITH_THUMBNAILS = 'WITH_THUMBNAILS' } -type ForAPIOptions = { - ids: number[] +export type ForAPIOptions = { + ids?: number[] videoPlaylistId?: number withFiles?: boolean + + withAccountBlockerIds?: number[] } -type AvailableForListIDsOptions = { +export type AvailableForListIDsOptions = { serverAccountId: number followerActorId: number includeLocalVideos: boolean - withoutId?: boolean + attributesType?: 'none' | 'id' | 'all' filter?: VideoFilter categoryOneOf?: number[] @@ -236,14 +239,16 @@ type AvailableForListIDsOptions = { @Scopes(() => ({ [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { const query: FindOptions = { - where: { - id: { - [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken - } - }, include: [ { - model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }), + model: VideoChannelModel.scope({ + method: [ + VideoChannelScopeNames.SUMMARY, { + withAccount: true, + withAccountBlockerIds: options.withAccountBlockerIds + } as SummaryOptions + ] + }), required: true }, { @@ -254,6 +259,14 @@ type AvailableForListIDsOptions = { ] } + if (options.ids) { + query.where = { + id: { + [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken + } + } + } + if (options.withFiles === true) { query.include.push({ model: VideoFileModel.unscoped(), @@ -278,10 +291,14 @@ type AvailableForListIDsOptions = { const query: FindOptions = { raw: true, - attributes: options.withoutId === true ? [] : [ 'id' ], include: [] } + const attributesType = options.attributesType || 'id' + + if (attributesType === 'id') query.attributes = [ 'id' ] + else if (attributesType === 'none') query.attributes = [ ] + whereAnd.push({ id: { [ Op.notIn ]: Sequelize.literal( @@ -290,17 +307,19 @@ type AvailableForListIDsOptions = { } }) - whereAnd.push({ - channelId: { - [ Op.notIn ]: Sequelize.literal( - '(' + - 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + - buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + - ')' + - ')' - ) - } - }) + if (options.serverAccountId) { + whereAnd.push({ + channelId: { + [ Op.notIn ]: Sequelize.literal( + '(' + + 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + + buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + + ')' + + ')' + ) + } + }) + } // Only list public/published videos if (!options.filter || options.filter !== 'all-local') { @@ -527,6 +546,9 @@ type AvailableForListIDsOptions = { } return query + }, + [ScopeNames.WITH_BLOCKLIST]: { + }, [ ScopeNames.WITH_THUMBNAILS ]: { include: [ @@ -845,9 +867,9 @@ export class VideoModel extends Model { @HasMany(() => VideoPlaylistElementModel, { foreignKey: { name: 'videoId', - allowNull: false + allowNull: true }, - onDelete: 'cascade' + onDelete: 'set null' }) VideoPlaylistElements: VideoPlaylistElementModel[] @@ -1586,7 +1608,7 @@ export class VideoModel extends Model { serverAccountId: serverActor.Account.id, followerActorId, includeLocalVideos: true, - withoutId: true // Don't break aggregation + attributesType: 'none' // Don't break aggregation } const query: FindOptions = { @@ -1719,6 +1741,11 @@ export class VideoModel extends Model { return !!this.VideoBlacklist } + isBlocked () { + return (this.VideoChannel.Account.Actor.Server && this.VideoChannel.Account.Actor.Server.isBlocked()) || + this.VideoChannel.Account.isBlocked() + } + getOriginalFile () { if (Array.isArray(this.VideoFiles) === false) return undefined diff --git a/server/tests/api/check-params/video-playlists.ts b/server/tests/api/check-params/video-playlists.ts index 8c5e44bdd..ae5aa287f 100644 --- a/server/tests/api/check-params/video-playlists.ts +++ b/server/tests/api/check-params/video-playlists.ts @@ -37,6 +37,7 @@ describe('Test video playlists API validator', function () { let watchLaterPlaylistId: number let videoId: number let videoId2: number + let playlistElementId: number // --------------------------------------------------------------- @@ -132,18 +133,18 @@ describe('Test video playlists API validator', function () { }) describe('When listing videos of a playlist', function () { - const path = '/api/v1/video-playlists' + const path = '/api/v1/video-playlists/' it('Should fail with a bad start pagination', async function () { - await checkBadStartPagination(server.url, path, server.accessToken) + await checkBadStartPagination(server.url, path + playlistUUID + '/videos', server.accessToken) }) it('Should fail with a bad count pagination', async function () { - await checkBadCountPagination(server.url, path, server.accessToken) + await checkBadCountPagination(server.url, path + playlistUUID + '/videos', server.accessToken) }) - it('Should fail with a bad filter', async function () { - await checkBadSortPagination(server.url, path, server.accessToken) + it('Should success with the correct parameters', async function () { + await makeGetRequest({ url: server.url, path: path + playlistUUID + '/videos', statusCodeExpected: 200 }) }) }) @@ -296,7 +297,7 @@ describe('Test video playlists API validator', function () { token: server.accessToken, playlistId: playlistUUID, elementAttrs: Object.assign({ - videoId: videoId, + videoId, startTimestamp: 2, stopTimestamp: 3 }, elementAttrs) @@ -344,7 +345,8 @@ describe('Test video playlists API validator', function () { it('Succeed with the correct params', async function () { const params = getBase({}, { expectedStatus: 200 }) - await addVideoInPlaylist(params) + const res = await addVideoInPlaylist(params) + playlistElementId = res.body.videoPlaylistElement.id }) it('Should fail if the video was already added in the playlist', async function () { @@ -362,7 +364,7 @@ describe('Test video playlists API validator', function () { startTimestamp: 1, stopTimestamp: 2 }, elementAttrs), - videoId: videoId, + playlistElementId, playlistId: playlistUUID, expectedStatus: 400 }, wrapper) @@ -390,14 +392,14 @@ describe('Test video playlists API validator', function () { } }) - it('Should fail with an unknown or incorrect video id', async function () { + it('Should fail with an unknown or incorrect playlistElement id', async function () { { - const params = getBase({}, { videoId: 'toto' }) + const params = getBase({}, { playlistElementId: 'toto' }) await updateVideoPlaylistElement(params) } { - const params = getBase({}, { videoId: 42, expectedStatus: 404 }) + const params = getBase({}, { playlistElementId: 42, expectedStatus: 404 }) await updateVideoPlaylistElement(params) } }) @@ -415,7 +417,7 @@ describe('Test video playlists API validator', function () { }) it('Should fail with an unknown element', async function () { - const params = getBase({}, { videoId: videoId2, expectedStatus: 404 }) + const params = getBase({}, { playlistElementId: 888, expectedStatus: 404 }) await updateVideoPlaylistElement(params) }) @@ -587,7 +589,7 @@ describe('Test video playlists API validator', function () { return Object.assign({ url: server.url, token: server.accessToken, - videoId: videoId, + playlistElementId, playlistId: playlistUUID, expectedStatus: 400 }, wrapper) @@ -617,18 +619,18 @@ describe('Test video playlists API validator', function () { it('Should fail with an unknown or incorrect video id', async function () { { - const params = getBase({ videoId: 'toto' }) + const params = getBase({ playlistElementId: 'toto' }) await removeVideoFromPlaylist(params) } { - const params = getBase({ videoId: 42, expectedStatus: 404 }) + const params = getBase({ playlistElementId: 42, expectedStatus: 404 }) await removeVideoFromPlaylist(params) } }) it('Should fail with an unknown element', async function () { - const params = getBase({ videoId: videoId2, expectedStatus: 404 }) + const params = getBase({ playlistElementId: 888, expectedStatus: 404 }) await removeVideoFromPlaylist(params) }) diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts index babef8223..5a5668665 100644 --- a/server/tests/api/check-params/videos-filter.ts +++ b/server/tests/api/check-params/videos-filter.ts @@ -15,13 +15,12 @@ import { import { UserRole } from '../../../../shared/models/users' import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' -async function testEndpoints (server: ServerInfo, token: string, filter: string, playlistUUID: string, statusCodeExpected: number) { +async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: number) { const paths = [ '/api/v1/video-channels/root_channel/videos', '/api/v1/accounts/root/videos', '/api/v1/videos', - '/api/v1/search/videos', - '/api/v1/video-playlists/' + playlistUUID + '/videos' + '/api/v1/search/videos' ] for (const path of paths) { @@ -70,39 +69,28 @@ describe('Test videos filters', function () { } ) moderatorAccessToken = await userLogin(server, moderator) - - const res = await createVideoPlaylist({ - url: server.url, - token: server.accessToken, - playlistAttrs: { - displayName: 'super playlist', - privacy: VideoPlaylistPrivacy.PUBLIC, - videoChannelId: server.videoChannel.id - } - }) - playlistUUID = res.body.videoPlaylist.uuid }) describe('When setting a video filter', function () { it('Should fail with a bad filter', async function () { - await testEndpoints(server, server.accessToken, 'bad-filter', playlistUUID, 400) + await testEndpoints(server, server.accessToken, 'bad-filter', 400) }) it('Should succeed with a good filter', async function () { - await testEndpoints(server, server.accessToken,'local', playlistUUID, 200) + await testEndpoints(server, server.accessToken,'local', 200) }) it('Should fail to list all-local with a simple user', async function () { - await testEndpoints(server, userAccessToken, 'all-local', playlistUUID, 401) + await testEndpoints(server, userAccessToken, 'all-local', 401) }) it('Should succeed to list all-local with a moderator', async function () { - await testEndpoints(server, moderatorAccessToken, 'all-local', playlistUUID, 200) + await testEndpoints(server, moderatorAccessToken, 'all-local', 200) }) it('Should succeed to list all-local with an admin', async function () { - await testEndpoints(server, server.accessToken, 'all-local', playlistUUID, 200) + await testEndpoints(server, server.accessToken, 'all-local', 200) }) // Because we cannot authenticate the user on the RSS endpoint diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index 51e592a15..fa6d6f622 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts @@ -62,7 +62,7 @@ describe('Test videos API validator', function () { } }) - describe('When listing a video', function () { + describe('When listing videos', function () { it('Should fail with a bad start pagination', async function () { await checkBadStartPagination(server.url, path) }) diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts index f82c8cbce..7d5e3914b 100644 --- a/server/tests/api/videos/video-playlists.ts +++ b/server/tests/api/videos/video-playlists.ts @@ -5,6 +5,7 @@ import 'mocha' import { addVideoChannel, addVideoInPlaylist, + addVideoToBlacklist, checkPlaylistFilesWereRemoved, cleanupTests, createUser, @@ -14,6 +15,8 @@ import { doubleFollow, doVideosExistInMyPlaylist, flushAndRunMultipleServers, + generateUserAccessToken, + getAccessToken, getAccountPlaylistsList, getAccountPlaylistsListWithToken, getMyUserInformation, @@ -24,6 +27,7 @@ import { getVideoPlaylistsList, getVideoPlaylistWithToken, removeUser, + removeVideoFromBlacklist, removeVideoFromPlaylist, reorderVideosPlaylist, ServerInfo, @@ -31,23 +35,58 @@ import { setDefaultVideoChannel, testImage, unfollow, + updateVideo, updateVideoPlaylist, updateVideoPlaylistElement, uploadVideo, uploadVideoAndGetId, userLogin, - waitJobs, - generateUserAccessToken + waitJobs } from '../../../../shared/extra-utils' import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' import { VideoPlaylist } from '../../../../shared/models/videos/playlist/video-playlist.model' -import { Video } from '../../../../shared/models/videos' +import { VideoPrivacy } from '../../../../shared/models/videos' import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model' import { VideoExistInPlaylist } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model' import { User } from '../../../../shared/models/users' +import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../../shared/models/videos/playlist/video-playlist-element.model' +import { + addAccountToAccountBlocklist, + addAccountToServerBlocklist, + addServerToAccountBlocklist, + addServerToServerBlocklist, + removeAccountFromAccountBlocklist, + removeAccountFromServerBlocklist, + removeServerFromAccountBlocklist, + removeServerFromServerBlocklist +} from '../../../../shared/extra-utils/users/blocklist' const expect = chai.expect +async function checkPlaylistElementType ( + servers: ServerInfo[], + playlistId: string, + type: VideoPlaylistElementType, + position: number, + name: string, + total: number +) { + for (const server of servers) { + const res = await getPlaylistVideos(server.url, server.accessToken, playlistId, 0, 10) + expect(res.body.total).to.equal(total) + + const videoElement: VideoPlaylistElement = res.body.data.find((e: VideoPlaylistElement) => e.position === position) + expect(videoElement.type).to.equal(type, 'On server ' + server.url) + + if (type === VideoPlaylistElementType.REGULAR) { + expect(videoElement.video).to.not.be.null + expect(videoElement.video.name).to.equal(name) + } else { + expect(videoElement.video).to.be.null + } + } +} + describe('Test video playlists', function () { let servers: ServerInfo[] = [] @@ -57,9 +96,16 @@ describe('Test video playlists', function () { let playlistServer1Id: number let playlistServer1UUID: string + let playlistServer1UUID2: string + + let playlistElementServer1Video4: number + let playlistElementServer1Video5: number + let playlistElementNSFW: number let nsfwVideoServer1: number + let userAccessTokenServer1: string + before(async function () { this.timeout(120000) @@ -97,456 +143,737 @@ describe('Test video playlists', function () { nsfwVideoServer1 = (await uploadVideoAndGetId({ server: servers[ 0 ], videoName: 'NSFW video', nsfw: true })).id + { + await createUser({ + url: servers[ 0 ].url, + accessToken: servers[ 0 ].accessToken, + username: 'user1', + password: 'password' + }) + userAccessTokenServer1 = await getAccessToken(servers[0].url, 'user1', 'password') + } + await waitJobs(servers) }) - it('Should list video playlist privacies', async function () { - const res = await getVideoPlaylistPrivacies(servers[0].url) + describe('Get default playlists', function () { + it('Should list video playlist privacies', async function () { + const res = await getVideoPlaylistPrivacies(servers[ 0 ].url) - const privacies = res.body - expect(Object.keys(privacies)).to.have.length.at.least(3) + const privacies = res.body + expect(Object.keys(privacies)).to.have.length.at.least(3) - expect(privacies[3]).to.equal('Private') - }) + expect(privacies[ 3 ]).to.equal('Private') + }) - it('Should list watch later playlist', async function () { - const url = servers[ 0 ].url - const accessToken = servers[ 0 ].accessToken + it('Should list watch later playlist', async function () { + const url = servers[ 0 ].url + const accessToken = servers[ 0 ].accessToken - { - const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.WATCH_LATER) + { + const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.WATCH_LATER) - expect(res.body.total).to.equal(1) - expect(res.body.data).to.have.lengthOf(1) + expect(res.body.total).to.equal(1) + expect(res.body.data).to.have.lengthOf(1) - const playlist: VideoPlaylist = res.body.data[ 0 ] - expect(playlist.displayName).to.equal('Watch later') - expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER) - expect(playlist.type.label).to.equal('Watch later') - } + const playlist: VideoPlaylist = res.body.data[ 0 ] + expect(playlist.displayName).to.equal('Watch later') + expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER) + expect(playlist.type.label).to.equal('Watch later') + } - { - const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.REGULAR) + { + const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.REGULAR) - expect(res.body.total).to.equal(0) - expect(res.body.data).to.have.lengthOf(0) - } + expect(res.body.total).to.equal(0) + expect(res.body.data).to.have.lengthOf(0) + } - { - const res = await getAccountPlaylistsList(url, 'root', 0, 5) - expect(res.body.total).to.equal(0) - expect(res.body.data).to.have.lengthOf(0) - } - }) - - it('Should get private playlist for a classic user', async function () { - const token = await generateUserAccessToken(servers[0], 'toto') - - const res = await getAccountPlaylistsListWithToken(servers[0].url, token, 'toto', 0, 5) - - expect(res.body.total).to.equal(1) - expect(res.body.data).to.have.lengthOf(1) - - const playlistId = res.body.data[0].id - await getPlaylistVideos(servers[0].url, token, playlistId, 0, 5) - }) - - it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () { - this.timeout(30000) - - await createVideoPlaylist({ - url: servers[0].url, - token: servers[0].accessToken, - playlistAttrs: { - displayName: 'my super playlist', - privacy: VideoPlaylistPrivacy.PUBLIC, - description: 'my super description', - thumbnailfile: 'thumbnail.jpg', - videoChannelId: servers[0].videoChannel.id + { + const res = await getAccountPlaylistsList(url, 'root', 0, 5) + expect(res.body.total).to.equal(0) + expect(res.body.data).to.have.lengthOf(0) } }) - await waitJobs(servers) + it('Should get private playlist for a classic user', async function () { + const token = await generateUserAccessToken(servers[ 0 ], 'toto') + + const res = await getAccountPlaylistsListWithToken(servers[ 0 ].url, token, 'toto', 0, 5) - for (const server of servers) { - const res = await getVideoPlaylistsList(server.url, 0, 5) expect(res.body.total).to.equal(1) expect(res.body.data).to.have.lengthOf(1) - const playlistFromList = res.body.data[0] as VideoPlaylist + const playlistId = res.body.data[ 0 ].id + await getPlaylistVideos(servers[ 0 ].url, token, playlistId, 0, 5) + }) + }) - const res2 = await getVideoPlaylist(server.url, playlistFromList.uuid) - const playlistFromGet = res2.body + describe('Create and federate playlists', function () { - for (const playlist of [ playlistFromGet, playlistFromList ]) { - expect(playlist.id).to.be.a('number') - expect(playlist.uuid).to.be.a('string') + it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () { + this.timeout(30000) - expect(playlist.isLocal).to.equal(server.serverNumber === 1) + await createVideoPlaylist({ + url: servers[ 0 ].url, + token: servers[ 0 ].accessToken, + playlistAttrs: { + displayName: 'my super playlist', + privacy: VideoPlaylistPrivacy.PUBLIC, + description: 'my super description', + thumbnailfile: 'thumbnail.jpg', + videoChannelId: servers[ 0 ].videoChannel.id + } + }) + + await waitJobs(servers) + + for (const server of servers) { + const res = await getVideoPlaylistsList(server.url, 0, 5) + expect(res.body.total).to.equal(1) + expect(res.body.data).to.have.lengthOf(1) + + const playlistFromList = res.body.data[ 0 ] as VideoPlaylist + + const res2 = await getVideoPlaylist(server.url, playlistFromList.uuid) + const playlistFromGet = res2.body + + for (const playlist of [ playlistFromGet, playlistFromList ]) { + expect(playlist.id).to.be.a('number') + expect(playlist.uuid).to.be.a('string') + + expect(playlist.isLocal).to.equal(server.serverNumber === 1) + + expect(playlist.displayName).to.equal('my super playlist') + expect(playlist.description).to.equal('my super description') + expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC) + expect(playlist.privacy.label).to.equal('Public') + expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR) + expect(playlist.type.label).to.equal('Regular') + + expect(playlist.videosLength).to.equal(0) + + expect(playlist.ownerAccount.name).to.equal('root') + expect(playlist.ownerAccount.displayName).to.equal('root') + expect(playlist.videoChannel.name).to.equal('root_channel') + expect(playlist.videoChannel.displayName).to.equal('Main root channel') + } + } + }) + + it('Should create a playlist on server 2 and have the playlist on server 1 but not on server 3', async function () { + this.timeout(30000) + + { + const res = await createVideoPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistAttrs: { + displayName: 'playlist 2', + privacy: VideoPlaylistPrivacy.PUBLIC, + videoChannelId: servers[ 1 ].videoChannel.id + } + }) + playlistServer2Id1 = res.body.videoPlaylist.id + } + + { + const res = await createVideoPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistAttrs: { + displayName: 'playlist 3', + privacy: VideoPlaylistPrivacy.PUBLIC, + thumbnailfile: 'thumbnail.jpg', + videoChannelId: servers[ 1 ].videoChannel.id + } + }) + + playlistServer2Id2 = res.body.videoPlaylist.id + playlistServer2UUID2 = res.body.videoPlaylist.uuid + } + + for (let id of [ playlistServer2Id1, playlistServer2Id2 ]) { + await addVideoInPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistId: id, + elementAttrs: { videoId: servers[ 1 ].videos[ 0 ].id, startTimestamp: 1, stopTimestamp: 2 } + }) + await addVideoInPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistId: id, + elementAttrs: { videoId: servers[ 1 ].videos[ 1 ].id } + }) + } + + await waitJobs(servers) + + for (const server of [ servers[ 0 ], servers[ 1 ] ]) { + const res = await getVideoPlaylistsList(server.url, 0, 5) + + const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2') + expect(playlist2).to.not.be.undefined + await testImage(server.url, 'thumbnail-playlist', playlist2.thumbnailPath) + + const playlist3 = res.body.data.find(p => p.displayName === 'playlist 3') + expect(playlist3).to.not.be.undefined + await testImage(server.url, 'thumbnail', playlist3.thumbnailPath) + } + + const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) + expect(res.body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined + expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined + }) + + it('Should have the playlist on server 3 after a new follow', async function () { + this.timeout(30000) + + // Server 2 and server 3 follow each other + await doubleFollow(servers[ 1 ], servers[ 2 ]) + + const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) + + const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2') + expect(playlist2).to.not.be.undefined + await testImage(servers[ 2 ].url, 'thumbnail-playlist', playlist2.thumbnailPath) + + expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined + }) + }) + + describe('List playlists', function () { + it('Should correctly list the playlists', async function () { + this.timeout(30000) + + { + const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, 'createdAt') + + expect(res.body.total).to.equal(3) + + const data: VideoPlaylist[] = res.body.data + expect(data).to.have.lengthOf(2) + expect(data[ 0 ].displayName).to.equal('playlist 2') + expect(data[ 1 ].displayName).to.equal('playlist 3') + } + + { + const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, '-createdAt') + + expect(res.body.total).to.equal(3) + + const data: VideoPlaylist[] = res.body.data + expect(data).to.have.lengthOf(2) + expect(data[ 0 ].displayName).to.equal('playlist 2') + expect(data[ 1 ].displayName).to.equal('my super playlist') + } + }) + + it('Should list video channel playlists', async function () { + this.timeout(30000) + + { + const res = await getVideoChannelPlaylistsList(servers[ 0 ].url, 'root_channel', 0, 2, '-createdAt') + + expect(res.body.total).to.equal(1) + + const data: VideoPlaylist[] = res.body.data + expect(data).to.have.lengthOf(1) + expect(data[ 0 ].displayName).to.equal('my super playlist') + } + }) + + it('Should list account playlists', async function () { + this.timeout(30000) + + { + const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, '-createdAt') + + expect(res.body.total).to.equal(2) + + const data: VideoPlaylist[] = res.body.data + expect(data).to.have.lengthOf(1) + expect(data[ 0 ].displayName).to.equal('playlist 2') + } + + { + const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, 'createdAt') + + expect(res.body.total).to.equal(2) + + const data: VideoPlaylist[] = res.body.data + expect(data).to.have.lengthOf(1) + expect(data[ 0 ].displayName).to.equal('playlist 3') + } + }) + + it('Should not list unlisted or private playlists', async function () { + this.timeout(30000) + + await createVideoPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistAttrs: { + displayName: 'playlist unlisted', + privacy: VideoPlaylistPrivacy.UNLISTED + } + }) + + await createVideoPlaylist({ + url: servers[ 1 ].url, + token: servers[ 1 ].accessToken, + playlistAttrs: { + displayName: 'playlist private', + privacy: VideoPlaylistPrivacy.PRIVATE + } + }) + + await waitJobs(servers) + + for (const server of servers) { + const results = [ + await getAccountPlaylistsList(server.url, 'root@localhost:' + servers[ 1 ].port, 0, 5, '-createdAt'), + await getVideoPlaylistsList(server.url, 0, 2, '-createdAt') + ] + + expect(results[ 0 ].body.total).to.equal(2) + expect(results[ 1 ].body.total).to.equal(3) + + for (const res of results) { + const data: VideoPlaylist[] = res.body.data + expect(data).to.have.lengthOf(2) + expect(data[ 0 ].displayName).to.equal('playlist 3') + expect(data[ 1 ].displayName).to.equal('playlist 2') + } + } + }) + }) + + describe('Update playlists', function () { + + it('Should update a playlist', async function () { + this.timeout(30000) + + await updateVideoPlaylist({ + url: servers[1].url, + token: servers[1].accessToken, + playlistAttrs: { + displayName: 'playlist 3 updated', + description: 'description updated', + privacy: VideoPlaylistPrivacy.UNLISTED, + thumbnailfile: 'thumbnail.jpg', + videoChannelId: servers[1].videoChannel.id + }, + playlistId: playlistServer2Id2 + }) + + await waitJobs(servers) + + for (const server of servers) { + const res = await getVideoPlaylist(server.url, playlistServer2UUID2) + const playlist: VideoPlaylist = res.body + + expect(playlist.displayName).to.equal('playlist 3 updated') + expect(playlist.description).to.equal('description updated') + + expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.UNLISTED) + expect(playlist.privacy.label).to.equal('Unlisted') - expect(playlist.displayName).to.equal('my super playlist') - expect(playlist.description).to.equal('my super description') - expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC) - expect(playlist.privacy.label).to.equal('Public') expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR) expect(playlist.type.label).to.equal('Regular') - expect(playlist.videosLength).to.equal(0) + expect(playlist.videosLength).to.equal(2) expect(playlist.ownerAccount.name).to.equal('root') expect(playlist.ownerAccount.displayName).to.equal('root') expect(playlist.videoChannel.name).to.equal('root_channel') expect(playlist.videoChannel.displayName).to.equal('Main root channel') } - } + }) }) - it('Should create a playlist on server 2 and have the playlist on server 1 but not on server 3', async function () { - this.timeout(30000) + describe('Element timestamps', function () { - { - const res = await createVideoPlaylist({ - url: servers[1].url, - token: servers[1].accessToken, - playlistAttrs: { - displayName: 'playlist 2', - privacy: VideoPlaylistPrivacy.PUBLIC, - videoChannelId: servers[1].videoChannel.id - } - }) - playlistServer2Id1 = res.body.videoPlaylist.id - } + it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () { + this.timeout(30000) + + const addVideo = (elementAttrs: any) => { + return addVideoInPlaylist({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, playlistId: playlistServer1Id, elementAttrs }) + } - { const res = await createVideoPlaylist({ - url: servers[ 1 ].url, - token: servers[ 1 ].accessToken, + url: servers[ 0 ].url, + token: servers[ 0 ].accessToken, playlistAttrs: { - displayName: 'playlist 3', + displayName: 'playlist 4', privacy: VideoPlaylistPrivacy.PUBLIC, - thumbnailfile: 'thumbnail.jpg', - videoChannelId: servers[1].videoChannel.id + videoChannelId: servers[ 0 ].videoChannel.id } }) - playlistServer2Id2 = res.body.videoPlaylist.id - playlistServer2UUID2 = res.body.videoPlaylist.uuid - } + playlistServer1Id = res.body.videoPlaylist.id + playlistServer1UUID = res.body.videoPlaylist.uuid - for (let id of [ playlistServer2Id1, playlistServer2Id2 ]) { - await addVideoInPlaylist({ - url: servers[ 1 ].url, - token: servers[ 1 ].accessToken, - playlistId: id, - elementAttrs: { videoId: servers[ 1 ].videos[ 0 ].id, startTimestamp: 1, stopTimestamp: 2 } - }) - await addVideoInPlaylist({ - url: servers[ 1 ].url, - token: servers[ 1 ].accessToken, - playlistId: id, - elementAttrs: { videoId: servers[ 1 ].videos[ 1 ].id } - }) - } + await addVideo({ videoId: servers[ 0 ].videos[ 0 ].uuid, startTimestamp: 15, stopTimestamp: 28 }) + await addVideo({ videoId: servers[ 2 ].videos[ 1 ].uuid, startTimestamp: 35 }) + await addVideo({ videoId: servers[ 2 ].videos[ 2 ].uuid }) + { + const res = await addVideo({ videoId: servers[ 0 ].videos[ 3 ].uuid, stopTimestamp: 35 }) + playlistElementServer1Video4 = res.body.videoPlaylistElement.id + } - await waitJobs(servers) + { + const res = await addVideo({ videoId: servers[ 0 ].videos[ 4 ].uuid, startTimestamp: 45, stopTimestamp: 60 }) + playlistElementServer1Video5 = res.body.videoPlaylistElement.id + } - for (const server of [ servers[0], servers[1] ]) { - const res = await getVideoPlaylistsList(server.url, 0, 5) + { + const res = await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 }) + playlistElementNSFW = res.body.videoPlaylistElement.id + } - const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2') - expect(playlist2).to.not.be.undefined - await testImage(server.url, 'thumbnail-playlist', playlist2.thumbnailPath) + await waitJobs(servers) + }) - const playlist3 = res.body.data.find(p => p.displayName === 'playlist 3') - expect(playlist3).to.not.be.undefined - await testImage(server.url, 'thumbnail', playlist3.thumbnailPath) - } + it('Should correctly list playlist videos', async function () { + this.timeout(30000) - const res = await getVideoPlaylistsList(servers[2].url, 0, 5) - expect(res.body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined - expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined + for (const server of servers) { + const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) + + expect(res.body.total).to.equal(6) + + const videoElements: VideoPlaylistElement[] = res.body.data + expect(videoElements).to.have.lengthOf(6) + + expect(videoElements[ 0 ].video.name).to.equal('video 0 server 1') + expect(videoElements[ 0 ].position).to.equal(1) + expect(videoElements[ 0 ].startTimestamp).to.equal(15) + expect(videoElements[ 0 ].stopTimestamp).to.equal(28) + + expect(videoElements[ 1 ].video.name).to.equal('video 1 server 3') + expect(videoElements[ 1 ].position).to.equal(2) + expect(videoElements[ 1 ].startTimestamp).to.equal(35) + expect(videoElements[ 1 ].stopTimestamp).to.be.null + + expect(videoElements[ 2 ].video.name).to.equal('video 2 server 3') + expect(videoElements[ 2 ].position).to.equal(3) + expect(videoElements[ 2 ].startTimestamp).to.be.null + expect(videoElements[ 2 ].stopTimestamp).to.be.null + + expect(videoElements[ 3 ].video.name).to.equal('video 3 server 1') + expect(videoElements[ 3 ].position).to.equal(4) + expect(videoElements[ 3 ].startTimestamp).to.be.null + expect(videoElements[ 3 ].stopTimestamp).to.equal(35) + + expect(videoElements[ 4 ].video.name).to.equal('video 4 server 1') + expect(videoElements[ 4 ].position).to.equal(5) + expect(videoElements[ 4 ].startTimestamp).to.equal(45) + expect(videoElements[ 4 ].stopTimestamp).to.equal(60) + + expect(videoElements[ 5 ].video.name).to.equal('NSFW video') + expect(videoElements[ 5 ].position).to.equal(6) + expect(videoElements[ 5 ].startTimestamp).to.equal(5) + expect(videoElements[ 5 ].stopTimestamp).to.be.null + + const res3 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 2) + expect(res3.body.data).to.have.lengthOf(2) + } + }) }) - it('Should have the playlist on server 3 after a new follow', async function () { - this.timeout(30000) + describe('Element type', function () { + let groupUser1: ServerInfo[] + let groupWithoutToken1: ServerInfo[] + let group1: ServerInfo[] + let group2: ServerInfo[] - // Server 2 and server 3 follow each other - await doubleFollow(servers[1], servers[2]) + let video1: string + let video2: string + let video3: string - const res = await getVideoPlaylistsList(servers[2].url, 0, 5) + before(async function () { + this.timeout(30000) - const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2') - expect(playlist2).to.not.be.undefined - await testImage(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath) + groupUser1 = [ Object.assign({}, servers[ 0 ], { accessToken: userAccessTokenServer1 }) ] + groupWithoutToken1 = [ Object.assign({}, servers[ 0 ], { accessToken: undefined }) ] + group1 = [ servers[ 0 ] ] + group2 = [ servers[ 1 ], servers[ 2 ] ] - expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined - }) + const res = await createVideoPlaylist({ + url: servers[ 0 ].url, + token: userAccessTokenServer1, + playlistAttrs: { + displayName: 'playlist 56', + privacy: VideoPlaylistPrivacy.PUBLIC, + videoChannelId: servers[ 0 ].videoChannel.id + } + }) - it('Should correctly list the playlists', async function () { - this.timeout(30000) + const playlistServer1Id2 = res.body.videoPlaylist.id + playlistServer1UUID2 = res.body.videoPlaylist.uuid - { - const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, 'createdAt') + const addVideo = (elementAttrs: any) => { + return addVideoInPlaylist({ url: servers[ 0 ].url, token: userAccessTokenServer1, playlistId: playlistServer1Id2, elementAttrs }) + } + video1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 89', token: userAccessTokenServer1 })).uuid + video2 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 90' })).uuid + video3 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 91', nsfw: true })).uuid + + await addVideo({ videoId: video1, startTimestamp: 15, stopTimestamp: 28 }) + await addVideo({ videoId: video2, startTimestamp: 35 }) + await addVideo({ videoId: video3 }) + + await waitJobs(servers) + }) + + it('Should update the element type if the video is private', async function () { + this.timeout(20000) + + const name = 'video 89' + const position = 1 + + { + await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, video1, { privacy: VideoPrivacy.PRIVATE }) + await waitJobs(servers) + + await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) + await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) + await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) + } + + { + await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, video1, { privacy: VideoPrivacy.PUBLIC }) + await waitJobs(servers) + + await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + // We deleted the video, so even if we recreated it, the old entry is still deleted + await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) + } + }) + + it('Should update the element type if the video is blacklisted', async function () { + this.timeout(20000) + + const name = 'video 89' + const position = 1 + + { + await addVideoToBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video1, 'reason', true) + await waitJobs(servers) + + await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) + await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) + await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) + } + + { + await removeVideoFromBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video1) + await waitJobs(servers) + + await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + // We deleted the video (because unfederated), so even if we recreated it, the old entry is still deleted + await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) + } + }) + + it('Should update the element type if the account or server of the video is blocked', async function () { + this.timeout(90000) + + const name = 'video 90' + const position = 2 + + { + await addAccountToAccountBlocklist(servers[ 0 ].url, userAccessTokenServer1, 'root@localhost:' + servers[1].port) + await waitJobs(servers) + + await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) + await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + + await removeAccountFromAccountBlocklist(servers[ 0 ].url, userAccessTokenServer1, 'root@localhost:' + servers[1].port) + await waitJobs(servers) + + await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + } + + { + await addServerToAccountBlocklist(servers[ 0 ].url, userAccessTokenServer1, 'localhost:' + servers[1].port) + await waitJobs(servers) + + await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) + await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + + await removeServerFromAccountBlocklist(servers[ 0 ].url, userAccessTokenServer1, 'localhost:' + servers[1].port) + await waitJobs(servers) + + await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + } + + { + await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'root@localhost:' + servers[1].port) + await waitJobs(servers) + + await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) + await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + + await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'root@localhost:' + servers[1].port) + await waitJobs(servers) + + await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + } + + { + await addServerToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port) + await waitJobs(servers) + + await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) + await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + + await removeServerFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port) + await waitJobs(servers) + + await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) + } + }) + + it('Should hide the video if it is NSFW', async function () { + const res = await getPlaylistVideos(servers[0].url, userAccessTokenServer1, playlistServer1UUID2, 0, 10, { nsfw: false }) expect(res.body.total).to.equal(3) - const data: VideoPlaylist[] = res.body.data - expect(data).to.have.lengthOf(2) - expect(data[ 0 ].displayName).to.equal('playlist 2') - expect(data[ 1 ].displayName).to.equal('playlist 3') - } + const elements: VideoPlaylistElement[] = res.body.data + const element = elements.find(e => e.position === 3) - { - const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, '-createdAt') + expect(element).to.exist + expect(element.video).to.be.null + expect(element.type).to.equal(VideoPlaylistElementType.UNAVAILABLE) + }) - expect(res.body.total).to.equal(3) - - const data: VideoPlaylist[] = res.body.data - expect(data).to.have.lengthOf(2) - expect(data[ 0 ].displayName).to.equal('playlist 2') - expect(data[ 1 ].displayName).to.equal('my super playlist') - } }) - it('Should list video channel playlists', async function () { - this.timeout(30000) + describe('Managing playlist elements', function () { - { - const res = await getVideoChannelPlaylistsList(servers[ 0 ].url, 'root_channel', 0, 2, '-createdAt') + it('Should reorder the playlist', async function () { + this.timeout(30000) - expect(res.body.total).to.equal(1) + { + await reorderVideosPlaylist({ + url: servers[ 0 ].url, + token: servers[ 0 ].accessToken, + playlistId: playlistServer1Id, + elementAttrs: { + startPosition: 2, + insertAfterPosition: 3 + } + }) - const data: VideoPlaylist[] = res.body.data - expect(data).to.have.lengthOf(1) - expect(data[ 0 ].displayName).to.equal('my super playlist') - } - }) + await waitJobs(servers) - it('Should list account playlists', async function () { - this.timeout(30000) + for (const server of servers) { + const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) + const names = (res.body.data as VideoPlaylistElement[]).map(v => v.video.name) - { - const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, '-createdAt') + expect(names).to.deep.equal([ + 'video 0 server 1', + 'video 2 server 3', + 'video 1 server 3', + 'video 3 server 1', + 'video 4 server 1', + 'NSFW video' + ]) + } + } - expect(res.body.total).to.equal(2) + { + await reorderVideosPlaylist({ + url: servers[ 0 ].url, + token: servers[ 0 ].accessToken, + playlistId: playlistServer1Id, + elementAttrs: { + startPosition: 1, + reorderLength: 3, + insertAfterPosition: 4 + } + }) - const data: VideoPlaylist[] = res.body.data - expect(data).to.have.lengthOf(1) - expect(data[ 0 ].displayName).to.equal('playlist 2') - } + await waitJobs(servers) - { - const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, 'createdAt') + for (const server of servers) { + const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) + const names = (res.body.data as VideoPlaylistElement[]).map(v => v.video.name) - expect(res.body.total).to.equal(2) + expect(names).to.deep.equal([ + 'video 3 server 1', + 'video 0 server 1', + 'video 2 server 3', + 'video 1 server 3', + 'video 4 server 1', + 'NSFW video' + ]) + } + } - const data: VideoPlaylist[] = res.body.data - expect(data).to.have.lengthOf(1) - expect(data[ 0 ].displayName).to.equal('playlist 3') - } - }) + { + await reorderVideosPlaylist({ + url: servers[ 0 ].url, + token: servers[ 0 ].accessToken, + playlistId: playlistServer1Id, + elementAttrs: { + startPosition: 6, + insertAfterPosition: 3 + } + }) - it('Should not list unlisted or private playlists', async function () { - this.timeout(30000) + await waitJobs(servers) - await createVideoPlaylist({ - url: servers[ 1 ].url, - token: servers[ 1 ].accessToken, - playlistAttrs: { - displayName: 'playlist unlisted', - privacy: VideoPlaylistPrivacy.UNLISTED + for (const server of servers) { + const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) + const elements: VideoPlaylistElement[] = res.body.data + const names = elements.map(v => v.video.name) + + expect(names).to.deep.equal([ + 'video 3 server 1', + 'video 0 server 1', + 'video 2 server 3', + 'NSFW video', + 'video 1 server 3', + 'video 4 server 1' + ]) + + for (let i = 1; i <= elements.length; i++) { + expect(elements[ i - 1 ].position).to.equal(i) + } + } } }) - await createVideoPlaylist({ - url: servers[ 1 ].url, - token: servers[ 1 ].accessToken, - playlistAttrs: { - displayName: 'playlist private', - privacy: VideoPlaylistPrivacy.PRIVATE - } - }) + it('Should update startTimestamp/endTimestamp of some elements', async function () { + this.timeout(30000) - await waitJobs(servers) - - for (const server of servers) { - const results = [ - await getAccountPlaylistsList(server.url, 'root@localhost:' + servers[1].port, 0, 5, '-createdAt'), - await getVideoPlaylistsList(server.url, 0, 2, '-createdAt') - ] - - expect(results[0].body.total).to.equal(2) - expect(results[1].body.total).to.equal(3) - - for (const res of results) { - const data: VideoPlaylist[] = res.body.data - expect(data).to.have.lengthOf(2) - expect(data[ 0 ].displayName).to.equal('playlist 3') - expect(data[ 1 ].displayName).to.equal('playlist 2') - } - } - }) - - it('Should update a playlist', async function () { - this.timeout(30000) - - await updateVideoPlaylist({ - url: servers[1].url, - token: servers[1].accessToken, - playlistAttrs: { - displayName: 'playlist 3 updated', - description: 'description updated', - privacy: VideoPlaylistPrivacy.UNLISTED, - thumbnailfile: 'thumbnail.jpg', - videoChannelId: servers[1].videoChannel.id - }, - playlistId: playlistServer2Id2 - }) - - await waitJobs(servers) - - for (const server of servers) { - const res = await getVideoPlaylist(server.url, playlistServer2UUID2) - const playlist: VideoPlaylist = res.body - - expect(playlist.displayName).to.equal('playlist 3 updated') - expect(playlist.description).to.equal('description updated') - - expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.UNLISTED) - expect(playlist.privacy.label).to.equal('Unlisted') - - expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR) - expect(playlist.type.label).to.equal('Regular') - - expect(playlist.videosLength).to.equal(2) - - expect(playlist.ownerAccount.name).to.equal('root') - expect(playlist.ownerAccount.displayName).to.equal('root') - expect(playlist.videoChannel.name).to.equal('root_channel') - expect(playlist.videoChannel.displayName).to.equal('Main root channel') - } - }) - - it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () { - this.timeout(30000) - - const addVideo = (elementAttrs: any) => { - return addVideoInPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistId: playlistServer1Id, elementAttrs }) - } - - const res = await createVideoPlaylist({ - url: servers[ 0 ].url, - token: servers[ 0 ].accessToken, - playlistAttrs: { - displayName: 'playlist 4', - privacy: VideoPlaylistPrivacy.PUBLIC, - videoChannelId: servers[0].videoChannel.id - } - }) - - playlistServer1Id = res.body.videoPlaylist.id - playlistServer1UUID = res.body.videoPlaylist.uuid - - await addVideo({ videoId: servers[0].videos[0].uuid, startTimestamp: 15, stopTimestamp: 28 }) - await addVideo({ videoId: servers[2].videos[1].uuid, startTimestamp: 35 }) - await addVideo({ videoId: servers[2].videos[2].uuid }) - await addVideo({ videoId: servers[0].videos[3].uuid, stopTimestamp: 35 }) - await addVideo({ videoId: servers[0].videos[4].uuid, startTimestamp: 45, stopTimestamp: 60 }) - await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 }) - - await waitJobs(servers) - }) - - it('Should correctly list playlist videos', async function () { - this.timeout(30000) - - for (const server of servers) { - const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) - - expect(res.body.total).to.equal(6) - - const videos: Video[] = res.body.data - expect(videos).to.have.lengthOf(6) - - expect(videos[0].name).to.equal('video 0 server 1') - expect(videos[0].playlistElement.position).to.equal(1) - expect(videos[0].playlistElement.startTimestamp).to.equal(15) - expect(videos[0].playlistElement.stopTimestamp).to.equal(28) - - expect(videos[1].name).to.equal('video 1 server 3') - expect(videos[1].playlistElement.position).to.equal(2) - expect(videos[1].playlistElement.startTimestamp).to.equal(35) - expect(videos[1].playlistElement.stopTimestamp).to.be.null - - expect(videos[2].name).to.equal('video 2 server 3') - expect(videos[2].playlistElement.position).to.equal(3) - expect(videos[2].playlistElement.startTimestamp).to.be.null - expect(videos[2].playlistElement.stopTimestamp).to.be.null - - expect(videos[3].name).to.equal('video 3 server 1') - expect(videos[3].playlistElement.position).to.equal(4) - expect(videos[3].playlistElement.startTimestamp).to.be.null - expect(videos[3].playlistElement.stopTimestamp).to.equal(35) - - expect(videos[4].name).to.equal('video 4 server 1') - expect(videos[4].playlistElement.position).to.equal(5) - expect(videos[4].playlistElement.startTimestamp).to.equal(45) - expect(videos[4].playlistElement.stopTimestamp).to.equal(60) - - expect(videos[5].name).to.equal('NSFW video') - expect(videos[5].playlistElement.position).to.equal(6) - expect(videos[5].playlistElement.startTimestamp).to.equal(5) - expect(videos[5].playlistElement.stopTimestamp).to.be.null - - const res2 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10, { nsfw: false }) - expect(res2.body.total).to.equal(5) - expect(res2.body.data.find(v => v.name === 'NSFW video')).to.be.undefined - - const res3 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 2) - expect(res3.body.data).to.have.lengthOf(2) - } - }) - - it('Should reorder the playlist', async function () { - this.timeout(30000) - - { - await reorderVideosPlaylist({ + await updateVideoPlaylistElement({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, playlistId: playlistServer1Id, + playlistElementId: playlistElementServer1Video4, elementAttrs: { - startPosition: 2, - insertAfterPosition: 3 + startTimestamp: 1 } }) - await waitJobs(servers) - - for (const server of servers) { - const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) - const names = res.body.data.map(v => v.name) - - expect(names).to.deep.equal([ - 'video 0 server 1', - 'video 2 server 3', - 'video 1 server 3', - 'video 3 server 1', - 'video 4 server 1', - 'NSFW video' - ]) - } - } - - { - await reorderVideosPlaylist({ - url: servers[0].url, - token: servers[0].accessToken, + await updateVideoPlaylistElement({ + url: servers[ 0 ].url, + token: servers[ 0 ].accessToken, playlistId: playlistServer1Id, + playlistElementId: playlistElementServer1Video5, elementAttrs: { - startPosition: 1, - reorderLength: 3, - insertAfterPosition: 4 + stopTimestamp: null } }) @@ -554,357 +881,301 @@ describe('Test video playlists', function () { for (const server of servers) { const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) - const names = res.body.data.map(v => v.name) + const elements: VideoPlaylistElement[] = res.body.data - expect(names).to.deep.equal([ - 'video 3 server 1', - 'video 0 server 1', - 'video 2 server 3', - 'video 1 server 3', - 'video 4 server 1', - 'NSFW video' - ]) + expect(elements[ 0 ].video.name).to.equal('video 3 server 1') + expect(elements[ 0 ].position).to.equal(1) + expect(elements[ 0 ].startTimestamp).to.equal(1) + expect(elements[ 0 ].stopTimestamp).to.equal(35) + + expect(elements[ 5 ].video.name).to.equal('video 4 server 1') + expect(elements[ 5 ].position).to.equal(6) + expect(elements[ 5 ].startTimestamp).to.equal(45) + expect(elements[ 5 ].stopTimestamp).to.be.null } - } + }) - { - await reorderVideosPlaylist({ - url: servers[0].url, - token: servers[0].accessToken, + it('Should check videos existence in my playlist', async function () { + const videoIds = [ + servers[ 0 ].videos[ 0 ].id, + 42000, + servers[ 0 ].videos[ 3 ].id, + 43000, + servers[ 0 ].videos[ 4 ].id + ] + const res = await doVideosExistInMyPlaylist(servers[ 0 ].url, servers[ 0 ].accessToken, videoIds) + const obj = res.body as VideoExistInPlaylist + + { + const elem = obj[ servers[ 0 ].videos[ 0 ].id ] + expect(elem).to.have.lengthOf(1) + expect(elem[ 0 ].playlistElementId).to.exist + expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) + expect(elem[ 0 ].startTimestamp).to.equal(15) + expect(elem[ 0 ].stopTimestamp).to.equal(28) + } + + { + const elem = obj[ servers[ 0 ].videos[ 3 ].id ] + expect(elem).to.have.lengthOf(1) + expect(elem[ 0 ].playlistElementId).to.equal(playlistElementServer1Video4) + expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) + expect(elem[ 0 ].startTimestamp).to.equal(1) + expect(elem[ 0 ].stopTimestamp).to.equal(35) + } + + { + const elem = obj[ servers[ 0 ].videos[ 4 ].id ] + expect(elem).to.have.lengthOf(1) + expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) + expect(elem[ 0 ].startTimestamp).to.equal(45) + expect(elem[ 0 ].stopTimestamp).to.equal(null) + } + + expect(obj[ 42000 ]).to.have.lengthOf(0) + expect(obj[ 43000 ]).to.have.lengthOf(0) + }) + + it('Should automatically update updatedAt field of playlists', async function () { + const server = servers[ 1 ] + const videoId = servers[ 1 ].videos[ 5 ].id + + async function getPlaylistNames () { + const res = await getAccountPlaylistsListWithToken(server.url, server.accessToken, 'root', 0, 5, undefined, '-updatedAt') + + return (res.body.data as VideoPlaylist[]).map(p => p.displayName) + } + + const elementAttrs = { videoId } + const res1 = await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id1, elementAttrs }) + const res2 = await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id2, elementAttrs }) + + const element1 = res1.body.videoPlaylistElement.id + const element2 = res2.body.videoPlaylistElement.id + + const names1 = await getPlaylistNames() + expect(names1[ 0 ]).to.equal('playlist 3 updated') + expect(names1[ 1 ]).to.equal('playlist 2') + + await removeVideoFromPlaylist({ + url: server.url, + token: server.accessToken, + playlistId: playlistServer2Id1, + playlistElementId: element1 + }) + + const names2 = await getPlaylistNames() + expect(names2[ 0 ]).to.equal('playlist 2') + expect(names2[ 1 ]).to.equal('playlist 3 updated') + + await removeVideoFromPlaylist({ + url: server.url, + token: server.accessToken, + playlistId: playlistServer2Id2, + playlistElementId: element2 + }) + + const names3 = await getPlaylistNames() + expect(names3[ 0 ]).to.equal('playlist 3 updated') + expect(names3[ 1 ]).to.equal('playlist 2') + }) + + it('Should delete some elements', async function () { + this.timeout(30000) + + await removeVideoFromPlaylist({ + url: servers[ 0 ].url, + token: servers[ 0 ].accessToken, playlistId: playlistServer1Id, - elementAttrs: { - startPosition: 6, - insertAfterPosition: 3 - } + playlistElementId: playlistElementServer1Video4 + }) + + await removeVideoFromPlaylist({ + url: servers[ 0 ].url, + token: servers[ 0 ].accessToken, + playlistId: playlistServer1Id, + playlistElementId: playlistElementNSFW }) await waitJobs(servers) for (const server of servers) { const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) - const videos: Video[] = res.body.data - const names = videos.map(v => v.name) + expect(res.body.total).to.equal(4) - expect(names).to.deep.equal([ - 'video 3 server 1', - 'video 0 server 1', - 'video 2 server 3', - 'NSFW video', - 'video 1 server 3', - 'video 4 server 1' - ]) + const elements: VideoPlaylistElement[] = res.body.data + expect(elements).to.have.lengthOf(4) - for (let i = 1; i <= videos.length; i++) { - expect(videos[i - 1].playlistElement.position).to.equal(i) + expect(elements[ 0 ].video.name).to.equal('video 0 server 1') + expect(elements[ 0 ].position).to.equal(1) + + expect(elements[ 1 ].video.name).to.equal('video 2 server 3') + expect(elements[ 1 ].position).to.equal(2) + + expect(elements[ 2 ].video.name).to.equal('video 1 server 3') + expect(elements[ 2 ].position).to.equal(3) + + expect(elements[ 3 ].video.name).to.equal('video 4 server 1') + expect(elements[ 3 ].position).to.equal(4) + } + }) + + it('Should be able to create a public playlist, and set it to private', async function () { + this.timeout(30000) + + const res = await createVideoPlaylist({ + url: servers[ 0 ].url, + token: servers[ 0 ].accessToken, + playlistAttrs: { + displayName: 'my super public playlist', + privacy: VideoPlaylistPrivacy.PUBLIC, + videoChannelId: servers[ 0 ].videoChannel.id } + }) + const videoPlaylistIds = res.body.videoPlaylist + + await waitJobs(servers) + + for (const server of servers) { + await getVideoPlaylist(server.url, videoPlaylistIds.uuid, 200) } - } + + const playlistAttrs = { privacy: VideoPlaylistPrivacy.PRIVATE } + await updateVideoPlaylist({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, playlistId: videoPlaylistIds.id, playlistAttrs }) + + await waitJobs(servers) + + for (const server of [ servers[ 1 ], servers[ 2 ] ]) { + await getVideoPlaylist(server.url, videoPlaylistIds.uuid, 404) + } + await getVideoPlaylist(servers[ 0 ].url, videoPlaylistIds.uuid, 401) + + await getVideoPlaylistWithToken(servers[ 0 ].url, servers[ 0 ].accessToken, videoPlaylistIds.uuid, 200) + }) }) - it('Should update startTimestamp/endTimestamp of some elements', async function () { - this.timeout(30000) + describe('Playlist deletion', function () { - await updateVideoPlaylistElement({ - url: servers[0].url, - token: servers[0].accessToken, - playlistId: playlistServer1Id, - videoId: servers[0].videos[3].uuid, - elementAttrs: { - startTimestamp: 1 + it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () { + this.timeout(30000) + + await deleteVideoPlaylist(servers[ 0 ].url, servers[ 0 ].accessToken, playlistServer1Id) + + await waitJobs(servers) + + for (const server of servers) { + await getVideoPlaylist(server.url, playlistServer1UUID, 404) } }) - await updateVideoPlaylistElement({ - url: servers[0].url, - token: servers[0].accessToken, - playlistId: playlistServer1Id, - videoId: servers[0].videos[4].uuid, - elementAttrs: { - stopTimestamp: null + it('Should have deleted the thumbnail on server 1, 2 and 3', async function () { + this.timeout(30000) + + for (const server of servers) { + await checkPlaylistFilesWereRemoved(playlistServer1UUID, server.internalServerNumber) } }) - await waitJobs(servers) + it('Should unfollow servers 1 and 2 and hide their playlists', async function () { + this.timeout(30000) - for (const server of servers) { - const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) - const videos: Video[] = res.body.data + const finder = data => data.find(p => p.displayName === 'my super playlist') - expect(videos[0].name).to.equal('video 3 server 1') - expect(videos[0].playlistElement.position).to.equal(1) - expect(videos[0].playlistElement.startTimestamp).to.equal(1) - expect(videos[0].playlistElement.stopTimestamp).to.equal(35) - - expect(videos[5].name).to.equal('video 4 server 1') - expect(videos[5].playlistElement.position).to.equal(6) - expect(videos[5].playlistElement.startTimestamp).to.equal(45) - expect(videos[5].playlistElement.stopTimestamp).to.be.null - } - }) - - it('Should check videos existence in my playlist', async function () { - const videoIds = [ - servers[0].videos[0].id, - 42000, - servers[0].videos[3].id, - 43000, - servers[0].videos[4].id - ] - const res = await doVideosExistInMyPlaylist(servers[ 0 ].url, servers[ 0 ].accessToken, videoIds) - const obj = res.body as VideoExistInPlaylist - - { - const elem = obj[servers[0].videos[0].id] - expect(elem).to.have.lengthOf(1) - expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) - expect(elem[ 0 ].startTimestamp).to.equal(15) - expect(elem[ 0 ].stopTimestamp).to.equal(28) - } - - { - const elem = obj[servers[0].videos[3].id] - expect(elem).to.have.lengthOf(1) - expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) - expect(elem[ 0 ].startTimestamp).to.equal(1) - expect(elem[ 0 ].stopTimestamp).to.equal(35) - } - - { - const elem = obj[servers[0].videos[4].id] - expect(elem).to.have.lengthOf(1) - expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) - expect(elem[ 0 ].startTimestamp).to.equal(45) - expect(elem[ 0 ].stopTimestamp).to.equal(null) - } - - expect(obj[42000]).to.have.lengthOf(0) - expect(obj[43000]).to.have.lengthOf(0) - }) - - it('Should automatically update updatedAt field of playlists', async function () { - const server = servers[1] - const videoId = servers[1].videos[5].id - - async function getPlaylistNames () { - const res = await getAccountPlaylistsListWithToken(server.url, server.accessToken, 'root', 0, 5, undefined, '-updatedAt') - - return (res.body.data as VideoPlaylist[]).map(p => p.displayName) - } - - const elementAttrs = { videoId } - await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id1, elementAttrs }) - await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id2, elementAttrs }) - - const names1 = await getPlaylistNames() - expect(names1[0]).to.equal('playlist 3 updated') - expect(names1[1]).to.equal('playlist 2') - - await removeVideoFromPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id1, videoId }) - - const names2 = await getPlaylistNames() - expect(names2[0]).to.equal('playlist 2') - expect(names2[1]).to.equal('playlist 3 updated') - - await removeVideoFromPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id2, videoId }) - - const names3 = await getPlaylistNames() - expect(names3[0]).to.equal('playlist 3 updated') - expect(names3[1]).to.equal('playlist 2') - }) - - it('Should delete some elements', async function () { - this.timeout(30000) - - await removeVideoFromPlaylist({ - url: servers[0].url, - token: servers[0].accessToken, - playlistId: playlistServer1Id, - videoId: servers[0].videos[3].uuid - }) - - await removeVideoFromPlaylist({ - url: servers[0].url, - token: servers[0].accessToken, - playlistId: playlistServer1Id, - videoId: nsfwVideoServer1 - }) - - await waitJobs(servers) - - for (const server of servers) { - const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) - - expect(res.body.total).to.equal(4) - - const videos: Video[] = res.body.data - expect(videos).to.have.lengthOf(4) - - expect(videos[ 0 ].name).to.equal('video 0 server 1') - expect(videos[ 0 ].playlistElement.position).to.equal(1) - - expect(videos[ 1 ].name).to.equal('video 2 server 3') - expect(videos[ 1 ].playlistElement.position).to.equal(2) - - expect(videos[ 2 ].name).to.equal('video 1 server 3') - expect(videos[ 2 ].playlistElement.position).to.equal(3) - - expect(videos[ 3 ].name).to.equal('video 4 server 1') - expect(videos[ 3 ].playlistElement.position).to.equal(4) - } - }) - - it('Should be able to create a public playlist, and set it to private', async function () { - this.timeout(30000) - - const res = await createVideoPlaylist({ - url: servers[0].url, - token: servers[0].accessToken, - playlistAttrs: { - displayName: 'my super public playlist', - privacy: VideoPlaylistPrivacy.PUBLIC, - videoChannelId: servers[0].videoChannel.id - } - }) - const videoPlaylistIds = res.body.videoPlaylist - - await waitJobs(servers) - - for (const server of servers) { - await getVideoPlaylist(server.url, videoPlaylistIds.uuid, 200) - } - - const playlistAttrs = { privacy: VideoPlaylistPrivacy.PRIVATE } - await updateVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistId: videoPlaylistIds.id, playlistAttrs }) - - await waitJobs(servers) - - for (const server of [ servers[1], servers[2] ]) { - await getVideoPlaylist(server.url, videoPlaylistIds.uuid, 404) - } - await getVideoPlaylist(servers[0].url, videoPlaylistIds.uuid, 401) - - await getVideoPlaylistWithToken(servers[0].url, servers[0].accessToken, videoPlaylistIds.uuid, 200) - }) - - it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () { - this.timeout(30000) - - await deleteVideoPlaylist(servers[0].url, servers[0].accessToken, playlistServer1Id) - - await waitJobs(servers) - - for (const server of servers) { - await getVideoPlaylist(server.url, playlistServer1UUID, 404) - } - }) - - it('Should have deleted the thumbnail on server 1, 2 and 3', async function () { - this.timeout(30000) - - for (const server of servers) { - await checkPlaylistFilesWereRemoved(playlistServer1UUID, server.internalServerNumber) - } - }) - - it('Should unfollow servers 1 and 2 and hide their playlists', async function () { - this.timeout(30000) - - const finder = data => data.find(p => p.displayName === 'my super playlist') - - { - const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) - expect(res.body.total).to.equal(2) - expect(finder(res.body.data)).to.not.be.undefined - } - - await unfollow(servers[2].url, servers[2].accessToken, servers[0]) - - { - const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) - expect(res.body.total).to.equal(1) - - expect(finder(res.body.data)).to.be.undefined - } - }) - - it('Should delete a channel and put the associated playlist in private mode', async function () { - this.timeout(30000) - - const res = await addVideoChannel(servers[0].url, servers[0].accessToken, { name: 'super_channel', displayName: 'super channel' }) - const videoChannelId = res.body.videoChannel.id - - const res2 = await createVideoPlaylist({ - url: servers[0].url, - token: servers[0].accessToken, - playlistAttrs: { - displayName: 'channel playlist', - privacy: VideoPlaylistPrivacy.PUBLIC, - videoChannelId - } - }) - const videoPlaylistUUID = res2.body.videoPlaylist.uuid - - await waitJobs(servers) - - await deleteVideoChannel(servers[0].url, servers[0].accessToken, 'super_channel') - - await waitJobs(servers) - - const res3 = await getVideoPlaylistWithToken(servers[0].url, servers[0].accessToken, videoPlaylistUUID) - expect(res3.body.displayName).to.equal('channel playlist') - expect(res3.body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE) - - await getVideoPlaylist(servers[1].url, videoPlaylistUUID, 404) - }) - - it('Should delete an account and delete its playlists', async function () { - this.timeout(30000) - - const user = { username: 'user_1', password: 'password' } - const res = await createUser({ - url: servers[ 0 ].url, - accessToken: servers[ 0 ].accessToken, - username: user.username, - password: user.password - }) - - const userId = res.body.user.id - const userAccessToken = await userLogin(servers[0], user) - - const resChannel = await getMyUserInformation(servers[0].url, userAccessToken) - const userChannel = (resChannel.body as User).videoChannels[0] - - await createVideoPlaylist({ - url: servers[0].url, - token: userAccessToken, - playlistAttrs: { - displayName: 'playlist to be deleted', - privacy: VideoPlaylistPrivacy.PUBLIC, - videoChannelId: userChannel.id - } - }) - - await waitJobs(servers) - - const finder = data => data.find(p => p.displayName === 'playlist to be deleted') - - { - for (const server of [ servers[0], servers[1] ]) { - const res = await getVideoPlaylistsList(server.url, 0, 15) + { + const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) + expect(res.body.total).to.equal(3) expect(finder(res.body.data)).to.not.be.undefined } - } - await removeUser(servers[0].url, userId, servers[0].accessToken) - await waitJobs(servers) + await unfollow(servers[ 2 ].url, servers[ 2 ].accessToken, servers[ 0 ]) + + { + const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) + expect(res.body.total).to.equal(1) - { - for (const server of [ servers[0], servers[1] ]) { - const res = await getVideoPlaylistsList(server.url, 0, 15) expect(finder(res.body.data)).to.be.undefined } - } + }) + + it('Should delete a channel and put the associated playlist in private mode', async function () { + this.timeout(30000) + + const res = await addVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'super_channel', displayName: 'super channel' }) + const videoChannelId = res.body.videoChannel.id + + const res2 = await createVideoPlaylist({ + url: servers[ 0 ].url, + token: servers[ 0 ].accessToken, + playlistAttrs: { + displayName: 'channel playlist', + privacy: VideoPlaylistPrivacy.PUBLIC, + videoChannelId + } + }) + const videoPlaylistUUID = res2.body.videoPlaylist.uuid + + await waitJobs(servers) + + await deleteVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, 'super_channel') + + await waitJobs(servers) + + const res3 = await getVideoPlaylistWithToken(servers[ 0 ].url, servers[ 0 ].accessToken, videoPlaylistUUID) + expect(res3.body.displayName).to.equal('channel playlist') + expect(res3.body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE) + + await getVideoPlaylist(servers[ 1 ].url, videoPlaylistUUID, 404) + }) + + it('Should delete an account and delete its playlists', async function () { + this.timeout(30000) + + const user = { username: 'user_1', password: 'password' } + const res = await createUser({ + url: servers[ 0 ].url, + accessToken: servers[ 0 ].accessToken, + username: user.username, + password: user.password + }) + + const userId = res.body.user.id + const userAccessToken = await userLogin(servers[ 0 ], user) + + const resChannel = await getMyUserInformation(servers[ 0 ].url, userAccessToken) + const userChannel = (resChannel.body as User).videoChannels[ 0 ] + + await createVideoPlaylist({ + url: servers[ 0 ].url, + token: userAccessToken, + playlistAttrs: { + displayName: 'playlist to be deleted', + privacy: VideoPlaylistPrivacy.PUBLIC, + videoChannelId: userChannel.id + } + }) + + await waitJobs(servers) + + const finder = data => data.find(p => p.displayName === 'playlist to be deleted') + + { + for (const server of [ servers[ 0 ], servers[ 1 ] ]) { + const res = await getVideoPlaylistsList(server.url, 0, 15) + expect(finder(res.body.data)).to.not.be.undefined + } + } + + await removeUser(servers[ 0 ].url, userId, servers[ 0 ].accessToken) + await waitJobs(servers) + + { + for (const server of [ servers[ 0 ], servers[ 1 ] ]) { + const res = await getVideoPlaylistsList(server.url, 0, 15) + expect(finder(res.body.data)).to.be.undefined + } + } + }) }) after(async function () { diff --git a/shared/extra-utils/server/jobs.ts b/shared/extra-utils/server/jobs.ts index 11b570f60..b3db885e8 100644 --- a/shared/extra-utils/server/jobs.ts +++ b/shared/extra-utils/server/jobs.ts @@ -2,7 +2,6 @@ import * as request from 'supertest' import { Job, JobState } from '../../models' import { wait } from '../miscs/miscs' import { ServerInfo } from './servers' -import { inspect } from 'util' function getJobsList (url: string, accessToken: string, state: JobState) { const path = '/api/v1/jobs/' + state @@ -37,11 +36,10 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) { else servers = serversArg as ServerInfo[] const states: JobState[] = [ 'waiting', 'active', 'delayed' ] - let pendingRequests = false + let pendingRequests: boolean function tasksBuilder () { const tasks: Promise[] = [] - pendingRequests = false // Check if each server has pending request for (const server of servers) { @@ -62,6 +60,7 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) { } do { + pendingRequests = false await Promise.all(tasksBuilder()) // Retry, in case of new jobs were created diff --git a/shared/extra-utils/videos/video-playlists.ts b/shared/extra-utils/videos/video-playlists.ts index fd62bef19..cbb073fbc 100644 --- a/shared/extra-utils/videos/video-playlists.ts +++ b/shared/extra-utils/videos/video-playlists.ts @@ -196,11 +196,11 @@ function updateVideoPlaylistElement (options: { url: string, token: string, playlistId: number | string, - videoId: number | string, + playlistElementId: number | string, elementAttrs: VideoPlaylistElementUpdate, expectedStatus?: number }) { - const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.videoId + const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId return makePutBodyRequest({ url: options.url, @@ -215,10 +215,10 @@ function removeVideoFromPlaylist (options: { url: string, token: string, playlistId: number | string, - videoId: number | string, + playlistElementId: number, expectedStatus?: number }) { - const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.videoId + const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId return makeDeleteRequest({ url: options.url, diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index e3d78220e..194ae1b96 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts @@ -19,6 +19,7 @@ export * from './playlist/video-playlist-privacy.model' export * from './playlist/video-playlist-type.model' export * from './playlist/video-playlist-update.model' export * from './playlist/video-playlist.model' +export * from './playlist/video-playlist-element.model' export * from './video-change-ownership.model' export * from './video-change-ownership-create.model' export * from './video-create.model' diff --git a/shared/models/videos/playlist/video-exist-in-playlist.model.ts b/shared/models/videos/playlist/video-exist-in-playlist.model.ts index 71240f51d..1b57257e2 100644 --- a/shared/models/videos/playlist/video-exist-in-playlist.model.ts +++ b/shared/models/videos/playlist/video-exist-in-playlist.model.ts @@ -1,5 +1,6 @@ export type VideoExistInPlaylist = { [videoId: number ]: { + playlistElementId: number playlistId: number startTimestamp?: number stopTimestamp?: number diff --git a/shared/models/videos/playlist/video-playlist-element.model.ts b/shared/models/videos/playlist/video-playlist-element.model.ts new file mode 100644 index 000000000..9a1203892 --- /dev/null +++ b/shared/models/videos/playlist/video-playlist-element.model.ts @@ -0,0 +1,19 @@ +import { Video } from '../video.model' + +export enum VideoPlaylistElementType { + REGULAR = 0, + DELETED = 1, + PRIVATE = 2, + UNAVAILABLE = 3 // Blacklisted, blocked by the user/instance, NSFW... +} + +export interface VideoPlaylistElement { + id: number + position: number + startTimestamp: number + stopTimestamp: number + + type: VideoPlaylistElementType + + video?: Video +} diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 0489147e4..e057b3e06 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts @@ -17,12 +17,6 @@ export interface VideoFile { fps: number } -export interface PlaylistElement { - position: number - startTimestamp: number - stopTimestamp: number -} - export interface Video { id: number uuid: string @@ -59,8 +53,6 @@ export interface Video { userHistory?: { currentTime: number } - - playlistElement?: PlaylistElement } export interface VideoDetails extends Video { diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index a6f61b3b2..39fa3cef5 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml @@ -1922,6 +1922,9 @@ components: type: number stopTimestamp: type: number + video: + nullable: true + $ref: '#/components/schemas/Video' VideoFile: properties: magnetUri: @@ -2029,9 +2032,6 @@ components: properties: currentTime: type: number - playlistElement: - nullable: true - $ref: '#/components/schemas/PlaylistElement' VideoDetails: allOf: - $ref: '#/components/schemas/Video'