Fix broken playlist api

This commit is contained in:
Chocobozzz 2019-07-31 15:57:32 +02:00 committed by Chocobozzz
parent 85394ba22a
commit bfbd912886
45 changed files with 1581 additions and 1014 deletions

View File

@ -18,10 +18,13 @@
<div *ngIf="getVideosOf(videoChannel)" class="videos"> <div *ngIf="getVideosOf(videoChannel)" class="videos">
<div class="no-results" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel does not have videos.</div> <div class="no-results" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel does not have videos.</div>
<my-video-miniature *ngFor="let video of getVideosOf(videoChannel)" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature> <my-video-miniature
*ngFor="let video of getVideosOf(videoChannel)"
[video]="video" [user]="user" [displayVideoActions]="true"
></my-video-miniature>
</div> </div>
<a class="show-more" i18n [routerLink]="getVideoChannelLink(videoChannel)"> <a *ngIf="getVideosOf(videoChannel).length !== 0" class="show-more" i18n [routerLink]="getVideoChannelLink(videoChannel)">
Show this channel Show this channel
</a> </a>
</div> </div>

View File

@ -23,6 +23,11 @@
height: 50px; height: 50px;
} }
} }
my-video-miniature ::ng-deep my-video-actions-dropdown > my-action-dropdown {
// Fix our overflow
position: absolute;
}
} }

View File

@ -14,10 +14,10 @@
class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"
cdkDropList (cdkDropListDropped)="drop($event)" cdkDropList (cdkDropListDropped)="drop($event)"
> >
<div class="video" *ngFor="let video of videos; trackBy: trackByFn" cdkDrag (cdkDragMoved)="onDragMove($event)"> <div class="video" *ngFor="let playlistElement of playlistElements; trackBy: trackByFn" cdkDrag>
<my-video-playlist-element-miniature <my-video-playlist-element-miniature
[video]="video" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)" [playlistElement]="playlistElement" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)"
[position]="video.playlistElement.position" [position]="playlistElement.position"
> >
</my-video-playlist-element-miniature> </my-video-playlist-element-miniature>
</div> </div>

View File

@ -3,15 +3,13 @@ import { Notifier, ServerService } from '@app/core'
import { AuthService } from '../../core/auth' import { AuthService } from '../../core/auth'
import { ConfirmService } from '../../core/confirm' import { ConfirmService } from '../../core/confirm'
import { ComponentPagination } from '@app/shared/rest/component-pagination.model' import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
import { Video } from '@app/shared/video/video.model' import { Subscription } from 'rxjs'
import { Subject, Subscription } from 'rxjs'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { VideoService } from '@app/shared/video/video.service'
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { CdkDragDrop, CdkDragMove } from '@angular/cdk/drag-drop' import { CdkDragDrop } from '@angular/cdk/drag-drop'
import { throttleTime } from 'rxjs/operators' import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
@Component({ @Component({
selector: 'my-account-video-playlist-elements', selector: 'my-account-video-playlist-elements',
@ -19,7 +17,7 @@ import { throttleTime } from 'rxjs/operators'
styleUrls: [ './my-account-video-playlist-elements.component.scss' ] styleUrls: [ './my-account-video-playlist-elements.component.scss' ]
}) })
export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy { export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy {
videos: Video[] = [] playlistElements: VideoPlaylistElement[] = []
playlist: VideoPlaylist playlist: VideoPlaylist
pagination: ComponentPagination = { pagination: ComponentPagination = {
@ -30,7 +28,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
private videoPlaylistId: string | number private videoPlaylistId: string | number
private paramsSub: Subscription private paramsSub: Subscription
private dragMoveSubject = new Subject<number>()
constructor ( constructor (
private authService: AuthService, private authService: AuthService,
@ -39,7 +36,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
private confirmService: ConfirmService, private confirmService: ConfirmService,
private route: ActivatedRoute, private route: ActivatedRoute,
private i18n: I18n, private i18n: I18n,
private videoService: VideoService,
private videoPlaylistService: VideoPlaylistService private videoPlaylistService: VideoPlaylistService
) {} ) {}
@ -50,10 +46,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
this.loadPlaylistInfo() this.loadPlaylistInfo()
}) })
this.dragMoveSubject.asObservable()
.pipe(throttleTime(200))
.subscribe(y => this.checkScroll(y))
} }
ngOnDestroy () { ngOnDestroy () {
@ -66,8 +58,8 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
if (previousIndex === newIndex) return if (previousIndex === newIndex) return
const oldPosition = this.videos[previousIndex].playlistElement.position const oldPosition = this.playlistElements[previousIndex].position
let insertAfter = this.videos[newIndex].playlistElement.position let insertAfter = this.playlistElements[newIndex].position
if (oldPosition > insertAfter) insertAfter-- if (oldPosition > insertAfter) insertAfter--
@ -78,42 +70,16 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
err => this.notifier.error(err.message) err => this.notifier.error(err.message)
) )
const video = this.videos[previousIndex] const element = this.playlistElements[previousIndex]
this.videos.splice(previousIndex, 1) this.playlistElements.splice(previousIndex, 1)
this.videos.splice(newIndex, 0, video) this.playlistElements.splice(newIndex, 0, element)
this.reorderClientPositions() this.reorderClientPositions()
} }
onDragMove (event: CdkDragMove<any>) { onElementRemoved (element: VideoPlaylistElement) {
this.dragMoveSubject.next(event.pointerPosition.y) this.playlistElements = this.playlistElements.filter(v => v.id !== element.id)
}
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)
this.reorderClientPositions() this.reorderClientPositions()
} }
@ -125,14 +91,14 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
this.loadElements() this.loadElements()
} }
trackByFn (index: number, elem: Video) { trackByFn (index: number, elem: VideoPlaylistElement) {
return elem.id return elem.id
} }
private loadElements () { private loadElements () {
this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination) this.videoPlaylistService.getPlaylistVideos(this.videoPlaylistId, this.pagination)
.subscribe(({ total, data }) => { .subscribe(({ total, data }) => {
this.videos = this.videos.concat(data) this.playlistElements = this.playlistElements.concat(data)
this.pagination.totalItems = total this.pagination.totalItems = total
}) })
} }
@ -147,8 +113,8 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
private reorderClientPositions () { private reorderClientPositions () {
let i = 1 let i = 1
for (const video of this.videos) { for (const element of this.playlistElements) {
video.playlistElement.position = i element.position = i
i++ i++
} }
} }

View File

@ -37,6 +37,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
} }
displayOptions = false displayOptions = false
private playlistElementId: number
constructor ( constructor (
protected formValidatorService: FormValidatorService, protected formValidatorService: FormValidatorService,
private authService: AuthService, private authService: AuthService,
@ -96,6 +98,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined, startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined,
stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined
}) })
this.playlistElementId = existingPlaylist ? existingPlaylist.playlistElementId : undefined
} }
this.cd.markForCheck() this.cd.markForCheck()
@ -177,7 +181,9 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
} }
private removeVideoFromPlaylist (playlist: PlaylistSummary) { private removeVideoFromPlaylist (playlist: PlaylistSummary) {
this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, this.video.id) if (!this.playlistElementId) return
this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, this.playlistElementId)
.subscribe( .subscribe(
() => { () => {
this.notifier.success(this.i18n('Video removed from {{name}}', { name: playlist.displayName })) this.notifier.success(this.i18n('Video removed from {{name}}', { name: playlist.displayName }))

View File

@ -6,29 +6,44 @@
</div> </div>
<my-video-thumbnail <my-video-thumbnail
[video]="video" [nsfw]="isVideoBlur(video)" *ngIf="playlistElement.video"
[video]="playlistElement.video" [nsfw]="isVideoBlur(playlistElement.video)"
[routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()" [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
></my-video-thumbnail> ></my-video-thumbnail>
<div class="fake-thumbnail" *ngIf="!playlistElement.video"></div>
<div class="video-info"> <div class="video-info">
<ng-container *ngIf="playlistElement.video">
<a tabindex="-1" class="video-info-name" <a tabindex="-1" class="video-info-name"
[routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()" [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
[attr.title]="video.name" [attr.title]="playlistElement.video.name"
>{{ video.name }}</a> >{{ playlistElement.video.name }}</a>
<a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a> <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', playlistElement.video.byAccount ]">
<span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ video.byAccount }}</span> {{ playlistElement.video.byAccount }}
</a>
<span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ playlistElement.video.byAccount }}</span>
<span tabindex="-1" class="video-info-timestamp">{{ formatTimestamp(video) }}</span> <span tabindex="-1" class="video-info-timestamp">{{ formatTimestamp(playlistElement) }}</span>
</ng-container>
<span *ngIf="!playlistElement.video" class="video-info-name">
<ng-container i18n *ngIf="isUnavailable(playlistElement)">Unavailable</ng-container>
<ng-container i18n *ngIf="isPrivate(playlistElement)">Private</ng-container>
<ng-container i18n *ngIf="isDeleted(playlistElement)">Deleted</ng-container>
</span>
</div> </div>
</a> </a>
<div *ngIf="owned" class="more" ngbDropdown #moreDropdown="ngbDropdown" placement="bottom-right" (openChange)="onDropdownOpenChange()" <div *ngIf="owned" class="more" ngbDropdown #moreDropdown="ngbDropdown" placement="bottom-right"
autoClose="outside"> (openChange)="onDropdownOpenChange()" autoClose="outside"
>
<my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more" (click)="$event.preventDefault()"></my-global-icon> <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more" (click)="$event.preventDefault()"></my-global-icon>
<div ngbDropdownMenu> <div ngbDropdownMenu>
<div class="dropdown-item" (click)="toggleDisplayTimestampsOptions($event, video)"> <ng-container *ngIf="playlistElement.video">
<div class="dropdown-item" (click)="toggleDisplayTimestampsOptions($event, playlistElement)">
<my-global-icon iconName="edit"></my-global-icon> <my-global-icon iconName="edit"></my-global-icon>
<ng-container i18n>Edit starts/stops at</ng-container> <ng-container i18n>Edit starts/stops at</ng-container>
</div> </div>
@ -42,7 +57,7 @@
<my-timestamp-input <my-timestamp-input
[timestamp]="timestampOptions.startTimestamp" [timestamp]="timestampOptions.startTimestamp"
[maxTimestamp]="video.duration" [maxTimestamp]="playlistElement.video.duration"
[disabled]="!timestampOptions.startTimestampEnabled" [disabled]="!timestampOptions.startTimestampEnabled"
[(ngModel)]="timestampOptions.startTimestamp" [(ngModel)]="timestampOptions.startTimestamp"
></my-timestamp-input> ></my-timestamp-input>
@ -56,16 +71,17 @@
<my-timestamp-input <my-timestamp-input
[timestamp]="timestampOptions.stopTimestamp" [timestamp]="timestampOptions.stopTimestamp"
[maxTimestamp]="video.duration" [maxTimestamp]="playlistElement.video.duration"
[disabled]="!timestampOptions.stopTimestampEnabled" [disabled]="!timestampOptions.stopTimestampEnabled"
[(ngModel)]="timestampOptions.stopTimestamp" [(ngModel)]="timestampOptions.stopTimestamp"
></my-timestamp-input> ></my-timestamp-input>
</div> </div>
<input type="submit" i18n-value value="Save" (click)="updateTimestamps(video)"> <input type="submit" i18n-value value="Save" (click)="updateTimestamps(playlistElement)">
</div> </div>
</ng-container>
<span class="dropdown-item" (click)="removeFromPlaylist(video)"> <span class="dropdown-item" (click)="removeFromPlaylist(playlistElement)">
<my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete from {{ playlist?.displayName }}</ng-container> <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete from {{ playlist?.displayName }}</ng-container>
</span> </span>
</div> </div>

View File

@ -2,9 +2,21 @@
@import '_mixins'; @import '_mixins';
@import '_miniature'; @import '_miniature';
my-video-thumbnail { $thumbnail-width: 130px;
@include thumbnail-size-component(130px, 72px); $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 display: flex; // Avoids an issue with line-height that adds space below the element
margin-right: 10px; margin-right: 10px;
} }
@ -31,6 +43,7 @@ my-video-thumbnail {
a { a {
@include disable-default-a-behaviour; @include disable-default-a-behaviour;
color: var(--mainForegroundColor);
display: flex; display: flex;
min-width: 0; min-width: 0;
align-items: center; align-items: center;
@ -58,7 +71,6 @@ my-video-thumbnail {
min-width: 0; min-width: 0;
a { a {
color: var(--mainForegroundColor);
width: auto; width: auto;
&:hover { &:hover {
@ -66,6 +78,12 @@ my-video-thumbnail {
} }
} }
.video-info-account, .video-info-timestamp {
color: $grey-foreground-color;
}
}
}
.video-info-name { .video-info-name {
font-size: 18px; font-size: 18px;
font-weight: $font-semibold; font-weight: $font-semibold;
@ -74,12 +92,6 @@ my-video-thumbnail {
@include ellipsis; @include ellipsis;
} }
.video-info-account, .video-info-timestamp {
color: $grey-foreground-color;
}
}
}
.more { .more {
justify-self: flex-end; justify-self: flex-end;
margin-left: auto; margin-left: auto;

View File

@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'
import { Video } from '@app/shared/video/video.model' 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 { AuthService, ConfirmService, Notifier, ServerService } from '@app/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { I18n } from '@ngx-translate/i18n-polyfill' 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 { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
import { secondsToTime } from '../../../assets/player/utils' import { secondsToTime } from '../../../assets/player/utils'
import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
@Component({ @Component({
selector: 'my-video-playlist-element-miniature', selector: 'my-video-playlist-element-miniature',
@ -20,14 +21,14 @@ export class VideoPlaylistElementMiniatureComponent {
@ViewChild('moreDropdown', { static: false }) moreDropdown: NgbDropdown @ViewChild('moreDropdown', { static: false }) moreDropdown: NgbDropdown
@Input() playlist: VideoPlaylist @Input() playlist: VideoPlaylist
@Input() video: Video @Input() playlistElement: VideoPlaylistElement
@Input() owned = false @Input() owned = false
@Input() playing = false @Input() playing = false
@Input() rowLink = false @Input() rowLink = false
@Input() accountLink = true @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<Video>() @Output() elementRemoved = new EventEmitter<VideoPlaylistElement>()
displayTimestampOptions = false displayTimestampOptions = false
@ -50,6 +51,18 @@ export class VideoPlaylistElementMiniatureComponent {
private cdr: ChangeDetectorRef private cdr: ChangeDetectorRef
) {} ) {}
isUnavailable (e: VideoPlaylistElement) {
return e.type === VideoPlaylistElementType.UNAVAILABLE
}
isPrivate (e: VideoPlaylistElement) {
return e.type === VideoPlaylistElementType.PRIVATE
}
isDeleted (e: VideoPlaylistElement) {
return e.type === VideoPlaylistElementType.DELETED
}
buildRouterLink () { buildRouterLink () {
if (!this.playlist) return null if (!this.playlist) return null
@ -57,12 +70,12 @@ export class VideoPlaylistElementMiniatureComponent {
} }
buildRouterQuery () { buildRouterQuery () {
if (!this.video) return {} if (!this.playlistElement || !this.playlistElement.video) return {}
return { return {
videoId: this.video.uuid, videoId: this.playlistElement.video.uuid,
start: this.video.playlistElement.startTimestamp, start: this.playlistElement.startTimestamp,
stop: this.video.playlistElement.stopTimestamp stop: this.playlistElement.stopTimestamp
} }
} }
@ -70,13 +83,13 @@ export class VideoPlaylistElementMiniatureComponent {
return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig()) return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig())
} }
removeFromPlaylist (video: Video) { removeFromPlaylist (playlistElement: VideoPlaylistElement) {
this.videoPlaylistService.removeVideoFromPlaylist(this.playlist.id, video.id) this.videoPlaylistService.removeVideoFromPlaylist(this.playlist.id, playlistElement.id)
.subscribe( .subscribe(
() => { () => {
this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName })) this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName }))
this.elementRemoved.emit(this.video) this.elementRemoved.emit(playlistElement)
}, },
err => this.notifier.error(err.message) err => this.notifier.error(err.message)
@ -85,19 +98,19 @@ export class VideoPlaylistElementMiniatureComponent {
this.moreDropdown.close() this.moreDropdown.close()
} }
updateTimestamps (video: Video) { updateTimestamps (playlistElement: VideoPlaylistElement) {
const body: VideoPlaylistElementUpdate = {} const body: VideoPlaylistElementUpdate = {}
body.startTimestamp = this.timestampOptions.startTimestampEnabled ? this.timestampOptions.startTimestamp : null body.startTimestamp = this.timestampOptions.startTimestampEnabled ? this.timestampOptions.startTimestamp : null
body.stopTimestamp = this.timestampOptions.stopTimestampEnabled ? this.timestampOptions.stopTimestamp : null body.stopTimestamp = this.timestampOptions.stopTimestampEnabled ? this.timestampOptions.stopTimestamp : null
this.videoPlaylistService.updateVideoOfPlaylist(this.playlist.id, video.id, body) this.videoPlaylistService.updateVideoOfPlaylist(this.playlist.id, playlistElement.id, body)
.subscribe( .subscribe(
() => { () => {
this.notifier.success(this.i18n('Timestamps updated')) this.notifier.success(this.i18n('Timestamps updated'))
video.playlistElement.startTimestamp = body.startTimestamp playlistElement.startTimestamp = body.startTimestamp
video.playlistElement.stopTimestamp = body.stopTimestamp playlistElement.stopTimestamp = body.stopTimestamp
this.cdr.detectChanges() this.cdr.detectChanges()
}, },
@ -108,9 +121,9 @@ export class VideoPlaylistElementMiniatureComponent {
this.moreDropdown.close() this.moreDropdown.close()
} }
formatTimestamp (video: Video) { formatTimestamp (playlistElement: VideoPlaylistElement) {
const start = video.playlistElement.startTimestamp const start = playlistElement.startTimestamp
const stop = video.playlistElement.stopTimestamp const stop = playlistElement.stopTimestamp
const startFormatted = secondsToTime(start, true, ':') const startFormatted = secondsToTime(start, true, ':')
const stopFormatted = secondsToTime(stop, true, ':') const stopFormatted = secondsToTime(stop, true, ':')
@ -127,7 +140,7 @@ export class VideoPlaylistElementMiniatureComponent {
this.displayTimestampOptions = false this.displayTimestampOptions = false
} }
toggleDisplayTimestampsOptions (event: Event, video: Video) { toggleDisplayTimestampsOptions (event: Event, playlistElement: VideoPlaylistElement) {
event.preventDefault() event.preventDefault()
this.displayTimestampOptions = !this.displayTimestampOptions this.displayTimestampOptions = !this.displayTimestampOptions
@ -137,17 +150,17 @@ export class VideoPlaylistElementMiniatureComponent {
startTimestampEnabled: false, startTimestampEnabled: false,
stopTimestampEnabled: false, stopTimestampEnabled: false,
startTimestamp: 0, startTimestamp: 0,
stopTimestamp: video.duration stopTimestamp: playlistElement.video.duration
} }
if (video.playlistElement.startTimestamp) { if (playlistElement.startTimestamp) {
this.timestampOptions.startTimestampEnabled = true this.timestampOptions.startTimestampEnabled = true
this.timestampOptions.startTimestamp = video.playlistElement.startTimestamp this.timestampOptions.startTimestamp = playlistElement.startTimestamp
} }
if (video.playlistElement.stopTimestamp) { if (playlistElement.stopTimestamp) {
this.timestampOptions.stopTimestampEnabled = true this.timestampOptions.stopTimestampEnabled = true
this.timestampOptions.stopTimestamp = video.playlistElement.stopTimestamp this.timestampOptions.stopTimestamp = playlistElement.stopTimestamp
} }
} }

View File

@ -0,0 +1,24 @@
import { VideoPlaylistElement as ServerVideoPlaylistElement, VideoPlaylistElementType } from '../../../../../shared/models/videos'
import { Video } from '@app/shared/video/video.model'
export class VideoPlaylistElement implements ServerVideoPlaylistElement {
id: number
position: number
startTimestamp: number
stopTimestamp: number
type: VideoPlaylistElementType
video?: Video
constructor (hash: ServerVideoPlaylistElement, translations: {}) {
this.id = hash.id
this.position = hash.position
this.startTimestamp = hash.startTimestamp
this.stopTimestamp = hash.stopTimestamp
this.type = hash.type
if (hash.video) this.video = new Video(hash.video, translations)
}
}

View File

@ -18,6 +18,9 @@ import { Account } from '@app/shared/account/account.model'
import { RestService } from '@app/shared/rest' import { RestService } from '@app/shared/rest'
import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model' import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model'
import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model' import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model'
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
import { VideoPlaylistElement as ServerVideoPlaylistElement } from '@shared/models/videos/playlist/video-playlist-element.model'
import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
@Injectable() @Injectable()
export class VideoPlaylistService { export class VideoPlaylistService {
@ -110,16 +113,16 @@ export class VideoPlaylistService {
) )
} }
updateVideoOfPlaylist (playlistId: number, videoId: number, body: VideoPlaylistElementUpdate) { updateVideoOfPlaylist (playlistId: number, playlistElementId: number, body: VideoPlaylistElementUpdate) {
return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + videoId, body) return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId, body)
.pipe( .pipe(
map(this.restExtractor.extractDataBool), map(this.restExtractor.extractDataBool),
catchError(err => this.restExtractor.handleError(err)) catchError(err => this.restExtractor.handleError(err))
) )
} }
removeVideoFromPlaylist (playlistId: number, videoId: number) { removeVideoFromPlaylist (playlistId: number, playlistElementId: number) {
return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + videoId) return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId)
.pipe( .pipe(
map(this.restExtractor.extractDataBool), map(this.restExtractor.extractDataBool),
catchError(err => this.restExtractor.handleError(err)) catchError(err => this.restExtractor.handleError(err))
@ -139,6 +142,24 @@ export class VideoPlaylistService {
) )
} }
getPlaylistVideos (
videoPlaylistId: number | string,
componentPagination: ComponentPagination
): Observable<ResultList<VideoPlaylistElement>> {
const path = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylistId + '/videos'
const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination)
return this.authHttp
.get<ResultList<ServerVideoPlaylistElement>>(path, { params })
.pipe(
switchMap(res => this.extractVideoPlaylistElements(res)),
catchError(err => this.restExtractor.handleError(err))
)
}
doesVideoExistInPlaylist (videoId: number) { doesVideoExistInPlaylist (videoId: number) {
this.videoExistsInPlaylistSubject.next(videoId) this.videoExistsInPlaylistSubject.next(videoId)
@ -167,6 +188,23 @@ export class VideoPlaylistService {
.pipe(map(translations => new VideoPlaylist(playlist, translations))) .pipe(map(translations => new VideoPlaylist(playlist, translations)))
} }
extractVideoPlaylistElements (result: ResultList<ServerVideoPlaylistElement>) {
return this.serverService.localeObservable
.pipe(
map(translations => {
const elementsJson = result.data
const total = result.total
const elements: VideoPlaylistElement[] = []
for (const elementJson of elementsJson) {
elements.push(new VideoPlaylistElement(elementJson, translations))
}
return { total, data: elements }
})
)
}
private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> { private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> {
const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist' const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist'
let params = new HttpParams() let params = new HttpParams()

View File

@ -1,5 +1,5 @@
import { User } from '../' import { User } from '../'
import { PlaylistElement, UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' import { UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
import { Avatar } from '../../../../../shared/models/avatars/avatar.model' import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model' import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model'
import { durationToString, getAbsoluteAPIUrl } from '../misc/utils' import { durationToString, getAbsoluteAPIUrl } from '../misc/utils'
@ -48,8 +48,6 @@ export class Video implements VideoServerModel {
blacklisted?: boolean blacklisted?: boolean
blacklistedReason?: string blacklistedReason?: string
playlistElement?: PlaylistElement
account: { account: {
id: number id: number
name: string name: string
@ -126,8 +124,6 @@ export class Video implements VideoServerModel {
this.blacklistedReason = hash.blacklistedReason this.blacklistedReason = hash.blacklistedReason
this.userHistory = hash.userHistory this.userHistory = hash.userHistory
this.playlistElement = hash.playlistElement
} }
isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {

View File

@ -31,7 +31,6 @@ import { ServerService } from '@app/core'
import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
import { VideoChannel } from '@app/shared/video-channel/video-channel.model' import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
import { I18n } from '@ngx-translate/i18n-polyfill' import { I18n } from '@ngx-translate/i18n-polyfill'
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
export interface VideosProvider { export interface VideosProvider {
getVideos (parameters: { getVideos (parameters: {
@ -172,23 +171,6 @@ export class VideoService implements VideosProvider {
) )
} }
getPlaylistVideos (
videoPlaylistId: number | string,
videoPagination: ComponentPagination
): Observable<ResultList<Video>> {
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination)
return this.authHttp
.get<ResultList<Video>>(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylistId + '/videos', { params })
.pipe(
switchMap(res => this.extractVideos(res)),
catchError(err => this.restExtractor.handleError(err))
)
}
getUserSubscriptionVideos (parameters: { getUserSubscriptionVideos (parameters: {
videoPagination: ComponentPagination, videoPagination: ComponentPagination,
sort: VideoSortField sort: VideoSortField

View File

@ -16,10 +16,10 @@
</div> </div>
</div> </div>
<div *ngFor="let playlistVideo of playlistVideos"> <div *ngFor="let playlistElement of playlistElements">
<my-video-playlist-element-miniature <my-video-playlist-element-miniature
[video]="playlistVideo" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)" [playlistElement]="playlistElement" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)"
[playing]="currentPlaylistPosition === playlistVideo.playlistElement.position" [accountLink]="false" [position]="playlistVideo.playlistElement.position" [playing]="currentPlaylistPosition === playlistElement.position" [accountLink]="false" [position]="playlistElement.position"
></my-video-playlist-element-miniature> ></my-video-playlist-element-miniature>
</div> </div>
</div> </div>

View File

@ -53,6 +53,11 @@
my-video-thumbnail { my-video-thumbnail {
@include thumbnail-size-component(90px, 50px); @include thumbnail-size-component(90px, 50px);
} }
.fake-thumbnail {
width: 90px;
height: 50px;
}
} }
} }
} }

View File

@ -1,11 +1,11 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
import { ComponentPagination } from '@app/shared/rest/component-pagination.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 { VideoDetails, VideoPlaylistPrivacy } from '@shared/models'
import { VideoService } from '@app/shared/video/video.service'
import { Router } from '@angular/router' import { Router } from '@angular/router'
import { AuthService } from '@app/core' 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({ @Component({
selector: 'my-video-watch-playlist', selector: 'my-video-watch-playlist',
@ -16,7 +16,7 @@ export class VideoWatchPlaylistComponent {
@Input() video: VideoDetails @Input() video: VideoDetails
@Input() playlist: VideoPlaylist @Input() playlist: VideoPlaylist
playlistVideos: Video[] = [] playlistElements: VideoPlaylistElement[] = []
playlistPagination: ComponentPagination = { playlistPagination: ComponentPagination = {
currentPage: 1, currentPage: 1,
itemsPerPage: 30, itemsPerPage: 30,
@ -28,7 +28,7 @@ export class VideoWatchPlaylistComponent {
constructor ( constructor (
private auth: AuthService, private auth: AuthService,
private videoService: VideoService, private videoPlaylist: VideoPlaylistService,
private router: Router private router: Router
) {} ) {}
@ -40,8 +40,8 @@ export class VideoWatchPlaylistComponent {
this.loadPlaylistElements(this.playlist,false) this.loadPlaylistElements(this.playlist,false)
} }
onElementRemoved (video: Video) { onElementRemoved (playlistElement: VideoPlaylistElement) {
this.playlistVideos = this.playlistVideos.filter(v => v.id !== video.id) this.playlistElements = this.playlistElements.filter(e => e.id !== playlistElement.id)
this.playlistPagination.totalItems-- this.playlistPagination.totalItems--
} }
@ -65,12 +65,13 @@ export class VideoWatchPlaylistComponent {
} }
loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) { loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) {
this.videoService.getPlaylistVideos(playlist.uuid, this.playlistPagination) this.videoPlaylist.getPlaylistVideos(playlist.uuid, this.playlistPagination)
.subscribe(({ total, data }) => { .subscribe(({ total, data }) => {
this.playlistVideos = this.playlistVideos.concat(data) this.playlistElements = this.playlistElements.concat(data)
this.playlistPagination.totalItems = total this.playlistPagination.totalItems = total
if (total === 0) { const firstAvailableVideos = this.playlistElements.find(e => !!e.video)
if (!firstAvailableVideos) {
this.noPlaylistVideos = true this.noPlaylistVideos = true
return return
} }
@ -79,7 +80,7 @@ export class VideoWatchPlaylistComponent {
if (redirectToFirst) { if (redirectToFirst) {
const extras = { const extras = {
queryParams: { videoId: this.playlistVideos[ 0 ].uuid }, queryParams: { videoId: firstAvailableVideos.video.uuid },
replaceUrl: true replaceUrl: true
} }
this.router.navigate([], extras) this.router.navigate([], extras)
@ -88,11 +89,11 @@ export class VideoWatchPlaylistComponent {
} }
updatePlaylistIndex (video: VideoDetails) { updatePlaylistIndex (video: VideoDetails) {
if (this.playlistVideos.length === 0 || !video) return if (this.playlistElements.length === 0 || !video) return
for (const playlistVideo of this.playlistVideos) { for (const playlistElement of this.playlistElements) {
if (playlistVideo.id === video.id) { if (playlistElement.video && playlistElement.video.id === video.id) {
this.currentPlaylistPosition = playlistVideo.playlistElement.position this.currentPlaylistPosition = playlistElement.position
return return
} }
} }
@ -103,11 +104,17 @@ export class VideoWatchPlaylistComponent {
navigateToNextPlaylistVideo () { navigateToNextPlaylistVideo () {
if (this.currentPlaylistPosition < this.playlistPagination.totalItems) { 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 if (!next || !next.video) {
const stop = next.playlistElement.stopTimestamp this.currentPlaylistPosition++
this.router.navigate([],{ queryParams: { videoId: next.uuid, start, stop } }) this.navigateToNextPlaylistVideo()
return
}
const start = next.startTimestamp
const stop = next.stopTimestamp
this.router.navigate([],{ queryParams: { videoId: next.video.uuid, start, stop } })
} }
} }
} }

View File

@ -464,7 +464,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
} }
this.zone.runOutsideAngular(async () => { 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)) this.player.on('customError', ({ err }: { err: any }) => this.handleError(err))

View File

@ -86,6 +86,7 @@ export class PeertubePlayerManager {
private static videojsLocaleCache: { [ path: string ]: any } = {} private static videojsLocaleCache: { [ path: string ]: any } = {}
private static playerElementClassName: string private static playerElementClassName: string
private static onPlayerChange: (player: any) => void
static getServerTranslations (serverUrl: string, locale: string) { static getServerTranslations (serverUrl: string, locale: string) {
const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) 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 let p2pMediaLoader: any
this.onPlayerChange = onPlayerChange
this.playerElementClassName = options.common.playerElement.className this.playerElementClassName = options.common.playerElement.className
if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin') if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin')
@ -171,6 +173,8 @@ export class PeertubePlayerManager {
const player = this const player = this
self.addContextMenu(mode, player, options.common.embedUrl) self.addContextMenu(mode, player, options.common.embedUrl)
PeertubePlayerManager.onPlayerChange(player)
}) })
} }

View File

@ -43,6 +43,9 @@ class SettingsMenuItem extends MenuItem {
player.ready(() => { player.ready(() => {
// Voodoo magic for IOS // Voodoo magic for IOS
setTimeout(() => { setTimeout(() => {
// Player was destroyed
if (!this.player_) return
this.build() this.build()
// Update on rate change // Update on rate change

View File

@ -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)) this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
window[ 'videojsPlayer' ] = this.player window[ 'videojsPlayer' ] = this.player

View File

@ -35,6 +35,7 @@ async function doVideosInPlaylistExist (req: express.Request, res: express.Respo
for (const result of results) { for (const result of results) {
for (const element of result.VideoPlaylistElements) { for (const element of result.VideoPlaylistElements) {
existObject[element.videoId].push({ existObject[element.videoId].push({
playlistElementId: element.id,
playlistId: result.id, playlistId: result.id,
startTimestamp: element.startTimestamp, startTimestamp: element.startTimestamp,
stopTimestamp: element.stopTimestamp stopTimestamp: element.stopTimestamp

View File

@ -4,14 +4,13 @@ import {
asyncMiddleware, asyncMiddleware,
asyncRetryTransactionMiddleware, asyncRetryTransactionMiddleware,
authenticate, authenticate,
commonVideosFiltersValidator,
optionalAuthenticate, optionalAuthenticate,
paginationValidator, paginationValidator,
setDefaultPagination, setDefaultPagination,
setDefaultSort setDefaultSort
} from '../../middlewares' } from '../../middlewares'
import { videoPlaylistsSortValidator } from '../../middlewares/validators' 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 { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { resetSequelizeInstance } from '../../helpers/database-utils' import { resetSequelizeInstance } from '../../helpers/database-utils'
@ -32,7 +31,6 @@ import { join } from 'path'
import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send'
import { getVideoPlaylistActivityPubUrl, getVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' import { getVideoPlaylistActivityPubUrl, getVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url'
import { VideoPlaylistUpdate } from '../../../shared/models/videos/playlist/video-playlist-update.model' 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 { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model'
import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model'
@ -88,7 +86,6 @@ videoPlaylistRouter.get('/:playlistId/videos',
paginationValidator, paginationValidator,
setDefaultPagination, setDefaultPagination,
optionalAuthenticate, optionalAuthenticate,
commonVideosFiltersValidator,
asyncMiddleware(getVideoPlaylistVideos) asyncMiddleware(getVideoPlaylistVideos)
) )
@ -104,13 +101,13 @@ videoPlaylistRouter.post('/:playlistId/videos/reorder',
asyncRetryTransactionMiddleware(reorderVideosPlaylist) asyncRetryTransactionMiddleware(reorderVideosPlaylist)
) )
videoPlaylistRouter.put('/:playlistId/videos/:videoId', videoPlaylistRouter.put('/:playlistId/videos/:playlistElementId',
authenticate, authenticate,
asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator), asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator),
asyncRetryTransactionMiddleware(updateVideoPlaylistElement) asyncRetryTransactionMiddleware(updateVideoPlaylistElement)
) )
videoPlaylistRouter.delete('/:playlistId/videos/:videoId', videoPlaylistRouter.delete('/:playlistId/videos/:playlistElementId',
authenticate, authenticate,
asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator), asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator),
asyncRetryTransactionMiddleware(removeVideoFromPlaylist) asyncRetryTransactionMiddleware(removeVideoFromPlaylist)
@ -426,26 +423,20 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons
async function getVideoPlaylistVideos (req: express.Request, res: express.Response) { async function getVideoPlaylistVideos (req: express.Request, res: express.Response) {
const videoPlaylistInstance = res.locals.videoPlaylist 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({ const resultList = await VideoPlaylistElementModel.listForApi({
followerActorId,
start: req.query.start, start: req.query.start,
count: req.query.count, 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, videoPlaylistId: videoPlaylistInstance.id,
user: res.locals.oauth ? res.locals.oauth.token.User : undefined serverAccount: server.Account,
user
}) })
const additionalAttributes = { playlistInfo: true } const options = {
return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes })) displayNSFW: buildNSFWFilter(res, req.query.nsfw),
accountId: user ? user.Account.id : undefined
}
return res.json(getFormattedObjects(resultList.data, resultList.total, options))
} }

View File

@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 405 const LAST_MIGRATION_VERSION = 410
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -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<void> {
{
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
}

View File

@ -207,8 +207,8 @@ const videoPlaylistsAddVideoValidator = [
const videoPlaylistsUpdateOrRemoveVideoValidator = [ const videoPlaylistsUpdateOrRemoveVideoValidator = [
param('playlistId') param('playlistId')
.custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
param('videoId') param('playlistElementId')
.custom(isIdOrUUIDValid).withMessage('Should have an video id/uuid'), .custom(isIdValid).withMessage('Should have an element id/uuid'),
body('startTimestamp') body('startTimestamp')
.optional() .optional()
.custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'), .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
@ -222,12 +222,10 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) 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 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) { if (!videoPlaylistElement) {
res.status(404) res.status(404)
.json({ error: 'Video playlist element not found' }) .json({ error: 'Video playlist element not found' })

View File

@ -9,7 +9,7 @@ import { ActorModel } from '../activitypub/actor'
import { getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { AccountVideoRate } from '../../../shared' 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. Account rates per video.
@ -109,7 +109,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
required: true, required: true,
include: [ include: [
{ {
model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, true] }), model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
required: true required: true
} }
] ]

View File

@ -27,12 +27,19 @@ import { UserModel } from './user'
import { AvatarModel } from '../avatar/avatar' import { AvatarModel } from '../avatar/avatar'
import { VideoPlaylistModel } from '../video/video-playlist' import { VideoPlaylistModel } from '../video/video-playlist'
import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 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 { export enum ScopeNames {
SUMMARY = 'SUMMARY' SUMMARY = 'SUMMARY'
} }
export type SummaryOptions = {
whereActor?: WhereOptions
withAccountBlockerIds?: number[]
}
@DefaultScope(() => ({ @DefaultScope(() => ({
include: [ include: [
{ {
@ -42,8 +49,16 @@ export enum ScopeNames {
] ]
})) }))
@Scopes(() => ({ @Scopes(() => ({
[ ScopeNames.SUMMARY ]: (whereActor?: WhereOptions) => { [ ScopeNames.SUMMARY ]: (options: SummaryOptions = {}) => {
return { const whereActor = options.whereActor || undefined
const serverInclude: IncludeOptions = {
attributes: [ 'host' ],
model: ServerModel.unscoped(),
required: false
}
const query: FindOptions = {
attributes: [ 'id', 'name' ], attributes: [ 'id', 'name' ],
include: [ include: [
{ {
@ -52,11 +67,8 @@ export enum ScopeNames {
required: true, required: true,
where: whereActor, where: whereActor,
include: [ include: [
{ serverInclude,
attributes: [ 'host' ],
model: ServerModel.unscoped(),
required: false
},
{ {
model: AvatarModel.unscoped(), model: AvatarModel.unscoped(),
required: false 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({ @Table({
@ -163,6 +204,16 @@ export class AccountModel extends Model<AccountModel> {
}) })
VideoComments: VideoCommentModel[] VideoComments: VideoCommentModel[]
@HasMany(() => AccountBlocklistModel, {
foreignKey: {
name: 'targetAccountId',
allowNull: false
},
as: 'BlockedAccounts',
onDelete: 'CASCADE'
})
BlockedAccounts: AccountBlocklistModel[]
@BeforeDestroy @BeforeDestroy
static async sendDeleteIfOwned (instance: AccountModel, options) { static async sendDeleteIfOwned (instance: AccountModel, options) {
if (!instance.Actor) { if (!instance.Actor) {
@ -343,4 +394,8 @@ export class AccountModel extends Model<AccountModel> {
getDisplayName () { getDisplayName () {
return this.name return this.name
} }
isBlocked () {
return this.BlockedAccounts && this.BlockedAccounts.length !== 0
}
} }

View File

@ -67,7 +67,6 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
@BelongsTo(() => ServerModel, { @BelongsTo(() => ServerModel, {
foreignKey: { foreignKey: {
name: 'targetServerId',
allowNull: false allowNull: false
}, },
onDelete: 'CASCADE' onDelete: 'CASCADE'

View File

@ -2,6 +2,8 @@ import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, Updat
import { isHostValid } from '../../helpers/custom-validators/servers' import { isHostValid } from '../../helpers/custom-validators/servers'
import { ActorModel } from '../activitypub/actor' import { ActorModel } from '../activitypub/actor'
import { throwIfNotValid } from '../utils' import { throwIfNotValid } from '../utils'
import { AccountBlocklistModel } from '../account/account-blocklist'
import { ServerBlocklistModel } from './server-blocklist'
@Table({ @Table({
tableName: 'server', tableName: 'server',
@ -40,6 +42,14 @@ export class ServerModel extends Model<ServerModel> {
}) })
Actors: ActorModel[] Actors: ActorModel[]
@HasMany(() => ServerBlocklistModel, {
foreignKey: {
allowNull: false
},
onDelete: 'CASCADE'
})
BlockedByAccounts: ServerBlocklistModel[]
static loadByHost (host: string) { static loadByHost (host: string) {
const query = { const query = {
where: { where: {
@ -50,6 +60,10 @@ export class ServerModel extends Model<ServerModel> {
return ServerModel.findOne(query) return ServerModel.findOne(query)
} }
isBlocked () {
return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0
}
toFormattedJSON () { toFormattedJSON () {
return { return {
host: this.host host: this.host

View File

@ -1,7 +1,7 @@
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { getSortOnModel, SortType, throwIfNotValid } from '../utils' import { getSortOnModel, SortType, throwIfNotValid } from '../utils'
import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' 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 { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants' import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
@ -71,7 +71,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
required: true, required: true,
include: [ include: [
{ {
model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }), model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
required: true required: true
}, },
{ {

View File

@ -24,7 +24,7 @@ import {
isVideoChannelSupportValid isVideoChannelSupportValid
} from '../../helpers/custom-validators/video-channels' } from '../../helpers/custom-validators/video-channels'
import { sendDeleteActor } from '../../lib/activitypub/send' 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 { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video' import { VideoModel } from './video'
@ -58,6 +58,11 @@ type AvailableForListOptions = {
actorId: number actorId: number
} }
export type SummaryOptions = {
withAccount?: boolean // Default: false
withAccountBlockerIds?: number[]
}
@DefaultScope(() => ({ @DefaultScope(() => ({
include: [ include: [
{ {
@ -67,7 +72,7 @@ type AvailableForListOptions = {
] ]
})) }))
@Scopes(() => ({ @Scopes(() => ({
[ScopeNames.SUMMARY]: (withAccount = false) => { [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
const base: FindOptions = { const base: FindOptions = {
attributes: [ 'name', 'description', 'id', 'actorId' ], attributes: [ 'name', 'description', 'id', 'actorId' ],
include: [ include: [
@ -90,9 +95,11 @@ type AvailableForListOptions = {
] ]
} }
if (withAccount === true) { if (options.withAccount === true) {
base.include.push({ base.include.push({
model: AccountModel.scope(AccountModelScopeNames.SUMMARY), model: AccountModel.scope({
method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ]
}),
required: true required: true
}) })
} }

View File

@ -26,7 +26,6 @@ export type VideoFormattingJSONOptions = {
waitTranscoding?: boolean, waitTranscoding?: boolean,
scheduledUpdate?: boolean, scheduledUpdate?: boolean,
blacklistInfo?: boolean blacklistInfo?: boolean
playlistInfo?: boolean
} }
} }
function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video { function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video {
@ -98,17 +97,6 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
videoObject.blacklisted = !!video.VideoBlacklist videoObject.blacklisted = !!video.VideoBlacklist
videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null 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 return videoObject

View File

@ -13,14 +13,18 @@ import {
Table, Table,
UpdatedAt UpdatedAt
} from 'sequelize-typescript' } from 'sequelize-typescript'
import { VideoModel } from './video' import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
import { VideoPlaylistModel } from './video-playlist' import { VideoPlaylistModel } from './video-playlist'
import { getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants' import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
import * as validator from 'validator' 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({ @Table({
tableName: 'videoPlaylistElement', tableName: 'videoPlaylistElement',
@ -90,9 +94,9 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
@BelongsTo(() => VideoModel, { @BelongsTo(() => VideoModel, {
foreignKey: { foreignKey: {
allowNull: false allowNull: true
}, },
onDelete: 'CASCADE' onDelete: 'set null'
}) })
Video: VideoModel Video: VideoModel
@ -107,6 +111,57 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
return VideoPlaylistElementModel.destroy(query) 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) { static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) {
const query = { const query = {
where: { where: {
@ -118,6 +173,10 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
return VideoPlaylistElementModel.findOne(query) return VideoPlaylistElementModel.findOne(query)
} }
static loadById (playlistElementId: number) {
return VideoPlaylistElementModel.findByPk(playlistElementId)
}
static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) { static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) {
const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId } const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId }
const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId } const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId }
@ -213,6 +272,42 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
return VideoPlaylistElementModel.increment({ position: by }, query) 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 { toActivityPubObject (): PlaylistElementObject {
const base: PlaylistElementObject = { const base: PlaylistElementObject = {
id: this.url, id: this.url,

View File

@ -33,7 +33,7 @@ import {
WEBSERVER WEBSERVER
} from '../../initializers/constants' } from '../../initializers/constants'
import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' 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 { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
import { join } from 'path' import { join } from 'path'
import { VideoPlaylistElementModel } from './video-playlist-element' import { VideoPlaylistElementModel } from './video-playlist-element'
@ -115,7 +115,7 @@ type AvailableForListOptions = {
[ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => { [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => {
// Only list local playlists OR playlists that are on an instance followed by actorId // Only list local playlists OR playlists that are on an instance followed by actorId
const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
const actorWhere = { const whereActor = {
[ Op.or ]: [ [ Op.or ]: [
{ {
serverId: null serverId: null
@ -159,7 +159,7 @@ type AvailableForListOptions = {
} }
const accountScope = { const accountScope = {
method: [ AccountScopeNames.SUMMARY, actorWhere ] method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ]
} }
return { return {
@ -341,7 +341,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
}, },
include: [ include: [
{ {
attributes: [ 'videoId', 'startTimestamp', 'stopTimestamp' ], attributes: [ 'id', 'videoId', 'startTimestamp', 'stopTimestamp' ],
model: VideoPlaylistElementModel.unscoped(), model: VideoPlaylistElementModel.unscoped(),
where: { where: {
videoId: { videoId: {

View File

@ -91,7 +91,7 @@ import {
} from '../utils' } from '../utils'
import { TagModel } from './tag' import { TagModel } from './tag'
import { VideoAbuseModel } from './video-abuse' 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 { VideoCommentModel } from './video-comment'
import { VideoFileModel } from './video-file' import { VideoFileModel } from './video-file'
import { VideoShareModel } from './video-share' import { VideoShareModel } from './video-share'
@ -190,26 +190,29 @@ export enum ScopeNames {
WITH_FILES = 'WITH_FILES', WITH_FILES = 'WITH_FILES',
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
WITH_BLACKLISTED = 'WITH_BLACKLISTED', WITH_BLACKLISTED = 'WITH_BLACKLISTED',
WITH_BLOCKLIST = 'WITH_BLOCKLIST',
WITH_USER_HISTORY = 'WITH_USER_HISTORY', WITH_USER_HISTORY = 'WITH_USER_HISTORY',
WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
WITH_USER_ID = 'WITH_USER_ID', WITH_USER_ID = 'WITH_USER_ID',
WITH_THUMBNAILS = 'WITH_THUMBNAILS' WITH_THUMBNAILS = 'WITH_THUMBNAILS'
} }
type ForAPIOptions = { export type ForAPIOptions = {
ids: number[] ids?: number[]
videoPlaylistId?: number videoPlaylistId?: number
withFiles?: boolean withFiles?: boolean
withAccountBlockerIds?: number[]
} }
type AvailableForListIDsOptions = { export type AvailableForListIDsOptions = {
serverAccountId: number serverAccountId: number
followerActorId: number followerActorId: number
includeLocalVideos: boolean includeLocalVideos: boolean
withoutId?: boolean attributesType?: 'none' | 'id' | 'all'
filter?: VideoFilter filter?: VideoFilter
categoryOneOf?: number[] categoryOneOf?: number[]
@ -236,14 +239,16 @@ type AvailableForListIDsOptions = {
@Scopes(() => ({ @Scopes(() => ({
[ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => {
const query: FindOptions = { const query: FindOptions = {
where: {
id: {
[ Op.in ]: options.ids // FIXME: sequelize ANY seems broken
}
},
include: [ include: [
{ {
model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }), model: VideoChannelModel.scope({
method: [
VideoChannelScopeNames.SUMMARY, {
withAccount: true,
withAccountBlockerIds: options.withAccountBlockerIds
} as SummaryOptions
]
}),
required: true 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) { if (options.withFiles === true) {
query.include.push({ query.include.push({
model: VideoFileModel.unscoped(), model: VideoFileModel.unscoped(),
@ -278,10 +291,14 @@ type AvailableForListIDsOptions = {
const query: FindOptions = { const query: FindOptions = {
raw: true, raw: true,
attributes: options.withoutId === true ? [] : [ 'id' ],
include: [] include: []
} }
const attributesType = options.attributesType || 'id'
if (attributesType === 'id') query.attributes = [ 'id' ]
else if (attributesType === 'none') query.attributes = [ ]
whereAnd.push({ whereAnd.push({
id: { id: {
[ Op.notIn ]: Sequelize.literal( [ Op.notIn ]: Sequelize.literal(
@ -290,6 +307,7 @@ type AvailableForListIDsOptions = {
} }
}) })
if (options.serverAccountId) {
whereAnd.push({ whereAnd.push({
channelId: { channelId: {
[ Op.notIn ]: Sequelize.literal( [ Op.notIn ]: Sequelize.literal(
@ -301,6 +319,7 @@ type AvailableForListIDsOptions = {
) )
} }
}) })
}
// Only list public/published videos // Only list public/published videos
if (!options.filter || options.filter !== 'all-local') { if (!options.filter || options.filter !== 'all-local') {
@ -527,6 +546,9 @@ type AvailableForListIDsOptions = {
} }
return query return query
},
[ScopeNames.WITH_BLOCKLIST]: {
}, },
[ ScopeNames.WITH_THUMBNAILS ]: { [ ScopeNames.WITH_THUMBNAILS ]: {
include: [ include: [
@ -845,9 +867,9 @@ export class VideoModel extends Model<VideoModel> {
@HasMany(() => VideoPlaylistElementModel, { @HasMany(() => VideoPlaylistElementModel, {
foreignKey: { foreignKey: {
name: 'videoId', name: 'videoId',
allowNull: false allowNull: true
}, },
onDelete: 'cascade' onDelete: 'set null'
}) })
VideoPlaylistElements: VideoPlaylistElementModel[] VideoPlaylistElements: VideoPlaylistElementModel[]
@ -1586,7 +1608,7 @@ export class VideoModel extends Model<VideoModel> {
serverAccountId: serverActor.Account.id, serverAccountId: serverActor.Account.id,
followerActorId, followerActorId,
includeLocalVideos: true, includeLocalVideos: true,
withoutId: true // Don't break aggregation attributesType: 'none' // Don't break aggregation
} }
const query: FindOptions = { const query: FindOptions = {
@ -1719,6 +1741,11 @@ export class VideoModel extends Model<VideoModel> {
return !!this.VideoBlacklist return !!this.VideoBlacklist
} }
isBlocked () {
return (this.VideoChannel.Account.Actor.Server && this.VideoChannel.Account.Actor.Server.isBlocked()) ||
this.VideoChannel.Account.isBlocked()
}
getOriginalFile () { getOriginalFile () {
if (Array.isArray(this.VideoFiles) === false) return undefined if (Array.isArray(this.VideoFiles) === false) return undefined

View File

@ -37,6 +37,7 @@ describe('Test video playlists API validator', function () {
let watchLaterPlaylistId: number let watchLaterPlaylistId: number
let videoId: number let videoId: number
let videoId2: 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 () { 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 () { 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 () { 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 () { it('Should success with the correct parameters', async function () {
await checkBadSortPagination(server.url, path, server.accessToken) 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, token: server.accessToken,
playlistId: playlistUUID, playlistId: playlistUUID,
elementAttrs: Object.assign({ elementAttrs: Object.assign({
videoId: videoId, videoId,
startTimestamp: 2, startTimestamp: 2,
stopTimestamp: 3 stopTimestamp: 3
}, elementAttrs) }, elementAttrs)
@ -344,7 +345,8 @@ describe('Test video playlists API validator', function () {
it('Succeed with the correct params', async function () { it('Succeed with the correct params', async function () {
const params = getBase({}, { expectedStatus: 200 }) 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 () { 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, startTimestamp: 1,
stopTimestamp: 2 stopTimestamp: 2
}, elementAttrs), }, elementAttrs),
videoId: videoId, playlistElementId,
playlistId: playlistUUID, playlistId: playlistUUID,
expectedStatus: 400 expectedStatus: 400
}, wrapper) }, 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) await updateVideoPlaylistElement(params)
} }
{ {
const params = getBase({}, { videoId: 42, expectedStatus: 404 }) const params = getBase({}, { playlistElementId: 42, expectedStatus: 404 })
await updateVideoPlaylistElement(params) await updateVideoPlaylistElement(params)
} }
}) })
@ -415,7 +417,7 @@ describe('Test video playlists API validator', function () {
}) })
it('Should fail with an unknown element', async 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) await updateVideoPlaylistElement(params)
}) })
@ -587,7 +589,7 @@ describe('Test video playlists API validator', function () {
return Object.assign({ return Object.assign({
url: server.url, url: server.url,
token: server.accessToken, token: server.accessToken,
videoId: videoId, playlistElementId,
playlistId: playlistUUID, playlistId: playlistUUID,
expectedStatus: 400 expectedStatus: 400
}, wrapper) }, wrapper)
@ -617,18 +619,18 @@ 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 video id', async function () {
{ {
const params = getBase({ videoId: 'toto' }) const params = getBase({ playlistElementId: 'toto' })
await removeVideoFromPlaylist(params) await removeVideoFromPlaylist(params)
} }
{ {
const params = getBase({ videoId: 42, expectedStatus: 404 }) const params = getBase({ playlistElementId: 42, expectedStatus: 404 })
await removeVideoFromPlaylist(params) await removeVideoFromPlaylist(params)
} }
}) })
it('Should fail with an unknown element', async 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 removeVideoFromPlaylist(params) await removeVideoFromPlaylist(params)
}) })

View File

@ -15,13 +15,12 @@ import {
import { UserRole } from '../../../../shared/models/users' import { UserRole } from '../../../../shared/models/users'
import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' 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 = [ const paths = [
'/api/v1/video-channels/root_channel/videos', '/api/v1/video-channels/root_channel/videos',
'/api/v1/accounts/root/videos', '/api/v1/accounts/root/videos',
'/api/v1/videos', '/api/v1/videos',
'/api/v1/search/videos', '/api/v1/search/videos'
'/api/v1/video-playlists/' + playlistUUID + '/videos'
] ]
for (const path of paths) { for (const path of paths) {
@ -70,39 +69,28 @@ describe('Test videos filters', function () {
} }
) )
moderatorAccessToken = await userLogin(server, moderator) 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 () { describe('When setting a video filter', function () {
it('Should fail with a bad filter', async 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 () { 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 () { 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 () { 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 () { 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 // Because we cannot authenticate the user on the RSS endpoint

View File

@ -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 () { it('Should fail with a bad start pagination', async function () {
await checkBadStartPagination(server.url, path) await checkBadStartPagination(server.url, path)
}) })

View File

@ -5,6 +5,7 @@ import 'mocha'
import { import {
addVideoChannel, addVideoChannel,
addVideoInPlaylist, addVideoInPlaylist,
addVideoToBlacklist,
checkPlaylistFilesWereRemoved, checkPlaylistFilesWereRemoved,
cleanupTests, cleanupTests,
createUser, createUser,
@ -14,6 +15,8 @@ import {
doubleFollow, doubleFollow,
doVideosExistInMyPlaylist, doVideosExistInMyPlaylist,
flushAndRunMultipleServers, flushAndRunMultipleServers,
generateUserAccessToken,
getAccessToken,
getAccountPlaylistsList, getAccountPlaylistsList,
getAccountPlaylistsListWithToken, getAccountPlaylistsListWithToken,
getMyUserInformation, getMyUserInformation,
@ -24,6 +27,7 @@ import {
getVideoPlaylistsList, getVideoPlaylistsList,
getVideoPlaylistWithToken, getVideoPlaylistWithToken,
removeUser, removeUser,
removeVideoFromBlacklist,
removeVideoFromPlaylist, removeVideoFromPlaylist,
reorderVideosPlaylist, reorderVideosPlaylist,
ServerInfo, ServerInfo,
@ -31,23 +35,58 @@ import {
setDefaultVideoChannel, setDefaultVideoChannel,
testImage, testImage,
unfollow, unfollow,
updateVideo,
updateVideoPlaylist, updateVideoPlaylist,
updateVideoPlaylistElement, updateVideoPlaylistElement,
uploadVideo, uploadVideo,
uploadVideoAndGetId, uploadVideoAndGetId,
userLogin, userLogin,
waitJobs, waitJobs
generateUserAccessToken
} from '../../../../shared/extra-utils' } from '../../../../shared/extra-utils'
import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
import { VideoPlaylist } from '../../../../shared/models/videos/playlist/video-playlist.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 { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model'
import { VideoExistInPlaylist } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model' import { VideoExistInPlaylist } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model'
import { User } from '../../../../shared/models/users' 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 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 () { describe('Test video playlists', function () {
let servers: ServerInfo[] = [] let servers: ServerInfo[] = []
@ -57,9 +96,16 @@ describe('Test video playlists', function () {
let playlistServer1Id: number let playlistServer1Id: number
let playlistServer1UUID: string let playlistServer1UUID: string
let playlistServer1UUID2: string
let playlistElementServer1Video4: number
let playlistElementServer1Video5: number
let playlistElementNSFW: number
let nsfwVideoServer1: number let nsfwVideoServer1: number
let userAccessTokenServer1: string
before(async function () { before(async function () {
this.timeout(120000) this.timeout(120000)
@ -97,9 +143,20 @@ describe('Test video playlists', function () {
nsfwVideoServer1 = (await uploadVideoAndGetId({ server: servers[ 0 ], videoName: 'NSFW video', nsfw: true })).id 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) await waitJobs(servers)
}) })
describe('Get default playlists', function () {
it('Should list video playlist privacies', async function () { it('Should list video playlist privacies', async function () {
const res = await getVideoPlaylistPrivacies(servers[ 0 ].url) const res = await getVideoPlaylistPrivacies(servers[ 0 ].url)
@ -150,6 +207,9 @@ describe('Test video playlists', function () {
const playlistId = res.body.data[ 0 ].id const playlistId = res.body.data[ 0 ].id
await getPlaylistVideos(servers[ 0 ].url, token, playlistId, 0, 5) await getPlaylistVideos(servers[ 0 ].url, token, playlistId, 0, 5)
}) })
})
describe('Create and federate playlists', function () {
it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () { it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () {
this.timeout(30000) this.timeout(30000)
@ -281,7 +341,9 @@ describe('Test video playlists', function () {
expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined 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 () { it('Should correctly list the playlists', async function () {
this.timeout(30000) this.timeout(30000)
@ -386,6 +448,9 @@ describe('Test video playlists', function () {
} }
} }
}) })
})
describe('Update playlists', function () {
it('Should update a playlist', async function () { it('Should update a playlist', async function () {
this.timeout(30000) this.timeout(30000)
@ -426,6 +491,9 @@ describe('Test video playlists', function () {
expect(playlist.videoChannel.displayName).to.equal('Main root channel') expect(playlist.videoChannel.displayName).to.equal('Main root channel')
} }
}) })
})
describe('Element timestamps', function () {
it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () { it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () {
this.timeout(30000) this.timeout(30000)
@ -450,9 +518,20 @@ describe('Test video playlists', function () {
await addVideo({ videoId: servers[ 0 ].videos[ 0 ].uuid, startTimestamp: 15, stopTimestamp: 28 }) 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[ 1 ].uuid, startTimestamp: 35 })
await addVideo({ videoId: servers[ 2 ].videos[ 2 ].uuid }) 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 }) const res = await addVideo({ videoId: servers[ 0 ].videos[ 3 ].uuid, stopTimestamp: 35 })
await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 }) playlistElementServer1Video4 = res.body.videoPlaylistElement.id
}
{
const res = await addVideo({ videoId: servers[ 0 ].videos[ 4 ].uuid, startTimestamp: 45, stopTimestamp: 60 })
playlistElementServer1Video5 = res.body.videoPlaylistElement.id
}
{
const res = await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 })
playlistElementNSFW = res.body.videoPlaylistElement.id
}
await waitJobs(servers) await waitJobs(servers)
}) })
@ -465,47 +544,221 @@ describe('Test video playlists', function () {
expect(res.body.total).to.equal(6) expect(res.body.total).to.equal(6)
const videos: Video[] = res.body.data const videoElements: VideoPlaylistElement[] = res.body.data
expect(videos).to.have.lengthOf(6) expect(videoElements).to.have.lengthOf(6)
expect(videos[0].name).to.equal('video 0 server 1') expect(videoElements[ 0 ].video.name).to.equal('video 0 server 1')
expect(videos[0].playlistElement.position).to.equal(1) expect(videoElements[ 0 ].position).to.equal(1)
expect(videos[0].playlistElement.startTimestamp).to.equal(15) expect(videoElements[ 0 ].startTimestamp).to.equal(15)
expect(videos[0].playlistElement.stopTimestamp).to.equal(28) expect(videoElements[ 0 ].stopTimestamp).to.equal(28)
expect(videos[1].name).to.equal('video 1 server 3') expect(videoElements[ 1 ].video.name).to.equal('video 1 server 3')
expect(videos[1].playlistElement.position).to.equal(2) expect(videoElements[ 1 ].position).to.equal(2)
expect(videos[1].playlistElement.startTimestamp).to.equal(35) expect(videoElements[ 1 ].startTimestamp).to.equal(35)
expect(videos[1].playlistElement.stopTimestamp).to.be.null expect(videoElements[ 1 ].stopTimestamp).to.be.null
expect(videos[2].name).to.equal('video 2 server 3') expect(videoElements[ 2 ].video.name).to.equal('video 2 server 3')
expect(videos[2].playlistElement.position).to.equal(3) expect(videoElements[ 2 ].position).to.equal(3)
expect(videos[2].playlistElement.startTimestamp).to.be.null expect(videoElements[ 2 ].startTimestamp).to.be.null
expect(videos[2].playlistElement.stopTimestamp).to.be.null expect(videoElements[ 2 ].stopTimestamp).to.be.null
expect(videos[3].name).to.equal('video 3 server 1') expect(videoElements[ 3 ].video.name).to.equal('video 3 server 1')
expect(videos[3].playlistElement.position).to.equal(4) expect(videoElements[ 3 ].position).to.equal(4)
expect(videos[3].playlistElement.startTimestamp).to.be.null expect(videoElements[ 3 ].startTimestamp).to.be.null
expect(videos[3].playlistElement.stopTimestamp).to.equal(35) expect(videoElements[ 3 ].stopTimestamp).to.equal(35)
expect(videos[4].name).to.equal('video 4 server 1') expect(videoElements[ 4 ].video.name).to.equal('video 4 server 1')
expect(videos[4].playlistElement.position).to.equal(5) expect(videoElements[ 4 ].position).to.equal(5)
expect(videos[4].playlistElement.startTimestamp).to.equal(45) expect(videoElements[ 4 ].startTimestamp).to.equal(45)
expect(videos[4].playlistElement.stopTimestamp).to.equal(60) expect(videoElements[ 4 ].stopTimestamp).to.equal(60)
expect(videos[5].name).to.equal('NSFW video') expect(videoElements[ 5 ].video.name).to.equal('NSFW video')
expect(videos[5].playlistElement.position).to.equal(6) expect(videoElements[ 5 ].position).to.equal(6)
expect(videos[5].playlistElement.startTimestamp).to.equal(5) expect(videoElements[ 5 ].startTimestamp).to.equal(5)
expect(videos[5].playlistElement.stopTimestamp).to.be.null expect(videoElements[ 5 ].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) const res3 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 2)
expect(res3.body.data).to.have.lengthOf(2) expect(res3.body.data).to.have.lengthOf(2)
} }
}) })
})
describe('Element type', function () {
let groupUser1: ServerInfo[]
let groupWithoutToken1: ServerInfo[]
let group1: ServerInfo[]
let group2: ServerInfo[]
let video1: string
let video2: string
let video3: string
before(async function () {
this.timeout(30000)
groupUser1 = [ Object.assign({}, servers[ 0 ], { accessToken: userAccessTokenServer1 }) ]
groupWithoutToken1 = [ Object.assign({}, servers[ 0 ], { accessToken: undefined }) ]
group1 = [ servers[ 0 ] ]
group2 = [ servers[ 1 ], servers[ 2 ] ]
const res = await createVideoPlaylist({
url: servers[ 0 ].url,
token: userAccessTokenServer1,
playlistAttrs: {
displayName: 'playlist 56',
privacy: VideoPlaylistPrivacy.PUBLIC,
videoChannelId: servers[ 0 ].videoChannel.id
}
})
const playlistServer1Id2 = res.body.videoPlaylist.id
playlistServer1UUID2 = res.body.videoPlaylist.uuid
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 elements: VideoPlaylistElement[] = res.body.data
const element = elements.find(e => e.position === 3)
expect(element).to.exist
expect(element.video).to.be.null
expect(element.type).to.equal(VideoPlaylistElementType.UNAVAILABLE)
})
})
describe('Managing playlist elements', function () {
it('Should reorder the playlist', async function () { it('Should reorder the playlist', async function () {
this.timeout(30000) this.timeout(30000)
@ -525,7 +778,7 @@ describe('Test video playlists', function () {
for (const server of servers) { for (const server of servers) {
const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
const names = res.body.data.map(v => v.name) const names = (res.body.data as VideoPlaylistElement[]).map(v => v.video.name)
expect(names).to.deep.equal([ expect(names).to.deep.equal([
'video 0 server 1', 'video 0 server 1',
@ -554,7 +807,7 @@ describe('Test video playlists', function () {
for (const server of servers) { for (const server of servers) {
const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
const names = res.body.data.map(v => v.name) const names = (res.body.data as VideoPlaylistElement[]).map(v => v.video.name)
expect(names).to.deep.equal([ expect(names).to.deep.equal([
'video 3 server 1', 'video 3 server 1',
@ -582,9 +835,8 @@ describe('Test video playlists', function () {
for (const server of servers) { for (const server of servers) {
const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
const videos: Video[] = res.body.data const elements: VideoPlaylistElement[] = res.body.data
const names = elements.map(v => v.video.name)
const names = videos.map(v => v.name)
expect(names).to.deep.equal([ expect(names).to.deep.equal([
'video 3 server 1', 'video 3 server 1',
@ -595,8 +847,8 @@ describe('Test video playlists', function () {
'video 4 server 1' 'video 4 server 1'
]) ])
for (let i = 1; i <= videos.length; i++) { for (let i = 1; i <= elements.length; i++) {
expect(videos[i - 1].playlistElement.position).to.equal(i) expect(elements[ i - 1 ].position).to.equal(i)
} }
} }
} }
@ -609,7 +861,7 @@ describe('Test video playlists', function () {
url: servers[ 0 ].url, url: servers[ 0 ].url,
token: servers[ 0 ].accessToken, token: servers[ 0 ].accessToken,
playlistId: playlistServer1Id, playlistId: playlistServer1Id,
videoId: servers[0].videos[3].uuid, playlistElementId: playlistElementServer1Video4,
elementAttrs: { elementAttrs: {
startTimestamp: 1 startTimestamp: 1
} }
@ -619,7 +871,7 @@ describe('Test video playlists', function () {
url: servers[ 0 ].url, url: servers[ 0 ].url,
token: servers[ 0 ].accessToken, token: servers[ 0 ].accessToken,
playlistId: playlistServer1Id, playlistId: playlistServer1Id,
videoId: servers[0].videos[4].uuid, playlistElementId: playlistElementServer1Video5,
elementAttrs: { elementAttrs: {
stopTimestamp: null stopTimestamp: null
} }
@ -629,17 +881,17 @@ describe('Test video playlists', function () {
for (const server of servers) { for (const server of servers) {
const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10)
const videos: Video[] = res.body.data const elements: VideoPlaylistElement[] = res.body.data
expect(videos[0].name).to.equal('video 3 server 1') expect(elements[ 0 ].video.name).to.equal('video 3 server 1')
expect(videos[0].playlistElement.position).to.equal(1) expect(elements[ 0 ].position).to.equal(1)
expect(videos[0].playlistElement.startTimestamp).to.equal(1) expect(elements[ 0 ].startTimestamp).to.equal(1)
expect(videos[0].playlistElement.stopTimestamp).to.equal(35) expect(elements[ 0 ].stopTimestamp).to.equal(35)
expect(videos[5].name).to.equal('video 4 server 1') expect(elements[ 5 ].video.name).to.equal('video 4 server 1')
expect(videos[5].playlistElement.position).to.equal(6) expect(elements[ 5 ].position).to.equal(6)
expect(videos[5].playlistElement.startTimestamp).to.equal(45) expect(elements[ 5 ].startTimestamp).to.equal(45)
expect(videos[5].playlistElement.stopTimestamp).to.be.null expect(elements[ 5 ].stopTimestamp).to.be.null
} }
}) })
@ -657,6 +909,7 @@ describe('Test video playlists', function () {
{ {
const elem = obj[ servers[ 0 ].videos[ 0 ].id ] const elem = obj[ servers[ 0 ].videos[ 0 ].id ]
expect(elem).to.have.lengthOf(1) expect(elem).to.have.lengthOf(1)
expect(elem[ 0 ].playlistElementId).to.exist
expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id)
expect(elem[ 0 ].startTimestamp).to.equal(15) expect(elem[ 0 ].startTimestamp).to.equal(15)
expect(elem[ 0 ].stopTimestamp).to.equal(28) expect(elem[ 0 ].stopTimestamp).to.equal(28)
@ -665,6 +918,7 @@ describe('Test video playlists', function () {
{ {
const elem = obj[ servers[ 0 ].videos[ 3 ].id ] const elem = obj[ servers[ 0 ].videos[ 3 ].id ]
expect(elem).to.have.lengthOf(1) expect(elem).to.have.lengthOf(1)
expect(elem[ 0 ].playlistElementId).to.equal(playlistElementServer1Video4)
expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id)
expect(elem[ 0 ].startTimestamp).to.equal(1) expect(elem[ 0 ].startTimestamp).to.equal(1)
expect(elem[ 0 ].stopTimestamp).to.equal(35) expect(elem[ 0 ].stopTimestamp).to.equal(35)
@ -693,20 +947,33 @@ describe('Test video playlists', function () {
} }
const elementAttrs = { videoId } const elementAttrs = { videoId }
await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id1, elementAttrs }) const res1 = await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id1, elementAttrs })
await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id2, 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() const names1 = await getPlaylistNames()
expect(names1[ 0 ]).to.equal('playlist 3 updated') expect(names1[ 0 ]).to.equal('playlist 3 updated')
expect(names1[ 1 ]).to.equal('playlist 2') expect(names1[ 1 ]).to.equal('playlist 2')
await removeVideoFromPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id1, videoId }) await removeVideoFromPlaylist({
url: server.url,
token: server.accessToken,
playlistId: playlistServer2Id1,
playlistElementId: element1
})
const names2 = await getPlaylistNames() const names2 = await getPlaylistNames()
expect(names2[ 0 ]).to.equal('playlist 2') expect(names2[ 0 ]).to.equal('playlist 2')
expect(names2[ 1 ]).to.equal('playlist 3 updated') expect(names2[ 1 ]).to.equal('playlist 3 updated')
await removeVideoFromPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id2, videoId }) await removeVideoFromPlaylist({
url: server.url,
token: server.accessToken,
playlistId: playlistServer2Id2,
playlistElementId: element2
})
const names3 = await getPlaylistNames() const names3 = await getPlaylistNames()
expect(names3[ 0 ]).to.equal('playlist 3 updated') expect(names3[ 0 ]).to.equal('playlist 3 updated')
@ -720,14 +987,14 @@ describe('Test video playlists', function () {
url: servers[ 0 ].url, url: servers[ 0 ].url,
token: servers[ 0 ].accessToken, token: servers[ 0 ].accessToken,
playlistId: playlistServer1Id, playlistId: playlistServer1Id,
videoId: servers[0].videos[3].uuid playlistElementId: playlistElementServer1Video4
}) })
await removeVideoFromPlaylist({ await removeVideoFromPlaylist({
url: servers[ 0 ].url, url: servers[ 0 ].url,
token: servers[ 0 ].accessToken, token: servers[ 0 ].accessToken,
playlistId: playlistServer1Id, playlistId: playlistServer1Id,
videoId: nsfwVideoServer1 playlistElementId: playlistElementNSFW
}) })
await waitJobs(servers) await waitJobs(servers)
@ -737,20 +1004,20 @@ describe('Test video playlists', function () {
expect(res.body.total).to.equal(4) expect(res.body.total).to.equal(4)
const videos: Video[] = res.body.data const elements: VideoPlaylistElement[] = res.body.data
expect(videos).to.have.lengthOf(4) expect(elements).to.have.lengthOf(4)
expect(videos[ 0 ].name).to.equal('video 0 server 1') expect(elements[ 0 ].video.name).to.equal('video 0 server 1')
expect(videos[ 0 ].playlistElement.position).to.equal(1) expect(elements[ 0 ].position).to.equal(1)
expect(videos[ 1 ].name).to.equal('video 2 server 3') expect(elements[ 1 ].video.name).to.equal('video 2 server 3')
expect(videos[ 1 ].playlistElement.position).to.equal(2) expect(elements[ 1 ].position).to.equal(2)
expect(videos[ 2 ].name).to.equal('video 1 server 3') expect(elements[ 2 ].video.name).to.equal('video 1 server 3')
expect(videos[ 2 ].playlistElement.position).to.equal(3) expect(elements[ 2 ].position).to.equal(3)
expect(videos[ 3 ].name).to.equal('video 4 server 1') expect(elements[ 3 ].video.name).to.equal('video 4 server 1')
expect(videos[ 3 ].playlistElement.position).to.equal(4) expect(elements[ 3 ].position).to.equal(4)
} }
}) })
@ -786,6 +1053,9 @@ describe('Test video playlists', function () {
await getVideoPlaylistWithToken(servers[ 0 ].url, servers[ 0 ].accessToken, videoPlaylistIds.uuid, 200) await getVideoPlaylistWithToken(servers[ 0 ].url, servers[ 0 ].accessToken, videoPlaylistIds.uuid, 200)
}) })
})
describe('Playlist deletion', function () {
it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () { it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () {
this.timeout(30000) this.timeout(30000)
@ -814,7 +1084,7 @@ describe('Test video playlists', function () {
{ {
const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5)
expect(res.body.total).to.equal(2) expect(res.body.total).to.equal(3)
expect(finder(res.body.data)).to.not.be.undefined expect(finder(res.body.data)).to.not.be.undefined
} }
@ -906,6 +1176,7 @@ describe('Test video playlists', function () {
} }
} }
}) })
})
after(async function () { after(async function () {
await cleanupTests(servers) await cleanupTests(servers)

View File

@ -2,7 +2,6 @@ import * as request from 'supertest'
import { Job, JobState } from '../../models' import { Job, JobState } from '../../models'
import { wait } from '../miscs/miscs' import { wait } from '../miscs/miscs'
import { ServerInfo } from './servers' import { ServerInfo } from './servers'
import { inspect } from 'util'
function getJobsList (url: string, accessToken: string, state: JobState) { function getJobsList (url: string, accessToken: string, state: JobState) {
const path = '/api/v1/jobs/' + state const path = '/api/v1/jobs/' + state
@ -37,11 +36,10 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
else servers = serversArg as ServerInfo[] else servers = serversArg as ServerInfo[]
const states: JobState[] = [ 'waiting', 'active', 'delayed' ] const states: JobState[] = [ 'waiting', 'active', 'delayed' ]
let pendingRequests = false let pendingRequests: boolean
function tasksBuilder () { function tasksBuilder () {
const tasks: Promise<any>[] = [] const tasks: Promise<any>[] = []
pendingRequests = false
// Check if each server has pending request // Check if each server has pending request
for (const server of servers) { for (const server of servers) {
@ -62,6 +60,7 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
} }
do { do {
pendingRequests = false
await Promise.all(tasksBuilder()) await Promise.all(tasksBuilder())
// Retry, in case of new jobs were created // Retry, in case of new jobs were created

View File

@ -196,11 +196,11 @@ function updateVideoPlaylistElement (options: {
url: string, url: string,
token: string, token: string,
playlistId: number | string, playlistId: number | string,
videoId: number | string, playlistElementId: number | string,
elementAttrs: VideoPlaylistElementUpdate, elementAttrs: VideoPlaylistElementUpdate,
expectedStatus?: 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 makePutBodyRequest({ return makePutBodyRequest({
url: options.url, url: options.url,
@ -215,10 +215,10 @@ function removeVideoFromPlaylist (options: {
url: string, url: string,
token: string, token: string,
playlistId: number | string, playlistId: number | string,
videoId: number | string, playlistElementId: number,
expectedStatus?: 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({ return makeDeleteRequest({
url: options.url, url: options.url,

View File

@ -19,6 +19,7 @@ export * from './playlist/video-playlist-privacy.model'
export * from './playlist/video-playlist-type.model' export * from './playlist/video-playlist-type.model'
export * from './playlist/video-playlist-update.model' export * from './playlist/video-playlist-update.model'
export * from './playlist/video-playlist.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.model'
export * from './video-change-ownership-create.model' export * from './video-change-ownership-create.model'
export * from './video-create.model' export * from './video-create.model'

View File

@ -1,5 +1,6 @@
export type VideoExistInPlaylist = { export type VideoExistInPlaylist = {
[videoId: number ]: { [videoId: number ]: {
playlistElementId: number
playlistId: number playlistId: number
startTimestamp?: number startTimestamp?: number
stopTimestamp?: number stopTimestamp?: number

View File

@ -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
}

View File

@ -17,12 +17,6 @@ export interface VideoFile {
fps: number fps: number
} }
export interface PlaylistElement {
position: number
startTimestamp: number
stopTimestamp: number
}
export interface Video { export interface Video {
id: number id: number
uuid: string uuid: string
@ -59,8 +53,6 @@ export interface Video {
userHistory?: { userHistory?: {
currentTime: number currentTime: number
} }
playlistElement?: PlaylistElement
} }
export interface VideoDetails extends Video { export interface VideoDetails extends Video {

View File

@ -1922,6 +1922,9 @@ components:
type: number type: number
stopTimestamp: stopTimestamp:
type: number type: number
video:
nullable: true
$ref: '#/components/schemas/Video'
VideoFile: VideoFile:
properties: properties:
magnetUri: magnetUri:
@ -2029,9 +2032,6 @@ components:
properties: properties:
currentTime: currentTime:
type: number type: number
playlistElement:
nullable: true
$ref: '#/components/schemas/PlaylistElement'
VideoDetails: VideoDetails:
allOf: allOf:
- $ref: '#/components/schemas/Video' - $ref: '#/components/schemas/Video'