diff options
author | Chocobozzz <me@florianbigard.com> | 2019-07-31 15:57:32 +0200 |
---|---|---|
committer | Chocobozzz <chocobozzz@cpy.re> | 2019-08-01 09:11:04 +0200 |
commit | bfbd912886eba17b4aa9a40dcef2fddc685d85bf (patch) | |
tree | 85e0f22980210a8ccd0888eb5e1790b152074677 | |
parent | 85394ba22a07bde1dfccebf3f591a5d6dbe9df56 (diff) | |
download | PeerTube-bfbd912886eba17b4aa9a40dcef2fddc685d85bf.tar.gz PeerTube-bfbd912886eba17b4aa9a40dcef2fddc685d85bf.tar.zst PeerTube-bfbd912886eba17b4aa9a40dcef2fddc685d85bf.zip |
Fix broken playlist api
45 files changed, 1496 insertions, 929 deletions
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html index cb23bb522..ea5f61b18 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.html +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.html | |||
@@ -18,10 +18,13 @@ | |||
18 | <div *ngIf="getVideosOf(videoChannel)" class="videos"> | 18 | <div *ngIf="getVideosOf(videoChannel)" class="videos"> |
19 | <div class="no-results" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel does not have videos.</div> | 19 | <div class="no-results" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel does not have videos.</div> |
20 | 20 | ||
21 | <my-video-miniature *ngFor="let video of getVideosOf(videoChannel)" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature> | 21 | <my-video-miniature |
22 | *ngFor="let video of getVideosOf(videoChannel)" | ||
23 | [video]="video" [user]="user" [displayVideoActions]="true" | ||
24 | ></my-video-miniature> | ||
22 | </div> | 25 | </div> |
23 | 26 | ||
24 | <a class="show-more" i18n [routerLink]="getVideoChannelLink(videoChannel)"> | 27 | <a *ngIf="getVideosOf(videoChannel).length !== 0" class="show-more" i18n [routerLink]="getVideoChannelLink(videoChannel)"> |
25 | Show this channel | 28 | Show this channel |
26 | </a> | 29 | </a> |
27 | </div> | 30 | </div> |
diff --git a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss index 98931f0c2..7f7652460 100644 --- a/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss +++ b/client/src/app/+accounts/account-video-channels/account-video-channels.component.scss | |||
@@ -23,6 +23,11 @@ | |||
23 | height: 50px; | 23 | height: 50px; |
24 | } | 24 | } |
25 | } | 25 | } |
26 | |||
27 | my-video-miniature ::ng-deep my-video-actions-dropdown > my-action-dropdown { | ||
28 | // Fix our overflow | ||
29 | position: absolute; | ||
30 | } | ||
26 | } | 31 | } |
27 | 32 | ||
28 | 33 | ||
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html index 284694b7f..4de4e69da 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.html | |||
@@ -14,10 +14,10 @@ | |||
14 | class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" | 14 | class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()" |
15 | cdkDropList (cdkDropListDropped)="drop($event)" | 15 | cdkDropList (cdkDropListDropped)="drop($event)" |
16 | > | 16 | > |
17 | <div class="video" *ngFor="let video of videos; trackBy: trackByFn" cdkDrag (cdkDragMoved)="onDragMove($event)"> | 17 | <div class="video" *ngFor="let playlistElement of playlistElements; trackBy: trackByFn" cdkDrag> |
18 | <my-video-playlist-element-miniature | 18 | <my-video-playlist-element-miniature |
19 | [video]="video" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)" | 19 | [playlistElement]="playlistElement" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)" |
20 | [position]="video.playlistElement.position" | 20 | [position]="playlistElement.position" |
21 | > | 21 | > |
22 | </my-video-playlist-element-miniature> | 22 | </my-video-playlist-element-miniature> |
23 | </div> | 23 | </div> |
diff --git a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts index d5122aeba..6434b9e50 100644 --- a/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts +++ b/client/src/app/+my-account/my-account-video-playlists/my-account-video-playlist-elements.component.ts | |||
@@ -3,15 +3,13 @@ import { Notifier, ServerService } from '@app/core' | |||
3 | import { AuthService } from '../../core/auth' | 3 | import { AuthService } from '../../core/auth' |
4 | import { ConfirmService } from '../../core/confirm' | 4 | import { ConfirmService } from '../../core/confirm' |
5 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 5 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
6 | import { Video } from '@app/shared/video/video.model' | 6 | import { Subscription } from 'rxjs' |
7 | import { Subject, Subscription } from 'rxjs' | ||
8 | import { ActivatedRoute } from '@angular/router' | 7 | import { ActivatedRoute } from '@angular/router' |
9 | import { VideoService } from '@app/shared/video/video.service' | ||
10 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | 8 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' |
11 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | 9 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' |
12 | import { I18n } from '@ngx-translate/i18n-polyfill' | 10 | import { I18n } from '@ngx-translate/i18n-polyfill' |
13 | import { CdkDragDrop, CdkDragMove } from '@angular/cdk/drag-drop' | 11 | import { CdkDragDrop } from '@angular/cdk/drag-drop' |
14 | import { throttleTime } from 'rxjs/operators' | 12 | import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model' |
15 | 13 | ||
16 | @Component({ | 14 | @Component({ |
17 | selector: 'my-account-video-playlist-elements', | 15 | selector: 'my-account-video-playlist-elements', |
@@ -19,7 +17,7 @@ import { throttleTime } from 'rxjs/operators' | |||
19 | styleUrls: [ './my-account-video-playlist-elements.component.scss' ] | 17 | styleUrls: [ './my-account-video-playlist-elements.component.scss' ] |
20 | }) | 18 | }) |
21 | export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy { | 19 | export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy { |
22 | videos: Video[] = [] | 20 | playlistElements: VideoPlaylistElement[] = [] |
23 | playlist: VideoPlaylist | 21 | playlist: VideoPlaylist |
24 | 22 | ||
25 | pagination: ComponentPagination = { | 23 | pagination: ComponentPagination = { |
@@ -30,7 +28,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro | |||
30 | 28 | ||
31 | private videoPlaylistId: string | number | 29 | private videoPlaylistId: string | number |
32 | private paramsSub: Subscription | 30 | private paramsSub: Subscription |
33 | private dragMoveSubject = new Subject<number>() | ||
34 | 31 | ||
35 | constructor ( | 32 | constructor ( |
36 | private authService: AuthService, | 33 | private authService: AuthService, |
@@ -39,7 +36,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro | |||
39 | private confirmService: ConfirmService, | 36 | private confirmService: ConfirmService, |
40 | private route: ActivatedRoute, | 37 | private route: ActivatedRoute, |
41 | private i18n: I18n, | 38 | private i18n: I18n, |
42 | private videoService: VideoService, | ||
43 | private videoPlaylistService: VideoPlaylistService | 39 | private videoPlaylistService: VideoPlaylistService |
44 | ) {} | 40 | ) {} |
45 | 41 | ||
@@ -50,10 +46,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro | |||
50 | 46 | ||
51 | this.loadPlaylistInfo() | 47 | this.loadPlaylistInfo() |
52 | }) | 48 | }) |
53 | |||
54 | this.dragMoveSubject.asObservable() | ||
55 | .pipe(throttleTime(200)) | ||
56 | .subscribe(y => this.checkScroll(y)) | ||
57 | } | 49 | } |
58 | 50 | ||
59 | ngOnDestroy () { | 51 | ngOnDestroy () { |
@@ -66,8 +58,8 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro | |||
66 | 58 | ||
67 | if (previousIndex === newIndex) return | 59 | if (previousIndex === newIndex) return |
68 | 60 | ||
69 | const oldPosition = this.videos[previousIndex].playlistElement.position | 61 | const oldPosition = this.playlistElements[previousIndex].position |
70 | let insertAfter = this.videos[newIndex].playlistElement.position | 62 | let insertAfter = this.playlistElements[newIndex].position |
71 | 63 | ||
72 | if (oldPosition > insertAfter) insertAfter-- | 64 | if (oldPosition > insertAfter) insertAfter-- |
73 | 65 | ||
@@ -78,42 +70,16 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro | |||
78 | err => this.notifier.error(err.message) | 70 | err => this.notifier.error(err.message) |
79 | ) | 71 | ) |
80 | 72 | ||
81 | const video = this.videos[previousIndex] | 73 | const element = this.playlistElements[previousIndex] |
82 | 74 | ||
83 | this.videos.splice(previousIndex, 1) | 75 | this.playlistElements.splice(previousIndex, 1) |
84 | this.videos.splice(newIndex, 0, video) | 76 | this.playlistElements.splice(newIndex, 0, element) |
85 | 77 | ||
86 | this.reorderClientPositions() | 78 | this.reorderClientPositions() |
87 | } | 79 | } |
88 | 80 | ||
89 | onDragMove (event: CdkDragMove<any>) { | 81 | onElementRemoved (element: VideoPlaylistElement) { |
90 | this.dragMoveSubject.next(event.pointerPosition.y) | 82 | this.playlistElements = this.playlistElements.filter(v => v.id !== element.id) |
91 | } | ||
92 | |||
93 | checkScroll (pointerY: number) { | ||
94 | // FIXME: Uncomment when https://github.com/angular/material2/issues/14098 is fixed | ||
95 | // FIXME: Remove when https://github.com/angular/material2/issues/13588 is implemented | ||
96 | // if (pointerY < 150) { | ||
97 | // window.scrollBy({ | ||
98 | // left: 0, | ||
99 | // top: -20, | ||
100 | // behavior: 'smooth' | ||
101 | // }) | ||
102 | // | ||
103 | // return | ||
104 | // } | ||
105 | // | ||
106 | // if (window.innerHeight - pointerY <= 50) { | ||
107 | // window.scrollBy({ | ||
108 | // left: 0, | ||
109 | // top: 20, | ||
110 | // behavior: 'smooth' | ||
111 | // }) | ||
112 | // } | ||
113 | } | ||
114 | |||
115 | onElementRemoved (video: Video) { | ||
116 | this.videos = this.videos.filter(v => v.id !== video.id) | ||
117 | this.reorderClientPositions() | 83 | this.reorderClientPositions() |
118 | } | 84 | } |
119 | 85 | ||
@@ -125,14 +91,14 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro | |||
125 | this.loadElements() | 91 | this.loadElements() |
126 | } | 92 | } |
127 | 93 | ||
128 | trackByFn (index: number, elem: Video) { | 94 | trackByFn (index: number, elem: VideoPlaylistElement) { |
129 | return elem.id | 95 | return elem.id |
130 | } | 96 | } |
131 | 97 | ||
132 | private loadElements () { | 98 | private loadElements () { |
133 | this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination) | 99 | this.videoPlaylistService.getPlaylistVideos(this.videoPlaylistId, this.pagination) |
134 | .subscribe(({ total, data }) => { | 100 | .subscribe(({ total, data }) => { |
135 | this.videos = this.videos.concat(data) | 101 | this.playlistElements = this.playlistElements.concat(data) |
136 | this.pagination.totalItems = total | 102 | this.pagination.totalItems = total |
137 | }) | 103 | }) |
138 | } | 104 | } |
@@ -147,8 +113,8 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro | |||
147 | private reorderClientPositions () { | 113 | private reorderClientPositions () { |
148 | let i = 1 | 114 | let i = 1 |
149 | 115 | ||
150 | for (const video of this.videos) { | 116 | for (const element of this.playlistElements) { |
151 | video.playlistElement.position = i | 117 | element.position = i |
152 | i++ | 118 | i++ |
153 | } | 119 | } |
154 | } | 120 | } |
diff --git a/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts b/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts index c6cff03a4..08ceb21bc 100644 --- a/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts +++ b/client/src/app/shared/video-playlist/video-add-to-playlist.component.ts | |||
@@ -37,6 +37,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
37 | } | 37 | } |
38 | displayOptions = false | 38 | displayOptions = false |
39 | 39 | ||
40 | private playlistElementId: number | ||
41 | |||
40 | constructor ( | 42 | constructor ( |
41 | protected formValidatorService: FormValidatorService, | 43 | protected formValidatorService: FormValidatorService, |
42 | private authService: AuthService, | 44 | private authService: AuthService, |
@@ -96,6 +98,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
96 | startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined, | 98 | startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined, |
97 | stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined | 99 | stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined |
98 | }) | 100 | }) |
101 | |||
102 | this.playlistElementId = existingPlaylist ? existingPlaylist.playlistElementId : undefined | ||
99 | } | 103 | } |
100 | 104 | ||
101 | this.cd.markForCheck() | 105 | this.cd.markForCheck() |
@@ -177,7 +181,9 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit, | |||
177 | } | 181 | } |
178 | 182 | ||
179 | private removeVideoFromPlaylist (playlist: PlaylistSummary) { | 183 | private removeVideoFromPlaylist (playlist: PlaylistSummary) { |
180 | this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, this.video.id) | 184 | if (!this.playlistElementId) return |
185 | |||
186 | this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, this.playlistElementId) | ||
181 | .subscribe( | 187 | .subscribe( |
182 | () => { | 188 | () => { |
183 | this.notifier.success(this.i18n('Video removed from {{name}}', { name: playlist.displayName })) | 189 | this.notifier.success(this.i18n('Video removed from {{name}}', { name: playlist.displayName })) |
diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html index ab5a78928..25d4783fb 100644 --- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html +++ b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.html | |||
@@ -6,66 +6,82 @@ | |||
6 | </div> | 6 | </div> |
7 | 7 | ||
8 | <my-video-thumbnail | 8 | <my-video-thumbnail |
9 | [video]="video" [nsfw]="isVideoBlur(video)" | 9 | *ngIf="playlistElement.video" |
10 | [video]="playlistElement.video" [nsfw]="isVideoBlur(playlistElement.video)" | ||
10 | [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()" | 11 | [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()" |
11 | ></my-video-thumbnail> | 12 | ></my-video-thumbnail> |
12 | 13 | ||
14 | <div class="fake-thumbnail" *ngIf="!playlistElement.video"></div> | ||
15 | |||
13 | <div class="video-info"> | 16 | <div class="video-info"> |
14 | <a tabindex="-1" class="video-info-name" | 17 | <ng-container *ngIf="playlistElement.video"> |
15 | [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()" | 18 | <a tabindex="-1" class="video-info-name" |
16 | [attr.title]="video.name" | 19 | [routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()" |
17 | >{{ video.name }}</a> | 20 | [attr.title]="playlistElement.video.name" |
21 | >{{ playlistElement.video.name }}</a> | ||
22 | |||
23 | <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', playlistElement.video.byAccount ]"> | ||
24 | {{ playlistElement.video.byAccount }} | ||
25 | </a> | ||
26 | <span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ playlistElement.video.byAccount }}</span> | ||
18 | 27 | ||
19 | <a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a> | 28 | <span tabindex="-1" class="video-info-timestamp">{{ formatTimestamp(playlistElement) }}</span> |
20 | <span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ video.byAccount }}</span> | 29 | </ng-container> |
21 | 30 | ||
22 | <span tabindex="-1" class="video-info-timestamp">{{ formatTimestamp(video) }}</span> | 31 | <span *ngIf="!playlistElement.video" class="video-info-name"> |
32 | <ng-container i18n *ngIf="isUnavailable(playlistElement)">Unavailable</ng-container> | ||
33 | <ng-container i18n *ngIf="isPrivate(playlistElement)">Private</ng-container> | ||
34 | <ng-container i18n *ngIf="isDeleted(playlistElement)">Deleted</ng-container> | ||
35 | </span> | ||
23 | </div> | 36 | </div> |
24 | </a> | 37 | </a> |
25 | 38 | ||
26 | <div *ngIf="owned" class="more" ngbDropdown #moreDropdown="ngbDropdown" placement="bottom-right" (openChange)="onDropdownOpenChange()" | 39 | <div *ngIf="owned" class="more" ngbDropdown #moreDropdown="ngbDropdown" placement="bottom-right" |
27 | autoClose="outside"> | 40 | (openChange)="onDropdownOpenChange()" autoClose="outside" |
41 | > | ||
28 | <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more" (click)="$event.preventDefault()"></my-global-icon> | 42 | <my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more" (click)="$event.preventDefault()"></my-global-icon> |
29 | 43 | ||
30 | <div ngbDropdownMenu> | 44 | <div ngbDropdownMenu> |
31 | <div class="dropdown-item" (click)="toggleDisplayTimestampsOptions($event, video)"> | 45 | <ng-container *ngIf="playlistElement.video"> |
32 | <my-global-icon iconName="edit"></my-global-icon> | 46 | <div class="dropdown-item" (click)="toggleDisplayTimestampsOptions($event, playlistElement)"> |
33 | <ng-container i18n>Edit starts/stops at</ng-container> | 47 | <my-global-icon iconName="edit"></my-global-icon> |
34 | </div> | 48 | <ng-container i18n>Edit starts/stops at</ng-container> |
49 | </div> | ||
35 | 50 | ||
36 | <div class="timestamp-options" *ngIf="displayTimestampOptions"> | 51 | <div class="timestamp-options" *ngIf="displayTimestampOptions"> |
37 | <div> | 52 | <div> |
38 | <my-peertube-checkbox | 53 | <my-peertube-checkbox |
39 | inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled" | 54 | inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled" |
40 | i18n-labelText labelText="Start at" | 55 | i18n-labelText labelText="Start at" |
41 | ></my-peertube-checkbox> | 56 | ></my-peertube-checkbox> |
42 | 57 | ||
43 | <my-timestamp-input | 58 | <my-timestamp-input |
44 | [timestamp]="timestampOptions.startTimestamp" | 59 | [timestamp]="timestampOptions.startTimestamp" |
45 | [maxTimestamp]="video.duration" | 60 | [maxTimestamp]="playlistElement.video.duration" |
46 | [disabled]="!timestampOptions.startTimestampEnabled" | 61 | [disabled]="!timestampOptions.startTimestampEnabled" |
47 | [(ngModel)]="timestampOptions.startTimestamp" | 62 | [(ngModel)]="timestampOptions.startTimestamp" |
48 | ></my-timestamp-input> | 63 | ></my-timestamp-input> |
49 | </div> | 64 | </div> |
50 | 65 | ||
51 | <div> | 66 | <div> |
52 | <my-peertube-checkbox | 67 | <my-peertube-checkbox |
53 | inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled" | 68 | inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled" |
54 | i18n-labelText labelText="Stop at" | 69 | i18n-labelText labelText="Stop at" |
55 | ></my-peertube-checkbox> | 70 | ></my-peertube-checkbox> |
56 | 71 | ||
57 | <my-timestamp-input | 72 | <my-timestamp-input |
58 | [timestamp]="timestampOptions.stopTimestamp" | 73 | [timestamp]="timestampOptions.stopTimestamp" |
59 | [maxTimestamp]="video.duration" | 74 | [maxTimestamp]="playlistElement.video.duration" |
60 | [disabled]="!timestampOptions.stopTimestampEnabled" | 75 | [disabled]="!timestampOptions.stopTimestampEnabled" |
61 | [(ngModel)]="timestampOptions.stopTimestamp" | 76 | [(ngModel)]="timestampOptions.stopTimestamp" |
62 | ></my-timestamp-input> | 77 | ></my-timestamp-input> |
63 | </div> | 78 | </div> |
64 | 79 | ||
65 | <input type="submit" i18n-value value="Save" (click)="updateTimestamps(video)"> | 80 | <input type="submit" i18n-value value="Save" (click)="updateTimestamps(playlistElement)"> |
66 | </div> | 81 | </div> |
82 | </ng-container> | ||
67 | 83 | ||
68 | <span class="dropdown-item" (click)="removeFromPlaylist(video)"> | 84 | <span class="dropdown-item" (click)="removeFromPlaylist(playlistElement)"> |
69 | <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete from {{ playlist?.displayName }}</ng-container> | 85 | <my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete from {{ playlist?.displayName }}</ng-container> |
70 | </span> | 86 | </span> |
71 | </div> | 87 | </div> |
diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss index cb7072d7f..9f4061b02 100644 --- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss +++ b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.scss | |||
@@ -2,9 +2,21 @@ | |||
2 | @import '_mixins'; | 2 | @import '_mixins'; |
3 | @import '_miniature'; | 3 | @import '_miniature'; |
4 | 4 | ||
5 | $thumbnail-width: 130px; | ||
6 | $thumbnail-height: 72px; | ||
7 | |||
5 | my-video-thumbnail { | 8 | my-video-thumbnail { |
6 | @include thumbnail-size-component(130px, 72px); | 9 | @include thumbnail-size-component($thumbnail-width, $thumbnail-height); |
10 | } | ||
7 | 11 | ||
12 | .fake-thumbnail { | ||
13 | width: $thumbnail-width; | ||
14 | height: $thumbnail-height; | ||
15 | background-color: #ececec; | ||
16 | } | ||
17 | |||
18 | my-video-thumbnail, | ||
19 | .fake-thumbnail { | ||
8 | display: flex; // Avoids an issue with line-height that adds space below the element | 20 | display: flex; // Avoids an issue with line-height that adds space below the element |
9 | margin-right: 10px; | 21 | margin-right: 10px; |
10 | } | 22 | } |
@@ -31,6 +43,7 @@ my-video-thumbnail { | |||
31 | a { | 43 | a { |
32 | @include disable-default-a-behaviour; | 44 | @include disable-default-a-behaviour; |
33 | 45 | ||
46 | color: var(--mainForegroundColor); | ||
34 | display: flex; | 47 | display: flex; |
35 | min-width: 0; | 48 | min-width: 0; |
36 | align-items: center; | 49 | align-items: center; |
@@ -58,7 +71,6 @@ my-video-thumbnail { | |||
58 | min-width: 0; | 71 | min-width: 0; |
59 | 72 | ||
60 | a { | 73 | a { |
61 | color: var(--mainForegroundColor); | ||
62 | width: auto; | 74 | width: auto; |
63 | 75 | ||
64 | &:hover { | 76 | &:hover { |
@@ -66,20 +78,20 @@ my-video-thumbnail { | |||
66 | } | 78 | } |
67 | } | 79 | } |
68 | 80 | ||
69 | .video-info-name { | ||
70 | font-size: 18px; | ||
71 | font-weight: $font-semibold; | ||
72 | display: inline-block; | ||
73 | |||
74 | @include ellipsis; | ||
75 | } | ||
76 | |||
77 | .video-info-account, .video-info-timestamp { | 81 | .video-info-account, .video-info-timestamp { |
78 | color: $grey-foreground-color; | 82 | color: $grey-foreground-color; |
79 | } | 83 | } |
80 | } | 84 | } |
81 | } | 85 | } |
82 | 86 | ||
87 | .video-info-name { | ||
88 | font-size: 18px; | ||
89 | font-weight: $font-semibold; | ||
90 | display: inline-block; | ||
91 | |||
92 | @include ellipsis; | ||
93 | } | ||
94 | |||
83 | .more { | 95 | .more { |
84 | justify-self: flex-end; | 96 | justify-self: flex-end; |
85 | margin-left: auto; | 97 | margin-left: auto; |
diff --git a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts index 62cf6536d..a8e5a4885 100644 --- a/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts +++ b/client/src/app/shared/video-playlist/video-playlist-element-miniature.component.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' | 1 | import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core' |
2 | import { Video } from '@app/shared/video/video.model' | 2 | import { Video } from '@app/shared/video/video.model' |
3 | import { VideoPlaylistElementUpdate } from '@shared/models' | 3 | import { VideoPlaylistElementType, VideoPlaylistElementUpdate } from '@shared/models' |
4 | import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core' | 4 | import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core' |
5 | import { ActivatedRoute } from '@angular/router' | 5 | import { ActivatedRoute } from '@angular/router' |
6 | import { I18n } from '@ngx-translate/i18n-polyfill' | 6 | import { I18n } from '@ngx-translate/i18n-polyfill' |
@@ -9,6 +9,7 @@ import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist. | |||
9 | import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' | 9 | import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap' |
10 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | 10 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' |
11 | import { secondsToTime } from '../../../assets/player/utils' | 11 | import { secondsToTime } from '../../../assets/player/utils' |
12 | import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model' | ||
12 | 13 | ||
13 | @Component({ | 14 | @Component({ |
14 | selector: 'my-video-playlist-element-miniature', | 15 | selector: 'my-video-playlist-element-miniature', |
@@ -20,14 +21,14 @@ export class VideoPlaylistElementMiniatureComponent { | |||
20 | @ViewChild('moreDropdown', { static: false }) moreDropdown: NgbDropdown | 21 | @ViewChild('moreDropdown', { static: false }) moreDropdown: NgbDropdown |
21 | 22 | ||
22 | @Input() playlist: VideoPlaylist | 23 | @Input() playlist: VideoPlaylist |
23 | @Input() video: Video | 24 | @Input() playlistElement: VideoPlaylistElement |
24 | @Input() owned = false | 25 | @Input() owned = false |
25 | @Input() playing = false | 26 | @Input() playing = false |
26 | @Input() rowLink = false | 27 | @Input() rowLink = false |
27 | @Input() accountLink = true | 28 | @Input() accountLink = true |
28 | @Input() position: number | 29 | @Input() position: number // Keep this property because we're in the OnPush change detection strategy |
29 | 30 | ||
30 | @Output() elementRemoved = new EventEmitter<Video>() | 31 | @Output() elementRemoved = new EventEmitter<VideoPlaylistElement>() |
31 | 32 | ||
32 | displayTimestampOptions = false | 33 | displayTimestampOptions = false |
33 | 34 | ||
@@ -50,6 +51,18 @@ export class VideoPlaylistElementMiniatureComponent { | |||
50 | private cdr: ChangeDetectorRef | 51 | private cdr: ChangeDetectorRef |
51 | ) {} | 52 | ) {} |
52 | 53 | ||
54 | isUnavailable (e: VideoPlaylistElement) { | ||
55 | return e.type === VideoPlaylistElementType.UNAVAILABLE | ||
56 | } | ||
57 | |||
58 | isPrivate (e: VideoPlaylistElement) { | ||
59 | return e.type === VideoPlaylistElementType.PRIVATE | ||
60 | } | ||
61 | |||
62 | isDeleted (e: VideoPlaylistElement) { | ||
63 | return e.type === VideoPlaylistElementType.DELETED | ||
64 | } | ||
65 | |||
53 | buildRouterLink () { | 66 | buildRouterLink () { |
54 | if (!this.playlist) return null | 67 | if (!this.playlist) return null |
55 | 68 | ||
@@ -57,12 +70,12 @@ export class VideoPlaylistElementMiniatureComponent { | |||
57 | } | 70 | } |
58 | 71 | ||
59 | buildRouterQuery () { | 72 | buildRouterQuery () { |
60 | if (!this.video) return {} | 73 | if (!this.playlistElement || !this.playlistElement.video) return {} |
61 | 74 | ||
62 | return { | 75 | return { |
63 | videoId: this.video.uuid, | 76 | videoId: this.playlistElement.video.uuid, |
64 | start: this.video.playlistElement.startTimestamp, | 77 | start: this.playlistElement.startTimestamp, |
65 | stop: this.video.playlistElement.stopTimestamp | 78 | stop: this.playlistElement.stopTimestamp |
66 | } | 79 | } |
67 | } | 80 | } |
68 | 81 | ||
@@ -70,13 +83,13 @@ export class VideoPlaylistElementMiniatureComponent { | |||
70 | return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig()) | 83 | return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig()) |
71 | } | 84 | } |
72 | 85 | ||
73 | removeFromPlaylist (video: Video) { | 86 | removeFromPlaylist (playlistElement: VideoPlaylistElement) { |
74 | this.videoPlaylistService.removeVideoFromPlaylist(this.playlist.id, video.id) | 87 | this.videoPlaylistService.removeVideoFromPlaylist(this.playlist.id, playlistElement.id) |
75 | .subscribe( | 88 | .subscribe( |
76 | () => { | 89 | () => { |
77 | this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName })) | 90 | this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName })) |
78 | 91 | ||
79 | this.elementRemoved.emit(this.video) | 92 | this.elementRemoved.emit(playlistElement) |
80 | }, | 93 | }, |
81 | 94 | ||
82 | err => this.notifier.error(err.message) | 95 | err => this.notifier.error(err.message) |
@@ -85,19 +98,19 @@ export class VideoPlaylistElementMiniatureComponent { | |||
85 | this.moreDropdown.close() | 98 | this.moreDropdown.close() |
86 | } | 99 | } |
87 | 100 | ||
88 | updateTimestamps (video: Video) { | 101 | updateTimestamps (playlistElement: VideoPlaylistElement) { |
89 | const body: VideoPlaylistElementUpdate = {} | 102 | const body: VideoPlaylistElementUpdate = {} |
90 | 103 | ||
91 | body.startTimestamp = this.timestampOptions.startTimestampEnabled ? this.timestampOptions.startTimestamp : null | 104 | body.startTimestamp = this.timestampOptions.startTimestampEnabled ? this.timestampOptions.startTimestamp : null |
92 | body.stopTimestamp = this.timestampOptions.stopTimestampEnabled ? this.timestampOptions.stopTimestamp : null | 105 | body.stopTimestamp = this.timestampOptions.stopTimestampEnabled ? this.timestampOptions.stopTimestamp : null |
93 | 106 | ||
94 | this.videoPlaylistService.updateVideoOfPlaylist(this.playlist.id, video.id, body) | 107 | this.videoPlaylistService.updateVideoOfPlaylist(this.playlist.id, playlistElement.id, body) |
95 | .subscribe( | 108 | .subscribe( |
96 | () => { | 109 | () => { |
97 | this.notifier.success(this.i18n('Timestamps updated')) | 110 | this.notifier.success(this.i18n('Timestamps updated')) |
98 | 111 | ||
99 | video.playlistElement.startTimestamp = body.startTimestamp | 112 | playlistElement.startTimestamp = body.startTimestamp |
100 | video.playlistElement.stopTimestamp = body.stopTimestamp | 113 | playlistElement.stopTimestamp = body.stopTimestamp |
101 | 114 | ||
102 | this.cdr.detectChanges() | 115 | this.cdr.detectChanges() |
103 | }, | 116 | }, |
@@ -108,9 +121,9 @@ export class VideoPlaylistElementMiniatureComponent { | |||
108 | this.moreDropdown.close() | 121 | this.moreDropdown.close() |
109 | } | 122 | } |
110 | 123 | ||
111 | formatTimestamp (video: Video) { | 124 | formatTimestamp (playlistElement: VideoPlaylistElement) { |
112 | const start = video.playlistElement.startTimestamp | 125 | const start = playlistElement.startTimestamp |
113 | const stop = video.playlistElement.stopTimestamp | 126 | const stop = playlistElement.stopTimestamp |
114 | 127 | ||
115 | const startFormatted = secondsToTime(start, true, ':') | 128 | const startFormatted = secondsToTime(start, true, ':') |
116 | const stopFormatted = secondsToTime(stop, true, ':') | 129 | const stopFormatted = secondsToTime(stop, true, ':') |
@@ -127,7 +140,7 @@ export class VideoPlaylistElementMiniatureComponent { | |||
127 | this.displayTimestampOptions = false | 140 | this.displayTimestampOptions = false |
128 | } | 141 | } |
129 | 142 | ||
130 | toggleDisplayTimestampsOptions (event: Event, video: Video) { | 143 | toggleDisplayTimestampsOptions (event: Event, playlistElement: VideoPlaylistElement) { |
131 | event.preventDefault() | 144 | event.preventDefault() |
132 | 145 | ||
133 | this.displayTimestampOptions = !this.displayTimestampOptions | 146 | this.displayTimestampOptions = !this.displayTimestampOptions |
@@ -137,17 +150,17 @@ export class VideoPlaylistElementMiniatureComponent { | |||
137 | startTimestampEnabled: false, | 150 | startTimestampEnabled: false, |
138 | stopTimestampEnabled: false, | 151 | stopTimestampEnabled: false, |
139 | startTimestamp: 0, | 152 | startTimestamp: 0, |
140 | stopTimestamp: video.duration | 153 | stopTimestamp: playlistElement.video.duration |
141 | } | 154 | } |
142 | 155 | ||
143 | if (video.playlistElement.startTimestamp) { | 156 | if (playlistElement.startTimestamp) { |
144 | this.timestampOptions.startTimestampEnabled = true | 157 | this.timestampOptions.startTimestampEnabled = true |
145 | this.timestampOptions.startTimestamp = video.playlistElement.startTimestamp | 158 | this.timestampOptions.startTimestamp = playlistElement.startTimestamp |
146 | } | 159 | } |
147 | 160 | ||
148 | if (video.playlistElement.stopTimestamp) { | 161 | if (playlistElement.stopTimestamp) { |
149 | this.timestampOptions.stopTimestampEnabled = true | 162 | this.timestampOptions.stopTimestampEnabled = true |
150 | this.timestampOptions.stopTimestamp = video.playlistElement.stopTimestamp | 163 | this.timestampOptions.stopTimestamp = playlistElement.stopTimestamp |
151 | } | 164 | } |
152 | } | 165 | } |
153 | 166 | ||
diff --git a/client/src/app/shared/video-playlist/video-playlist-element.model.ts b/client/src/app/shared/video-playlist/video-playlist-element.model.ts new file mode 100644 index 000000000..f1c46d1eb --- /dev/null +++ b/client/src/app/shared/video-playlist/video-playlist-element.model.ts | |||
@@ -0,0 +1,24 @@ | |||
1 | import { VideoPlaylistElement as ServerVideoPlaylistElement, VideoPlaylistElementType } from '../../../../../shared/models/videos' | ||
2 | import { Video } from '@app/shared/video/video.model' | ||
3 | |||
4 | export class VideoPlaylistElement implements ServerVideoPlaylistElement { | ||
5 | id: number | ||
6 | position: number | ||
7 | startTimestamp: number | ||
8 | stopTimestamp: number | ||
9 | |||
10 | type: VideoPlaylistElementType | ||
11 | |||
12 | video?: Video | ||
13 | |||
14 | constructor (hash: ServerVideoPlaylistElement, translations: {}) { | ||
15 | this.id = hash.id | ||
16 | this.position = hash.position | ||
17 | this.startTimestamp = hash.startTimestamp | ||
18 | this.stopTimestamp = hash.stopTimestamp | ||
19 | |||
20 | this.type = hash.type | ||
21 | |||
22 | if (hash.video) this.video = new Video(hash.video, translations) | ||
23 | } | ||
24 | } | ||
diff --git a/client/src/app/shared/video-playlist/video-playlist.service.ts b/client/src/app/shared/video-playlist/video-playlist.service.ts index da7437507..b93a19356 100644 --- a/client/src/app/shared/video-playlist/video-playlist.service.ts +++ b/client/src/app/shared/video-playlist/video-playlist.service.ts | |||
@@ -18,6 +18,9 @@ import { Account } from '@app/shared/account/account.model' | |||
18 | import { RestService } from '@app/shared/rest' | 18 | import { RestService } from '@app/shared/rest' |
19 | import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model' | 19 | import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model' |
20 | import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model' | 20 | import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model' |
21 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | ||
22 | import { VideoPlaylistElement as ServerVideoPlaylistElement } from '@shared/models/videos/playlist/video-playlist-element.model' | ||
23 | import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model' | ||
21 | 24 | ||
22 | @Injectable() | 25 | @Injectable() |
23 | export class VideoPlaylistService { | 26 | export class VideoPlaylistService { |
@@ -110,16 +113,16 @@ export class VideoPlaylistService { | |||
110 | ) | 113 | ) |
111 | } | 114 | } |
112 | 115 | ||
113 | updateVideoOfPlaylist (playlistId: number, videoId: number, body: VideoPlaylistElementUpdate) { | 116 | updateVideoOfPlaylist (playlistId: number, playlistElementId: number, body: VideoPlaylistElementUpdate) { |
114 | return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + videoId, body) | 117 | return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId, body) |
115 | .pipe( | 118 | .pipe( |
116 | map(this.restExtractor.extractDataBool), | 119 | map(this.restExtractor.extractDataBool), |
117 | catchError(err => this.restExtractor.handleError(err)) | 120 | catchError(err => this.restExtractor.handleError(err)) |
118 | ) | 121 | ) |
119 | } | 122 | } |
120 | 123 | ||
121 | removeVideoFromPlaylist (playlistId: number, videoId: number) { | 124 | removeVideoFromPlaylist (playlistId: number, playlistElementId: number) { |
122 | return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + videoId) | 125 | return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId) |
123 | .pipe( | 126 | .pipe( |
124 | map(this.restExtractor.extractDataBool), | 127 | map(this.restExtractor.extractDataBool), |
125 | catchError(err => this.restExtractor.handleError(err)) | 128 | catchError(err => this.restExtractor.handleError(err)) |
@@ -139,6 +142,24 @@ export class VideoPlaylistService { | |||
139 | ) | 142 | ) |
140 | } | 143 | } |
141 | 144 | ||
145 | getPlaylistVideos ( | ||
146 | videoPlaylistId: number | string, | ||
147 | componentPagination: ComponentPagination | ||
148 | ): Observable<ResultList<VideoPlaylistElement>> { | ||
149 | const path = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylistId + '/videos' | ||
150 | const pagination = this.restService.componentPaginationToRestPagination(componentPagination) | ||
151 | |||
152 | let params = new HttpParams() | ||
153 | params = this.restService.addRestGetParams(params, pagination) | ||
154 | |||
155 | return this.authHttp | ||
156 | .get<ResultList<ServerVideoPlaylistElement>>(path, { params }) | ||
157 | .pipe( | ||
158 | switchMap(res => this.extractVideoPlaylistElements(res)), | ||
159 | catchError(err => this.restExtractor.handleError(err)) | ||
160 | ) | ||
161 | } | ||
162 | |||
142 | doesVideoExistInPlaylist (videoId: number) { | 163 | doesVideoExistInPlaylist (videoId: number) { |
143 | this.videoExistsInPlaylistSubject.next(videoId) | 164 | this.videoExistsInPlaylistSubject.next(videoId) |
144 | 165 | ||
@@ -167,6 +188,23 @@ export class VideoPlaylistService { | |||
167 | .pipe(map(translations => new VideoPlaylist(playlist, translations))) | 188 | .pipe(map(translations => new VideoPlaylist(playlist, translations))) |
168 | } | 189 | } |
169 | 190 | ||
191 | extractVideoPlaylistElements (result: ResultList<ServerVideoPlaylistElement>) { | ||
192 | return this.serverService.localeObservable | ||
193 | .pipe( | ||
194 | map(translations => { | ||
195 | const elementsJson = result.data | ||
196 | const total = result.total | ||
197 | const elements: VideoPlaylistElement[] = [] | ||
198 | |||
199 | for (const elementJson of elementsJson) { | ||
200 | elements.push(new VideoPlaylistElement(elementJson, translations)) | ||
201 | } | ||
202 | |||
203 | return { total, data: elements } | ||
204 | }) | ||
205 | ) | ||
206 | } | ||
207 | |||
170 | private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> { | 208 | private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> { |
171 | const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist' | 209 | const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist' |
172 | let params = new HttpParams() | 210 | let params = new HttpParams() |
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index 6f9de9241..fb98d5382 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import { User } from '../' | 1 | import { User } from '../' |
2 | import { PlaylistElement, UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' | 2 | import { UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared' |
3 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | 3 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' |
4 | import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model' | 4 | import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model' |
5 | import { durationToString, getAbsoluteAPIUrl } from '../misc/utils' | 5 | import { durationToString, getAbsoluteAPIUrl } from '../misc/utils' |
@@ -48,8 +48,6 @@ export class Video implements VideoServerModel { | |||
48 | blacklisted?: boolean | 48 | blacklisted?: boolean |
49 | blacklistedReason?: string | 49 | blacklistedReason?: string |
50 | 50 | ||
51 | playlistElement?: PlaylistElement | ||
52 | |||
53 | account: { | 51 | account: { |
54 | id: number | 52 | id: number |
55 | name: string | 53 | name: string |
@@ -126,8 +124,6 @@ export class Video implements VideoServerModel { | |||
126 | this.blacklistedReason = hash.blacklistedReason | 124 | this.blacklistedReason = hash.blacklistedReason |
127 | 125 | ||
128 | this.userHistory = hash.userHistory | 126 | this.userHistory = hash.userHistory |
129 | |||
130 | this.playlistElement = hash.playlistElement | ||
131 | } | 127 | } |
132 | 128 | ||
133 | isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { | 129 | isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { |
diff --git a/client/src/app/shared/video/video.service.ts b/client/src/app/shared/video/video.service.ts index 114b014ad..45366e3e3 100644 --- a/client/src/app/shared/video/video.service.ts +++ b/client/src/app/shared/video/video.service.ts | |||
@@ -31,7 +31,6 @@ import { ServerService } from '@app/core' | |||
31 | import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' | 31 | import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service' |
32 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' | 32 | import { VideoChannel } from '@app/shared/video-channel/video-channel.model' |
33 | import { I18n } from '@ngx-translate/i18n-polyfill' | 33 | import { I18n } from '@ngx-translate/i18n-polyfill' |
34 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | ||
35 | 34 | ||
36 | export interface VideosProvider { | 35 | export interface VideosProvider { |
37 | getVideos (parameters: { | 36 | getVideos (parameters: { |
@@ -172,23 +171,6 @@ export class VideoService implements VideosProvider { | |||
172 | ) | 171 | ) |
173 | } | 172 | } |
174 | 173 | ||
175 | getPlaylistVideos ( | ||
176 | videoPlaylistId: number | string, | ||
177 | videoPagination: ComponentPagination | ||
178 | ): Observable<ResultList<Video>> { | ||
179 | const pagination = this.restService.componentPaginationToRestPagination(videoPagination) | ||
180 | |||
181 | let params = new HttpParams() | ||
182 | params = this.restService.addRestGetParams(params, pagination) | ||
183 | |||
184 | return this.authHttp | ||
185 | .get<ResultList<Video>>(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylistId + '/videos', { params }) | ||
186 | .pipe( | ||
187 | switchMap(res => this.extractVideos(res)), | ||
188 | catchError(err => this.restExtractor.handleError(err)) | ||
189 | ) | ||
190 | } | ||
191 | |||
192 | getUserSubscriptionVideos (parameters: { | 174 | getUserSubscriptionVideos (parameters: { |
193 | videoPagination: ComponentPagination, | 175 | videoPagination: ComponentPagination, |
194 | sort: VideoSortField | 176 | sort: VideoSortField |
diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.html b/client/src/app/videos/+video-watch/video-watch-playlist.component.html index c168a3130..c89936bd1 100644 --- a/client/src/app/videos/+video-watch/video-watch-playlist.component.html +++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.html | |||
@@ -16,10 +16,10 @@ | |||
16 | </div> | 16 | </div> |
17 | </div> | 17 | </div> |
18 | 18 | ||
19 | <div *ngFor="let playlistVideo of playlistVideos"> | 19 | <div *ngFor="let playlistElement of playlistElements"> |
20 | <my-video-playlist-element-miniature | 20 | <my-video-playlist-element-miniature |
21 | [video]="playlistVideo" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)" | 21 | [playlistElement]="playlistElement" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)" |
22 | [playing]="currentPlaylistPosition === playlistVideo.playlistElement.position" [accountLink]="false" [position]="playlistVideo.playlistElement.position" | 22 | [playing]="currentPlaylistPosition === playlistElement.position" [accountLink]="false" [position]="playlistElement.position" |
23 | ></my-video-playlist-element-miniature> | 23 | ></my-video-playlist-element-miniature> |
24 | </div> | 24 | </div> |
25 | </div> | 25 | </div> |
diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.scss b/client/src/app/videos/+video-watch/video-watch-playlist.component.scss index eeb763bd9..4c24d6b05 100644 --- a/client/src/app/videos/+video-watch/video-watch-playlist.component.scss +++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.scss | |||
@@ -53,6 +53,11 @@ | |||
53 | my-video-thumbnail { | 53 | my-video-thumbnail { |
54 | @include thumbnail-size-component(90px, 50px); | 54 | @include thumbnail-size-component(90px, 50px); |
55 | } | 55 | } |
56 | |||
57 | .fake-thumbnail { | ||
58 | width: 90px; | ||
59 | height: 50px; | ||
60 | } | ||
56 | } | 61 | } |
57 | } | 62 | } |
58 | } | 63 | } |
diff --git a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts index 2fb0cb0e5..6e8d58cd8 100644 --- a/client/src/app/videos/+video-watch/video-watch-playlist.component.ts +++ b/client/src/app/videos/+video-watch/video-watch-playlist.component.ts | |||
@@ -1,11 +1,11 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' | 2 | import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model' |
3 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' | 3 | import { ComponentPagination } from '@app/shared/rest/component-pagination.model' |
4 | import { Video } from '@app/shared/video/video.model' | ||
5 | import { VideoDetails, VideoPlaylistPrivacy } from '@shared/models' | 4 | import { VideoDetails, VideoPlaylistPrivacy } from '@shared/models' |
6 | import { VideoService } from '@app/shared/video/video.service' | ||
7 | import { Router } from '@angular/router' | 5 | import { Router } from '@angular/router' |
8 | import { AuthService } from '@app/core' | 6 | import { AuthService } from '@app/core' |
7 | import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service' | ||
8 | import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model' | ||
9 | 9 | ||
10 | @Component({ | 10 | @Component({ |
11 | selector: 'my-video-watch-playlist', | 11 | selector: 'my-video-watch-playlist', |
@@ -16,7 +16,7 @@ export class VideoWatchPlaylistComponent { | |||
16 | @Input() video: VideoDetails | 16 | @Input() video: VideoDetails |
17 | @Input() playlist: VideoPlaylist | 17 | @Input() playlist: VideoPlaylist |
18 | 18 | ||
19 | playlistVideos: Video[] = [] | 19 | playlistElements: VideoPlaylistElement[] = [] |
20 | playlistPagination: ComponentPagination = { | 20 | playlistPagination: ComponentPagination = { |
21 | currentPage: 1, | 21 | currentPage: 1, |
22 | itemsPerPage: 30, | 22 | itemsPerPage: 30, |
@@ -28,7 +28,7 @@ export class VideoWatchPlaylistComponent { | |||
28 | 28 | ||
29 | constructor ( | 29 | constructor ( |
30 | private auth: AuthService, | 30 | private auth: AuthService, |
31 | private videoService: VideoService, | 31 | private videoPlaylist: VideoPlaylistService, |
32 | private router: Router | 32 | private router: Router |
33 | ) {} | 33 | ) {} |
34 | 34 | ||
@@ -40,8 +40,8 @@ export class VideoWatchPlaylistComponent { | |||
40 | this.loadPlaylistElements(this.playlist,false) | 40 | this.loadPlaylistElements(this.playlist,false) |
41 | } | 41 | } |
42 | 42 | ||
43 | onElementRemoved (video: Video) { | 43 | onElementRemoved (playlistElement: VideoPlaylistElement) { |
44 | this.playlistVideos = this.playlistVideos.filter(v => v.id !== video.id) | 44 | this.playlistElements = this.playlistElements.filter(e => e.id !== playlistElement.id) |
45 | 45 | ||
46 | this.playlistPagination.totalItems-- | 46 | this.playlistPagination.totalItems-- |
47 | } | 47 | } |
@@ -65,12 +65,13 @@ export class VideoWatchPlaylistComponent { | |||
65 | } | 65 | } |
66 | 66 | ||
67 | loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) { | 67 | loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) { |
68 | this.videoService.getPlaylistVideos(playlist.uuid, this.playlistPagination) | 68 | this.videoPlaylist.getPlaylistVideos(playlist.uuid, this.playlistPagination) |
69 | .subscribe(({ total, data }) => { | 69 | .subscribe(({ total, data }) => { |
70 | this.playlistVideos = this.playlistVideos.concat(data) | 70 | this.playlistElements = this.playlistElements.concat(data) |
71 | this.playlistPagination.totalItems = total | 71 | this.playlistPagination.totalItems = total |
72 | 72 | ||
73 | if (total === 0) { | 73 | const firstAvailableVideos = this.playlistElements.find(e => !!e.video) |
74 | if (!firstAvailableVideos) { | ||
74 | this.noPlaylistVideos = true | 75 | this.noPlaylistVideos = true |
75 | return | 76 | return |
76 | } | 77 | } |
@@ -79,7 +80,7 @@ export class VideoWatchPlaylistComponent { | |||
79 | 80 | ||
80 | if (redirectToFirst) { | 81 | if (redirectToFirst) { |
81 | const extras = { | 82 | const extras = { |
82 | queryParams: { videoId: this.playlistVideos[ 0 ].uuid }, | 83 | queryParams: { videoId: firstAvailableVideos.video.uuid }, |
83 | replaceUrl: true | 84 | replaceUrl: true |
84 | } | 85 | } |
85 | this.router.navigate([], extras) | 86 | this.router.navigate([], extras) |
@@ -88,11 +89,11 @@ export class VideoWatchPlaylistComponent { | |||
88 | } | 89 | } |
89 | 90 | ||
90 | updatePlaylistIndex (video: VideoDetails) { | 91 | updatePlaylistIndex (video: VideoDetails) { |
91 | if (this.playlistVideos.length === 0 || !video) return | 92 | if (this.playlistElements.length === 0 || !video) return |
92 | 93 | ||
93 | for (const playlistVideo of this.playlistVideos) { | 94 | for (const playlistElement of this.playlistElements) { |
94 | if (playlistVideo.id === video.id) { | 95 | if (playlistElement.video && playlistElement.video.id === video.id) { |
95 | this.currentPlaylistPosition = playlistVideo.playlistElement.position | 96 | this.currentPlaylistPosition = playlistElement.position |
96 | return | 97 | return |
97 | } | 98 | } |
98 | } | 99 | } |
@@ -103,11 +104,17 @@ export class VideoWatchPlaylistComponent { | |||
103 | 104 | ||
104 | navigateToNextPlaylistVideo () { | 105 | navigateToNextPlaylistVideo () { |
105 | if (this.currentPlaylistPosition < this.playlistPagination.totalItems) { | 106 | if (this.currentPlaylistPosition < this.playlistPagination.totalItems) { |
106 | const next = this.playlistVideos.find(v => v.playlistElement.position === this.currentPlaylistPosition + 1) | 107 | const next = this.playlistElements.find(e => e.position === this.currentPlaylistPosition + 1) |
107 | 108 | ||
108 | const start = next.playlistElement.startTimestamp | 109 | if (!next || !next.video) { |
109 | const stop = next.playlistElement.stopTimestamp | 110 | this.currentPlaylistPosition++ |
110 | this.router.navigate([],{ queryParams: { videoId: next.uuid, start, stop } }) | 111 | this.navigateToNextPlaylistVideo() |
112 | return | ||
113 | } | ||
114 | |||
115 | const start = next.startTimestamp | ||
116 | const stop = next.stopTimestamp | ||
117 | this.router.navigate([],{ queryParams: { videoId: next.video.uuid, start, stop } }) | ||
111 | } | 118 | } |
112 | } | 119 | } |
113 | } | 120 | } |
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts index 0d499d47f..d7c7b7497 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -464,7 +464,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
464 | } | 464 | } |
465 | 465 | ||
466 | this.zone.runOutsideAngular(async () => { | 466 | this.zone.runOutsideAngular(async () => { |
467 | this.player = await PeertubePlayerManager.initialize(mode, options) | 467 | this.player = await PeertubePlayerManager.initialize(mode, options, player => this.player = player) |
468 | 468 | ||
469 | this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) | 469 | this.player.on('customError', ({ err }: { err: any }) => this.handleError(err)) |
470 | 470 | ||
diff --git a/client/src/assets/player/peertube-player-manager.ts b/client/src/assets/player/peertube-player-manager.ts index 083c621d2..6c8b13087 100644 --- a/client/src/assets/player/peertube-player-manager.ts +++ b/client/src/assets/player/peertube-player-manager.ts | |||
@@ -86,6 +86,7 @@ export class PeertubePlayerManager { | |||
86 | 86 | ||
87 | private static videojsLocaleCache: { [ path: string ]: any } = {} | 87 | private static videojsLocaleCache: { [ path: string ]: any } = {} |
88 | private static playerElementClassName: string | 88 | private static playerElementClassName: string |
89 | private static onPlayerChange: (player: any) => void | ||
89 | 90 | ||
90 | static getServerTranslations (serverUrl: string, locale: string) { | 91 | static getServerTranslations (serverUrl: string, locale: string) { |
91 | const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) | 92 | const path = PeertubePlayerManager.getLocalePath(serverUrl, locale) |
@@ -100,9 +101,10 @@ export class PeertubePlayerManager { | |||
100 | }) | 101 | }) |
101 | } | 102 | } |
102 | 103 | ||
103 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) { | 104 | static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: any) => void) { |
104 | let p2pMediaLoader: any | 105 | let p2pMediaLoader: any |
105 | 106 | ||
107 | this.onPlayerChange = onPlayerChange | ||
106 | this.playerElementClassName = options.common.playerElement.className | 108 | this.playerElementClassName = options.common.playerElement.className |
107 | 109 | ||
108 | if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin') | 110 | if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin') |
@@ -171,6 +173,8 @@ export class PeertubePlayerManager { | |||
171 | const player = this | 173 | const player = this |
172 | 174 | ||
173 | self.addContextMenu(mode, player, options.common.embedUrl) | 175 | self.addContextMenu(mode, player, options.common.embedUrl) |
176 | |||
177 | PeertubePlayerManager.onPlayerChange(player) | ||
174 | }) | 178 | }) |
175 | } | 179 | } |
176 | 180 | ||
diff --git a/client/src/assets/player/videojs-components/settings-menu-item.ts b/client/src/assets/player/videojs-components/settings-menu-item.ts index 78879a2ec..24b7e0c70 100644 --- a/client/src/assets/player/videojs-components/settings-menu-item.ts +++ b/client/src/assets/player/videojs-components/settings-menu-item.ts | |||
@@ -43,6 +43,9 @@ class SettingsMenuItem extends MenuItem { | |||
43 | player.ready(() => { | 43 | player.ready(() => { |
44 | // Voodoo magic for IOS | 44 | // Voodoo magic for IOS |
45 | setTimeout(() => { | 45 | setTimeout(() => { |
46 | // Player was destroyed | ||
47 | if (!this.player_) return | ||
48 | |||
46 | this.build() | 49 | this.build() |
47 | 50 | ||
48 | // Update on rate change | 51 | // Update on rate change |
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index cfe8e94b1..6ff3efef1 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts | |||
@@ -212,7 +212,7 @@ export class PeerTubeEmbed { | |||
212 | }) | 212 | }) |
213 | } | 213 | } |
214 | 214 | ||
215 | this.player = await PeertubePlayerManager.initialize(this.mode, options) | 215 | this.player = await PeertubePlayerManager.initialize(this.mode, options, player => this.player = player) |
216 | this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) | 216 | this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations)) |
217 | 217 | ||
218 | window[ 'videojsPlayer' ] = this.player | 218 | window[ 'videojsPlayer' ] = this.player |
diff --git a/server/controllers/api/users/my-video-playlists.ts b/server/controllers/api/users/my-video-playlists.ts index 15e92f4f3..735a3cbee 100644 --- a/server/controllers/api/users/my-video-playlists.ts +++ b/server/controllers/api/users/my-video-playlists.ts | |||
@@ -35,6 +35,7 @@ async function doVideosInPlaylistExist (req: express.Request, res: express.Respo | |||
35 | for (const result of results) { | 35 | for (const result of results) { |
36 | for (const element of result.VideoPlaylistElements) { | 36 | for (const element of result.VideoPlaylistElements) { |
37 | existObject[element.videoId].push({ | 37 | existObject[element.videoId].push({ |
38 | playlistElementId: element.id, | ||
38 | playlistId: result.id, | 39 | playlistId: result.id, |
39 | startTimestamp: element.startTimestamp, | 40 | startTimestamp: element.startTimestamp, |
40 | stopTimestamp: element.stopTimestamp | 41 | stopTimestamp: element.stopTimestamp |
diff --git a/server/controllers/api/video-playlist.ts b/server/controllers/api/video-playlist.ts index 62490e63b..540120cca 100644 --- a/server/controllers/api/video-playlist.ts +++ b/server/controllers/api/video-playlist.ts | |||
@@ -4,14 +4,13 @@ import { | |||
4 | asyncMiddleware, | 4 | asyncMiddleware, |
5 | asyncRetryTransactionMiddleware, | 5 | asyncRetryTransactionMiddleware, |
6 | authenticate, | 6 | authenticate, |
7 | commonVideosFiltersValidator, | ||
8 | optionalAuthenticate, | 7 | optionalAuthenticate, |
9 | paginationValidator, | 8 | paginationValidator, |
10 | setDefaultPagination, | 9 | setDefaultPagination, |
11 | setDefaultSort | 10 | setDefaultSort |
12 | } from '../../middlewares' | 11 | } from '../../middlewares' |
13 | import { videoPlaylistsSortValidator } from '../../middlewares/validators' | 12 | import { videoPlaylistsSortValidator } from '../../middlewares/validators' |
14 | import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils' | 13 | import { buildNSFWFilter, createReqFiles } from '../../helpers/express-utils' |
15 | import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants' | 14 | import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants' |
16 | import { logger } from '../../helpers/logger' | 15 | import { logger } from '../../helpers/logger' |
17 | import { resetSequelizeInstance } from '../../helpers/database-utils' | 16 | import { resetSequelizeInstance } from '../../helpers/database-utils' |
@@ -32,7 +31,6 @@ import { join } from 'path' | |||
32 | import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' | 31 | import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send' |
33 | import { getVideoPlaylistActivityPubUrl, getVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' | 32 | import { getVideoPlaylistActivityPubUrl, getVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url' |
34 | import { VideoPlaylistUpdate } from '../../../shared/models/videos/playlist/video-playlist-update.model' | 33 | import { VideoPlaylistUpdate } from '../../../shared/models/videos/playlist/video-playlist-update.model' |
35 | import { VideoModel } from '../../models/video/video' | ||
36 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' | 34 | import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element' |
37 | import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' | 35 | import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model' |
38 | import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' | 36 | import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model' |
@@ -88,7 +86,6 @@ videoPlaylistRouter.get('/:playlistId/videos', | |||
88 | paginationValidator, | 86 | paginationValidator, |
89 | setDefaultPagination, | 87 | setDefaultPagination, |
90 | optionalAuthenticate, | 88 | optionalAuthenticate, |
91 | commonVideosFiltersValidator, | ||
92 | asyncMiddleware(getVideoPlaylistVideos) | 89 | asyncMiddleware(getVideoPlaylistVideos) |
93 | ) | 90 | ) |
94 | 91 | ||
@@ -104,13 +101,13 @@ videoPlaylistRouter.post('/:playlistId/videos/reorder', | |||
104 | asyncRetryTransactionMiddleware(reorderVideosPlaylist) | 101 | asyncRetryTransactionMiddleware(reorderVideosPlaylist) |
105 | ) | 102 | ) |
106 | 103 | ||
107 | videoPlaylistRouter.put('/:playlistId/videos/:videoId', | 104 | videoPlaylistRouter.put('/:playlistId/videos/:playlistElementId', |
108 | authenticate, | 105 | authenticate, |
109 | asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator), | 106 | asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator), |
110 | asyncRetryTransactionMiddleware(updateVideoPlaylistElement) | 107 | asyncRetryTransactionMiddleware(updateVideoPlaylistElement) |
111 | ) | 108 | ) |
112 | 109 | ||
113 | videoPlaylistRouter.delete('/:playlistId/videos/:videoId', | 110 | videoPlaylistRouter.delete('/:playlistId/videos/:playlistElementId', |
114 | authenticate, | 111 | authenticate, |
115 | asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator), | 112 | asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator), |
116 | asyncRetryTransactionMiddleware(removeVideoFromPlaylist) | 113 | asyncRetryTransactionMiddleware(removeVideoFromPlaylist) |
@@ -426,26 +423,20 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons | |||
426 | 423 | ||
427 | async function getVideoPlaylistVideos (req: express.Request, res: express.Response) { | 424 | async function getVideoPlaylistVideos (req: express.Request, res: express.Response) { |
428 | const videoPlaylistInstance = res.locals.videoPlaylist | 425 | const videoPlaylistInstance = res.locals.videoPlaylist |
429 | const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined | 426 | const user = res.locals.oauth ? res.locals.oauth.token.User : undefined |
427 | const server = await getServerActor() | ||
430 | 428 | ||
431 | const resultList = await VideoModel.listForApi({ | 429 | const resultList = await VideoPlaylistElementModel.listForApi({ |
432 | followerActorId, | ||
433 | start: req.query.start, | 430 | start: req.query.start, |
434 | count: req.query.count, | 431 | count: req.query.count, |
435 | sort: 'VideoPlaylistElements.position', | ||
436 | includeLocalVideos: true, | ||
437 | categoryOneOf: req.query.categoryOneOf, | ||
438 | licenceOneOf: req.query.licenceOneOf, | ||
439 | languageOneOf: req.query.languageOneOf, | ||
440 | tagsOneOf: req.query.tagsOneOf, | ||
441 | tagsAllOf: req.query.tagsAllOf, | ||
442 | filter: req.query.filter, | ||
443 | nsfw: buildNSFWFilter(res, req.query.nsfw), | ||
444 | withFiles: false, | ||
445 | videoPlaylistId: videoPlaylistInstance.id, | 432 | videoPlaylistId: videoPlaylistInstance.id, |
446 | user: res.locals.oauth ? res.locals.oauth.token.User : undefined | 433 | serverAccount: server.Account, |
434 | user | ||
447 | }) | 435 | }) |
448 | 436 | ||
449 | const additionalAttributes = { playlistInfo: true } | 437 | const options = { |
450 | return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes })) | 438 | displayNSFW: buildNSFWFilter(res, req.query.nsfw), |
439 | accountId: user ? user.Account.id : undefined | ||
440 | } | ||
441 | return res.json(getFormattedObjects(resultList.data, resultList.total, options)) | ||
451 | } | 442 | } |
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 5fe7d416c..8ab7c6bbd 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config' | |||
14 | 14 | ||
15 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
16 | 16 | ||
17 | const LAST_MIGRATION_VERSION = 405 | 17 | const LAST_MIGRATION_VERSION = 410 |
18 | 18 | ||
19 | // --------------------------------------------------------------------------- | 19 | // --------------------------------------------------------------------------- |
20 | 20 | ||
diff --git a/server/initializers/migrations/0410-video-playlist-element.ts b/server/initializers/migrations/0410-video-playlist-element.ts new file mode 100644 index 000000000..f536632a2 --- /dev/null +++ b/server/initializers/migrations/0410-video-playlist-element.ts | |||
@@ -0,0 +1,39 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize, | ||
7 | db: any | ||
8 | }): Promise<void> { | ||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.INTEGER, | ||
12 | allowNull: true, | ||
13 | defaultValue: null | ||
14 | } | ||
15 | |||
16 | await utils.queryInterface.changeColumn('videoPlaylistElement', 'videoId', data) | ||
17 | } | ||
18 | |||
19 | await utils.queryInterface.removeConstraint('videoPlaylistElement', 'videoPlaylistElement_videoId_fkey') | ||
20 | |||
21 | await utils.queryInterface.addConstraint('videoPlaylistElement', [ 'videoId' ], { | ||
22 | type: 'foreign key', | ||
23 | references: { | ||
24 | table: 'video', | ||
25 | field: 'id' | ||
26 | }, | ||
27 | onDelete: 'set null', | ||
28 | onUpdate: 'CASCADE' | ||
29 | }) | ||
30 | } | ||
31 | |||
32 | function down (options) { | ||
33 | throw new Error('Not implemented.') | ||
34 | } | ||
35 | |||
36 | export { | ||
37 | up, | ||
38 | down | ||
39 | } | ||
diff --git a/server/middlewares/validators/videos/video-playlists.ts b/server/middlewares/validators/videos/video-playlists.ts index 2e9c8aa33..5823795be 100644 --- a/server/middlewares/validators/videos/video-playlists.ts +++ b/server/middlewares/validators/videos/video-playlists.ts | |||
@@ -207,8 +207,8 @@ const videoPlaylistsAddVideoValidator = [ | |||
207 | const videoPlaylistsUpdateOrRemoveVideoValidator = [ | 207 | const videoPlaylistsUpdateOrRemoveVideoValidator = [ |
208 | param('playlistId') | 208 | param('playlistId') |
209 | .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), | 209 | .custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'), |
210 | param('videoId') | 210 | param('playlistElementId') |
211 | .custom(isIdOrUUIDValid).withMessage('Should have an video id/uuid'), | 211 | .custom(isIdValid).withMessage('Should have an element id/uuid'), |
212 | body('startTimestamp') | 212 | body('startTimestamp') |
213 | .optional() | 213 | .optional() |
214 | .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'), | 214 | .custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'), |
@@ -222,12 +222,10 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [ | |||
222 | if (areValidationErrors(req, res)) return | 222 | if (areValidationErrors(req, res)) return |
223 | 223 | ||
224 | if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return | 224 | if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return |
225 | if (!await doesVideoExist(req.params.videoId, res, 'id')) return | ||
226 | 225 | ||
227 | const videoPlaylist = res.locals.videoPlaylist | 226 | const videoPlaylist = res.locals.videoPlaylist |
228 | const video = res.locals.video | ||
229 | 227 | ||
230 | const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndVideo(videoPlaylist.id, video.id) | 228 | const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId) |
231 | if (!videoPlaylistElement) { | 229 | if (!videoPlaylistElement) { |
232 | res.status(404) | 230 | res.status(404) |
233 | .json({ error: 'Video playlist element not found' }) | 231 | .json({ error: 'Video playlist element not found' }) |
diff --git a/server/models/account/account-video-rate.ts b/server/models/account/account-video-rate.ts index 59f586b54..85af9e378 100644 --- a/server/models/account/account-video-rate.ts +++ b/server/models/account/account-video-rate.ts | |||
@@ -9,7 +9,7 @@ import { ActorModel } from '../activitypub/actor' | |||
9 | import { getSort, throwIfNotValid } from '../utils' | 9 | import { getSort, throwIfNotValid } from '../utils' |
10 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 10 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
11 | import { AccountVideoRate } from '../../../shared' | 11 | import { AccountVideoRate } from '../../../shared' |
12 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from '../video/video-channel' | 12 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel' |
13 | 13 | ||
14 | /* | 14 | /* |
15 | Account rates per video. | 15 | Account rates per video. |
@@ -109,7 +109,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> { | |||
109 | required: true, | 109 | required: true, |
110 | include: [ | 110 | include: [ |
111 | { | 111 | { |
112 | model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, true] }), | 112 | model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), |
113 | required: true | 113 | required: true |
114 | } | 114 | } |
115 | ] | 115 | ] |
diff --git a/server/models/account/account.ts b/server/models/account/account.ts index 09cada096..28014946f 100644 --- a/server/models/account/account.ts +++ b/server/models/account/account.ts | |||
@@ -27,12 +27,19 @@ import { UserModel } from './user' | |||
27 | import { AvatarModel } from '../avatar/avatar' | 27 | import { AvatarModel } from '../avatar/avatar' |
28 | import { VideoPlaylistModel } from '../video/video-playlist' | 28 | import { VideoPlaylistModel } from '../video/video-playlist' |
29 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' | 29 | import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' |
30 | import { Op, Transaction, WhereOptions } from 'sequelize' | 30 | import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequelize' |
31 | import { AccountBlocklistModel } from './account-blocklist' | ||
32 | import { ServerBlocklistModel } from '../server/server-blocklist' | ||
31 | 33 | ||
32 | export enum ScopeNames { | 34 | export enum ScopeNames { |
33 | SUMMARY = 'SUMMARY' | 35 | SUMMARY = 'SUMMARY' |
34 | } | 36 | } |
35 | 37 | ||
38 | export type SummaryOptions = { | ||
39 | whereActor?: WhereOptions | ||
40 | withAccountBlockerIds?: number[] | ||
41 | } | ||
42 | |||
36 | @DefaultScope(() => ({ | 43 | @DefaultScope(() => ({ |
37 | include: [ | 44 | include: [ |
38 | { | 45 | { |
@@ -42,8 +49,16 @@ export enum ScopeNames { | |||
42 | ] | 49 | ] |
43 | })) | 50 | })) |
44 | @Scopes(() => ({ | 51 | @Scopes(() => ({ |
45 | [ ScopeNames.SUMMARY ]: (whereActor?: WhereOptions) => { | 52 | [ ScopeNames.SUMMARY ]: (options: SummaryOptions = {}) => { |
46 | return { | 53 | const whereActor = options.whereActor || undefined |
54 | |||
55 | const serverInclude: IncludeOptions = { | ||
56 | attributes: [ 'host' ], | ||
57 | model: ServerModel.unscoped(), | ||
58 | required: false | ||
59 | } | ||
60 | |||
61 | const query: FindOptions = { | ||
47 | attributes: [ 'id', 'name' ], | 62 | attributes: [ 'id', 'name' ], |
48 | include: [ | 63 | include: [ |
49 | { | 64 | { |
@@ -52,11 +67,8 @@ export enum ScopeNames { | |||
52 | required: true, | 67 | required: true, |
53 | where: whereActor, | 68 | where: whereActor, |
54 | include: [ | 69 | include: [ |
55 | { | 70 | serverInclude, |
56 | attributes: [ 'host' ], | 71 | |
57 | model: ServerModel.unscoped(), | ||
58 | required: false | ||
59 | }, | ||
60 | { | 72 | { |
61 | model: AvatarModel.unscoped(), | 73 | model: AvatarModel.unscoped(), |
62 | required: false | 74 | required: false |
@@ -65,6 +77,35 @@ export enum ScopeNames { | |||
65 | } | 77 | } |
66 | ] | 78 | ] |
67 | } | 79 | } |
80 | |||
81 | if (options.withAccountBlockerIds) { | ||
82 | query.include.push({ | ||
83 | attributes: [ 'id' ], | ||
84 | model: AccountBlocklistModel.unscoped(), | ||
85 | as: 'BlockedAccounts', | ||
86 | required: false, | ||
87 | where: { | ||
88 | accountId: { | ||
89 | [Op.in]: options.withAccountBlockerIds | ||
90 | } | ||
91 | } | ||
92 | }) | ||
93 | |||
94 | serverInclude.include = [ | ||
95 | { | ||
96 | attributes: [ 'id' ], | ||
97 | model: ServerBlocklistModel.unscoped(), | ||
98 | required: false, | ||
99 | where: { | ||
100 | accountId: { | ||
101 | [Op.in]: options.withAccountBlockerIds | ||
102 | } | ||
103 | } | ||
104 | } | ||
105 | ] | ||
106 | } | ||
107 | |||
108 | return query | ||
68 | } | 109 | } |
69 | })) | 110 | })) |
70 | @Table({ | 111 | @Table({ |
@@ -163,6 +204,16 @@ export class AccountModel extends Model<AccountModel> { | |||
163 | }) | 204 | }) |
164 | VideoComments: VideoCommentModel[] | 205 | VideoComments: VideoCommentModel[] |
165 | 206 | ||
207 | @HasMany(() => AccountBlocklistModel, { | ||
208 | foreignKey: { | ||
209 | name: 'targetAccountId', | ||
210 | allowNull: false | ||
211 | }, | ||
212 | as: 'BlockedAccounts', | ||
213 | onDelete: 'CASCADE' | ||
214 | }) | ||
215 | BlockedAccounts: AccountBlocklistModel[] | ||
216 | |||
166 | @BeforeDestroy | 217 | @BeforeDestroy |
167 | static async sendDeleteIfOwned (instance: AccountModel, options) { | 218 | static async sendDeleteIfOwned (instance: AccountModel, options) { |
168 | if (!instance.Actor) { | 219 | if (!instance.Actor) { |
@@ -343,4 +394,8 @@ export class AccountModel extends Model<AccountModel> { | |||
343 | getDisplayName () { | 394 | getDisplayName () { |
344 | return this.name | 395 | return this.name |
345 | } | 396 | } |
397 | |||
398 | isBlocked () { | ||
399 | return this.BlockedAccounts && this.BlockedAccounts.length !== 0 | ||
400 | } | ||
346 | } | 401 | } |
diff --git a/server/models/server/server-blocklist.ts b/server/models/server/server-blocklist.ts index 92c01f642..5138b0f76 100644 --- a/server/models/server/server-blocklist.ts +++ b/server/models/server/server-blocklist.ts | |||
@@ -67,7 +67,6 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> { | |||
67 | 67 | ||
68 | @BelongsTo(() => ServerModel, { | 68 | @BelongsTo(() => ServerModel, { |
69 | foreignKey: { | 69 | foreignKey: { |
70 | name: 'targetServerId', | ||
71 | allowNull: false | 70 | allowNull: false |
72 | }, | 71 | }, |
73 | onDelete: 'CASCADE' | 72 | onDelete: 'CASCADE' |
diff --git a/server/models/server/server.ts b/server/models/server/server.ts index 300d70938..1d211f1e0 100644 --- a/server/models/server/server.ts +++ b/server/models/server/server.ts | |||
@@ -2,6 +2,8 @@ import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, Updat | |||
2 | import { isHostValid } from '../../helpers/custom-validators/servers' | 2 | import { isHostValid } from '../../helpers/custom-validators/servers' |
3 | import { ActorModel } from '../activitypub/actor' | 3 | import { ActorModel } from '../activitypub/actor' |
4 | import { throwIfNotValid } from '../utils' | 4 | import { throwIfNotValid } from '../utils' |
5 | import { AccountBlocklistModel } from '../account/account-blocklist' | ||
6 | import { ServerBlocklistModel } from './server-blocklist' | ||
5 | 7 | ||
6 | @Table({ | 8 | @Table({ |
7 | tableName: 'server', | 9 | tableName: 'server', |
@@ -40,6 +42,14 @@ export class ServerModel extends Model<ServerModel> { | |||
40 | }) | 42 | }) |
41 | Actors: ActorModel[] | 43 | Actors: ActorModel[] |
42 | 44 | ||
45 | @HasMany(() => ServerBlocklistModel, { | ||
46 | foreignKey: { | ||
47 | allowNull: false | ||
48 | }, | ||
49 | onDelete: 'CASCADE' | ||
50 | }) | ||
51 | BlockedByAccounts: ServerBlocklistModel[] | ||
52 | |||
43 | static loadByHost (host: string) { | 53 | static loadByHost (host: string) { |
44 | const query = { | 54 | const query = { |
45 | where: { | 55 | where: { |
@@ -50,6 +60,10 @@ export class ServerModel extends Model<ServerModel> { | |||
50 | return ServerModel.findOne(query) | 60 | return ServerModel.findOne(query) |
51 | } | 61 | } |
52 | 62 | ||
63 | isBlocked () { | ||
64 | return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0 | ||
65 | } | ||
66 | |||
53 | toFormattedJSON () { | 67 | toFormattedJSON () { |
54 | return { | 68 | return { |
55 | host: this.host | 69 | host: this.host |
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts index baef1d6ce..22d949da0 100644 --- a/server/models/video/video-blacklist.ts +++ b/server/models/video/video-blacklist.ts | |||
@@ -1,7 +1,7 @@ | |||
1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' | 1 | import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' |
2 | import { getSortOnModel, SortType, throwIfNotValid } from '../utils' | 2 | import { getSortOnModel, SortType, throwIfNotValid } from '../utils' |
3 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' | 3 | import { ScopeNames as VideoModelScopeNames, VideoModel } from './video' |
4 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 4 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' |
5 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' | 5 | import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist' |
6 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' | 6 | import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos' |
7 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 7 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
@@ -71,7 +71,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> { | |||
71 | required: true, | 71 | required: true, |
72 | include: [ | 72 | include: [ |
73 | { | 73 | { |
74 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }), | 74 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }), |
75 | required: true | 75 | required: true |
76 | }, | 76 | }, |
77 | { | 77 | { |
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts index b0b261c88..6241a75a3 100644 --- a/server/models/video/video-channel.ts +++ b/server/models/video/video-channel.ts | |||
@@ -24,7 +24,7 @@ import { | |||
24 | isVideoChannelSupportValid | 24 | isVideoChannelSupportValid |
25 | } from '../../helpers/custom-validators/video-channels' | 25 | } from '../../helpers/custom-validators/video-channels' |
26 | import { sendDeleteActor } from '../../lib/activitypub/send' | 26 | import { sendDeleteActor } from '../../lib/activitypub/send' |
27 | import { AccountModel, ScopeNames as AccountModelScopeNames } from '../account/account' | 27 | import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' |
28 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' | 28 | import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' |
29 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' | 29 | import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' |
30 | import { VideoModel } from './video' | 30 | import { VideoModel } from './video' |
@@ -58,6 +58,11 @@ type AvailableForListOptions = { | |||
58 | actorId: number | 58 | actorId: number |
59 | } | 59 | } |
60 | 60 | ||
61 | export type SummaryOptions = { | ||
62 | withAccount?: boolean // Default: false | ||
63 | withAccountBlockerIds?: number[] | ||
64 | } | ||
65 | |||
61 | @DefaultScope(() => ({ | 66 | @DefaultScope(() => ({ |
62 | include: [ | 67 | include: [ |
63 | { | 68 | { |
@@ -67,7 +72,7 @@ type AvailableForListOptions = { | |||
67 | ] | 72 | ] |
68 | })) | 73 | })) |
69 | @Scopes(() => ({ | 74 | @Scopes(() => ({ |
70 | [ScopeNames.SUMMARY]: (withAccount = false) => { | 75 | [ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => { |
71 | const base: FindOptions = { | 76 | const base: FindOptions = { |
72 | attributes: [ 'name', 'description', 'id', 'actorId' ], | 77 | attributes: [ 'name', 'description', 'id', 'actorId' ], |
73 | include: [ | 78 | include: [ |
@@ -90,9 +95,11 @@ type AvailableForListOptions = { | |||
90 | ] | 95 | ] |
91 | } | 96 | } |
92 | 97 | ||
93 | if (withAccount === true) { | 98 | if (options.withAccount === true) { |
94 | base.include.push({ | 99 | base.include.push({ |
95 | model: AccountModel.scope(AccountModelScopeNames.SUMMARY), | 100 | model: AccountModel.scope({ |
101 | method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ] | ||
102 | }), | ||
96 | required: true | 103 | required: true |
97 | }) | 104 | }) |
98 | } | 105 | } |
diff --git a/server/models/video/video-format-utils.ts b/server/models/video/video-format-utils.ts index b947eb16f..284539def 100644 --- a/server/models/video/video-format-utils.ts +++ b/server/models/video/video-format-utils.ts | |||
@@ -26,7 +26,6 @@ export type VideoFormattingJSONOptions = { | |||
26 | waitTranscoding?: boolean, | 26 | waitTranscoding?: boolean, |
27 | scheduledUpdate?: boolean, | 27 | scheduledUpdate?: boolean, |
28 | blacklistInfo?: boolean | 28 | blacklistInfo?: boolean |
29 | playlistInfo?: boolean | ||
30 | } | 29 | } |
31 | } | 30 | } |
32 | function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video { | 31 | function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video { |
@@ -98,17 +97,6 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting | |||
98 | videoObject.blacklisted = !!video.VideoBlacklist | 97 | videoObject.blacklisted = !!video.VideoBlacklist |
99 | videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null | 98 | videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null |
100 | } | 99 | } |
101 | |||
102 | if (options.additionalAttributes.playlistInfo === true) { | ||
103 | // We filtered on a specific videoId/videoPlaylistId, that is unique | ||
104 | const playlistElement = video.VideoPlaylistElements[0] | ||
105 | |||
106 | videoObject.playlistElement = { | ||
107 | position: playlistElement.position, | ||
108 | startTimestamp: playlistElement.startTimestamp, | ||
109 | stopTimestamp: playlistElement.stopTimestamp | ||
110 | } | ||
111 | } | ||
112 | } | 100 | } |
113 | 101 | ||
114 | return videoObject | 102 | return videoObject |
diff --git a/server/models/video/video-playlist-element.ts b/server/models/video/video-playlist-element.ts index eeb3d6bbd..bed6f8eaf 100644 --- a/server/models/video/video-playlist-element.ts +++ b/server/models/video/video-playlist-element.ts | |||
@@ -13,14 +13,18 @@ import { | |||
13 | Table, | 13 | Table, |
14 | UpdatedAt | 14 | UpdatedAt |
15 | } from 'sequelize-typescript' | 15 | } from 'sequelize-typescript' |
16 | import { VideoModel } from './video' | 16 | import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video' |
17 | import { VideoPlaylistModel } from './video-playlist' | 17 | import { VideoPlaylistModel } from './video-playlist' |
18 | import { getSort, throwIfNotValid } from '../utils' | 18 | import { getSort, throwIfNotValid } from '../utils' |
19 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' | 19 | import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc' |
20 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' | 20 | import { CONSTRAINTS_FIELDS } from '../../initializers/constants' |
21 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' | 21 | import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object' |
22 | import * as validator from 'validator' | 22 | import * as validator from 'validator' |
23 | import { AggregateOptions, Op, Sequelize, Transaction } from 'sequelize' | 23 | import { AggregateOptions, Op, ScopeOptions, Sequelize, Transaction } from 'sequelize' |
24 | import { UserModel } from '../account/user' | ||
25 | import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model' | ||
26 | import { AccountModel } from '../account/account' | ||
27 | import { VideoPrivacy } from '../../../shared/models/videos' | ||
24 | 28 | ||
25 | @Table({ | 29 | @Table({ |
26 | tableName: 'videoPlaylistElement', | 30 | tableName: 'videoPlaylistElement', |
@@ -90,9 +94,9 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel> | |||
90 | 94 | ||
91 | @BelongsTo(() => VideoModel, { | 95 | @BelongsTo(() => VideoModel, { |
92 | foreignKey: { | 96 | foreignKey: { |
93 | allowNull: false | 97 | allowNull: true |
94 | }, | 98 | }, |
95 | onDelete: 'CASCADE' | 99 | onDelete: 'set null' |
96 | }) | 100 | }) |
97 | Video: VideoModel | 101 | Video: VideoModel |
98 | 102 | ||
@@ -107,6 +111,57 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel> | |||
107 | return VideoPlaylistElementModel.destroy(query) | 111 | return VideoPlaylistElementModel.destroy(query) |
108 | } | 112 | } |
109 | 113 | ||
114 | static listForApi (options: { | ||
115 | start: number, | ||
116 | count: number, | ||
117 | videoPlaylistId: number, | ||
118 | serverAccount: AccountModel, | ||
119 | user?: UserModel | ||
120 | }) { | ||
121 | const accountIds = [ options.serverAccount.id ] | ||
122 | const videoScope: (ScopeOptions | string)[] = [ | ||
123 | VideoScopeNames.WITH_BLACKLISTED | ||
124 | ] | ||
125 | |||
126 | if (options.user) { | ||
127 | accountIds.push(options.user.Account.id) | ||
128 | videoScope.push({ method: [ VideoScopeNames.WITH_USER_HISTORY, options.user.id ] }) | ||
129 | } | ||
130 | |||
131 | const forApiOptions: ForAPIOptions = { withAccountBlockerIds: accountIds } | ||
132 | videoScope.push({ | ||
133 | method: [ | ||
134 | VideoScopeNames.FOR_API, forApiOptions | ||
135 | ] | ||
136 | }) | ||
137 | |||
138 | const findQuery = { | ||
139 | offset: options.start, | ||
140 | limit: options.count, | ||
141 | order: getSort('position'), | ||
142 | where: { | ||
143 | videoPlaylistId: options.videoPlaylistId | ||
144 | }, | ||
145 | include: [ | ||
146 | { | ||
147 | model: VideoModel.scope(videoScope), | ||
148 | required: false | ||
149 | } | ||
150 | ] | ||
151 | } | ||
152 | |||
153 | const countQuery = { | ||
154 | where: { | ||
155 | videoPlaylistId: options.videoPlaylistId | ||
156 | } | ||
157 | } | ||
158 | |||
159 | return Promise.all([ | ||
160 | VideoPlaylistElementModel.count(countQuery), | ||
161 | VideoPlaylistElementModel.findAll(findQuery) | ||
162 | ]).then(([ total, data ]) => ({ total, data })) | ||
163 | } | ||
164 | |||
110 | static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) { | 165 | static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) { |
111 | const query = { | 166 | const query = { |
112 | where: { | 167 | where: { |
@@ -118,6 +173,10 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel> | |||
118 | return VideoPlaylistElementModel.findOne(query) | 173 | return VideoPlaylistElementModel.findOne(query) |
119 | } | 174 | } |
120 | 175 | ||
176 | static loadById (playlistElementId: number) { | ||
177 | return VideoPlaylistElementModel.findByPk(playlistElementId) | ||
178 | } | ||
179 | |||
121 | static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) { | 180 | static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) { |
122 | const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId } | 181 | const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId } |
123 | const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId } | 182 | const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId } |
@@ -213,6 +272,42 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel> | |||
213 | return VideoPlaylistElementModel.increment({ position: by }, query) | 272 | return VideoPlaylistElementModel.increment({ position: by }, query) |
214 | } | 273 | } |
215 | 274 | ||
275 | getType (displayNSFW?: boolean, accountId?: number) { | ||
276 | const video = this.Video | ||
277 | |||
278 | if (!video) return VideoPlaylistElementType.DELETED | ||
279 | |||
280 | // Owned video, don't filter it | ||
281 | if (accountId && video.VideoChannel.Account.id === accountId) return VideoPlaylistElementType.REGULAR | ||
282 | |||
283 | if (video.privacy === VideoPrivacy.PRIVATE) return VideoPlaylistElementType.PRIVATE | ||
284 | |||
285 | if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE | ||
286 | if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE | ||
287 | |||
288 | return VideoPlaylistElementType.REGULAR | ||
289 | } | ||
290 | |||
291 | getVideoElement (displayNSFW?: boolean, accountId?: number) { | ||
292 | if (!this.Video) return null | ||
293 | if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null | ||
294 | |||
295 | return this.Video.toFormattedJSON() | ||
296 | } | ||
297 | |||
298 | toFormattedJSON (options: { displayNSFW?: boolean, accountId?: number } = {}): VideoPlaylistElement { | ||
299 | return { | ||
300 | id: this.id, | ||
301 | position: this.position, | ||
302 | startTimestamp: this.startTimestamp, | ||
303 | stopTimestamp: this.stopTimestamp, | ||
304 | |||
305 | type: this.getType(options.displayNSFW, options.accountId), | ||
306 | |||
307 | video: this.getVideoElement(options.displayNSFW, options.accountId) | ||
308 | } | ||
309 | } | ||
310 | |||
216 | toActivityPubObject (): PlaylistElementObject { | 311 | toActivityPubObject (): PlaylistElementObject { |
217 | const base: PlaylistElementObject = { | 312 | const base: PlaylistElementObject = { |
218 | id: this.url, | 313 | id: this.url, |
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts index 63b4a0715..61ff78bd2 100644 --- a/server/models/video/video-playlist.ts +++ b/server/models/video/video-playlist.ts | |||
@@ -33,7 +33,7 @@ import { | |||
33 | WEBSERVER | 33 | WEBSERVER |
34 | } from '../../initializers/constants' | 34 | } from '../../initializers/constants' |
35 | import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' | 35 | import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model' |
36 | import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' | 36 | import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account' |
37 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 37 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' |
38 | import { join } from 'path' | 38 | import { join } from 'path' |
39 | import { VideoPlaylistElementModel } from './video-playlist-element' | 39 | import { VideoPlaylistElementModel } from './video-playlist-element' |
@@ -115,7 +115,7 @@ type AvailableForListOptions = { | |||
115 | [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => { | 115 | [ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => { |
116 | // Only list local playlists OR playlists that are on an instance followed by actorId | 116 | // Only list local playlists OR playlists that are on an instance followed by actorId |
117 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) | 117 | const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) |
118 | const actorWhere = { | 118 | const whereActor = { |
119 | [ Op.or ]: [ | 119 | [ Op.or ]: [ |
120 | { | 120 | { |
121 | serverId: null | 121 | serverId: null |
@@ -159,7 +159,7 @@ type AvailableForListOptions = { | |||
159 | } | 159 | } |
160 | 160 | ||
161 | const accountScope = { | 161 | const accountScope = { |
162 | method: [ AccountScopeNames.SUMMARY, actorWhere ] | 162 | method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ] |
163 | } | 163 | } |
164 | 164 | ||
165 | return { | 165 | return { |
@@ -341,7 +341,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> { | |||
341 | }, | 341 | }, |
342 | include: [ | 342 | include: [ |
343 | { | 343 | { |
344 | attributes: [ 'videoId', 'startTimestamp', 'stopTimestamp' ], | 344 | attributes: [ 'id', 'videoId', 'startTimestamp', 'stopTimestamp' ], |
345 | model: VideoPlaylistElementModel.unscoped(), | 345 | model: VideoPlaylistElementModel.unscoped(), |
346 | where: { | 346 | where: { |
347 | videoId: { | 347 | videoId: { |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index c7f2658ed..05d625fc1 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -91,7 +91,7 @@ import { | |||
91 | } from '../utils' | 91 | } from '../utils' |
92 | import { TagModel } from './tag' | 92 | import { TagModel } from './tag' |
93 | import { VideoAbuseModel } from './video-abuse' | 93 | import { VideoAbuseModel } from './video-abuse' |
94 | import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' | 94 | import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel' |
95 | import { VideoCommentModel } from './video-comment' | 95 | import { VideoCommentModel } from './video-comment' |
96 | import { VideoFileModel } from './video-file' | 96 | import { VideoFileModel } from './video-file' |
97 | import { VideoShareModel } from './video-share' | 97 | import { VideoShareModel } from './video-share' |
@@ -190,26 +190,29 @@ export enum ScopeNames { | |||
190 | WITH_FILES = 'WITH_FILES', | 190 | WITH_FILES = 'WITH_FILES', |
191 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', | 191 | WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', |
192 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', | 192 | WITH_BLACKLISTED = 'WITH_BLACKLISTED', |
193 | WITH_BLOCKLIST = 'WITH_BLOCKLIST', | ||
193 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', | 194 | WITH_USER_HISTORY = 'WITH_USER_HISTORY', |
194 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', | 195 | WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', |
195 | WITH_USER_ID = 'WITH_USER_ID', | 196 | WITH_USER_ID = 'WITH_USER_ID', |
196 | WITH_THUMBNAILS = 'WITH_THUMBNAILS' | 197 | WITH_THUMBNAILS = 'WITH_THUMBNAILS' |
197 | } | 198 | } |
198 | 199 | ||
199 | type ForAPIOptions = { | 200 | export type ForAPIOptions = { |
200 | ids: number[] | 201 | ids?: number[] |
201 | 202 | ||
202 | videoPlaylistId?: number | 203 | videoPlaylistId?: number |
203 | 204 | ||
204 | withFiles?: boolean | 205 | withFiles?: boolean |
206 | |||
207 | withAccountBlockerIds?: number[] | ||
205 | } | 208 | } |
206 | 209 | ||
207 | type AvailableForListIDsOptions = { | 210 | export type AvailableForListIDsOptions = { |
208 | serverAccountId: number | 211 | serverAccountId: number |
209 | followerActorId: number | 212 | followerActorId: number |
210 | includeLocalVideos: boolean | 213 | includeLocalVideos: boolean |
211 | 214 | ||
212 | withoutId?: boolean | 215 | attributesType?: 'none' | 'id' | 'all' |
213 | 216 | ||
214 | filter?: VideoFilter | 217 | filter?: VideoFilter |
215 | categoryOneOf?: number[] | 218 | categoryOneOf?: number[] |
@@ -236,14 +239,16 @@ type AvailableForListIDsOptions = { | |||
236 | @Scopes(() => ({ | 239 | @Scopes(() => ({ |
237 | [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { | 240 | [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => { |
238 | const query: FindOptions = { | 241 | const query: FindOptions = { |
239 | where: { | ||
240 | id: { | ||
241 | [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken | ||
242 | } | ||
243 | }, | ||
244 | include: [ | 242 | include: [ |
245 | { | 243 | { |
246 | model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }), | 244 | model: VideoChannelModel.scope({ |
245 | method: [ | ||
246 | VideoChannelScopeNames.SUMMARY, { | ||
247 | withAccount: true, | ||
248 | withAccountBlockerIds: options.withAccountBlockerIds | ||
249 | } as SummaryOptions | ||
250 | ] | ||
251 | }), | ||
247 | required: true | 252 | required: true |
248 | }, | 253 | }, |
249 | { | 254 | { |
@@ -254,6 +259,14 @@ type AvailableForListIDsOptions = { | |||
254 | ] | 259 | ] |
255 | } | 260 | } |
256 | 261 | ||
262 | if (options.ids) { | ||
263 | query.where = { | ||
264 | id: { | ||
265 | [ Op.in ]: options.ids // FIXME: sequelize ANY seems broken | ||
266 | } | ||
267 | } | ||
268 | } | ||
269 | |||
257 | if (options.withFiles === true) { | 270 | if (options.withFiles === true) { |
258 | query.include.push({ | 271 | query.include.push({ |
259 | model: VideoFileModel.unscoped(), | 272 | model: VideoFileModel.unscoped(), |
@@ -278,10 +291,14 @@ type AvailableForListIDsOptions = { | |||
278 | 291 | ||
279 | const query: FindOptions = { | 292 | const query: FindOptions = { |
280 | raw: true, | 293 | raw: true, |
281 | attributes: options.withoutId === true ? [] : [ 'id' ], | ||
282 | include: [] | 294 | include: [] |
283 | } | 295 | } |
284 | 296 | ||
297 | const attributesType = options.attributesType || 'id' | ||
298 | |||
299 | if (attributesType === 'id') query.attributes = [ 'id' ] | ||
300 | else if (attributesType === 'none') query.attributes = [ ] | ||
301 | |||
285 | whereAnd.push({ | 302 | whereAnd.push({ |
286 | id: { | 303 | id: { |
287 | [ Op.notIn ]: Sequelize.literal( | 304 | [ Op.notIn ]: Sequelize.literal( |
@@ -290,17 +307,19 @@ type AvailableForListIDsOptions = { | |||
290 | } | 307 | } |
291 | }) | 308 | }) |
292 | 309 | ||
293 | whereAnd.push({ | 310 | if (options.serverAccountId) { |
294 | channelId: { | 311 | whereAnd.push({ |
295 | [ Op.notIn ]: Sequelize.literal( | 312 | channelId: { |
296 | '(' + | 313 | [ Op.notIn ]: Sequelize.literal( |
297 | 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + | 314 | '(' + |
298 | buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + | 315 | 'SELECT id FROM "videoChannel" WHERE "accountId" IN (' + |
299 | ')' + | 316 | buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) + |
300 | ')' | 317 | ')' + |
301 | ) | 318 | ')' |
302 | } | 319 | ) |
303 | }) | 320 | } |
321 | }) | ||
322 | } | ||
304 | 323 | ||
305 | // Only list public/published videos | 324 | // Only list public/published videos |
306 | if (!options.filter || options.filter !== 'all-local') { | 325 | if (!options.filter || options.filter !== 'all-local') { |
@@ -528,6 +547,9 @@ type AvailableForListIDsOptions = { | |||
528 | 547 | ||
529 | return query | 548 | return query |
530 | }, | 549 | }, |
550 | [ScopeNames.WITH_BLOCKLIST]: { | ||
551 | |||
552 | }, | ||
531 | [ ScopeNames.WITH_THUMBNAILS ]: { | 553 | [ ScopeNames.WITH_THUMBNAILS ]: { |
532 | include: [ | 554 | include: [ |
533 | { | 555 | { |
@@ -845,9 +867,9 @@ export class VideoModel extends Model<VideoModel> { | |||
845 | @HasMany(() => VideoPlaylistElementModel, { | 867 | @HasMany(() => VideoPlaylistElementModel, { |
846 | foreignKey: { | 868 | foreignKey: { |
847 | name: 'videoId', | 869 | name: 'videoId', |
848 | allowNull: false | 870 | allowNull: true |
849 | }, | 871 | }, |
850 | onDelete: 'cascade' | 872 | onDelete: 'set null' |
851 | }) | 873 | }) |
852 | VideoPlaylistElements: VideoPlaylistElementModel[] | 874 | VideoPlaylistElements: VideoPlaylistElementModel[] |
853 | 875 | ||
@@ -1586,7 +1608,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1586 | serverAccountId: serverActor.Account.id, | 1608 | serverAccountId: serverActor.Account.id, |
1587 | followerActorId, | 1609 | followerActorId, |
1588 | includeLocalVideos: true, | 1610 | includeLocalVideos: true, |
1589 | withoutId: true // Don't break aggregation | 1611 | attributesType: 'none' // Don't break aggregation |
1590 | } | 1612 | } |
1591 | 1613 | ||
1592 | const query: FindOptions = { | 1614 | const query: FindOptions = { |
@@ -1719,6 +1741,11 @@ export class VideoModel extends Model<VideoModel> { | |||
1719 | return !!this.VideoBlacklist | 1741 | return !!this.VideoBlacklist |
1720 | } | 1742 | } |
1721 | 1743 | ||
1744 | isBlocked () { | ||
1745 | return (this.VideoChannel.Account.Actor.Server && this.VideoChannel.Account.Actor.Server.isBlocked()) || | ||
1746 | this.VideoChannel.Account.isBlocked() | ||
1747 | } | ||
1748 | |||
1722 | getOriginalFile () { | 1749 | getOriginalFile () { |
1723 | if (Array.isArray(this.VideoFiles) === false) return undefined | 1750 | if (Array.isArray(this.VideoFiles) === false) return undefined |
1724 | 1751 | ||
diff --git a/server/tests/api/check-params/video-playlists.ts b/server/tests/api/check-params/video-playlists.ts index 8c5e44bdd..ae5aa287f 100644 --- a/server/tests/api/check-params/video-playlists.ts +++ b/server/tests/api/check-params/video-playlists.ts | |||
@@ -37,6 +37,7 @@ describe('Test video playlists API validator', function () { | |||
37 | let watchLaterPlaylistId: number | 37 | let watchLaterPlaylistId: number |
38 | let videoId: number | 38 | let videoId: number |
39 | let videoId2: number | 39 | let videoId2: number |
40 | let playlistElementId: number | ||
40 | 41 | ||
41 | // --------------------------------------------------------------- | 42 | // --------------------------------------------------------------- |
42 | 43 | ||
@@ -132,18 +133,18 @@ describe('Test video playlists API validator', function () { | |||
132 | }) | 133 | }) |
133 | 134 | ||
134 | describe('When listing videos of a playlist', function () { | 135 | describe('When listing videos of a playlist', function () { |
135 | const path = '/api/v1/video-playlists' | 136 | const path = '/api/v1/video-playlists/' |
136 | 137 | ||
137 | it('Should fail with a bad start pagination', async function () { | 138 | it('Should fail with a bad start pagination', async function () { |
138 | await checkBadStartPagination(server.url, path, server.accessToken) | 139 | await checkBadStartPagination(server.url, path + playlistUUID + '/videos', server.accessToken) |
139 | }) | 140 | }) |
140 | 141 | ||
141 | it('Should fail with a bad count pagination', async function () { | 142 | it('Should fail with a bad count pagination', async function () { |
142 | await checkBadCountPagination(server.url, path, server.accessToken) | 143 | await checkBadCountPagination(server.url, path + playlistUUID + '/videos', server.accessToken) |
143 | }) | 144 | }) |
144 | 145 | ||
145 | it('Should fail with a bad filter', async function () { | 146 | it('Should success with the correct parameters', async function () { |
146 | await checkBadSortPagination(server.url, path, server.accessToken) | 147 | await makeGetRequest({ url: server.url, path: path + playlistUUID + '/videos', statusCodeExpected: 200 }) |
147 | }) | 148 | }) |
148 | }) | 149 | }) |
149 | 150 | ||
@@ -296,7 +297,7 @@ describe('Test video playlists API validator', function () { | |||
296 | token: server.accessToken, | 297 | token: server.accessToken, |
297 | playlistId: playlistUUID, | 298 | playlistId: playlistUUID, |
298 | elementAttrs: Object.assign({ | 299 | elementAttrs: Object.assign({ |
299 | videoId: videoId, | 300 | videoId, |
300 | startTimestamp: 2, | 301 | startTimestamp: 2, |
301 | stopTimestamp: 3 | 302 | stopTimestamp: 3 |
302 | }, elementAttrs) | 303 | }, elementAttrs) |
@@ -344,7 +345,8 @@ describe('Test video playlists API validator', function () { | |||
344 | 345 | ||
345 | it('Succeed with the correct params', async function () { | 346 | it('Succeed with the correct params', async function () { |
346 | const params = getBase({}, { expectedStatus: 200 }) | 347 | const params = getBase({}, { expectedStatus: 200 }) |
347 | await addVideoInPlaylist(params) | 348 | const res = await addVideoInPlaylist(params) |
349 | playlistElementId = res.body.videoPlaylistElement.id | ||
348 | }) | 350 | }) |
349 | 351 | ||
350 | it('Should fail if the video was already added in the playlist', async function () { | 352 | 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 () { | |||
362 | startTimestamp: 1, | 364 | startTimestamp: 1, |
363 | stopTimestamp: 2 | 365 | stopTimestamp: 2 |
364 | }, elementAttrs), | 366 | }, elementAttrs), |
365 | videoId: videoId, | 367 | playlistElementId, |
366 | playlistId: playlistUUID, | 368 | playlistId: playlistUUID, |
367 | expectedStatus: 400 | 369 | expectedStatus: 400 |
368 | }, wrapper) | 370 | }, wrapper) |
@@ -390,14 +392,14 @@ describe('Test video playlists API validator', function () { | |||
390 | } | 392 | } |
391 | }) | 393 | }) |
392 | 394 | ||
393 | it('Should fail with an unknown or incorrect video id', async function () { | 395 | it('Should fail with an unknown or incorrect playlistElement id', async function () { |
394 | { | 396 | { |
395 | const params = getBase({}, { videoId: 'toto' }) | 397 | const params = getBase({}, { playlistElementId: 'toto' }) |
396 | await updateVideoPlaylistElement(params) | 398 | await updateVideoPlaylistElement(params) |
397 | } | 399 | } |
398 | 400 | ||
399 | { | 401 | { |
400 | const params = getBase({}, { videoId: 42, expectedStatus: 404 }) | 402 | const params = getBase({}, { playlistElementId: 42, expectedStatus: 404 }) |
401 | await updateVideoPlaylistElement(params) | 403 | await updateVideoPlaylistElement(params) |
402 | } | 404 | } |
403 | }) | 405 | }) |
@@ -415,7 +417,7 @@ describe('Test video playlists API validator', function () { | |||
415 | }) | 417 | }) |
416 | 418 | ||
417 | it('Should fail with an unknown element', async function () { | 419 | it('Should fail with an unknown element', async function () { |
418 | const params = getBase({}, { videoId: videoId2, expectedStatus: 404 }) | 420 | const params = getBase({}, { playlistElementId: 888, expectedStatus: 404 }) |
419 | await updateVideoPlaylistElement(params) | 421 | await updateVideoPlaylistElement(params) |
420 | }) | 422 | }) |
421 | 423 | ||
@@ -587,7 +589,7 @@ describe('Test video playlists API validator', function () { | |||
587 | return Object.assign({ | 589 | return Object.assign({ |
588 | url: server.url, | 590 | url: server.url, |
589 | token: server.accessToken, | 591 | token: server.accessToken, |
590 | videoId: videoId, | 592 | playlistElementId, |
591 | playlistId: playlistUUID, | 593 | playlistId: playlistUUID, |
592 | expectedStatus: 400 | 594 | expectedStatus: 400 |
593 | }, wrapper) | 595 | }, wrapper) |
@@ -617,18 +619,18 @@ describe('Test video playlists API validator', function () { | |||
617 | 619 | ||
618 | it('Should fail with an unknown or incorrect video id', async function () { | 620 | it('Should fail with an unknown or incorrect video id', async function () { |
619 | { | 621 | { |
620 | const params = getBase({ videoId: 'toto' }) | 622 | const params = getBase({ playlistElementId: 'toto' }) |
621 | await removeVideoFromPlaylist(params) | 623 | await removeVideoFromPlaylist(params) |
622 | } | 624 | } |
623 | 625 | ||
624 | { | 626 | { |
625 | const params = getBase({ videoId: 42, expectedStatus: 404 }) | 627 | const params = getBase({ playlistElementId: 42, expectedStatus: 404 }) |
626 | await removeVideoFromPlaylist(params) | 628 | await removeVideoFromPlaylist(params) |
627 | } | 629 | } |
628 | }) | 630 | }) |
629 | 631 | ||
630 | it('Should fail with an unknown element', async function () { | 632 | it('Should fail with an unknown element', async function () { |
631 | const params = getBase({ videoId: videoId2, expectedStatus: 404 }) | 633 | const params = getBase({ playlistElementId: 888, expectedStatus: 404 }) |
632 | await removeVideoFromPlaylist(params) | 634 | await removeVideoFromPlaylist(params) |
633 | }) | 635 | }) |
634 | 636 | ||
diff --git a/server/tests/api/check-params/videos-filter.ts b/server/tests/api/check-params/videos-filter.ts index babef8223..5a5668665 100644 --- a/server/tests/api/check-params/videos-filter.ts +++ b/server/tests/api/check-params/videos-filter.ts | |||
@@ -15,13 +15,12 @@ import { | |||
15 | import { UserRole } from '../../../../shared/models/users' | 15 | import { UserRole } from '../../../../shared/models/users' |
16 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | 16 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' |
17 | 17 | ||
18 | async function testEndpoints (server: ServerInfo, token: string, filter: string, playlistUUID: string, statusCodeExpected: number) { | 18 | async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: number) { |
19 | const paths = [ | 19 | const paths = [ |
20 | '/api/v1/video-channels/root_channel/videos', | 20 | '/api/v1/video-channels/root_channel/videos', |
21 | '/api/v1/accounts/root/videos', | 21 | '/api/v1/accounts/root/videos', |
22 | '/api/v1/videos', | 22 | '/api/v1/videos', |
23 | '/api/v1/search/videos', | 23 | '/api/v1/search/videos' |
24 | '/api/v1/video-playlists/' + playlistUUID + '/videos' | ||
25 | ] | 24 | ] |
26 | 25 | ||
27 | for (const path of paths) { | 26 | for (const path of paths) { |
@@ -70,39 +69,28 @@ describe('Test videos filters', function () { | |||
70 | } | 69 | } |
71 | ) | 70 | ) |
72 | moderatorAccessToken = await userLogin(server, moderator) | 71 | moderatorAccessToken = await userLogin(server, moderator) |
73 | |||
74 | const res = await createVideoPlaylist({ | ||
75 | url: server.url, | ||
76 | token: server.accessToken, | ||
77 | playlistAttrs: { | ||
78 | displayName: 'super playlist', | ||
79 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
80 | videoChannelId: server.videoChannel.id | ||
81 | } | ||
82 | }) | ||
83 | playlistUUID = res.body.videoPlaylist.uuid | ||
84 | }) | 72 | }) |
85 | 73 | ||
86 | describe('When setting a video filter', function () { | 74 | describe('When setting a video filter', function () { |
87 | 75 | ||
88 | it('Should fail with a bad filter', async function () { | 76 | it('Should fail with a bad filter', async function () { |
89 | await testEndpoints(server, server.accessToken, 'bad-filter', playlistUUID, 400) | 77 | await testEndpoints(server, server.accessToken, 'bad-filter', 400) |
90 | }) | 78 | }) |
91 | 79 | ||
92 | it('Should succeed with a good filter', async function () { | 80 | it('Should succeed with a good filter', async function () { |
93 | await testEndpoints(server, server.accessToken,'local', playlistUUID, 200) | 81 | await testEndpoints(server, server.accessToken,'local', 200) |
94 | }) | 82 | }) |
95 | 83 | ||
96 | it('Should fail to list all-local with a simple user', async function () { | 84 | it('Should fail to list all-local with a simple user', async function () { |
97 | await testEndpoints(server, userAccessToken, 'all-local', playlistUUID, 401) | 85 | await testEndpoints(server, userAccessToken, 'all-local', 401) |
98 | }) | 86 | }) |
99 | 87 | ||
100 | it('Should succeed to list all-local with a moderator', async function () { | 88 | it('Should succeed to list all-local with a moderator', async function () { |
101 | await testEndpoints(server, moderatorAccessToken, 'all-local', playlistUUID, 200) | 89 | await testEndpoints(server, moderatorAccessToken, 'all-local', 200) |
102 | }) | 90 | }) |
103 | 91 | ||
104 | it('Should succeed to list all-local with an admin', async function () { | 92 | it('Should succeed to list all-local with an admin', async function () { |
105 | await testEndpoints(server, server.accessToken, 'all-local', playlistUUID, 200) | 93 | await testEndpoints(server, server.accessToken, 'all-local', 200) |
106 | }) | 94 | }) |
107 | 95 | ||
108 | // Because we cannot authenticate the user on the RSS endpoint | 96 | // Because we cannot authenticate the user on the RSS endpoint |
diff --git a/server/tests/api/check-params/videos.ts b/server/tests/api/check-params/videos.ts index 51e592a15..fa6d6f622 100644 --- a/server/tests/api/check-params/videos.ts +++ b/server/tests/api/check-params/videos.ts | |||
@@ -62,7 +62,7 @@ describe('Test videos API validator', function () { | |||
62 | } | 62 | } |
63 | }) | 63 | }) |
64 | 64 | ||
65 | describe('When listing a video', function () { | 65 | describe('When listing videos', function () { |
66 | it('Should fail with a bad start pagination', async function () { | 66 | it('Should fail with a bad start pagination', async function () { |
67 | await checkBadStartPagination(server.url, path) | 67 | await checkBadStartPagination(server.url, path) |
68 | }) | 68 | }) |
diff --git a/server/tests/api/videos/video-playlists.ts b/server/tests/api/videos/video-playlists.ts index f82c8cbce..7d5e3914b 100644 --- a/server/tests/api/videos/video-playlists.ts +++ b/server/tests/api/videos/video-playlists.ts | |||
@@ -5,6 +5,7 @@ import 'mocha' | |||
5 | import { | 5 | import { |
6 | addVideoChannel, | 6 | addVideoChannel, |
7 | addVideoInPlaylist, | 7 | addVideoInPlaylist, |
8 | addVideoToBlacklist, | ||
8 | checkPlaylistFilesWereRemoved, | 9 | checkPlaylistFilesWereRemoved, |
9 | cleanupTests, | 10 | cleanupTests, |
10 | createUser, | 11 | createUser, |
@@ -14,6 +15,8 @@ import { | |||
14 | doubleFollow, | 15 | doubleFollow, |
15 | doVideosExistInMyPlaylist, | 16 | doVideosExistInMyPlaylist, |
16 | flushAndRunMultipleServers, | 17 | flushAndRunMultipleServers, |
18 | generateUserAccessToken, | ||
19 | getAccessToken, | ||
17 | getAccountPlaylistsList, | 20 | getAccountPlaylistsList, |
18 | getAccountPlaylistsListWithToken, | 21 | getAccountPlaylistsListWithToken, |
19 | getMyUserInformation, | 22 | getMyUserInformation, |
@@ -24,6 +27,7 @@ import { | |||
24 | getVideoPlaylistsList, | 27 | getVideoPlaylistsList, |
25 | getVideoPlaylistWithToken, | 28 | getVideoPlaylistWithToken, |
26 | removeUser, | 29 | removeUser, |
30 | removeVideoFromBlacklist, | ||
27 | removeVideoFromPlaylist, | 31 | removeVideoFromPlaylist, |
28 | reorderVideosPlaylist, | 32 | reorderVideosPlaylist, |
29 | ServerInfo, | 33 | ServerInfo, |
@@ -31,23 +35,58 @@ import { | |||
31 | setDefaultVideoChannel, | 35 | setDefaultVideoChannel, |
32 | testImage, | 36 | testImage, |
33 | unfollow, | 37 | unfollow, |
38 | updateVideo, | ||
34 | updateVideoPlaylist, | 39 | updateVideoPlaylist, |
35 | updateVideoPlaylistElement, | 40 | updateVideoPlaylistElement, |
36 | uploadVideo, | 41 | uploadVideo, |
37 | uploadVideoAndGetId, | 42 | uploadVideoAndGetId, |
38 | userLogin, | 43 | userLogin, |
39 | waitJobs, | 44 | waitJobs |
40 | generateUserAccessToken | ||
41 | } from '../../../../shared/extra-utils' | 45 | } from '../../../../shared/extra-utils' |
42 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' | 46 | import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model' |
43 | import { VideoPlaylist } from '../../../../shared/models/videos/playlist/video-playlist.model' | 47 | import { VideoPlaylist } from '../../../../shared/models/videos/playlist/video-playlist.model' |
44 | import { Video } from '../../../../shared/models/videos' | 48 | import { VideoPrivacy } from '../../../../shared/models/videos' |
45 | import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model' | 49 | import { VideoPlaylistType } from '../../../../shared/models/videos/playlist/video-playlist-type.model' |
46 | import { VideoExistInPlaylist } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model' | 50 | import { VideoExistInPlaylist } from '../../../../shared/models/videos/playlist/video-exist-in-playlist.model' |
47 | import { User } from '../../../../shared/models/users' | 51 | import { User } from '../../../../shared/models/users' |
52 | import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../../shared/models/videos/playlist/video-playlist-element.model' | ||
53 | import { | ||
54 | addAccountToAccountBlocklist, | ||
55 | addAccountToServerBlocklist, | ||
56 | addServerToAccountBlocklist, | ||
57 | addServerToServerBlocklist, | ||
58 | removeAccountFromAccountBlocklist, | ||
59 | removeAccountFromServerBlocklist, | ||
60 | removeServerFromAccountBlocklist, | ||
61 | removeServerFromServerBlocklist | ||
62 | } from '../../../../shared/extra-utils/users/blocklist' | ||
48 | 63 | ||
49 | const expect = chai.expect | 64 | const expect = chai.expect |
50 | 65 | ||
66 | async function checkPlaylistElementType ( | ||
67 | servers: ServerInfo[], | ||
68 | playlistId: string, | ||
69 | type: VideoPlaylistElementType, | ||
70 | position: number, | ||
71 | name: string, | ||
72 | total: number | ||
73 | ) { | ||
74 | for (const server of servers) { | ||
75 | const res = await getPlaylistVideos(server.url, server.accessToken, playlistId, 0, 10) | ||
76 | expect(res.body.total).to.equal(total) | ||
77 | |||
78 | const videoElement: VideoPlaylistElement = res.body.data.find((e: VideoPlaylistElement) => e.position === position) | ||
79 | expect(videoElement.type).to.equal(type, 'On server ' + server.url) | ||
80 | |||
81 | if (type === VideoPlaylistElementType.REGULAR) { | ||
82 | expect(videoElement.video).to.not.be.null | ||
83 | expect(videoElement.video.name).to.equal(name) | ||
84 | } else { | ||
85 | expect(videoElement.video).to.be.null | ||
86 | } | ||
87 | } | ||
88 | } | ||
89 | |||
51 | describe('Test video playlists', function () { | 90 | describe('Test video playlists', function () { |
52 | let servers: ServerInfo[] = [] | 91 | let servers: ServerInfo[] = [] |
53 | 92 | ||
@@ -57,9 +96,16 @@ describe('Test video playlists', function () { | |||
57 | 96 | ||
58 | let playlistServer1Id: number | 97 | let playlistServer1Id: number |
59 | let playlistServer1UUID: string | 98 | let playlistServer1UUID: string |
99 | let playlistServer1UUID2: string | ||
100 | |||
101 | let playlistElementServer1Video4: number | ||
102 | let playlistElementServer1Video5: number | ||
103 | let playlistElementNSFW: number | ||
60 | 104 | ||
61 | let nsfwVideoServer1: number | 105 | let nsfwVideoServer1: number |
62 | 106 | ||
107 | let userAccessTokenServer1: string | ||
108 | |||
63 | before(async function () { | 109 | before(async function () { |
64 | this.timeout(120000) | 110 | this.timeout(120000) |
65 | 111 | ||
@@ -97,814 +143,1039 @@ describe('Test video playlists', function () { | |||
97 | 143 | ||
98 | nsfwVideoServer1 = (await uploadVideoAndGetId({ server: servers[ 0 ], videoName: 'NSFW video', nsfw: true })).id | 144 | nsfwVideoServer1 = (await uploadVideoAndGetId({ server: servers[ 0 ], videoName: 'NSFW video', nsfw: true })).id |
99 | 145 | ||
146 | { | ||
147 | await createUser({ | ||
148 | url: servers[ 0 ].url, | ||
149 | accessToken: servers[ 0 ].accessToken, | ||
150 | username: 'user1', | ||
151 | password: 'password' | ||
152 | }) | ||
153 | userAccessTokenServer1 = await getAccessToken(servers[0].url, 'user1', 'password') | ||
154 | } | ||
155 | |||
100 | await waitJobs(servers) | 156 | await waitJobs(servers) |
101 | }) | 157 | }) |
102 | 158 | ||
103 | it('Should list video playlist privacies', async function () { | 159 | describe('Get default playlists', function () { |
104 | const res = await getVideoPlaylistPrivacies(servers[0].url) | 160 | it('Should list video playlist privacies', async function () { |
161 | const res = await getVideoPlaylistPrivacies(servers[ 0 ].url) | ||
105 | 162 | ||
106 | const privacies = res.body | 163 | const privacies = res.body |
107 | expect(Object.keys(privacies)).to.have.length.at.least(3) | 164 | expect(Object.keys(privacies)).to.have.length.at.least(3) |
108 | 165 | ||
109 | expect(privacies[3]).to.equal('Private') | 166 | expect(privacies[ 3 ]).to.equal('Private') |
110 | }) | 167 | }) |
111 | 168 | ||
112 | it('Should list watch later playlist', async function () { | 169 | it('Should list watch later playlist', async function () { |
113 | const url = servers[ 0 ].url | 170 | const url = servers[ 0 ].url |
114 | const accessToken = servers[ 0 ].accessToken | 171 | const accessToken = servers[ 0 ].accessToken |
115 | 172 | ||
116 | { | 173 | { |
117 | const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.WATCH_LATER) | 174 | const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.WATCH_LATER) |
175 | |||
176 | expect(res.body.total).to.equal(1) | ||
177 | expect(res.body.data).to.have.lengthOf(1) | ||
178 | |||
179 | const playlist: VideoPlaylist = res.body.data[ 0 ] | ||
180 | expect(playlist.displayName).to.equal('Watch later') | ||
181 | expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER) | ||
182 | expect(playlist.type.label).to.equal('Watch later') | ||
183 | } | ||
184 | |||
185 | { | ||
186 | const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.REGULAR) | ||
187 | |||
188 | expect(res.body.total).to.equal(0) | ||
189 | expect(res.body.data).to.have.lengthOf(0) | ||
190 | } | ||
191 | |||
192 | { | ||
193 | const res = await getAccountPlaylistsList(url, 'root', 0, 5) | ||
194 | expect(res.body.total).to.equal(0) | ||
195 | expect(res.body.data).to.have.lengthOf(0) | ||
196 | } | ||
197 | }) | ||
198 | |||
199 | it('Should get private playlist for a classic user', async function () { | ||
200 | const token = await generateUserAccessToken(servers[ 0 ], 'toto') | ||
201 | |||
202 | const res = await getAccountPlaylistsListWithToken(servers[ 0 ].url, token, 'toto', 0, 5) | ||
118 | 203 | ||
119 | expect(res.body.total).to.equal(1) | 204 | expect(res.body.total).to.equal(1) |
120 | expect(res.body.data).to.have.lengthOf(1) | 205 | expect(res.body.data).to.have.lengthOf(1) |
121 | 206 | ||
122 | const playlist: VideoPlaylist = res.body.data[ 0 ] | 207 | const playlistId = res.body.data[ 0 ].id |
123 | expect(playlist.displayName).to.equal('Watch later') | 208 | await getPlaylistVideos(servers[ 0 ].url, token, playlistId, 0, 5) |
124 | expect(playlist.type.id).to.equal(VideoPlaylistType.WATCH_LATER) | 209 | }) |
125 | expect(playlist.type.label).to.equal('Watch later') | 210 | }) |
126 | } | ||
127 | 211 | ||
128 | { | 212 | describe('Create and federate playlists', function () { |
129 | const res = await getAccountPlaylistsListWithToken(url, accessToken, 'root', 0, 5, VideoPlaylistType.REGULAR) | ||
130 | 213 | ||
131 | expect(res.body.total).to.equal(0) | 214 | it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () { |
132 | expect(res.body.data).to.have.lengthOf(0) | 215 | this.timeout(30000) |
133 | } | ||
134 | 216 | ||
135 | { | 217 | await createVideoPlaylist({ |
136 | const res = await getAccountPlaylistsList(url, 'root', 0, 5) | 218 | url: servers[ 0 ].url, |
137 | expect(res.body.total).to.equal(0) | 219 | token: servers[ 0 ].accessToken, |
138 | expect(res.body.data).to.have.lengthOf(0) | 220 | playlistAttrs: { |
139 | } | 221 | displayName: 'my super playlist', |
140 | }) | 222 | privacy: VideoPlaylistPrivacy.PUBLIC, |
223 | description: 'my super description', | ||
224 | thumbnailfile: 'thumbnail.jpg', | ||
225 | videoChannelId: servers[ 0 ].videoChannel.id | ||
226 | } | ||
227 | }) | ||
228 | |||
229 | await waitJobs(servers) | ||
230 | |||
231 | for (const server of servers) { | ||
232 | const res = await getVideoPlaylistsList(server.url, 0, 5) | ||
233 | expect(res.body.total).to.equal(1) | ||
234 | expect(res.body.data).to.have.lengthOf(1) | ||
235 | |||
236 | const playlistFromList = res.body.data[ 0 ] as VideoPlaylist | ||
237 | |||
238 | const res2 = await getVideoPlaylist(server.url, playlistFromList.uuid) | ||
239 | const playlistFromGet = res2.body | ||
240 | |||
241 | for (const playlist of [ playlistFromGet, playlistFromList ]) { | ||
242 | expect(playlist.id).to.be.a('number') | ||
243 | expect(playlist.uuid).to.be.a('string') | ||
244 | |||
245 | expect(playlist.isLocal).to.equal(server.serverNumber === 1) | ||
246 | |||
247 | expect(playlist.displayName).to.equal('my super playlist') | ||
248 | expect(playlist.description).to.equal('my super description') | ||
249 | expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC) | ||
250 | expect(playlist.privacy.label).to.equal('Public') | ||
251 | expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR) | ||
252 | expect(playlist.type.label).to.equal('Regular') | ||
253 | |||
254 | expect(playlist.videosLength).to.equal(0) | ||
141 | 255 | ||
142 | it('Should get private playlist for a classic user', async function () { | 256 | expect(playlist.ownerAccount.name).to.equal('root') |
143 | const token = await generateUserAccessToken(servers[0], 'toto') | 257 | expect(playlist.ownerAccount.displayName).to.equal('root') |
258 | expect(playlist.videoChannel.name).to.equal('root_channel') | ||
259 | expect(playlist.videoChannel.displayName).to.equal('Main root channel') | ||
260 | } | ||
261 | } | ||
262 | }) | ||
144 | 263 | ||
145 | const res = await getAccountPlaylistsListWithToken(servers[0].url, token, 'toto', 0, 5) | 264 | it('Should create a playlist on server 2 and have the playlist on server 1 but not on server 3', async function () { |
265 | this.timeout(30000) | ||
266 | |||
267 | { | ||
268 | const res = await createVideoPlaylist({ | ||
269 | url: servers[ 1 ].url, | ||
270 | token: servers[ 1 ].accessToken, | ||
271 | playlistAttrs: { | ||
272 | displayName: 'playlist 2', | ||
273 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
274 | videoChannelId: servers[ 1 ].videoChannel.id | ||
275 | } | ||
276 | }) | ||
277 | playlistServer2Id1 = res.body.videoPlaylist.id | ||
278 | } | ||
146 | 279 | ||
147 | expect(res.body.total).to.equal(1) | 280 | { |
148 | expect(res.body.data).to.have.lengthOf(1) | 281 | const res = await createVideoPlaylist({ |
282 | url: servers[ 1 ].url, | ||
283 | token: servers[ 1 ].accessToken, | ||
284 | playlistAttrs: { | ||
285 | displayName: 'playlist 3', | ||
286 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
287 | thumbnailfile: 'thumbnail.jpg', | ||
288 | videoChannelId: servers[ 1 ].videoChannel.id | ||
289 | } | ||
290 | }) | ||
291 | |||
292 | playlistServer2Id2 = res.body.videoPlaylist.id | ||
293 | playlistServer2UUID2 = res.body.videoPlaylist.uuid | ||
294 | } | ||
295 | |||
296 | for (let id of [ playlistServer2Id1, playlistServer2Id2 ]) { | ||
297 | await addVideoInPlaylist({ | ||
298 | url: servers[ 1 ].url, | ||
299 | token: servers[ 1 ].accessToken, | ||
300 | playlistId: id, | ||
301 | elementAttrs: { videoId: servers[ 1 ].videos[ 0 ].id, startTimestamp: 1, stopTimestamp: 2 } | ||
302 | }) | ||
303 | await addVideoInPlaylist({ | ||
304 | url: servers[ 1 ].url, | ||
305 | token: servers[ 1 ].accessToken, | ||
306 | playlistId: id, | ||
307 | elementAttrs: { videoId: servers[ 1 ].videos[ 1 ].id } | ||
308 | }) | ||
309 | } | ||
310 | |||
311 | await waitJobs(servers) | ||
312 | |||
313 | for (const server of [ servers[ 0 ], servers[ 1 ] ]) { | ||
314 | const res = await getVideoPlaylistsList(server.url, 0, 5) | ||
315 | |||
316 | const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2') | ||
317 | expect(playlist2).to.not.be.undefined | ||
318 | await testImage(server.url, 'thumbnail-playlist', playlist2.thumbnailPath) | ||
319 | |||
320 | const playlist3 = res.body.data.find(p => p.displayName === 'playlist 3') | ||
321 | expect(playlist3).to.not.be.undefined | ||
322 | await testImage(server.url, 'thumbnail', playlist3.thumbnailPath) | ||
323 | } | ||
149 | 324 | ||
150 | const playlistId = res.body.data[0].id | 325 | const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) |
151 | await getPlaylistVideos(servers[0].url, token, playlistId, 0, 5) | 326 | expect(res.body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined |
327 | expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined | ||
328 | }) | ||
329 | |||
330 | it('Should have the playlist on server 3 after a new follow', async function () { | ||
331 | this.timeout(30000) | ||
332 | |||
333 | // Server 2 and server 3 follow each other | ||
334 | await doubleFollow(servers[ 1 ], servers[ 2 ]) | ||
335 | |||
336 | const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) | ||
337 | |||
338 | const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2') | ||
339 | expect(playlist2).to.not.be.undefined | ||
340 | await testImage(servers[ 2 ].url, 'thumbnail-playlist', playlist2.thumbnailPath) | ||
341 | |||
342 | expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined | ||
343 | }) | ||
152 | }) | 344 | }) |
153 | 345 | ||
154 | it('Should create a playlist on server 1 and have the playlist on server 2 and 3', async function () { | 346 | describe('List playlists', function () { |
155 | this.timeout(30000) | 347 | it('Should correctly list the playlists', async function () { |
348 | this.timeout(30000) | ||
349 | |||
350 | { | ||
351 | const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, 'createdAt') | ||
156 | 352 | ||
157 | await createVideoPlaylist({ | 353 | expect(res.body.total).to.equal(3) |
158 | url: servers[0].url, | 354 | |
159 | token: servers[0].accessToken, | 355 | const data: VideoPlaylist[] = res.body.data |
160 | playlistAttrs: { | 356 | expect(data).to.have.lengthOf(2) |
161 | displayName: 'my super playlist', | 357 | expect(data[ 0 ].displayName).to.equal('playlist 2') |
162 | privacy: VideoPlaylistPrivacy.PUBLIC, | 358 | expect(data[ 1 ].displayName).to.equal('playlist 3') |
163 | description: 'my super description', | 359 | } |
164 | thumbnailfile: 'thumbnail.jpg', | 360 | |
165 | videoChannelId: servers[0].videoChannel.id | 361 | { |
362 | const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, '-createdAt') | ||
363 | |||
364 | expect(res.body.total).to.equal(3) | ||
365 | |||
366 | const data: VideoPlaylist[] = res.body.data | ||
367 | expect(data).to.have.lengthOf(2) | ||
368 | expect(data[ 0 ].displayName).to.equal('playlist 2') | ||
369 | expect(data[ 1 ].displayName).to.equal('my super playlist') | ||
166 | } | 370 | } |
167 | }) | 371 | }) |
168 | 372 | ||
169 | await waitJobs(servers) | 373 | it('Should list video channel playlists', async function () { |
374 | this.timeout(30000) | ||
170 | 375 | ||
171 | for (const server of servers) { | 376 | { |
172 | const res = await getVideoPlaylistsList(server.url, 0, 5) | 377 | const res = await getVideoChannelPlaylistsList(servers[ 0 ].url, 'root_channel', 0, 2, '-createdAt') |
173 | expect(res.body.total).to.equal(1) | ||
174 | expect(res.body.data).to.have.lengthOf(1) | ||
175 | 378 | ||
176 | const playlistFromList = res.body.data[0] as VideoPlaylist | 379 | expect(res.body.total).to.equal(1) |
177 | 380 | ||
178 | const res2 = await getVideoPlaylist(server.url, playlistFromList.uuid) | 381 | const data: VideoPlaylist[] = res.body.data |
179 | const playlistFromGet = res2.body | 382 | expect(data).to.have.lengthOf(1) |
383 | expect(data[ 0 ].displayName).to.equal('my super playlist') | ||
384 | } | ||
385 | }) | ||
180 | 386 | ||
181 | for (const playlist of [ playlistFromGet, playlistFromList ]) { | 387 | it('Should list account playlists', async function () { |
182 | expect(playlist.id).to.be.a('number') | 388 | this.timeout(30000) |
183 | expect(playlist.uuid).to.be.a('string') | ||
184 | 389 | ||
185 | expect(playlist.isLocal).to.equal(server.serverNumber === 1) | 390 | { |
391 | const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, '-createdAt') | ||
186 | 392 | ||
187 | expect(playlist.displayName).to.equal('my super playlist') | 393 | expect(res.body.total).to.equal(2) |
188 | expect(playlist.description).to.equal('my super description') | 394 | |
189 | expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC) | 395 | const data: VideoPlaylist[] = res.body.data |
190 | expect(playlist.privacy.label).to.equal('Public') | 396 | expect(data).to.have.lengthOf(1) |
191 | expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR) | 397 | expect(data[ 0 ].displayName).to.equal('playlist 2') |
192 | expect(playlist.type.label).to.equal('Regular') | 398 | } |
193 | 399 | ||
194 | expect(playlist.videosLength).to.equal(0) | 400 | { |
401 | const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, 'createdAt') | ||
195 | 402 | ||
196 | expect(playlist.ownerAccount.name).to.equal('root') | 403 | expect(res.body.total).to.equal(2) |
197 | expect(playlist.ownerAccount.displayName).to.equal('root') | 404 | |
198 | expect(playlist.videoChannel.name).to.equal('root_channel') | 405 | const data: VideoPlaylist[] = res.body.data |
199 | expect(playlist.videoChannel.displayName).to.equal('Main root channel') | 406 | expect(data).to.have.lengthOf(1) |
407 | expect(data[ 0 ].displayName).to.equal('playlist 3') | ||
200 | } | 408 | } |
201 | } | 409 | }) |
202 | }) | ||
203 | 410 | ||
204 | it('Should create a playlist on server 2 and have the playlist on server 1 but not on server 3', async function () { | 411 | it('Should not list unlisted or private playlists', async function () { |
205 | this.timeout(30000) | 412 | this.timeout(30000) |
206 | 413 | ||
207 | { | 414 | await createVideoPlaylist({ |
208 | const res = await createVideoPlaylist({ | 415 | url: servers[ 1 ].url, |
209 | url: servers[1].url, | 416 | token: servers[ 1 ].accessToken, |
210 | token: servers[1].accessToken, | ||
211 | playlistAttrs: { | 417 | playlistAttrs: { |
212 | displayName: 'playlist 2', | 418 | displayName: 'playlist unlisted', |
213 | privacy: VideoPlaylistPrivacy.PUBLIC, | 419 | privacy: VideoPlaylistPrivacy.UNLISTED |
214 | videoChannelId: servers[1].videoChannel.id | ||
215 | } | 420 | } |
216 | }) | 421 | }) |
217 | playlistServer2Id1 = res.body.videoPlaylist.id | ||
218 | } | ||
219 | 422 | ||
220 | { | 423 | await createVideoPlaylist({ |
221 | const res = await createVideoPlaylist({ | ||
222 | url: servers[ 1 ].url, | 424 | url: servers[ 1 ].url, |
223 | token: servers[ 1 ].accessToken, | 425 | token: servers[ 1 ].accessToken, |
224 | playlistAttrs: { | 426 | playlistAttrs: { |
225 | displayName: 'playlist 3', | 427 | displayName: 'playlist private', |
226 | privacy: VideoPlaylistPrivacy.PUBLIC, | 428 | privacy: VideoPlaylistPrivacy.PRIVATE |
227 | thumbnailfile: 'thumbnail.jpg', | ||
228 | videoChannelId: servers[1].videoChannel.id | ||
229 | } | 429 | } |
230 | }) | 430 | }) |
231 | 431 | ||
232 | playlistServer2Id2 = res.body.videoPlaylist.id | 432 | await waitJobs(servers) |
233 | playlistServer2UUID2 = res.body.videoPlaylist.uuid | ||
234 | } | ||
235 | 433 | ||
236 | for (let id of [ playlistServer2Id1, playlistServer2Id2 ]) { | 434 | for (const server of servers) { |
237 | await addVideoInPlaylist({ | 435 | const results = [ |
238 | url: servers[ 1 ].url, | 436 | await getAccountPlaylistsList(server.url, 'root@localhost:' + servers[ 1 ].port, 0, 5, '-createdAt'), |
239 | token: servers[ 1 ].accessToken, | 437 | await getVideoPlaylistsList(server.url, 0, 2, '-createdAt') |
240 | playlistId: id, | 438 | ] |
241 | elementAttrs: { videoId: servers[ 1 ].videos[ 0 ].id, startTimestamp: 1, stopTimestamp: 2 } | 439 | |
242 | }) | 440 | expect(results[ 0 ].body.total).to.equal(2) |
243 | await addVideoInPlaylist({ | 441 | expect(results[ 1 ].body.total).to.equal(3) |
244 | url: servers[ 1 ].url, | 442 | |
245 | token: servers[ 1 ].accessToken, | 443 | for (const res of results) { |
246 | playlistId: id, | 444 | const data: VideoPlaylist[] = res.body.data |
247 | elementAttrs: { videoId: servers[ 1 ].videos[ 1 ].id } | 445 | expect(data).to.have.lengthOf(2) |
248 | }) | 446 | expect(data[ 0 ].displayName).to.equal('playlist 3') |
249 | } | 447 | expect(data[ 1 ].displayName).to.equal('playlist 2') |
448 | } | ||
449 | } | ||
450 | }) | ||
451 | }) | ||
250 | 452 | ||
251 | await waitJobs(servers) | 453 | describe('Update playlists', function () { |
252 | 454 | ||
253 | for (const server of [ servers[0], servers[1] ]) { | 455 | it('Should update a playlist', async function () { |
254 | const res = await getVideoPlaylistsList(server.url, 0, 5) | 456 | this.timeout(30000) |
255 | 457 | ||
256 | const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2') | 458 | await updateVideoPlaylist({ |
257 | expect(playlist2).to.not.be.undefined | 459 | url: servers[1].url, |
258 | await testImage(server.url, 'thumbnail-playlist', playlist2.thumbnailPath) | 460 | token: servers[1].accessToken, |
461 | playlistAttrs: { | ||
462 | displayName: 'playlist 3 updated', | ||
463 | description: 'description updated', | ||
464 | privacy: VideoPlaylistPrivacy.UNLISTED, | ||
465 | thumbnailfile: 'thumbnail.jpg', | ||
466 | videoChannelId: servers[1].videoChannel.id | ||
467 | }, | ||
468 | playlistId: playlistServer2Id2 | ||
469 | }) | ||
259 | 470 | ||
260 | const playlist3 = res.body.data.find(p => p.displayName === 'playlist 3') | 471 | await waitJobs(servers) |
261 | expect(playlist3).to.not.be.undefined | ||
262 | await testImage(server.url, 'thumbnail', playlist3.thumbnailPath) | ||
263 | } | ||
264 | 472 | ||
265 | const res = await getVideoPlaylistsList(servers[2].url, 0, 5) | 473 | for (const server of servers) { |
266 | expect(res.body.data.find(p => p.displayName === 'playlist 2')).to.be.undefined | 474 | const res = await getVideoPlaylist(server.url, playlistServer2UUID2) |
267 | expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.be.undefined | 475 | const playlist: VideoPlaylist = res.body |
268 | }) | ||
269 | 476 | ||
270 | it('Should have the playlist on server 3 after a new follow', async function () { | 477 | expect(playlist.displayName).to.equal('playlist 3 updated') |
271 | this.timeout(30000) | 478 | expect(playlist.description).to.equal('description updated') |
272 | 479 | ||
273 | // Server 2 and server 3 follow each other | 480 | expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.UNLISTED) |
274 | await doubleFollow(servers[1], servers[2]) | 481 | expect(playlist.privacy.label).to.equal('Unlisted') |
275 | 482 | ||
276 | const res = await getVideoPlaylistsList(servers[2].url, 0, 5) | 483 | expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR) |
484 | expect(playlist.type.label).to.equal('Regular') | ||
277 | 485 | ||
278 | const playlist2 = res.body.data.find(p => p.displayName === 'playlist 2') | 486 | expect(playlist.videosLength).to.equal(2) |
279 | expect(playlist2).to.not.be.undefined | ||
280 | await testImage(servers[2].url, 'thumbnail-playlist', playlist2.thumbnailPath) | ||
281 | 487 | ||
282 | expect(res.body.data.find(p => p.displayName === 'playlist 3')).to.not.be.undefined | 488 | expect(playlist.ownerAccount.name).to.equal('root') |
489 | expect(playlist.ownerAccount.displayName).to.equal('root') | ||
490 | expect(playlist.videoChannel.name).to.equal('root_channel') | ||
491 | expect(playlist.videoChannel.displayName).to.equal('Main root channel') | ||
492 | } | ||
493 | }) | ||
283 | }) | 494 | }) |
284 | 495 | ||
285 | it('Should correctly list the playlists', async function () { | 496 | describe('Element timestamps', function () { |
286 | this.timeout(30000) | ||
287 | 497 | ||
288 | { | 498 | it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () { |
289 | const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, 'createdAt') | 499 | this.timeout(30000) |
290 | 500 | ||
291 | expect(res.body.total).to.equal(3) | 501 | const addVideo = (elementAttrs: any) => { |
502 | return addVideoInPlaylist({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, playlistId: playlistServer1Id, elementAttrs }) | ||
503 | } | ||
292 | 504 | ||
293 | const data: VideoPlaylist[] = res.body.data | 505 | const res = await createVideoPlaylist({ |
294 | expect(data).to.have.lengthOf(2) | 506 | url: servers[ 0 ].url, |
295 | expect(data[ 0 ].displayName).to.equal('playlist 2') | 507 | token: servers[ 0 ].accessToken, |
296 | expect(data[ 1 ].displayName).to.equal('playlist 3') | 508 | playlistAttrs: { |
297 | } | 509 | displayName: 'playlist 4', |
510 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
511 | videoChannelId: servers[ 0 ].videoChannel.id | ||
512 | } | ||
513 | }) | ||
298 | 514 | ||
299 | { | 515 | playlistServer1Id = res.body.videoPlaylist.id |
300 | const res = await getVideoPlaylistsList(servers[ 2 ].url, 1, 2, '-createdAt') | 516 | playlistServer1UUID = res.body.videoPlaylist.uuid |
301 | 517 | ||
302 | expect(res.body.total).to.equal(3) | 518 | await addVideo({ videoId: servers[ 0 ].videos[ 0 ].uuid, startTimestamp: 15, stopTimestamp: 28 }) |
519 | await addVideo({ videoId: servers[ 2 ].videos[ 1 ].uuid, startTimestamp: 35 }) | ||
520 | await addVideo({ videoId: servers[ 2 ].videos[ 2 ].uuid }) | ||
521 | { | ||
522 | const res = await addVideo({ videoId: servers[ 0 ].videos[ 3 ].uuid, stopTimestamp: 35 }) | ||
523 | playlistElementServer1Video4 = res.body.videoPlaylistElement.id | ||
524 | } | ||
303 | 525 | ||
304 | const data: VideoPlaylist[] = res.body.data | 526 | { |
305 | expect(data).to.have.lengthOf(2) | 527 | const res = await addVideo({ videoId: servers[ 0 ].videos[ 4 ].uuid, startTimestamp: 45, stopTimestamp: 60 }) |
306 | expect(data[ 0 ].displayName).to.equal('playlist 2') | 528 | playlistElementServer1Video5 = res.body.videoPlaylistElement.id |
307 | expect(data[ 1 ].displayName).to.equal('my super playlist') | 529 | } |
308 | } | ||
309 | }) | ||
310 | 530 | ||
311 | it('Should list video channel playlists', async function () { | 531 | { |
312 | this.timeout(30000) | 532 | const res = await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 }) |
533 | playlistElementNSFW = res.body.videoPlaylistElement.id | ||
534 | } | ||
313 | 535 | ||
314 | { | 536 | await waitJobs(servers) |
315 | const res = await getVideoChannelPlaylistsList(servers[ 0 ].url, 'root_channel', 0, 2, '-createdAt') | 537 | }) |
316 | 538 | ||
317 | expect(res.body.total).to.equal(1) | 539 | it('Should correctly list playlist videos', async function () { |
540 | this.timeout(30000) | ||
318 | 541 | ||
319 | const data: VideoPlaylist[] = res.body.data | 542 | for (const server of servers) { |
320 | expect(data).to.have.lengthOf(1) | 543 | const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) |
321 | expect(data[ 0 ].displayName).to.equal('my super playlist') | ||
322 | } | ||
323 | }) | ||
324 | 544 | ||
325 | it('Should list account playlists', async function () { | 545 | expect(res.body.total).to.equal(6) |
326 | this.timeout(30000) | ||
327 | 546 | ||
328 | { | 547 | const videoElements: VideoPlaylistElement[] = res.body.data |
329 | const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, '-createdAt') | 548 | expect(videoElements).to.have.lengthOf(6) |
330 | 549 | ||
331 | expect(res.body.total).to.equal(2) | 550 | expect(videoElements[ 0 ].video.name).to.equal('video 0 server 1') |
551 | expect(videoElements[ 0 ].position).to.equal(1) | ||
552 | expect(videoElements[ 0 ].startTimestamp).to.equal(15) | ||
553 | expect(videoElements[ 0 ].stopTimestamp).to.equal(28) | ||
332 | 554 | ||
333 | const data: VideoPlaylist[] = res.body.data | 555 | expect(videoElements[ 1 ].video.name).to.equal('video 1 server 3') |
334 | expect(data).to.have.lengthOf(1) | 556 | expect(videoElements[ 1 ].position).to.equal(2) |
335 | expect(data[ 0 ].displayName).to.equal('playlist 2') | 557 | expect(videoElements[ 1 ].startTimestamp).to.equal(35) |
336 | } | 558 | expect(videoElements[ 1 ].stopTimestamp).to.be.null |
337 | 559 | ||
338 | { | 560 | expect(videoElements[ 2 ].video.name).to.equal('video 2 server 3') |
339 | const res = await getAccountPlaylistsList(servers[ 1 ].url, 'root', 1, 2, 'createdAt') | 561 | expect(videoElements[ 2 ].position).to.equal(3) |
562 | expect(videoElements[ 2 ].startTimestamp).to.be.null | ||
563 | expect(videoElements[ 2 ].stopTimestamp).to.be.null | ||
340 | 564 | ||
341 | expect(res.body.total).to.equal(2) | 565 | expect(videoElements[ 3 ].video.name).to.equal('video 3 server 1') |
566 | expect(videoElements[ 3 ].position).to.equal(4) | ||
567 | expect(videoElements[ 3 ].startTimestamp).to.be.null | ||
568 | expect(videoElements[ 3 ].stopTimestamp).to.equal(35) | ||
342 | 569 | ||
343 | const data: VideoPlaylist[] = res.body.data | 570 | expect(videoElements[ 4 ].video.name).to.equal('video 4 server 1') |
344 | expect(data).to.have.lengthOf(1) | 571 | expect(videoElements[ 4 ].position).to.equal(5) |
345 | expect(data[ 0 ].displayName).to.equal('playlist 3') | 572 | expect(videoElements[ 4 ].startTimestamp).to.equal(45) |
346 | } | 573 | expect(videoElements[ 4 ].stopTimestamp).to.equal(60) |
347 | }) | ||
348 | 574 | ||
349 | it('Should not list unlisted or private playlists', async function () { | 575 | expect(videoElements[ 5 ].video.name).to.equal('NSFW video') |
350 | this.timeout(30000) | 576 | expect(videoElements[ 5 ].position).to.equal(6) |
577 | expect(videoElements[ 5 ].startTimestamp).to.equal(5) | ||
578 | expect(videoElements[ 5 ].stopTimestamp).to.be.null | ||
351 | 579 | ||
352 | await createVideoPlaylist({ | 580 | const res3 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 2) |
353 | url: servers[ 1 ].url, | 581 | expect(res3.body.data).to.have.lengthOf(2) |
354 | token: servers[ 1 ].accessToken, | ||
355 | playlistAttrs: { | ||
356 | displayName: 'playlist unlisted', | ||
357 | privacy: VideoPlaylistPrivacy.UNLISTED | ||
358 | } | 582 | } |
359 | }) | 583 | }) |
584 | }) | ||
360 | 585 | ||
361 | await createVideoPlaylist({ | 586 | describe('Element type', function () { |
362 | url: servers[ 1 ].url, | 587 | let groupUser1: ServerInfo[] |
363 | token: servers[ 1 ].accessToken, | 588 | let groupWithoutToken1: ServerInfo[] |
364 | playlistAttrs: { | 589 | let group1: ServerInfo[] |
365 | displayName: 'playlist private', | 590 | let group2: ServerInfo[] |
366 | privacy: VideoPlaylistPrivacy.PRIVATE | ||
367 | } | ||
368 | }) | ||
369 | 591 | ||
370 | await waitJobs(servers) | 592 | let video1: string |
593 | let video2: string | ||
594 | let video3: string | ||
371 | 595 | ||
372 | for (const server of servers) { | 596 | before(async function () { |
373 | const results = [ | 597 | this.timeout(30000) |
374 | await getAccountPlaylistsList(server.url, 'root@localhost:' + servers[1].port, 0, 5, '-createdAt'), | ||
375 | await getVideoPlaylistsList(server.url, 0, 2, '-createdAt') | ||
376 | ] | ||
377 | 598 | ||
378 | expect(results[0].body.total).to.equal(2) | 599 | groupUser1 = [ Object.assign({}, servers[ 0 ], { accessToken: userAccessTokenServer1 }) ] |
379 | expect(results[1].body.total).to.equal(3) | 600 | groupWithoutToken1 = [ Object.assign({}, servers[ 0 ], { accessToken: undefined }) ] |
601 | group1 = [ servers[ 0 ] ] | ||
602 | group2 = [ servers[ 1 ], servers[ 2 ] ] | ||
380 | 603 | ||
381 | for (const res of results) { | 604 | const res = await createVideoPlaylist({ |
382 | const data: VideoPlaylist[] = res.body.data | 605 | url: servers[ 0 ].url, |
383 | expect(data).to.have.lengthOf(2) | 606 | token: userAccessTokenServer1, |
384 | expect(data[ 0 ].displayName).to.equal('playlist 3') | 607 | playlistAttrs: { |
385 | expect(data[ 1 ].displayName).to.equal('playlist 2') | 608 | displayName: 'playlist 56', |
386 | } | 609 | privacy: VideoPlaylistPrivacy.PUBLIC, |
387 | } | 610 | videoChannelId: servers[ 0 ].videoChannel.id |
388 | }) | 611 | } |
612 | }) | ||
389 | 613 | ||
390 | it('Should update a playlist', async function () { | 614 | const playlistServer1Id2 = res.body.videoPlaylist.id |
391 | this.timeout(30000) | 615 | playlistServer1UUID2 = res.body.videoPlaylist.uuid |
392 | |||
393 | await updateVideoPlaylist({ | ||
394 | url: servers[1].url, | ||
395 | token: servers[1].accessToken, | ||
396 | playlistAttrs: { | ||
397 | displayName: 'playlist 3 updated', | ||
398 | description: 'description updated', | ||
399 | privacy: VideoPlaylistPrivacy.UNLISTED, | ||
400 | thumbnailfile: 'thumbnail.jpg', | ||
401 | videoChannelId: servers[1].videoChannel.id | ||
402 | }, | ||
403 | playlistId: playlistServer2Id2 | ||
404 | }) | ||
405 | 616 | ||
406 | await waitJobs(servers) | 617 | const addVideo = (elementAttrs: any) => { |
618 | return addVideoInPlaylist({ url: servers[ 0 ].url, token: userAccessTokenServer1, playlistId: playlistServer1Id2, elementAttrs }) | ||
619 | } | ||
407 | 620 | ||
408 | for (const server of servers) { | 621 | video1 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 89', token: userAccessTokenServer1 })).uuid |
409 | const res = await getVideoPlaylist(server.url, playlistServer2UUID2) | 622 | video2 = (await uploadVideoAndGetId({ server: servers[1], videoName: 'video 90' })).uuid |
410 | const playlist: VideoPlaylist = res.body | 623 | video3 = (await uploadVideoAndGetId({ server: servers[0], videoName: 'video 91', nsfw: true })).uuid |
411 | 624 | ||
412 | expect(playlist.displayName).to.equal('playlist 3 updated') | 625 | await addVideo({ videoId: video1, startTimestamp: 15, stopTimestamp: 28 }) |
413 | expect(playlist.description).to.equal('description updated') | 626 | await addVideo({ videoId: video2, startTimestamp: 35 }) |
627 | await addVideo({ videoId: video3 }) | ||
414 | 628 | ||
415 | expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.UNLISTED) | 629 | await waitJobs(servers) |
416 | expect(playlist.privacy.label).to.equal('Unlisted') | 630 | }) |
417 | 631 | ||
418 | expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR) | 632 | it('Should update the element type if the video is private', async function () { |
419 | expect(playlist.type.label).to.equal('Regular') | 633 | this.timeout(20000) |
420 | 634 | ||
421 | expect(playlist.videosLength).to.equal(2) | 635 | const name = 'video 89' |
636 | const position = 1 | ||
422 | 637 | ||
423 | expect(playlist.ownerAccount.name).to.equal('root') | 638 | { |
424 | expect(playlist.ownerAccount.displayName).to.equal('root') | 639 | await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, video1, { privacy: VideoPrivacy.PRIVATE }) |
425 | expect(playlist.videoChannel.name).to.equal('root_channel') | 640 | await waitJobs(servers) |
426 | expect(playlist.videoChannel.displayName).to.equal('Main root channel') | ||
427 | } | ||
428 | }) | ||
429 | 641 | ||
430 | it('Should create a playlist containing different startTimestamp/endTimestamp videos', async function () { | 642 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) |
431 | this.timeout(30000) | 643 | await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) |
644 | await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.PRIVATE, position, name, 3) | ||
645 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) | ||
646 | } | ||
432 | 647 | ||
433 | const addVideo = (elementAttrs: any) => { | 648 | { |
434 | return addVideoInPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistId: playlistServer1Id, elementAttrs }) | 649 | await updateVideo(servers[ 0 ].url, servers[ 0 ].accessToken, video1, { privacy: VideoPrivacy.PUBLIC }) |
435 | } | 650 | await waitJobs(servers) |
436 | 651 | ||
437 | const res = await createVideoPlaylist({ | 652 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) |
438 | url: servers[ 0 ].url, | 653 | await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) |
439 | token: servers[ 0 ].accessToken, | 654 | await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) |
440 | playlistAttrs: { | 655 | // We deleted the video, so even if we recreated it, the old entry is still deleted |
441 | displayName: 'playlist 4', | 656 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) |
442 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
443 | videoChannelId: servers[0].videoChannel.id | ||
444 | } | 657 | } |
445 | }) | 658 | }) |
446 | 659 | ||
447 | playlistServer1Id = res.body.videoPlaylist.id | 660 | it('Should update the element type if the video is blacklisted', async function () { |
448 | playlistServer1UUID = res.body.videoPlaylist.uuid | 661 | this.timeout(20000) |
449 | 662 | ||
450 | await addVideo({ videoId: servers[0].videos[0].uuid, startTimestamp: 15, stopTimestamp: 28 }) | 663 | const name = 'video 89' |
451 | await addVideo({ videoId: servers[2].videos[1].uuid, startTimestamp: 35 }) | 664 | const position = 1 |
452 | await addVideo({ videoId: servers[2].videos[2].uuid }) | ||
453 | await addVideo({ videoId: servers[0].videos[3].uuid, stopTimestamp: 35 }) | ||
454 | await addVideo({ videoId: servers[0].videos[4].uuid, startTimestamp: 45, stopTimestamp: 60 }) | ||
455 | await addVideo({ videoId: nsfwVideoServer1, startTimestamp: 5 }) | ||
456 | 665 | ||
457 | await waitJobs(servers) | 666 | { |
458 | }) | 667 | await addVideoToBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video1, 'reason', true) |
668 | await waitJobs(servers) | ||
459 | 669 | ||
460 | it('Should correctly list playlist videos', async function () { | 670 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) |
461 | this.timeout(30000) | 671 | await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) |
672 | await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) | ||
673 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) | ||
674 | } | ||
462 | 675 | ||
463 | for (const server of servers) { | 676 | { |
464 | const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) | 677 | await removeVideoFromBlacklist(servers[ 0 ].url, servers[ 0 ].accessToken, video1) |
678 | await waitJobs(servers) | ||
465 | 679 | ||
466 | expect(res.body.total).to.equal(6) | 680 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) |
681 | await checkPlaylistElementType(groupWithoutToken1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
682 | await checkPlaylistElementType(group1, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
683 | // We deleted the video (because unfederated), so even if we recreated it, the old entry is still deleted | ||
684 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.DELETED, position, name, 3) | ||
685 | } | ||
686 | }) | ||
467 | 687 | ||
468 | const videos: Video[] = res.body.data | 688 | it('Should update the element type if the account or server of the video is blocked', async function () { |
469 | expect(videos).to.have.lengthOf(6) | 689 | this.timeout(90000) |
470 | 690 | ||
471 | expect(videos[0].name).to.equal('video 0 server 1') | 691 | const name = 'video 90' |
472 | expect(videos[0].playlistElement.position).to.equal(1) | 692 | const position = 2 |
473 | expect(videos[0].playlistElement.startTimestamp).to.equal(15) | ||
474 | expect(videos[0].playlistElement.stopTimestamp).to.equal(28) | ||
475 | 693 | ||
476 | expect(videos[1].name).to.equal('video 1 server 3') | 694 | { |
477 | expect(videos[1].playlistElement.position).to.equal(2) | 695 | await addAccountToAccountBlocklist(servers[ 0 ].url, userAccessTokenServer1, 'root@localhost:' + servers[1].port) |
478 | expect(videos[1].playlistElement.startTimestamp).to.equal(35) | 696 | await waitJobs(servers) |
479 | expect(videos[1].playlistElement.stopTimestamp).to.be.null | ||
480 | 697 | ||
481 | expect(videos[2].name).to.equal('video 2 server 3') | 698 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) |
482 | expect(videos[2].playlistElement.position).to.equal(3) | 699 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) |
483 | expect(videos[2].playlistElement.startTimestamp).to.be.null | ||
484 | expect(videos[2].playlistElement.stopTimestamp).to.be.null | ||
485 | 700 | ||
486 | expect(videos[3].name).to.equal('video 3 server 1') | 701 | await removeAccountFromAccountBlocklist(servers[ 0 ].url, userAccessTokenServer1, 'root@localhost:' + servers[1].port) |
487 | expect(videos[3].playlistElement.position).to.equal(4) | 702 | await waitJobs(servers) |
488 | expect(videos[3].playlistElement.startTimestamp).to.be.null | ||
489 | expect(videos[3].playlistElement.stopTimestamp).to.equal(35) | ||
490 | 703 | ||
491 | expect(videos[4].name).to.equal('video 4 server 1') | 704 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) |
492 | expect(videos[4].playlistElement.position).to.equal(5) | 705 | } |
493 | expect(videos[4].playlistElement.startTimestamp).to.equal(45) | ||
494 | expect(videos[4].playlistElement.stopTimestamp).to.equal(60) | ||
495 | 706 | ||
496 | expect(videos[5].name).to.equal('NSFW video') | 707 | { |
497 | expect(videos[5].playlistElement.position).to.equal(6) | 708 | await addServerToAccountBlocklist(servers[ 0 ].url, userAccessTokenServer1, 'localhost:' + servers[1].port) |
498 | expect(videos[5].playlistElement.startTimestamp).to.equal(5) | 709 | await waitJobs(servers) |
499 | expect(videos[5].playlistElement.stopTimestamp).to.be.null | ||
500 | 710 | ||
501 | const res2 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10, { nsfw: false }) | 711 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) |
502 | expect(res2.body.total).to.equal(5) | 712 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) |
503 | expect(res2.body.data.find(v => v.name === 'NSFW video')).to.be.undefined | ||
504 | 713 | ||
505 | const res3 = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 2) | 714 | await removeServerFromAccountBlocklist(servers[ 0 ].url, userAccessTokenServer1, 'localhost:' + servers[1].port) |
506 | expect(res3.body.data).to.have.lengthOf(2) | 715 | await waitJobs(servers) |
507 | } | ||
508 | }) | ||
509 | 716 | ||
510 | it('Should reorder the playlist', async function () { | 717 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) |
511 | this.timeout(30000) | 718 | } |
512 | 719 | ||
513 | { | 720 | { |
514 | await reorderVideosPlaylist({ | 721 | await addAccountToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'root@localhost:' + servers[1].port) |
515 | url: servers[ 0 ].url, | 722 | await waitJobs(servers) |
516 | token: servers[ 0 ].accessToken, | ||
517 | playlistId: playlistServer1Id, | ||
518 | elementAttrs: { | ||
519 | startPosition: 2, | ||
520 | insertAfterPosition: 3 | ||
521 | } | ||
522 | }) | ||
523 | 723 | ||
524 | await waitJobs(servers) | 724 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) |
725 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
525 | 726 | ||
526 | for (const server of servers) { | 727 | await removeAccountFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'root@localhost:' + servers[1].port) |
527 | const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) | 728 | await waitJobs(servers) |
528 | const names = res.body.data.map(v => v.name) | ||
529 | 729 | ||
530 | expect(names).to.deep.equal([ | 730 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) |
531 | 'video 0 server 1', | ||
532 | 'video 2 server 3', | ||
533 | 'video 1 server 3', | ||
534 | 'video 3 server 1', | ||
535 | 'video 4 server 1', | ||
536 | 'NSFW video' | ||
537 | ]) | ||
538 | } | 731 | } |
539 | } | ||
540 | 732 | ||
541 | { | 733 | { |
542 | await reorderVideosPlaylist({ | 734 | await addServerToServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port) |
543 | url: servers[0].url, | 735 | await waitJobs(servers) |
544 | token: servers[0].accessToken, | ||
545 | playlistId: playlistServer1Id, | ||
546 | elementAttrs: { | ||
547 | startPosition: 1, | ||
548 | reorderLength: 3, | ||
549 | insertAfterPosition: 4 | ||
550 | } | ||
551 | }) | ||
552 | 736 | ||
553 | await waitJobs(servers) | 737 | await checkPlaylistElementType(groupUser1, playlistServer1UUID2, VideoPlaylistElementType.UNAVAILABLE, position, name, 3) |
738 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) | ||
554 | 739 | ||
555 | for (const server of servers) { | 740 | await removeServerFromServerBlocklist(servers[ 0 ].url, servers[ 0 ].accessToken, 'localhost:' + servers[1].port) |
556 | const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) | 741 | await waitJobs(servers) |
557 | const names = res.body.data.map(v => v.name) | ||
558 | 742 | ||
559 | expect(names).to.deep.equal([ | 743 | await checkPlaylistElementType(group2, playlistServer1UUID2, VideoPlaylistElementType.REGULAR, position, name, 3) |
560 | 'video 3 server 1', | ||
561 | 'video 0 server 1', | ||
562 | 'video 2 server 3', | ||
563 | 'video 1 server 3', | ||
564 | 'video 4 server 1', | ||
565 | 'NSFW video' | ||
566 | ]) | ||
567 | } | 744 | } |
568 | } | 745 | }) |
569 | |||
570 | { | ||
571 | await reorderVideosPlaylist({ | ||
572 | url: servers[0].url, | ||
573 | token: servers[0].accessToken, | ||
574 | playlistId: playlistServer1Id, | ||
575 | elementAttrs: { | ||
576 | startPosition: 6, | ||
577 | insertAfterPosition: 3 | ||
578 | } | ||
579 | }) | ||
580 | 746 | ||
581 | await waitJobs(servers) | 747 | it('Should hide the video if it is NSFW', async function () { |
748 | const res = await getPlaylistVideos(servers[0].url, userAccessTokenServer1, playlistServer1UUID2, 0, 10, { nsfw: false }) | ||
749 | expect(res.body.total).to.equal(3) | ||
582 | 750 | ||
583 | for (const server of servers) { | 751 | const elements: VideoPlaylistElement[] = res.body.data |
584 | const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) | 752 | const element = elements.find(e => e.position === 3) |
585 | const videos: Video[] = res.body.data | ||
586 | 753 | ||
587 | const names = videos.map(v => v.name) | 754 | expect(element).to.exist |
755 | expect(element.video).to.be.null | ||
756 | expect(element.type).to.equal(VideoPlaylistElementType.UNAVAILABLE) | ||
757 | }) | ||
588 | 758 | ||
589 | expect(names).to.deep.equal([ | 759 | }) |
590 | 'video 3 server 1', | ||
591 | 'video 0 server 1', | ||
592 | 'video 2 server 3', | ||
593 | 'NSFW video', | ||
594 | 'video 1 server 3', | ||
595 | 'video 4 server 1' | ||
596 | ]) | ||
597 | 760 | ||
598 | for (let i = 1; i <= videos.length; i++) { | 761 | describe('Managing playlist elements', function () { |
599 | expect(videos[i - 1].playlistElement.position).to.equal(i) | 762 | |
763 | it('Should reorder the playlist', async function () { | ||
764 | this.timeout(30000) | ||
765 | |||
766 | { | ||
767 | await reorderVideosPlaylist({ | ||
768 | url: servers[ 0 ].url, | ||
769 | token: servers[ 0 ].accessToken, | ||
770 | playlistId: playlistServer1Id, | ||
771 | elementAttrs: { | ||
772 | startPosition: 2, | ||
773 | insertAfterPosition: 3 | ||
774 | } | ||
775 | }) | ||
776 | |||
777 | await waitJobs(servers) | ||
778 | |||
779 | for (const server of servers) { | ||
780 | const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) | ||
781 | const names = (res.body.data as VideoPlaylistElement[]).map(v => v.video.name) | ||
782 | |||
783 | expect(names).to.deep.equal([ | ||
784 | 'video 0 server 1', | ||
785 | 'video 2 server 3', | ||
786 | 'video 1 server 3', | ||
787 | 'video 3 server 1', | ||
788 | 'video 4 server 1', | ||
789 | 'NSFW video' | ||
790 | ]) | ||
600 | } | 791 | } |
601 | } | 792 | } |
602 | } | ||
603 | }) | ||
604 | |||
605 | it('Should update startTimestamp/endTimestamp of some elements', async function () { | ||
606 | this.timeout(30000) | ||
607 | 793 | ||
608 | await updateVideoPlaylistElement({ | 794 | { |
609 | url: servers[0].url, | 795 | await reorderVideosPlaylist({ |
610 | token: servers[0].accessToken, | 796 | url: servers[ 0 ].url, |
611 | playlistId: playlistServer1Id, | 797 | token: servers[ 0 ].accessToken, |
612 | videoId: servers[0].videos[3].uuid, | 798 | playlistId: playlistServer1Id, |
613 | elementAttrs: { | 799 | elementAttrs: { |
614 | startTimestamp: 1 | 800 | startPosition: 1, |
801 | reorderLength: 3, | ||
802 | insertAfterPosition: 4 | ||
803 | } | ||
804 | }) | ||
805 | |||
806 | await waitJobs(servers) | ||
807 | |||
808 | for (const server of servers) { | ||
809 | const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) | ||
810 | const names = (res.body.data as VideoPlaylistElement[]).map(v => v.video.name) | ||
811 | |||
812 | expect(names).to.deep.equal([ | ||
813 | 'video 3 server 1', | ||
814 | 'video 0 server 1', | ||
815 | 'video 2 server 3', | ||
816 | 'video 1 server 3', | ||
817 | 'video 4 server 1', | ||
818 | 'NSFW video' | ||
819 | ]) | ||
820 | } | ||
615 | } | 821 | } |
616 | }) | ||
617 | 822 | ||
618 | await updateVideoPlaylistElement({ | 823 | { |
619 | url: servers[0].url, | 824 | await reorderVideosPlaylist({ |
620 | token: servers[0].accessToken, | 825 | url: servers[ 0 ].url, |
621 | playlistId: playlistServer1Id, | 826 | token: servers[ 0 ].accessToken, |
622 | videoId: servers[0].videos[4].uuid, | 827 | playlistId: playlistServer1Id, |
623 | elementAttrs: { | 828 | elementAttrs: { |
624 | stopTimestamp: null | 829 | startPosition: 6, |
830 | insertAfterPosition: 3 | ||
831 | } | ||
832 | }) | ||
833 | |||
834 | await waitJobs(servers) | ||
835 | |||
836 | for (const server of servers) { | ||
837 | const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) | ||
838 | const elements: VideoPlaylistElement[] = res.body.data | ||
839 | const names = elements.map(v => v.video.name) | ||
840 | |||
841 | expect(names).to.deep.equal([ | ||
842 | 'video 3 server 1', | ||
843 | 'video 0 server 1', | ||
844 | 'video 2 server 3', | ||
845 | 'NSFW video', | ||
846 | 'video 1 server 3', | ||
847 | 'video 4 server 1' | ||
848 | ]) | ||
849 | |||
850 | for (let i = 1; i <= elements.length; i++) { | ||
851 | expect(elements[ i - 1 ].position).to.equal(i) | ||
852 | } | ||
853 | } | ||
625 | } | 854 | } |
626 | }) | 855 | }) |
627 | 856 | ||
628 | await waitJobs(servers) | 857 | it('Should update startTimestamp/endTimestamp of some elements', async function () { |
858 | this.timeout(30000) | ||
859 | |||
860 | await updateVideoPlaylistElement({ | ||
861 | url: servers[ 0 ].url, | ||
862 | token: servers[ 0 ].accessToken, | ||
863 | playlistId: playlistServer1Id, | ||
864 | playlistElementId: playlistElementServer1Video4, | ||
865 | elementAttrs: { | ||
866 | startTimestamp: 1 | ||
867 | } | ||
868 | }) | ||
629 | 869 | ||
630 | for (const server of servers) { | 870 | await updateVideoPlaylistElement({ |
631 | const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) | 871 | url: servers[ 0 ].url, |
632 | const videos: Video[] = res.body.data | 872 | token: servers[ 0 ].accessToken, |
873 | playlistId: playlistServer1Id, | ||
874 | playlistElementId: playlistElementServer1Video5, | ||
875 | elementAttrs: { | ||
876 | stopTimestamp: null | ||
877 | } | ||
878 | }) | ||
633 | 879 | ||
634 | expect(videos[0].name).to.equal('video 3 server 1') | 880 | await waitJobs(servers) |
635 | expect(videos[0].playlistElement.position).to.equal(1) | ||
636 | expect(videos[0].playlistElement.startTimestamp).to.equal(1) | ||
637 | expect(videos[0].playlistElement.stopTimestamp).to.equal(35) | ||
638 | 881 | ||
639 | expect(videos[5].name).to.equal('video 4 server 1') | 882 | for (const server of servers) { |
640 | expect(videos[5].playlistElement.position).to.equal(6) | 883 | const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) |
641 | expect(videos[5].playlistElement.startTimestamp).to.equal(45) | 884 | const elements: VideoPlaylistElement[] = res.body.data |
642 | expect(videos[5].playlistElement.stopTimestamp).to.be.null | ||
643 | } | ||
644 | }) | ||
645 | 885 | ||
646 | it('Should check videos existence in my playlist', async function () { | 886 | expect(elements[ 0 ].video.name).to.equal('video 3 server 1') |
647 | const videoIds = [ | 887 | expect(elements[ 0 ].position).to.equal(1) |
648 | servers[0].videos[0].id, | 888 | expect(elements[ 0 ].startTimestamp).to.equal(1) |
649 | 42000, | 889 | expect(elements[ 0 ].stopTimestamp).to.equal(35) |
650 | servers[0].videos[3].id, | ||
651 | 43000, | ||
652 | servers[0].videos[4].id | ||
653 | ] | ||
654 | const res = await doVideosExistInMyPlaylist(servers[ 0 ].url, servers[ 0 ].accessToken, videoIds) | ||
655 | const obj = res.body as VideoExistInPlaylist | ||
656 | 890 | ||
657 | { | 891 | expect(elements[ 5 ].video.name).to.equal('video 4 server 1') |
658 | const elem = obj[servers[0].videos[0].id] | 892 | expect(elements[ 5 ].position).to.equal(6) |
659 | expect(elem).to.have.lengthOf(1) | 893 | expect(elements[ 5 ].startTimestamp).to.equal(45) |
660 | expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) | 894 | expect(elements[ 5 ].stopTimestamp).to.be.null |
661 | expect(elem[ 0 ].startTimestamp).to.equal(15) | 895 | } |
662 | expect(elem[ 0 ].stopTimestamp).to.equal(28) | 896 | }) |
663 | } | ||
664 | 897 | ||
665 | { | 898 | it('Should check videos existence in my playlist', async function () { |
666 | const elem = obj[servers[0].videos[3].id] | 899 | const videoIds = [ |
667 | expect(elem).to.have.lengthOf(1) | 900 | servers[ 0 ].videos[ 0 ].id, |
668 | expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) | 901 | 42000, |
669 | expect(elem[ 0 ].startTimestamp).to.equal(1) | 902 | servers[ 0 ].videos[ 3 ].id, |
670 | expect(elem[ 0 ].stopTimestamp).to.equal(35) | 903 | 43000, |
671 | } | 904 | servers[ 0 ].videos[ 4 ].id |
905 | ] | ||
906 | const res = await doVideosExistInMyPlaylist(servers[ 0 ].url, servers[ 0 ].accessToken, videoIds) | ||
907 | const obj = res.body as VideoExistInPlaylist | ||
908 | |||
909 | { | ||
910 | const elem = obj[ servers[ 0 ].videos[ 0 ].id ] | ||
911 | expect(elem).to.have.lengthOf(1) | ||
912 | expect(elem[ 0 ].playlistElementId).to.exist | ||
913 | expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) | ||
914 | expect(elem[ 0 ].startTimestamp).to.equal(15) | ||
915 | expect(elem[ 0 ].stopTimestamp).to.equal(28) | ||
916 | } | ||
672 | 917 | ||
673 | { | 918 | { |
674 | const elem = obj[servers[0].videos[4].id] | 919 | const elem = obj[ servers[ 0 ].videos[ 3 ].id ] |
675 | expect(elem).to.have.lengthOf(1) | 920 | expect(elem).to.have.lengthOf(1) |
676 | expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) | 921 | expect(elem[ 0 ].playlistElementId).to.equal(playlistElementServer1Video4) |
677 | expect(elem[ 0 ].startTimestamp).to.equal(45) | 922 | expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) |
678 | expect(elem[ 0 ].stopTimestamp).to.equal(null) | 923 | expect(elem[ 0 ].startTimestamp).to.equal(1) |
679 | } | 924 | expect(elem[ 0 ].stopTimestamp).to.equal(35) |
925 | } | ||
680 | 926 | ||
681 | expect(obj[42000]).to.have.lengthOf(0) | 927 | { |
682 | expect(obj[43000]).to.have.lengthOf(0) | 928 | const elem = obj[ servers[ 0 ].videos[ 4 ].id ] |
683 | }) | 929 | expect(elem).to.have.lengthOf(1) |
930 | expect(elem[ 0 ].playlistId).to.equal(playlistServer1Id) | ||
931 | expect(elem[ 0 ].startTimestamp).to.equal(45) | ||
932 | expect(elem[ 0 ].stopTimestamp).to.equal(null) | ||
933 | } | ||
684 | 934 | ||
685 | it('Should automatically update updatedAt field of playlists', async function () { | 935 | expect(obj[ 42000 ]).to.have.lengthOf(0) |
686 | const server = servers[1] | 936 | expect(obj[ 43000 ]).to.have.lengthOf(0) |
687 | const videoId = servers[1].videos[5].id | 937 | }) |
688 | 938 | ||
689 | async function getPlaylistNames () { | 939 | it('Should automatically update updatedAt field of playlists', async function () { |
690 | const res = await getAccountPlaylistsListWithToken(server.url, server.accessToken, 'root', 0, 5, undefined, '-updatedAt') | 940 | const server = servers[ 1 ] |
941 | const videoId = servers[ 1 ].videos[ 5 ].id | ||
691 | 942 | ||
692 | return (res.body.data as VideoPlaylist[]).map(p => p.displayName) | 943 | async function getPlaylistNames () { |
693 | } | 944 | const res = await getAccountPlaylistsListWithToken(server.url, server.accessToken, 'root', 0, 5, undefined, '-updatedAt') |
694 | 945 | ||
695 | const elementAttrs = { videoId } | 946 | return (res.body.data as VideoPlaylist[]).map(p => p.displayName) |
696 | await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id1, elementAttrs }) | 947 | } |
697 | await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id2, elementAttrs }) | ||
698 | 948 | ||
699 | const names1 = await getPlaylistNames() | 949 | const elementAttrs = { videoId } |
700 | expect(names1[0]).to.equal('playlist 3 updated') | 950 | const res1 = await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id1, elementAttrs }) |
701 | expect(names1[1]).to.equal('playlist 2') | 951 | const res2 = await addVideoInPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id2, elementAttrs }) |
702 | 952 | ||
703 | await removeVideoFromPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id1, videoId }) | 953 | const element1 = res1.body.videoPlaylistElement.id |
954 | const element2 = res2.body.videoPlaylistElement.id | ||
704 | 955 | ||
705 | const names2 = await getPlaylistNames() | 956 | const names1 = await getPlaylistNames() |
706 | expect(names2[0]).to.equal('playlist 2') | 957 | expect(names1[ 0 ]).to.equal('playlist 3 updated') |
707 | expect(names2[1]).to.equal('playlist 3 updated') | 958 | expect(names1[ 1 ]).to.equal('playlist 2') |
708 | 959 | ||
709 | await removeVideoFromPlaylist({ url: server.url, token: server.accessToken, playlistId: playlistServer2Id2, videoId }) | 960 | await removeVideoFromPlaylist({ |
961 | url: server.url, | ||
962 | token: server.accessToken, | ||
963 | playlistId: playlistServer2Id1, | ||
964 | playlistElementId: element1 | ||
965 | }) | ||
710 | 966 | ||
711 | const names3 = await getPlaylistNames() | 967 | const names2 = await getPlaylistNames() |
712 | expect(names3[0]).to.equal('playlist 3 updated') | 968 | expect(names2[ 0 ]).to.equal('playlist 2') |
713 | expect(names3[1]).to.equal('playlist 2') | 969 | expect(names2[ 1 ]).to.equal('playlist 3 updated') |
714 | }) | ||
715 | 970 | ||
716 | it('Should delete some elements', async function () { | 971 | await removeVideoFromPlaylist({ |
717 | this.timeout(30000) | 972 | url: server.url, |
973 | token: server.accessToken, | ||
974 | playlistId: playlistServer2Id2, | ||
975 | playlistElementId: element2 | ||
976 | }) | ||
718 | 977 | ||
719 | await removeVideoFromPlaylist({ | 978 | const names3 = await getPlaylistNames() |
720 | url: servers[0].url, | 979 | expect(names3[ 0 ]).to.equal('playlist 3 updated') |
721 | token: servers[0].accessToken, | 980 | expect(names3[ 1 ]).to.equal('playlist 2') |
722 | playlistId: playlistServer1Id, | ||
723 | videoId: servers[0].videos[3].uuid | ||
724 | }) | 981 | }) |
725 | 982 | ||
726 | await removeVideoFromPlaylist({ | 983 | it('Should delete some elements', async function () { |
727 | url: servers[0].url, | 984 | this.timeout(30000) |
728 | token: servers[0].accessToken, | ||
729 | playlistId: playlistServer1Id, | ||
730 | videoId: nsfwVideoServer1 | ||
731 | }) | ||
732 | 985 | ||
733 | await waitJobs(servers) | 986 | await removeVideoFromPlaylist({ |
987 | url: servers[ 0 ].url, | ||
988 | token: servers[ 0 ].accessToken, | ||
989 | playlistId: playlistServer1Id, | ||
990 | playlistElementId: playlistElementServer1Video4 | ||
991 | }) | ||
734 | 992 | ||
735 | for (const server of servers) { | 993 | await removeVideoFromPlaylist({ |
736 | const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) | 994 | url: servers[ 0 ].url, |
995 | token: servers[ 0 ].accessToken, | ||
996 | playlistId: playlistServer1Id, | ||
997 | playlistElementId: playlistElementNSFW | ||
998 | }) | ||
737 | 999 | ||
738 | expect(res.body.total).to.equal(4) | 1000 | await waitJobs(servers) |
739 | 1001 | ||
740 | const videos: Video[] = res.body.data | 1002 | for (const server of servers) { |
741 | expect(videos).to.have.lengthOf(4) | 1003 | const res = await getPlaylistVideos(server.url, server.accessToken, playlistServer1UUID, 0, 10) |
742 | 1004 | ||
743 | expect(videos[ 0 ].name).to.equal('video 0 server 1') | 1005 | expect(res.body.total).to.equal(4) |
744 | expect(videos[ 0 ].playlistElement.position).to.equal(1) | ||
745 | 1006 | ||
746 | expect(videos[ 1 ].name).to.equal('video 2 server 3') | 1007 | const elements: VideoPlaylistElement[] = res.body.data |
747 | expect(videos[ 1 ].playlistElement.position).to.equal(2) | 1008 | expect(elements).to.have.lengthOf(4) |
748 | 1009 | ||
749 | expect(videos[ 2 ].name).to.equal('video 1 server 3') | 1010 | expect(elements[ 0 ].video.name).to.equal('video 0 server 1') |
750 | expect(videos[ 2 ].playlistElement.position).to.equal(3) | 1011 | expect(elements[ 0 ].position).to.equal(1) |
751 | 1012 | ||
752 | expect(videos[ 3 ].name).to.equal('video 4 server 1') | 1013 | expect(elements[ 1 ].video.name).to.equal('video 2 server 3') |
753 | expect(videos[ 3 ].playlistElement.position).to.equal(4) | 1014 | expect(elements[ 1 ].position).to.equal(2) |
754 | } | ||
755 | }) | ||
756 | 1015 | ||
757 | it('Should be able to create a public playlist, and set it to private', async function () { | 1016 | expect(elements[ 2 ].video.name).to.equal('video 1 server 3') |
758 | this.timeout(30000) | 1017 | expect(elements[ 2 ].position).to.equal(3) |
759 | 1018 | ||
760 | const res = await createVideoPlaylist({ | 1019 | expect(elements[ 3 ].video.name).to.equal('video 4 server 1') |
761 | url: servers[0].url, | 1020 | expect(elements[ 3 ].position).to.equal(4) |
762 | token: servers[0].accessToken, | ||
763 | playlistAttrs: { | ||
764 | displayName: 'my super public playlist', | ||
765 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
766 | videoChannelId: servers[0].videoChannel.id | ||
767 | } | 1021 | } |
768 | }) | 1022 | }) |
769 | const videoPlaylistIds = res.body.videoPlaylist | ||
770 | 1023 | ||
771 | await waitJobs(servers) | 1024 | it('Should be able to create a public playlist, and set it to private', async function () { |
1025 | this.timeout(30000) | ||
772 | 1026 | ||
773 | for (const server of servers) { | 1027 | const res = await createVideoPlaylist({ |
774 | await getVideoPlaylist(server.url, videoPlaylistIds.uuid, 200) | 1028 | url: servers[ 0 ].url, |
775 | } | 1029 | token: servers[ 0 ].accessToken, |
1030 | playlistAttrs: { | ||
1031 | displayName: 'my super public playlist', | ||
1032 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
1033 | videoChannelId: servers[ 0 ].videoChannel.id | ||
1034 | } | ||
1035 | }) | ||
1036 | const videoPlaylistIds = res.body.videoPlaylist | ||
776 | 1037 | ||
777 | const playlistAttrs = { privacy: VideoPlaylistPrivacy.PRIVATE } | 1038 | await waitJobs(servers) |
778 | await updateVideoPlaylist({ url: servers[0].url, token: servers[0].accessToken, playlistId: videoPlaylistIds.id, playlistAttrs }) | ||
779 | 1039 | ||
780 | await waitJobs(servers) | 1040 | for (const server of servers) { |
1041 | await getVideoPlaylist(server.url, videoPlaylistIds.uuid, 200) | ||
1042 | } | ||
781 | 1043 | ||
782 | for (const server of [ servers[1], servers[2] ]) { | 1044 | const playlistAttrs = { privacy: VideoPlaylistPrivacy.PRIVATE } |
783 | await getVideoPlaylist(server.url, videoPlaylistIds.uuid, 404) | 1045 | await updateVideoPlaylist({ url: servers[ 0 ].url, token: servers[ 0 ].accessToken, playlistId: videoPlaylistIds.id, playlistAttrs }) |
784 | } | ||
785 | await getVideoPlaylist(servers[0].url, videoPlaylistIds.uuid, 401) | ||
786 | 1046 | ||
787 | await getVideoPlaylistWithToken(servers[0].url, servers[0].accessToken, videoPlaylistIds.uuid, 200) | 1047 | await waitJobs(servers) |
788 | }) | ||
789 | 1048 | ||
790 | it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () { | 1049 | for (const server of [ servers[ 1 ], servers[ 2 ] ]) { |
791 | this.timeout(30000) | 1050 | await getVideoPlaylist(server.url, videoPlaylistIds.uuid, 404) |
1051 | } | ||
1052 | await getVideoPlaylist(servers[ 0 ].url, videoPlaylistIds.uuid, 401) | ||
792 | 1053 | ||
793 | await deleteVideoPlaylist(servers[0].url, servers[0].accessToken, playlistServer1Id) | 1054 | await getVideoPlaylistWithToken(servers[ 0 ].url, servers[ 0 ].accessToken, videoPlaylistIds.uuid, 200) |
1055 | }) | ||
1056 | }) | ||
794 | 1057 | ||
795 | await waitJobs(servers) | 1058 | describe('Playlist deletion', function () { |
796 | 1059 | ||
797 | for (const server of servers) { | 1060 | it('Should delete the playlist on server 1 and delete on server 2 and 3', async function () { |
798 | await getVideoPlaylist(server.url, playlistServer1UUID, 404) | 1061 | this.timeout(30000) |
799 | } | ||
800 | }) | ||
801 | 1062 | ||
802 | it('Should have deleted the thumbnail on server 1, 2 and 3', async function () { | 1063 | await deleteVideoPlaylist(servers[ 0 ].url, servers[ 0 ].accessToken, playlistServer1Id) |
803 | this.timeout(30000) | ||
804 | 1064 | ||
805 | for (const server of servers) { | 1065 | await waitJobs(servers) |
806 | await checkPlaylistFilesWereRemoved(playlistServer1UUID, server.internalServerNumber) | ||
807 | } | ||
808 | }) | ||
809 | 1066 | ||
810 | it('Should unfollow servers 1 and 2 and hide their playlists', async function () { | 1067 | for (const server of servers) { |
811 | this.timeout(30000) | 1068 | await getVideoPlaylist(server.url, playlistServer1UUID, 404) |
1069 | } | ||
1070 | }) | ||
812 | 1071 | ||
813 | const finder = data => data.find(p => p.displayName === 'my super playlist') | 1072 | it('Should have deleted the thumbnail on server 1, 2 and 3', async function () { |
1073 | this.timeout(30000) | ||
814 | 1074 | ||
815 | { | 1075 | for (const server of servers) { |
816 | const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) | 1076 | await checkPlaylistFilesWereRemoved(playlistServer1UUID, server.internalServerNumber) |
817 | expect(res.body.total).to.equal(2) | 1077 | } |
818 | expect(finder(res.body.data)).to.not.be.undefined | 1078 | }) |
819 | } | ||
820 | 1079 | ||
821 | await unfollow(servers[2].url, servers[2].accessToken, servers[0]) | 1080 | it('Should unfollow servers 1 and 2 and hide their playlists', async function () { |
1081 | this.timeout(30000) | ||
822 | 1082 | ||
823 | { | 1083 | const finder = data => data.find(p => p.displayName === 'my super playlist') |
824 | const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) | ||
825 | expect(res.body.total).to.equal(1) | ||
826 | 1084 | ||
827 | expect(finder(res.body.data)).to.be.undefined | 1085 | { |
828 | } | 1086 | const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) |
829 | }) | 1087 | expect(res.body.total).to.equal(3) |
1088 | expect(finder(res.body.data)).to.not.be.undefined | ||
1089 | } | ||
830 | 1090 | ||
831 | it('Should delete a channel and put the associated playlist in private mode', async function () { | 1091 | await unfollow(servers[ 2 ].url, servers[ 2 ].accessToken, servers[ 0 ]) |
832 | this.timeout(30000) | ||
833 | 1092 | ||
834 | const res = await addVideoChannel(servers[0].url, servers[0].accessToken, { name: 'super_channel', displayName: 'super channel' }) | 1093 | { |
835 | const videoChannelId = res.body.videoChannel.id | 1094 | const res = await getVideoPlaylistsList(servers[ 2 ].url, 0, 5) |
1095 | expect(res.body.total).to.equal(1) | ||
836 | 1096 | ||
837 | const res2 = await createVideoPlaylist({ | 1097 | expect(finder(res.body.data)).to.be.undefined |
838 | url: servers[0].url, | ||
839 | token: servers[0].accessToken, | ||
840 | playlistAttrs: { | ||
841 | displayName: 'channel playlist', | ||
842 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
843 | videoChannelId | ||
844 | } | 1098 | } |
845 | }) | 1099 | }) |
846 | const videoPlaylistUUID = res2.body.videoPlaylist.uuid | ||
847 | 1100 | ||
848 | await waitJobs(servers) | 1101 | it('Should delete a channel and put the associated playlist in private mode', async function () { |
1102 | this.timeout(30000) | ||
849 | 1103 | ||
850 | await deleteVideoChannel(servers[0].url, servers[0].accessToken, 'super_channel') | 1104 | const res = await addVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, { name: 'super_channel', displayName: 'super channel' }) |
1105 | const videoChannelId = res.body.videoChannel.id | ||
851 | 1106 | ||
852 | await waitJobs(servers) | 1107 | const res2 = await createVideoPlaylist({ |
1108 | url: servers[ 0 ].url, | ||
1109 | token: servers[ 0 ].accessToken, | ||
1110 | playlistAttrs: { | ||
1111 | displayName: 'channel playlist', | ||
1112 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
1113 | videoChannelId | ||
1114 | } | ||
1115 | }) | ||
1116 | const videoPlaylistUUID = res2.body.videoPlaylist.uuid | ||
1117 | |||
1118 | await waitJobs(servers) | ||
853 | 1119 | ||
854 | const res3 = await getVideoPlaylistWithToken(servers[0].url, servers[0].accessToken, videoPlaylistUUID) | 1120 | await deleteVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, 'super_channel') |
855 | expect(res3.body.displayName).to.equal('channel playlist') | ||
856 | expect(res3.body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE) | ||
857 | 1121 | ||
858 | await getVideoPlaylist(servers[1].url, videoPlaylistUUID, 404) | 1122 | await waitJobs(servers) |
859 | }) | ||
860 | 1123 | ||
861 | it('Should delete an account and delete its playlists', async function () { | 1124 | const res3 = await getVideoPlaylistWithToken(servers[ 0 ].url, servers[ 0 ].accessToken, videoPlaylistUUID) |
862 | this.timeout(30000) | 1125 | expect(res3.body.displayName).to.equal('channel playlist') |
1126 | expect(res3.body.privacy.id).to.equal(VideoPlaylistPrivacy.PRIVATE) | ||
863 | 1127 | ||
864 | const user = { username: 'user_1', password: 'password' } | 1128 | await getVideoPlaylist(servers[ 1 ].url, videoPlaylistUUID, 404) |
865 | const res = await createUser({ | ||
866 | url: servers[ 0 ].url, | ||
867 | accessToken: servers[ 0 ].accessToken, | ||
868 | username: user.username, | ||
869 | password: user.password | ||
870 | }) | 1129 | }) |
871 | 1130 | ||
872 | const userId = res.body.user.id | 1131 | it('Should delete an account and delete its playlists', async function () { |
873 | const userAccessToken = await userLogin(servers[0], user) | 1132 | this.timeout(30000) |
874 | 1133 | ||
875 | const resChannel = await getMyUserInformation(servers[0].url, userAccessToken) | 1134 | const user = { username: 'user_1', password: 'password' } |
876 | const userChannel = (resChannel.body as User).videoChannels[0] | 1135 | const res = await createUser({ |
1136 | url: servers[ 0 ].url, | ||
1137 | accessToken: servers[ 0 ].accessToken, | ||
1138 | username: user.username, | ||
1139 | password: user.password | ||
1140 | }) | ||
877 | 1141 | ||
878 | await createVideoPlaylist({ | 1142 | const userId = res.body.user.id |
879 | url: servers[0].url, | 1143 | const userAccessToken = await userLogin(servers[ 0 ], user) |
880 | token: userAccessToken, | ||
881 | playlistAttrs: { | ||
882 | displayName: 'playlist to be deleted', | ||
883 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
884 | videoChannelId: userChannel.id | ||
885 | } | ||
886 | }) | ||
887 | 1144 | ||
888 | await waitJobs(servers) | 1145 | const resChannel = await getMyUserInformation(servers[ 0 ].url, userAccessToken) |
1146 | const userChannel = (resChannel.body as User).videoChannels[ 0 ] | ||
889 | 1147 | ||
890 | const finder = data => data.find(p => p.displayName === 'playlist to be deleted') | 1148 | await createVideoPlaylist({ |
1149 | url: servers[ 0 ].url, | ||
1150 | token: userAccessToken, | ||
1151 | playlistAttrs: { | ||
1152 | displayName: 'playlist to be deleted', | ||
1153 | privacy: VideoPlaylistPrivacy.PUBLIC, | ||
1154 | videoChannelId: userChannel.id | ||
1155 | } | ||
1156 | }) | ||
891 | 1157 | ||
892 | { | 1158 | await waitJobs(servers) |
893 | for (const server of [ servers[0], servers[1] ]) { | 1159 | |
894 | const res = await getVideoPlaylistsList(server.url, 0, 15) | 1160 | const finder = data => data.find(p => p.displayName === 'playlist to be deleted') |
895 | expect(finder(res.body.data)).to.not.be.undefined | 1161 | |
1162 | { | ||
1163 | for (const server of [ servers[ 0 ], servers[ 1 ] ]) { | ||
1164 | const res = await getVideoPlaylistsList(server.url, 0, 15) | ||
1165 | expect(finder(res.body.data)).to.not.be.undefined | ||
1166 | } | ||
896 | } | 1167 | } |
897 | } | ||
898 | 1168 | ||
899 | await removeUser(servers[0].url, userId, servers[0].accessToken) | 1169 | await removeUser(servers[ 0 ].url, userId, servers[ 0 ].accessToken) |
900 | await waitJobs(servers) | 1170 | await waitJobs(servers) |
901 | 1171 | ||
902 | { | 1172 | { |
903 | for (const server of [ servers[0], servers[1] ]) { | 1173 | for (const server of [ servers[ 0 ], servers[ 1 ] ]) { |
904 | const res = await getVideoPlaylistsList(server.url, 0, 15) | 1174 | const res = await getVideoPlaylistsList(server.url, 0, 15) |
905 | expect(finder(res.body.data)).to.be.undefined | 1175 | expect(finder(res.body.data)).to.be.undefined |
1176 | } | ||
906 | } | 1177 | } |
907 | } | 1178 | }) |
908 | }) | 1179 | }) |
909 | 1180 | ||
910 | after(async function () { | 1181 | after(async function () { |
diff --git a/shared/extra-utils/server/jobs.ts b/shared/extra-utils/server/jobs.ts index 11b570f60..b3db885e8 100644 --- a/shared/extra-utils/server/jobs.ts +++ b/shared/extra-utils/server/jobs.ts | |||
@@ -2,7 +2,6 @@ import * as request from 'supertest' | |||
2 | import { Job, JobState } from '../../models' | 2 | import { Job, JobState } from '../../models' |
3 | import { wait } from '../miscs/miscs' | 3 | import { wait } from '../miscs/miscs' |
4 | import { ServerInfo } from './servers' | 4 | import { ServerInfo } from './servers' |
5 | import { inspect } from 'util' | ||
6 | 5 | ||
7 | function getJobsList (url: string, accessToken: string, state: JobState) { | 6 | function getJobsList (url: string, accessToken: string, state: JobState) { |
8 | const path = '/api/v1/jobs/' + state | 7 | const path = '/api/v1/jobs/' + state |
@@ -37,11 +36,10 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) { | |||
37 | else servers = serversArg as ServerInfo[] | 36 | else servers = serversArg as ServerInfo[] |
38 | 37 | ||
39 | const states: JobState[] = [ 'waiting', 'active', 'delayed' ] | 38 | const states: JobState[] = [ 'waiting', 'active', 'delayed' ] |
40 | let pendingRequests = false | 39 | let pendingRequests: boolean |
41 | 40 | ||
42 | function tasksBuilder () { | 41 | function tasksBuilder () { |
43 | const tasks: Promise<any>[] = [] | 42 | const tasks: Promise<any>[] = [] |
44 | pendingRequests = false | ||
45 | 43 | ||
46 | // Check if each server has pending request | 44 | // Check if each server has pending request |
47 | for (const server of servers) { | 45 | for (const server of servers) { |
@@ -62,6 +60,7 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) { | |||
62 | } | 60 | } |
63 | 61 | ||
64 | do { | 62 | do { |
63 | pendingRequests = false | ||
65 | await Promise.all(tasksBuilder()) | 64 | await Promise.all(tasksBuilder()) |
66 | 65 | ||
67 | // Retry, in case of new jobs were created | 66 | // Retry, in case of new jobs were created |
diff --git a/shared/extra-utils/videos/video-playlists.ts b/shared/extra-utils/videos/video-playlists.ts index fd62bef19..cbb073fbc 100644 --- a/shared/extra-utils/videos/video-playlists.ts +++ b/shared/extra-utils/videos/video-playlists.ts | |||
@@ -196,11 +196,11 @@ function updateVideoPlaylistElement (options: { | |||
196 | url: string, | 196 | url: string, |
197 | token: string, | 197 | token: string, |
198 | playlistId: number | string, | 198 | playlistId: number | string, |
199 | videoId: number | string, | 199 | playlistElementId: number | string, |
200 | elementAttrs: VideoPlaylistElementUpdate, | 200 | elementAttrs: VideoPlaylistElementUpdate, |
201 | expectedStatus?: number | 201 | expectedStatus?: number |
202 | }) { | 202 | }) { |
203 | const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.videoId | 203 | const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId |
204 | 204 | ||
205 | return makePutBodyRequest({ | 205 | return makePutBodyRequest({ |
206 | url: options.url, | 206 | url: options.url, |
@@ -215,10 +215,10 @@ function removeVideoFromPlaylist (options: { | |||
215 | url: string, | 215 | url: string, |
216 | token: string, | 216 | token: string, |
217 | playlistId: number | string, | 217 | playlistId: number | string, |
218 | videoId: number | string, | 218 | playlistElementId: number, |
219 | expectedStatus?: number | 219 | expectedStatus?: number |
220 | }) { | 220 | }) { |
221 | const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.videoId | 221 | const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId |
222 | 222 | ||
223 | return makeDeleteRequest({ | 223 | return makeDeleteRequest({ |
224 | url: options.url, | 224 | url: options.url, |
diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index e3d78220e..194ae1b96 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts | |||
@@ -19,6 +19,7 @@ export * from './playlist/video-playlist-privacy.model' | |||
19 | export * from './playlist/video-playlist-type.model' | 19 | export * from './playlist/video-playlist-type.model' |
20 | export * from './playlist/video-playlist-update.model' | 20 | export * from './playlist/video-playlist-update.model' |
21 | export * from './playlist/video-playlist.model' | 21 | export * from './playlist/video-playlist.model' |
22 | export * from './playlist/video-playlist-element.model' | ||
22 | export * from './video-change-ownership.model' | 23 | export * from './video-change-ownership.model' |
23 | export * from './video-change-ownership-create.model' | 24 | export * from './video-change-ownership-create.model' |
24 | export * from './video-create.model' | 25 | export * from './video-create.model' |
diff --git a/shared/models/videos/playlist/video-exist-in-playlist.model.ts b/shared/models/videos/playlist/video-exist-in-playlist.model.ts index 71240f51d..1b57257e2 100644 --- a/shared/models/videos/playlist/video-exist-in-playlist.model.ts +++ b/shared/models/videos/playlist/video-exist-in-playlist.model.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | export type VideoExistInPlaylist = { | 1 | export type VideoExistInPlaylist = { |
2 | [videoId: number ]: { | 2 | [videoId: number ]: { |
3 | playlistElementId: number | ||
3 | playlistId: number | 4 | playlistId: number |
4 | startTimestamp?: number | 5 | startTimestamp?: number |
5 | stopTimestamp?: number | 6 | stopTimestamp?: number |
diff --git a/shared/models/videos/playlist/video-playlist-element.model.ts b/shared/models/videos/playlist/video-playlist-element.model.ts new file mode 100644 index 000000000..9a1203892 --- /dev/null +++ b/shared/models/videos/playlist/video-playlist-element.model.ts | |||
@@ -0,0 +1,19 @@ | |||
1 | import { Video } from '../video.model' | ||
2 | |||
3 | export enum VideoPlaylistElementType { | ||
4 | REGULAR = 0, | ||
5 | DELETED = 1, | ||
6 | PRIVATE = 2, | ||
7 | UNAVAILABLE = 3 // Blacklisted, blocked by the user/instance, NSFW... | ||
8 | } | ||
9 | |||
10 | export interface VideoPlaylistElement { | ||
11 | id: number | ||
12 | position: number | ||
13 | startTimestamp: number | ||
14 | stopTimestamp: number | ||
15 | |||
16 | type: VideoPlaylistElementType | ||
17 | |||
18 | video?: Video | ||
19 | } | ||
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 0489147e4..e057b3e06 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts | |||
@@ -17,12 +17,6 @@ export interface VideoFile { | |||
17 | fps: number | 17 | fps: number |
18 | } | 18 | } |
19 | 19 | ||
20 | export interface PlaylistElement { | ||
21 | position: number | ||
22 | startTimestamp: number | ||
23 | stopTimestamp: number | ||
24 | } | ||
25 | |||
26 | export interface Video { | 20 | export interface Video { |
27 | id: number | 21 | id: number |
28 | uuid: string | 22 | uuid: string |
@@ -59,8 +53,6 @@ export interface Video { | |||
59 | userHistory?: { | 53 | userHistory?: { |
60 | currentTime: number | 54 | currentTime: number |
61 | } | 55 | } |
62 | |||
63 | playlistElement?: PlaylistElement | ||
64 | } | 56 | } |
65 | 57 | ||
66 | export interface VideoDetails extends Video { | 58 | export interface VideoDetails extends Video { |
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml index a6f61b3b2..39fa3cef5 100644 --- a/support/doc/api/openapi.yaml +++ b/support/doc/api/openapi.yaml | |||
@@ -1922,6 +1922,9 @@ components: | |||
1922 | type: number | 1922 | type: number |
1923 | stopTimestamp: | 1923 | stopTimestamp: |
1924 | type: number | 1924 | type: number |
1925 | video: | ||
1926 | nullable: true | ||
1927 | $ref: '#/components/schemas/Video' | ||
1925 | VideoFile: | 1928 | VideoFile: |
1926 | properties: | 1929 | properties: |
1927 | magnetUri: | 1930 | magnetUri: |
@@ -2029,9 +2032,6 @@ components: | |||
2029 | properties: | 2032 | properties: |
2030 | currentTime: | 2033 | currentTime: |
2031 | type: number | 2034 | type: number |
2032 | playlistElement: | ||
2033 | nullable: true | ||
2034 | $ref: '#/components/schemas/PlaylistElement' | ||
2035 | VideoDetails: | 2035 | VideoDetails: |
2036 | allOf: | 2036 | allOf: |
2037 | - $ref: '#/components/schemas/Video' | 2037 | - $ref: '#/components/schemas/Video' |