Fix broken playlist api
This commit is contained in:
parent
85394ba22a
commit
bfbd912886
|
@ -18,10 +18,13 @@
|
||||||
<div *ngIf="getVideosOf(videoChannel)" class="videos">
|
<div *ngIf="getVideosOf(videoChannel)" class="videos">
|
||||||
<div class="no-results" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel does not have videos.</div>
|
<div class="no-results" i18n *ngIf="getVideosOf(videoChannel).length === 0">This channel does not have videos.</div>
|
||||||
|
|
||||||
<my-video-miniature *ngFor="let video of getVideosOf(videoChannel)" [video]="video" [user]="user" [displayVideoActions]="false"></my-video-miniature>
|
<my-video-miniature
|
||||||
|
*ngFor="let video of getVideosOf(videoChannel)"
|
||||||
|
[video]="video" [user]="user" [displayVideoActions]="true"
|
||||||
|
></my-video-miniature>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a class="show-more" i18n [routerLink]="getVideoChannelLink(videoChannel)">
|
<a *ngIf="getVideosOf(videoChannel).length !== 0" class="show-more" i18n [routerLink]="getVideoChannelLink(videoChannel)">
|
||||||
Show this channel
|
Show this channel
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,6 +23,11 @@
|
||||||
height: 50px;
|
height: 50px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
my-video-miniature ::ng-deep my-video-actions-dropdown > my-action-dropdown {
|
||||||
|
// Fix our overflow
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,10 +14,10 @@
|
||||||
class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"
|
class="videos" myInfiniteScroller [autoInit]="true" (nearOfBottom)="onNearOfBottom()"
|
||||||
cdkDropList (cdkDropListDropped)="drop($event)"
|
cdkDropList (cdkDropListDropped)="drop($event)"
|
||||||
>
|
>
|
||||||
<div class="video" *ngFor="let video of videos; trackBy: trackByFn" cdkDrag (cdkDragMoved)="onDragMove($event)">
|
<div class="video" *ngFor="let playlistElement of playlistElements; trackBy: trackByFn" cdkDrag>
|
||||||
<my-video-playlist-element-miniature
|
<my-video-playlist-element-miniature
|
||||||
[video]="video" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)"
|
[playlistElement]="playlistElement" [playlist]="playlist" [owned]="true" (elementRemoved)="onElementRemoved($event)"
|
||||||
[position]="video.playlistElement.position"
|
[position]="playlistElement.position"
|
||||||
>
|
>
|
||||||
</my-video-playlist-element-miniature>
|
</my-video-playlist-element-miniature>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,15 +3,13 @@ import { Notifier, ServerService } from '@app/core'
|
||||||
import { AuthService } from '../../core/auth'
|
import { AuthService } from '../../core/auth'
|
||||||
import { ConfirmService } from '../../core/confirm'
|
import { ConfirmService } from '../../core/confirm'
|
||||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||||
import { Video } from '@app/shared/video/video.model'
|
import { Subscription } from 'rxjs'
|
||||||
import { Subject, Subscription } from 'rxjs'
|
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { VideoService } from '@app/shared/video/video.service'
|
|
||||||
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
||||||
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { CdkDragDrop, CdkDragMove } from '@angular/cdk/drag-drop'
|
import { CdkDragDrop } from '@angular/cdk/drag-drop'
|
||||||
import { throttleTime } from 'rxjs/operators'
|
import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-account-video-playlist-elements',
|
selector: 'my-account-video-playlist-elements',
|
||||||
|
@ -19,7 +17,7 @@ import { throttleTime } from 'rxjs/operators'
|
||||||
styleUrls: [ './my-account-video-playlist-elements.component.scss' ]
|
styleUrls: [ './my-account-video-playlist-elements.component.scss' ]
|
||||||
})
|
})
|
||||||
export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy {
|
export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestroy {
|
||||||
videos: Video[] = []
|
playlistElements: VideoPlaylistElement[] = []
|
||||||
playlist: VideoPlaylist
|
playlist: VideoPlaylist
|
||||||
|
|
||||||
pagination: ComponentPagination = {
|
pagination: ComponentPagination = {
|
||||||
|
@ -30,7 +28,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
|
||||||
|
|
||||||
private videoPlaylistId: string | number
|
private videoPlaylistId: string | number
|
||||||
private paramsSub: Subscription
|
private paramsSub: Subscription
|
||||||
private dragMoveSubject = new Subject<number>()
|
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
@ -39,7 +36,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
|
||||||
private confirmService: ConfirmService,
|
private confirmService: ConfirmService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private i18n: I18n,
|
private i18n: I18n,
|
||||||
private videoService: VideoService,
|
|
||||||
private videoPlaylistService: VideoPlaylistService
|
private videoPlaylistService: VideoPlaylistService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -50,10 +46,6 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
|
||||||
|
|
||||||
this.loadPlaylistInfo()
|
this.loadPlaylistInfo()
|
||||||
})
|
})
|
||||||
|
|
||||||
this.dragMoveSubject.asObservable()
|
|
||||||
.pipe(throttleTime(200))
|
|
||||||
.subscribe(y => this.checkScroll(y))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy () {
|
ngOnDestroy () {
|
||||||
|
@ -66,8 +58,8 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
|
||||||
|
|
||||||
if (previousIndex === newIndex) return
|
if (previousIndex === newIndex) return
|
||||||
|
|
||||||
const oldPosition = this.videos[previousIndex].playlistElement.position
|
const oldPosition = this.playlistElements[previousIndex].position
|
||||||
let insertAfter = this.videos[newIndex].playlistElement.position
|
let insertAfter = this.playlistElements[newIndex].position
|
||||||
|
|
||||||
if (oldPosition > insertAfter) insertAfter--
|
if (oldPosition > insertAfter) insertAfter--
|
||||||
|
|
||||||
|
@ -78,42 +70,16 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
|
||||||
err => this.notifier.error(err.message)
|
err => this.notifier.error(err.message)
|
||||||
)
|
)
|
||||||
|
|
||||||
const video = this.videos[previousIndex]
|
const element = this.playlistElements[previousIndex]
|
||||||
|
|
||||||
this.videos.splice(previousIndex, 1)
|
this.playlistElements.splice(previousIndex, 1)
|
||||||
this.videos.splice(newIndex, 0, video)
|
this.playlistElements.splice(newIndex, 0, element)
|
||||||
|
|
||||||
this.reorderClientPositions()
|
this.reorderClientPositions()
|
||||||
}
|
}
|
||||||
|
|
||||||
onDragMove (event: CdkDragMove<any>) {
|
onElementRemoved (element: VideoPlaylistElement) {
|
||||||
this.dragMoveSubject.next(event.pointerPosition.y)
|
this.playlistElements = this.playlistElements.filter(v => v.id !== element.id)
|
||||||
}
|
|
||||||
|
|
||||||
checkScroll (pointerY: number) {
|
|
||||||
// FIXME: Uncomment when https://github.com/angular/material2/issues/14098 is fixed
|
|
||||||
// FIXME: Remove when https://github.com/angular/material2/issues/13588 is implemented
|
|
||||||
// if (pointerY < 150) {
|
|
||||||
// window.scrollBy({
|
|
||||||
// left: 0,
|
|
||||||
// top: -20,
|
|
||||||
// behavior: 'smooth'
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// if (window.innerHeight - pointerY <= 50) {
|
|
||||||
// window.scrollBy({
|
|
||||||
// left: 0,
|
|
||||||
// top: 20,
|
|
||||||
// behavior: 'smooth'
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
onElementRemoved (video: Video) {
|
|
||||||
this.videos = this.videos.filter(v => v.id !== video.id)
|
|
||||||
this.reorderClientPositions()
|
this.reorderClientPositions()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,14 +91,14 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
|
||||||
this.loadElements()
|
this.loadElements()
|
||||||
}
|
}
|
||||||
|
|
||||||
trackByFn (index: number, elem: Video) {
|
trackByFn (index: number, elem: VideoPlaylistElement) {
|
||||||
return elem.id
|
return elem.id
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadElements () {
|
private loadElements () {
|
||||||
this.videoService.getPlaylistVideos(this.videoPlaylistId, this.pagination)
|
this.videoPlaylistService.getPlaylistVideos(this.videoPlaylistId, this.pagination)
|
||||||
.subscribe(({ total, data }) => {
|
.subscribe(({ total, data }) => {
|
||||||
this.videos = this.videos.concat(data)
|
this.playlistElements = this.playlistElements.concat(data)
|
||||||
this.pagination.totalItems = total
|
this.pagination.totalItems = total
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -147,8 +113,8 @@ export class MyAccountVideoPlaylistElementsComponent implements OnInit, OnDestro
|
||||||
private reorderClientPositions () {
|
private reorderClientPositions () {
|
||||||
let i = 1
|
let i = 1
|
||||||
|
|
||||||
for (const video of this.videos) {
|
for (const element of this.playlistElements) {
|
||||||
video.playlistElement.position = i
|
element.position = i
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
||||||
}
|
}
|
||||||
displayOptions = false
|
displayOptions = false
|
||||||
|
|
||||||
|
private playlistElementId: number
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
protected formValidatorService: FormValidatorService,
|
protected formValidatorService: FormValidatorService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
|
@ -96,6 +98,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
||||||
startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined,
|
startTimestamp: existingPlaylist ? existingPlaylist.startTimestamp : undefined,
|
||||||
stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined
|
stopTimestamp: existingPlaylist ? existingPlaylist.stopTimestamp : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
|
this.playlistElementId = existingPlaylist ? existingPlaylist.playlistElementId : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
this.cd.markForCheck()
|
this.cd.markForCheck()
|
||||||
|
@ -177,7 +181,9 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeVideoFromPlaylist (playlist: PlaylistSummary) {
|
private removeVideoFromPlaylist (playlist: PlaylistSummary) {
|
||||||
this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, this.video.id)
|
if (!this.playlistElementId) return
|
||||||
|
|
||||||
|
this.videoPlaylistService.removeVideoFromPlaylist(playlist.id, this.playlistElementId)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.notifier.success(this.i18n('Video removed from {{name}}', { name: playlist.displayName }))
|
this.notifier.success(this.i18n('Video removed from {{name}}', { name: playlist.displayName }))
|
||||||
|
|
|
@ -6,66 +6,82 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<my-video-thumbnail
|
<my-video-thumbnail
|
||||||
[video]="video" [nsfw]="isVideoBlur(video)"
|
*ngIf="playlistElement.video"
|
||||||
|
[video]="playlistElement.video" [nsfw]="isVideoBlur(playlistElement.video)"
|
||||||
[routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
|
[routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
|
||||||
></my-video-thumbnail>
|
></my-video-thumbnail>
|
||||||
|
|
||||||
|
<div class="fake-thumbnail" *ngIf="!playlistElement.video"></div>
|
||||||
|
|
||||||
<div class="video-info">
|
<div class="video-info">
|
||||||
<a tabindex="-1" class="video-info-name"
|
<ng-container *ngIf="playlistElement.video">
|
||||||
[routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
|
<a tabindex="-1" class="video-info-name"
|
||||||
[attr.title]="video.name"
|
[routerLink]="buildRouterLink()" [queryParams]="buildRouterQuery()"
|
||||||
>{{ video.name }}</a>
|
[attr.title]="playlistElement.video.name"
|
||||||
|
>{{ playlistElement.video.name }}</a>
|
||||||
|
|
||||||
<a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', video.byAccount ]">{{ video.byAccount }}</a>
|
<a *ngIf="accountLink" tabindex="-1" class="video-info-account" [routerLink]="[ '/accounts', playlistElement.video.byAccount ]">
|
||||||
<span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ video.byAccount }}</span>
|
{{ playlistElement.video.byAccount }}
|
||||||
|
</a>
|
||||||
|
<span *ngIf="!accountLink" tabindex="-1" class="video-info-account">{{ playlistElement.video.byAccount }}</span>
|
||||||
|
|
||||||
<span tabindex="-1" class="video-info-timestamp">{{ formatTimestamp(video) }}</span>
|
<span tabindex="-1" class="video-info-timestamp">{{ formatTimestamp(playlistElement) }}</span>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<span *ngIf="!playlistElement.video" class="video-info-name">
|
||||||
|
<ng-container i18n *ngIf="isUnavailable(playlistElement)">Unavailable</ng-container>
|
||||||
|
<ng-container i18n *ngIf="isPrivate(playlistElement)">Private</ng-container>
|
||||||
|
<ng-container i18n *ngIf="isDeleted(playlistElement)">Deleted</ng-container>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div *ngIf="owned" class="more" ngbDropdown #moreDropdown="ngbDropdown" placement="bottom-right" (openChange)="onDropdownOpenChange()"
|
<div *ngIf="owned" class="more" ngbDropdown #moreDropdown="ngbDropdown" placement="bottom-right"
|
||||||
autoClose="outside">
|
(openChange)="onDropdownOpenChange()" autoClose="outside"
|
||||||
|
>
|
||||||
<my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more" (click)="$event.preventDefault()"></my-global-icon>
|
<my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more" (click)="$event.preventDefault()"></my-global-icon>
|
||||||
|
|
||||||
<div ngbDropdownMenu>
|
<div ngbDropdownMenu>
|
||||||
<div class="dropdown-item" (click)="toggleDisplayTimestampsOptions($event, video)">
|
<ng-container *ngIf="playlistElement.video">
|
||||||
<my-global-icon iconName="edit"></my-global-icon>
|
<div class="dropdown-item" (click)="toggleDisplayTimestampsOptions($event, playlistElement)">
|
||||||
<ng-container i18n>Edit starts/stops at</ng-container>
|
<my-global-icon iconName="edit"></my-global-icon>
|
||||||
</div>
|
<ng-container i18n>Edit starts/stops at</ng-container>
|
||||||
|
|
||||||
<div class="timestamp-options" *ngIf="displayTimestampOptions">
|
|
||||||
<div>
|
|
||||||
<my-peertube-checkbox
|
|
||||||
inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
|
|
||||||
i18n-labelText labelText="Start at"
|
|
||||||
></my-peertube-checkbox>
|
|
||||||
|
|
||||||
<my-timestamp-input
|
|
||||||
[timestamp]="timestampOptions.startTimestamp"
|
|
||||||
[maxTimestamp]="video.duration"
|
|
||||||
[disabled]="!timestampOptions.startTimestampEnabled"
|
|
||||||
[(ngModel)]="timestampOptions.startTimestamp"
|
|
||||||
></my-timestamp-input>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div class="timestamp-options" *ngIf="displayTimestampOptions">
|
||||||
<my-peertube-checkbox
|
<div>
|
||||||
inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled"
|
<my-peertube-checkbox
|
||||||
i18n-labelText labelText="Stop at"
|
inputName="startAt" [(ngModel)]="timestampOptions.startTimestampEnabled"
|
||||||
></my-peertube-checkbox>
|
i18n-labelText labelText="Start at"
|
||||||
|
></my-peertube-checkbox>
|
||||||
|
|
||||||
<my-timestamp-input
|
<my-timestamp-input
|
||||||
[timestamp]="timestampOptions.stopTimestamp"
|
[timestamp]="timestampOptions.startTimestamp"
|
||||||
[maxTimestamp]="video.duration"
|
[maxTimestamp]="playlistElement.video.duration"
|
||||||
[disabled]="!timestampOptions.stopTimestampEnabled"
|
[disabled]="!timestampOptions.startTimestampEnabled"
|
||||||
[(ngModel)]="timestampOptions.stopTimestamp"
|
[(ngModel)]="timestampOptions.startTimestamp"
|
||||||
></my-timestamp-input>
|
></my-timestamp-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<my-peertube-checkbox
|
||||||
|
inputName="stopAt" [(ngModel)]="timestampOptions.stopTimestampEnabled"
|
||||||
|
i18n-labelText labelText="Stop at"
|
||||||
|
></my-peertube-checkbox>
|
||||||
|
|
||||||
|
<my-timestamp-input
|
||||||
|
[timestamp]="timestampOptions.stopTimestamp"
|
||||||
|
[maxTimestamp]="playlistElement.video.duration"
|
||||||
|
[disabled]="!timestampOptions.stopTimestampEnabled"
|
||||||
|
[(ngModel)]="timestampOptions.stopTimestamp"
|
||||||
|
></my-timestamp-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="submit" i18n-value value="Save" (click)="updateTimestamps(playlistElement)">
|
||||||
</div>
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<input type="submit" i18n-value value="Save" (click)="updateTimestamps(video)">
|
<span class="dropdown-item" (click)="removeFromPlaylist(playlistElement)">
|
||||||
</div>
|
|
||||||
|
|
||||||
<span class="dropdown-item" (click)="removeFromPlaylist(video)">
|
|
||||||
<my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete from {{ playlist?.displayName }}</ng-container>
|
<my-global-icon iconName="delete"></my-global-icon> <ng-container i18n>Delete from {{ playlist?.displayName }}</ng-container>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,9 +2,21 @@
|
||||||
@import '_mixins';
|
@import '_mixins';
|
||||||
@import '_miniature';
|
@import '_miniature';
|
||||||
|
|
||||||
my-video-thumbnail {
|
$thumbnail-width: 130px;
|
||||||
@include thumbnail-size-component(130px, 72px);
|
$thumbnail-height: 72px;
|
||||||
|
|
||||||
|
my-video-thumbnail {
|
||||||
|
@include thumbnail-size-component($thumbnail-width, $thumbnail-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.fake-thumbnail {
|
||||||
|
width: $thumbnail-width;
|
||||||
|
height: $thumbnail-height;
|
||||||
|
background-color: #ececec;
|
||||||
|
}
|
||||||
|
|
||||||
|
my-video-thumbnail,
|
||||||
|
.fake-thumbnail {
|
||||||
display: flex; // Avoids an issue with line-height that adds space below the element
|
display: flex; // Avoids an issue with line-height that adds space below the element
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
@ -31,6 +43,7 @@ my-video-thumbnail {
|
||||||
a {
|
a {
|
||||||
@include disable-default-a-behaviour;
|
@include disable-default-a-behaviour;
|
||||||
|
|
||||||
|
color: var(--mainForegroundColor);
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -58,7 +71,6 @@ my-video-thumbnail {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--mainForegroundColor);
|
|
||||||
width: auto;
|
width: auto;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -66,20 +78,20 @@ my-video-thumbnail {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-info-name {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: $font-semibold;
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
@include ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-info-account, .video-info-timestamp {
|
.video-info-account, .video-info-timestamp {
|
||||||
color: $grey-foreground-color;
|
color: $grey-foreground-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.video-info-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: $font-semibold;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
@include ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.more {
|
.more {
|
||||||
justify-self: flex-end;
|
justify-self: flex-end;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Input, Output, ViewChild } from '@angular/core'
|
||||||
import { Video } from '@app/shared/video/video.model'
|
import { Video } from '@app/shared/video/video.model'
|
||||||
import { VideoPlaylistElementUpdate } from '@shared/models'
|
import { VideoPlaylistElementType, VideoPlaylistElementUpdate } from '@shared/models'
|
||||||
import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core'
|
import { AuthService, ConfirmService, Notifier, ServerService } from '@app/core'
|
||||||
import { ActivatedRoute } from '@angular/router'
|
import { ActivatedRoute } from '@angular/router'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
|
@ -9,6 +9,7 @@ import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.
|
||||||
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'
|
||||||
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
||||||
import { secondsToTime } from '../../../assets/player/utils'
|
import { secondsToTime } from '../../../assets/player/utils'
|
||||||
|
import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-playlist-element-miniature',
|
selector: 'my-video-playlist-element-miniature',
|
||||||
|
@ -20,14 +21,14 @@ export class VideoPlaylistElementMiniatureComponent {
|
||||||
@ViewChild('moreDropdown', { static: false }) moreDropdown: NgbDropdown
|
@ViewChild('moreDropdown', { static: false }) moreDropdown: NgbDropdown
|
||||||
|
|
||||||
@Input() playlist: VideoPlaylist
|
@Input() playlist: VideoPlaylist
|
||||||
@Input() video: Video
|
@Input() playlistElement: VideoPlaylistElement
|
||||||
@Input() owned = false
|
@Input() owned = false
|
||||||
@Input() playing = false
|
@Input() playing = false
|
||||||
@Input() rowLink = false
|
@Input() rowLink = false
|
||||||
@Input() accountLink = true
|
@Input() accountLink = true
|
||||||
@Input() position: number
|
@Input() position: number // Keep this property because we're in the OnPush change detection strategy
|
||||||
|
|
||||||
@Output() elementRemoved = new EventEmitter<Video>()
|
@Output() elementRemoved = new EventEmitter<VideoPlaylistElement>()
|
||||||
|
|
||||||
displayTimestampOptions = false
|
displayTimestampOptions = false
|
||||||
|
|
||||||
|
@ -50,6 +51,18 @@ export class VideoPlaylistElementMiniatureComponent {
|
||||||
private cdr: ChangeDetectorRef
|
private cdr: ChangeDetectorRef
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
isUnavailable (e: VideoPlaylistElement) {
|
||||||
|
return e.type === VideoPlaylistElementType.UNAVAILABLE
|
||||||
|
}
|
||||||
|
|
||||||
|
isPrivate (e: VideoPlaylistElement) {
|
||||||
|
return e.type === VideoPlaylistElementType.PRIVATE
|
||||||
|
}
|
||||||
|
|
||||||
|
isDeleted (e: VideoPlaylistElement) {
|
||||||
|
return e.type === VideoPlaylistElementType.DELETED
|
||||||
|
}
|
||||||
|
|
||||||
buildRouterLink () {
|
buildRouterLink () {
|
||||||
if (!this.playlist) return null
|
if (!this.playlist) return null
|
||||||
|
|
||||||
|
@ -57,12 +70,12 @@ export class VideoPlaylistElementMiniatureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildRouterQuery () {
|
buildRouterQuery () {
|
||||||
if (!this.video) return {}
|
if (!this.playlistElement || !this.playlistElement.video) return {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
videoId: this.video.uuid,
|
videoId: this.playlistElement.video.uuid,
|
||||||
start: this.video.playlistElement.startTimestamp,
|
start: this.playlistElement.startTimestamp,
|
||||||
stop: this.video.playlistElement.stopTimestamp
|
stop: this.playlistElement.stopTimestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,13 +83,13 @@ export class VideoPlaylistElementMiniatureComponent {
|
||||||
return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig())
|
return video.isVideoNSFWForUser(this.authService.getUser(), this.serverService.getConfig())
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFromPlaylist (video: Video) {
|
removeFromPlaylist (playlistElement: VideoPlaylistElement) {
|
||||||
this.videoPlaylistService.removeVideoFromPlaylist(this.playlist.id, video.id)
|
this.videoPlaylistService.removeVideoFromPlaylist(this.playlist.id, playlistElement.id)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName }))
|
this.notifier.success(this.i18n('Video removed from {{name}}', { name: this.playlist.displayName }))
|
||||||
|
|
||||||
this.elementRemoved.emit(this.video)
|
this.elementRemoved.emit(playlistElement)
|
||||||
},
|
},
|
||||||
|
|
||||||
err => this.notifier.error(err.message)
|
err => this.notifier.error(err.message)
|
||||||
|
@ -85,19 +98,19 @@ export class VideoPlaylistElementMiniatureComponent {
|
||||||
this.moreDropdown.close()
|
this.moreDropdown.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTimestamps (video: Video) {
|
updateTimestamps (playlistElement: VideoPlaylistElement) {
|
||||||
const body: VideoPlaylistElementUpdate = {}
|
const body: VideoPlaylistElementUpdate = {}
|
||||||
|
|
||||||
body.startTimestamp = this.timestampOptions.startTimestampEnabled ? this.timestampOptions.startTimestamp : null
|
body.startTimestamp = this.timestampOptions.startTimestampEnabled ? this.timestampOptions.startTimestamp : null
|
||||||
body.stopTimestamp = this.timestampOptions.stopTimestampEnabled ? this.timestampOptions.stopTimestamp : null
|
body.stopTimestamp = this.timestampOptions.stopTimestampEnabled ? this.timestampOptions.stopTimestamp : null
|
||||||
|
|
||||||
this.videoPlaylistService.updateVideoOfPlaylist(this.playlist.id, video.id, body)
|
this.videoPlaylistService.updateVideoOfPlaylist(this.playlist.id, playlistElement.id, body)
|
||||||
.subscribe(
|
.subscribe(
|
||||||
() => {
|
() => {
|
||||||
this.notifier.success(this.i18n('Timestamps updated'))
|
this.notifier.success(this.i18n('Timestamps updated'))
|
||||||
|
|
||||||
video.playlistElement.startTimestamp = body.startTimestamp
|
playlistElement.startTimestamp = body.startTimestamp
|
||||||
video.playlistElement.stopTimestamp = body.stopTimestamp
|
playlistElement.stopTimestamp = body.stopTimestamp
|
||||||
|
|
||||||
this.cdr.detectChanges()
|
this.cdr.detectChanges()
|
||||||
},
|
},
|
||||||
|
@ -108,9 +121,9 @@ export class VideoPlaylistElementMiniatureComponent {
|
||||||
this.moreDropdown.close()
|
this.moreDropdown.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
formatTimestamp (video: Video) {
|
formatTimestamp (playlistElement: VideoPlaylistElement) {
|
||||||
const start = video.playlistElement.startTimestamp
|
const start = playlistElement.startTimestamp
|
||||||
const stop = video.playlistElement.stopTimestamp
|
const stop = playlistElement.stopTimestamp
|
||||||
|
|
||||||
const startFormatted = secondsToTime(start, true, ':')
|
const startFormatted = secondsToTime(start, true, ':')
|
||||||
const stopFormatted = secondsToTime(stop, true, ':')
|
const stopFormatted = secondsToTime(stop, true, ':')
|
||||||
|
@ -127,7 +140,7 @@ export class VideoPlaylistElementMiniatureComponent {
|
||||||
this.displayTimestampOptions = false
|
this.displayTimestampOptions = false
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDisplayTimestampsOptions (event: Event, video: Video) {
|
toggleDisplayTimestampsOptions (event: Event, playlistElement: VideoPlaylistElement) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
this.displayTimestampOptions = !this.displayTimestampOptions
|
this.displayTimestampOptions = !this.displayTimestampOptions
|
||||||
|
@ -137,17 +150,17 @@ export class VideoPlaylistElementMiniatureComponent {
|
||||||
startTimestampEnabled: false,
|
startTimestampEnabled: false,
|
||||||
stopTimestampEnabled: false,
|
stopTimestampEnabled: false,
|
||||||
startTimestamp: 0,
|
startTimestamp: 0,
|
||||||
stopTimestamp: video.duration
|
stopTimestamp: playlistElement.video.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video.playlistElement.startTimestamp) {
|
if (playlistElement.startTimestamp) {
|
||||||
this.timestampOptions.startTimestampEnabled = true
|
this.timestampOptions.startTimestampEnabled = true
|
||||||
this.timestampOptions.startTimestamp = video.playlistElement.startTimestamp
|
this.timestampOptions.startTimestamp = playlistElement.startTimestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
if (video.playlistElement.stopTimestamp) {
|
if (playlistElement.stopTimestamp) {
|
||||||
this.timestampOptions.stopTimestampEnabled = true
|
this.timestampOptions.stopTimestampEnabled = true
|
||||||
this.timestampOptions.stopTimestamp = video.playlistElement.stopTimestamp
|
this.timestampOptions.stopTimestamp = playlistElement.stopTimestamp
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { VideoPlaylistElement as ServerVideoPlaylistElement, VideoPlaylistElementType } from '../../../../../shared/models/videos'
|
||||||
|
import { Video } from '@app/shared/video/video.model'
|
||||||
|
|
||||||
|
export class VideoPlaylistElement implements ServerVideoPlaylistElement {
|
||||||
|
id: number
|
||||||
|
position: number
|
||||||
|
startTimestamp: number
|
||||||
|
stopTimestamp: number
|
||||||
|
|
||||||
|
type: VideoPlaylistElementType
|
||||||
|
|
||||||
|
video?: Video
|
||||||
|
|
||||||
|
constructor (hash: ServerVideoPlaylistElement, translations: {}) {
|
||||||
|
this.id = hash.id
|
||||||
|
this.position = hash.position
|
||||||
|
this.startTimestamp = hash.startTimestamp
|
||||||
|
this.stopTimestamp = hash.stopTimestamp
|
||||||
|
|
||||||
|
this.type = hash.type
|
||||||
|
|
||||||
|
if (hash.video) this.video = new Video(hash.video, translations)
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,9 @@ import { Account } from '@app/shared/account/account.model'
|
||||||
import { RestService } from '@app/shared/rest'
|
import { RestService } from '@app/shared/rest'
|
||||||
import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model'
|
import { VideoExistInPlaylist } from '@shared/models/videos/playlist/video-exist-in-playlist.model'
|
||||||
import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model'
|
import { VideoPlaylistReorder } from '@shared/models/videos/playlist/video-playlist-reorder.model'
|
||||||
|
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||||
|
import { VideoPlaylistElement as ServerVideoPlaylistElement } from '@shared/models/videos/playlist/video-playlist-element.model'
|
||||||
|
import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class VideoPlaylistService {
|
export class VideoPlaylistService {
|
||||||
|
@ -110,16 +113,16 @@ export class VideoPlaylistService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateVideoOfPlaylist (playlistId: number, videoId: number, body: VideoPlaylistElementUpdate) {
|
updateVideoOfPlaylist (playlistId: number, playlistElementId: number, body: VideoPlaylistElementUpdate) {
|
||||||
return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + videoId, body)
|
return this.authHttp.put(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId, body)
|
||||||
.pipe(
|
.pipe(
|
||||||
map(this.restExtractor.extractDataBool),
|
map(this.restExtractor.extractDataBool),
|
||||||
catchError(err => this.restExtractor.handleError(err))
|
catchError(err => this.restExtractor.handleError(err))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
removeVideoFromPlaylist (playlistId: number, videoId: number) {
|
removeVideoFromPlaylist (playlistId: number, playlistElementId: number) {
|
||||||
return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + videoId)
|
return this.authHttp.delete(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + playlistId + '/videos/' + playlistElementId)
|
||||||
.pipe(
|
.pipe(
|
||||||
map(this.restExtractor.extractDataBool),
|
map(this.restExtractor.extractDataBool),
|
||||||
catchError(err => this.restExtractor.handleError(err))
|
catchError(err => this.restExtractor.handleError(err))
|
||||||
|
@ -139,6 +142,24 @@ export class VideoPlaylistService {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPlaylistVideos (
|
||||||
|
videoPlaylistId: number | string,
|
||||||
|
componentPagination: ComponentPagination
|
||||||
|
): Observable<ResultList<VideoPlaylistElement>> {
|
||||||
|
const path = VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylistId + '/videos'
|
||||||
|
const pagination = this.restService.componentPaginationToRestPagination(componentPagination)
|
||||||
|
|
||||||
|
let params = new HttpParams()
|
||||||
|
params = this.restService.addRestGetParams(params, pagination)
|
||||||
|
|
||||||
|
return this.authHttp
|
||||||
|
.get<ResultList<ServerVideoPlaylistElement>>(path, { params })
|
||||||
|
.pipe(
|
||||||
|
switchMap(res => this.extractVideoPlaylistElements(res)),
|
||||||
|
catchError(err => this.restExtractor.handleError(err))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
doesVideoExistInPlaylist (videoId: number) {
|
doesVideoExistInPlaylist (videoId: number) {
|
||||||
this.videoExistsInPlaylistSubject.next(videoId)
|
this.videoExistsInPlaylistSubject.next(videoId)
|
||||||
|
|
||||||
|
@ -167,6 +188,23 @@ export class VideoPlaylistService {
|
||||||
.pipe(map(translations => new VideoPlaylist(playlist, translations)))
|
.pipe(map(translations => new VideoPlaylist(playlist, translations)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extractVideoPlaylistElements (result: ResultList<ServerVideoPlaylistElement>) {
|
||||||
|
return this.serverService.localeObservable
|
||||||
|
.pipe(
|
||||||
|
map(translations => {
|
||||||
|
const elementsJson = result.data
|
||||||
|
const total = result.total
|
||||||
|
const elements: VideoPlaylistElement[] = []
|
||||||
|
|
||||||
|
for (const elementJson of elementsJson) {
|
||||||
|
elements.push(new VideoPlaylistElement(elementJson, translations))
|
||||||
|
}
|
||||||
|
|
||||||
|
return { total, data: elements }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> {
|
private doVideosExistInPlaylist (videoIds: number[]): Observable<VideoExistInPlaylist> {
|
||||||
const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist'
|
const url = VideoPlaylistService.MY_VIDEO_PLAYLIST_URL + 'videos-exist'
|
||||||
let params = new HttpParams()
|
let params = new HttpParams()
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { User } from '../'
|
import { User } from '../'
|
||||||
import { PlaylistElement, UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
|
import { UserRight, Video as VideoServerModel, VideoPrivacy, VideoState } from '../../../../../shared'
|
||||||
import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
|
import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
|
||||||
import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model'
|
import { VideoConstant } from '../../../../../shared/models/videos/video-constant.model'
|
||||||
import { durationToString, getAbsoluteAPIUrl } from '../misc/utils'
|
import { durationToString, getAbsoluteAPIUrl } from '../misc/utils'
|
||||||
|
@ -48,8 +48,6 @@ export class Video implements VideoServerModel {
|
||||||
blacklisted?: boolean
|
blacklisted?: boolean
|
||||||
blacklistedReason?: string
|
blacklistedReason?: string
|
||||||
|
|
||||||
playlistElement?: PlaylistElement
|
|
||||||
|
|
||||||
account: {
|
account: {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
|
@ -126,8 +124,6 @@ export class Video implements VideoServerModel {
|
||||||
this.blacklistedReason = hash.blacklistedReason
|
this.blacklistedReason = hash.blacklistedReason
|
||||||
|
|
||||||
this.userHistory = hash.userHistory
|
this.userHistory = hash.userHistory
|
||||||
|
|
||||||
this.playlistElement = hash.playlistElement
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
|
isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
|
||||||
|
|
|
@ -31,7 +31,6 @@ import { ServerService } from '@app/core'
|
||||||
import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
|
import { UserSubscriptionService } from '@app/shared/user-subscription/user-subscription.service'
|
||||||
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
import { VideoChannel } from '@app/shared/video-channel/video-channel.model'
|
||||||
import { I18n } from '@ngx-translate/i18n-polyfill'
|
import { I18n } from '@ngx-translate/i18n-polyfill'
|
||||||
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
|
||||||
|
|
||||||
export interface VideosProvider {
|
export interface VideosProvider {
|
||||||
getVideos (parameters: {
|
getVideos (parameters: {
|
||||||
|
@ -172,23 +171,6 @@ export class VideoService implements VideosProvider {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlaylistVideos (
|
|
||||||
videoPlaylistId: number | string,
|
|
||||||
videoPagination: ComponentPagination
|
|
||||||
): Observable<ResultList<Video>> {
|
|
||||||
const pagination = this.restService.componentPaginationToRestPagination(videoPagination)
|
|
||||||
|
|
||||||
let params = new HttpParams()
|
|
||||||
params = this.restService.addRestGetParams(params, pagination)
|
|
||||||
|
|
||||||
return this.authHttp
|
|
||||||
.get<ResultList<Video>>(VideoPlaylistService.BASE_VIDEO_PLAYLIST_URL + videoPlaylistId + '/videos', { params })
|
|
||||||
.pipe(
|
|
||||||
switchMap(res => this.extractVideos(res)),
|
|
||||||
catchError(err => this.restExtractor.handleError(err))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserSubscriptionVideos (parameters: {
|
getUserSubscriptionVideos (parameters: {
|
||||||
videoPagination: ComponentPagination,
|
videoPagination: ComponentPagination,
|
||||||
sort: VideoSortField
|
sort: VideoSortField
|
||||||
|
|
|
@ -16,10 +16,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div *ngFor="let playlistVideo of playlistVideos">
|
<div *ngFor="let playlistElement of playlistElements">
|
||||||
<my-video-playlist-element-miniature
|
<my-video-playlist-element-miniature
|
||||||
[video]="playlistVideo" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)"
|
[playlistElement]="playlistElement" [playlist]="playlist" [owned]="isPlaylistOwned()" (elementRemoved)="onElementRemoved($event)"
|
||||||
[playing]="currentPlaylistPosition === playlistVideo.playlistElement.position" [accountLink]="false" [position]="playlistVideo.playlistElement.position"
|
[playing]="currentPlaylistPosition === playlistElement.position" [accountLink]="false" [position]="playlistElement.position"
|
||||||
></my-video-playlist-element-miniature>
|
></my-video-playlist-element-miniature>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -53,6 +53,11 @@
|
||||||
my-video-thumbnail {
|
my-video-thumbnail {
|
||||||
@include thumbnail-size-component(90px, 50px);
|
@include thumbnail-size-component(90px, 50px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fake-thumbnail {
|
||||||
|
width: 90px;
|
||||||
|
height: 50px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { Component, Input } from '@angular/core'
|
import { Component, Input } from '@angular/core'
|
||||||
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
import { VideoPlaylist } from '@app/shared/video-playlist/video-playlist.model'
|
||||||
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
import { ComponentPagination } from '@app/shared/rest/component-pagination.model'
|
||||||
import { Video } from '@app/shared/video/video.model'
|
|
||||||
import { VideoDetails, VideoPlaylistPrivacy } from '@shared/models'
|
import { VideoDetails, VideoPlaylistPrivacy } from '@shared/models'
|
||||||
import { VideoService } from '@app/shared/video/video.service'
|
|
||||||
import { Router } from '@angular/router'
|
import { Router } from '@angular/router'
|
||||||
import { AuthService } from '@app/core'
|
import { AuthService } from '@app/core'
|
||||||
|
import { VideoPlaylistService } from '@app/shared/video-playlist/video-playlist.service'
|
||||||
|
import { VideoPlaylistElement } from '@app/shared/video-playlist/video-playlist-element.model'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'my-video-watch-playlist',
|
selector: 'my-video-watch-playlist',
|
||||||
|
@ -16,7 +16,7 @@ export class VideoWatchPlaylistComponent {
|
||||||
@Input() video: VideoDetails
|
@Input() video: VideoDetails
|
||||||
@Input() playlist: VideoPlaylist
|
@Input() playlist: VideoPlaylist
|
||||||
|
|
||||||
playlistVideos: Video[] = []
|
playlistElements: VideoPlaylistElement[] = []
|
||||||
playlistPagination: ComponentPagination = {
|
playlistPagination: ComponentPagination = {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
itemsPerPage: 30,
|
itemsPerPage: 30,
|
||||||
|
@ -28,7 +28,7 @@ export class VideoWatchPlaylistComponent {
|
||||||
|
|
||||||
constructor (
|
constructor (
|
||||||
private auth: AuthService,
|
private auth: AuthService,
|
||||||
private videoService: VideoService,
|
private videoPlaylist: VideoPlaylistService,
|
||||||
private router: Router
|
private router: Router
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@ -40,8 +40,8 @@ export class VideoWatchPlaylistComponent {
|
||||||
this.loadPlaylistElements(this.playlist,false)
|
this.loadPlaylistElements(this.playlist,false)
|
||||||
}
|
}
|
||||||
|
|
||||||
onElementRemoved (video: Video) {
|
onElementRemoved (playlistElement: VideoPlaylistElement) {
|
||||||
this.playlistVideos = this.playlistVideos.filter(v => v.id !== video.id)
|
this.playlistElements = this.playlistElements.filter(e => e.id !== playlistElement.id)
|
||||||
|
|
||||||
this.playlistPagination.totalItems--
|
this.playlistPagination.totalItems--
|
||||||
}
|
}
|
||||||
|
@ -65,12 +65,13 @@ export class VideoWatchPlaylistComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) {
|
loadPlaylistElements (playlist: VideoPlaylist, redirectToFirst = false) {
|
||||||
this.videoService.getPlaylistVideos(playlist.uuid, this.playlistPagination)
|
this.videoPlaylist.getPlaylistVideos(playlist.uuid, this.playlistPagination)
|
||||||
.subscribe(({ total, data }) => {
|
.subscribe(({ total, data }) => {
|
||||||
this.playlistVideos = this.playlistVideos.concat(data)
|
this.playlistElements = this.playlistElements.concat(data)
|
||||||
this.playlistPagination.totalItems = total
|
this.playlistPagination.totalItems = total
|
||||||
|
|
||||||
if (total === 0) {
|
const firstAvailableVideos = this.playlistElements.find(e => !!e.video)
|
||||||
|
if (!firstAvailableVideos) {
|
||||||
this.noPlaylistVideos = true
|
this.noPlaylistVideos = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -79,7 +80,7 @@ export class VideoWatchPlaylistComponent {
|
||||||
|
|
||||||
if (redirectToFirst) {
|
if (redirectToFirst) {
|
||||||
const extras = {
|
const extras = {
|
||||||
queryParams: { videoId: this.playlistVideos[ 0 ].uuid },
|
queryParams: { videoId: firstAvailableVideos.video.uuid },
|
||||||
replaceUrl: true
|
replaceUrl: true
|
||||||
}
|
}
|
||||||
this.router.navigate([], extras)
|
this.router.navigate([], extras)
|
||||||
|
@ -88,11 +89,11 @@ export class VideoWatchPlaylistComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePlaylistIndex (video: VideoDetails) {
|
updatePlaylistIndex (video: VideoDetails) {
|
||||||
if (this.playlistVideos.length === 0 || !video) return
|
if (this.playlistElements.length === 0 || !video) return
|
||||||
|
|
||||||
for (const playlistVideo of this.playlistVideos) {
|
for (const playlistElement of this.playlistElements) {
|
||||||
if (playlistVideo.id === video.id) {
|
if (playlistElement.video && playlistElement.video.id === video.id) {
|
||||||
this.currentPlaylistPosition = playlistVideo.playlistElement.position
|
this.currentPlaylistPosition = playlistElement.position
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -103,11 +104,17 @@ export class VideoWatchPlaylistComponent {
|
||||||
|
|
||||||
navigateToNextPlaylistVideo () {
|
navigateToNextPlaylistVideo () {
|
||||||
if (this.currentPlaylistPosition < this.playlistPagination.totalItems) {
|
if (this.currentPlaylistPosition < this.playlistPagination.totalItems) {
|
||||||
const next = this.playlistVideos.find(v => v.playlistElement.position === this.currentPlaylistPosition + 1)
|
const next = this.playlistElements.find(e => e.position === this.currentPlaylistPosition + 1)
|
||||||
|
|
||||||
const start = next.playlistElement.startTimestamp
|
if (!next || !next.video) {
|
||||||
const stop = next.playlistElement.stopTimestamp
|
this.currentPlaylistPosition++
|
||||||
this.router.navigate([],{ queryParams: { videoId: next.uuid, start, stop } })
|
this.navigateToNextPlaylistVideo()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = next.startTimestamp
|
||||||
|
const stop = next.stopTimestamp
|
||||||
|
this.router.navigate([],{ queryParams: { videoId: next.video.uuid, start, stop } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -464,7 +464,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.zone.runOutsideAngular(async () => {
|
this.zone.runOutsideAngular(async () => {
|
||||||
this.player = await PeertubePlayerManager.initialize(mode, options)
|
this.player = await PeertubePlayerManager.initialize(mode, options, player => this.player = player)
|
||||||
|
|
||||||
this.player.on('customError', ({ err }: { err: any }) => this.handleError(err))
|
this.player.on('customError', ({ err }: { err: any }) => this.handleError(err))
|
||||||
|
|
||||||
|
|
|
@ -86,6 +86,7 @@ export class PeertubePlayerManager {
|
||||||
|
|
||||||
private static videojsLocaleCache: { [ path: string ]: any } = {}
|
private static videojsLocaleCache: { [ path: string ]: any } = {}
|
||||||
private static playerElementClassName: string
|
private static playerElementClassName: string
|
||||||
|
private static onPlayerChange: (player: any) => void
|
||||||
|
|
||||||
static getServerTranslations (serverUrl: string, locale: string) {
|
static getServerTranslations (serverUrl: string, locale: string) {
|
||||||
const path = PeertubePlayerManager.getLocalePath(serverUrl, locale)
|
const path = PeertubePlayerManager.getLocalePath(serverUrl, locale)
|
||||||
|
@ -100,9 +101,10 @@ export class PeertubePlayerManager {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions) {
|
static async initialize (mode: PlayerMode, options: PeertubePlayerManagerOptions, onPlayerChange: (player: any) => void) {
|
||||||
let p2pMediaLoader: any
|
let p2pMediaLoader: any
|
||||||
|
|
||||||
|
this.onPlayerChange = onPlayerChange
|
||||||
this.playerElementClassName = options.common.playerElement.className
|
this.playerElementClassName = options.common.playerElement.className
|
||||||
|
|
||||||
if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin')
|
if (mode === 'webtorrent') await import('./webtorrent/webtorrent-plugin')
|
||||||
|
@ -171,6 +173,8 @@ export class PeertubePlayerManager {
|
||||||
const player = this
|
const player = this
|
||||||
|
|
||||||
self.addContextMenu(mode, player, options.common.embedUrl)
|
self.addContextMenu(mode, player, options.common.embedUrl)
|
||||||
|
|
||||||
|
PeertubePlayerManager.onPlayerChange(player)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,9 @@ class SettingsMenuItem extends MenuItem {
|
||||||
player.ready(() => {
|
player.ready(() => {
|
||||||
// Voodoo magic for IOS
|
// Voodoo magic for IOS
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
// Player was destroyed
|
||||||
|
if (!this.player_) return
|
||||||
|
|
||||||
this.build()
|
this.build()
|
||||||
|
|
||||||
// Update on rate change
|
// Update on rate change
|
||||||
|
|
|
@ -212,7 +212,7 @@ export class PeerTubeEmbed {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
this.player = await PeertubePlayerManager.initialize(this.mode, options)
|
this.player = await PeertubePlayerManager.initialize(this.mode, options, player => this.player = player)
|
||||||
this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
|
this.player.on('customError', (event: any, data: any) => this.handleError(data.err, serverTranslations))
|
||||||
|
|
||||||
window[ 'videojsPlayer' ] = this.player
|
window[ 'videojsPlayer' ] = this.player
|
||||||
|
|
|
@ -35,6 +35,7 @@ async function doVideosInPlaylistExist (req: express.Request, res: express.Respo
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
for (const element of result.VideoPlaylistElements) {
|
for (const element of result.VideoPlaylistElements) {
|
||||||
existObject[element.videoId].push({
|
existObject[element.videoId].push({
|
||||||
|
playlistElementId: element.id,
|
||||||
playlistId: result.id,
|
playlistId: result.id,
|
||||||
startTimestamp: element.startTimestamp,
|
startTimestamp: element.startTimestamp,
|
||||||
stopTimestamp: element.stopTimestamp
|
stopTimestamp: element.stopTimestamp
|
||||||
|
|
|
@ -4,14 +4,13 @@ import {
|
||||||
asyncMiddleware,
|
asyncMiddleware,
|
||||||
asyncRetryTransactionMiddleware,
|
asyncRetryTransactionMiddleware,
|
||||||
authenticate,
|
authenticate,
|
||||||
commonVideosFiltersValidator,
|
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
setDefaultSort
|
setDefaultSort
|
||||||
} from '../../middlewares'
|
} from '../../middlewares'
|
||||||
import { videoPlaylistsSortValidator } from '../../middlewares/validators'
|
import { videoPlaylistsSortValidator } from '../../middlewares/validators'
|
||||||
import { buildNSFWFilter, createReqFiles, isUserAbleToSearchRemoteURI } from '../../helpers/express-utils'
|
import { buildNSFWFilter, createReqFiles } from '../../helpers/express-utils'
|
||||||
import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants'
|
import { MIMETYPES, VIDEO_PLAYLIST_PRIVACIES } from '../../initializers/constants'
|
||||||
import { logger } from '../../helpers/logger'
|
import { logger } from '../../helpers/logger'
|
||||||
import { resetSequelizeInstance } from '../../helpers/database-utils'
|
import { resetSequelizeInstance } from '../../helpers/database-utils'
|
||||||
|
@ -32,7 +31,6 @@ import { join } from 'path'
|
||||||
import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send'
|
import { sendCreateVideoPlaylist, sendDeleteVideoPlaylist, sendUpdateVideoPlaylist } from '../../lib/activitypub/send'
|
||||||
import { getVideoPlaylistActivityPubUrl, getVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url'
|
import { getVideoPlaylistActivityPubUrl, getVideoPlaylistElementActivityPubUrl } from '../../lib/activitypub/url'
|
||||||
import { VideoPlaylistUpdate } from '../../../shared/models/videos/playlist/video-playlist-update.model'
|
import { VideoPlaylistUpdate } from '../../../shared/models/videos/playlist/video-playlist-update.model'
|
||||||
import { VideoModel } from '../../models/video/video'
|
|
||||||
import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
|
import { VideoPlaylistElementModel } from '../../models/video/video-playlist-element'
|
||||||
import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model'
|
import { VideoPlaylistElementCreate } from '../../../shared/models/videos/playlist/video-playlist-element-create.model'
|
||||||
import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model'
|
import { VideoPlaylistElementUpdate } from '../../../shared/models/videos/playlist/video-playlist-element-update.model'
|
||||||
|
@ -88,7 +86,6 @@ videoPlaylistRouter.get('/:playlistId/videos',
|
||||||
paginationValidator,
|
paginationValidator,
|
||||||
setDefaultPagination,
|
setDefaultPagination,
|
||||||
optionalAuthenticate,
|
optionalAuthenticate,
|
||||||
commonVideosFiltersValidator,
|
|
||||||
asyncMiddleware(getVideoPlaylistVideos)
|
asyncMiddleware(getVideoPlaylistVideos)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -104,13 +101,13 @@ videoPlaylistRouter.post('/:playlistId/videos/reorder',
|
||||||
asyncRetryTransactionMiddleware(reorderVideosPlaylist)
|
asyncRetryTransactionMiddleware(reorderVideosPlaylist)
|
||||||
)
|
)
|
||||||
|
|
||||||
videoPlaylistRouter.put('/:playlistId/videos/:videoId',
|
videoPlaylistRouter.put('/:playlistId/videos/:playlistElementId',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator),
|
asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator),
|
||||||
asyncRetryTransactionMiddleware(updateVideoPlaylistElement)
|
asyncRetryTransactionMiddleware(updateVideoPlaylistElement)
|
||||||
)
|
)
|
||||||
|
|
||||||
videoPlaylistRouter.delete('/:playlistId/videos/:videoId',
|
videoPlaylistRouter.delete('/:playlistId/videos/:playlistElementId',
|
||||||
authenticate,
|
authenticate,
|
||||||
asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator),
|
asyncMiddleware(videoPlaylistsUpdateOrRemoveVideoValidator),
|
||||||
asyncRetryTransactionMiddleware(removeVideoFromPlaylist)
|
asyncRetryTransactionMiddleware(removeVideoFromPlaylist)
|
||||||
|
@ -426,26 +423,20 @@ async function reorderVideosPlaylist (req: express.Request, res: express.Respons
|
||||||
|
|
||||||
async function getVideoPlaylistVideos (req: express.Request, res: express.Response) {
|
async function getVideoPlaylistVideos (req: express.Request, res: express.Response) {
|
||||||
const videoPlaylistInstance = res.locals.videoPlaylist
|
const videoPlaylistInstance = res.locals.videoPlaylist
|
||||||
const followerActorId = isUserAbleToSearchRemoteURI(res) ? null : undefined
|
const user = res.locals.oauth ? res.locals.oauth.token.User : undefined
|
||||||
|
const server = await getServerActor()
|
||||||
|
|
||||||
const resultList = await VideoModel.listForApi({
|
const resultList = await VideoPlaylistElementModel.listForApi({
|
||||||
followerActorId,
|
|
||||||
start: req.query.start,
|
start: req.query.start,
|
||||||
count: req.query.count,
|
count: req.query.count,
|
||||||
sort: 'VideoPlaylistElements.position',
|
|
||||||
includeLocalVideos: true,
|
|
||||||
categoryOneOf: req.query.categoryOneOf,
|
|
||||||
licenceOneOf: req.query.licenceOneOf,
|
|
||||||
languageOneOf: req.query.languageOneOf,
|
|
||||||
tagsOneOf: req.query.tagsOneOf,
|
|
||||||
tagsAllOf: req.query.tagsAllOf,
|
|
||||||
filter: req.query.filter,
|
|
||||||
nsfw: buildNSFWFilter(res, req.query.nsfw),
|
|
||||||
withFiles: false,
|
|
||||||
videoPlaylistId: videoPlaylistInstance.id,
|
videoPlaylistId: videoPlaylistInstance.id,
|
||||||
user: res.locals.oauth ? res.locals.oauth.token.User : undefined
|
serverAccount: server.Account,
|
||||||
|
user
|
||||||
})
|
})
|
||||||
|
|
||||||
const additionalAttributes = { playlistInfo: true }
|
const options = {
|
||||||
return res.json(getFormattedObjects(resultList.data, resultList.total, { additionalAttributes }))
|
displayNSFW: buildNSFWFilter(res, req.query.nsfw),
|
||||||
|
accountId: user ? user.Account.id : undefined
|
||||||
|
}
|
||||||
|
return res.json(getFormattedObjects(resultList.data, resultList.total, options))
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 405
|
const LAST_MIGRATION_VERSION = 410
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
import * as Sequelize from 'sequelize'
|
||||||
|
|
||||||
|
async function up (utils: {
|
||||||
|
transaction: Sequelize.Transaction,
|
||||||
|
queryInterface: Sequelize.QueryInterface,
|
||||||
|
sequelize: Sequelize.Sequelize,
|
||||||
|
db: any
|
||||||
|
}): Promise<void> {
|
||||||
|
{
|
||||||
|
const data = {
|
||||||
|
type: Sequelize.INTEGER,
|
||||||
|
allowNull: true,
|
||||||
|
defaultValue: null
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.queryInterface.changeColumn('videoPlaylistElement', 'videoId', data)
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.queryInterface.removeConstraint('videoPlaylistElement', 'videoPlaylistElement_videoId_fkey')
|
||||||
|
|
||||||
|
await utils.queryInterface.addConstraint('videoPlaylistElement', [ 'videoId' ], {
|
||||||
|
type: 'foreign key',
|
||||||
|
references: {
|
||||||
|
table: 'video',
|
||||||
|
field: 'id'
|
||||||
|
},
|
||||||
|
onDelete: 'set null',
|
||||||
|
onUpdate: 'CASCADE'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function down (options) {
|
||||||
|
throw new Error('Not implemented.')
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
up,
|
||||||
|
down
|
||||||
|
}
|
|
@ -207,8 +207,8 @@ const videoPlaylistsAddVideoValidator = [
|
||||||
const videoPlaylistsUpdateOrRemoveVideoValidator = [
|
const videoPlaylistsUpdateOrRemoveVideoValidator = [
|
||||||
param('playlistId')
|
param('playlistId')
|
||||||
.custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
|
.custom(isIdOrUUIDValid).withMessage('Should have a valid playlist id/uuid'),
|
||||||
param('videoId')
|
param('playlistElementId')
|
||||||
.custom(isIdOrUUIDValid).withMessage('Should have an video id/uuid'),
|
.custom(isIdValid).withMessage('Should have an element id/uuid'),
|
||||||
body('startTimestamp')
|
body('startTimestamp')
|
||||||
.optional()
|
.optional()
|
||||||
.custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
|
.custom(isVideoPlaylistTimestampValid).withMessage('Should have a valid start timestamp'),
|
||||||
|
@ -222,12 +222,10 @@ const videoPlaylistsUpdateOrRemoveVideoValidator = [
|
||||||
if (areValidationErrors(req, res)) return
|
if (areValidationErrors(req, res)) return
|
||||||
|
|
||||||
if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
|
if (!await doesVideoPlaylistExist(req.params.playlistId, res, 'all')) return
|
||||||
if (!await doesVideoExist(req.params.videoId, res, 'id')) return
|
|
||||||
|
|
||||||
const videoPlaylist = res.locals.videoPlaylist
|
const videoPlaylist = res.locals.videoPlaylist
|
||||||
const video = res.locals.video
|
|
||||||
|
|
||||||
const videoPlaylistElement = await VideoPlaylistElementModel.loadByPlaylistAndVideo(videoPlaylist.id, video.id)
|
const videoPlaylistElement = await VideoPlaylistElementModel.loadById(req.params.playlistElementId)
|
||||||
if (!videoPlaylistElement) {
|
if (!videoPlaylistElement) {
|
||||||
res.status(404)
|
res.status(404)
|
||||||
.json({ error: 'Video playlist element not found' })
|
.json({ error: 'Video playlist element not found' })
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { ActorModel } from '../activitypub/actor'
|
||||||
import { getSort, throwIfNotValid } from '../utils'
|
import { getSort, throwIfNotValid } from '../utils'
|
||||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||||
import { AccountVideoRate } from '../../../shared'
|
import { AccountVideoRate } from '../../../shared'
|
||||||
import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from '../video/video-channel'
|
import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Account rates per video.
|
Account rates per video.
|
||||||
|
@ -109,7 +109,7 @@ export class AccountVideoRateModel extends Model<AccountVideoRateModel> {
|
||||||
required: true,
|
required: true,
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, true] }),
|
model: VideoChannelModel.scope({ method: [VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
|
||||||
required: true
|
required: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
@ -27,12 +27,19 @@ import { UserModel } from './user'
|
||||||
import { AvatarModel } from '../avatar/avatar'
|
import { AvatarModel } from '../avatar/avatar'
|
||||||
import { VideoPlaylistModel } from '../video/video-playlist'
|
import { VideoPlaylistModel } from '../video/video-playlist'
|
||||||
import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
|
import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
|
||||||
import { Op, Transaction, WhereOptions } from 'sequelize'
|
import { FindOptions, IncludeOptions, Op, Transaction, WhereOptions } from 'sequelize'
|
||||||
|
import { AccountBlocklistModel } from './account-blocklist'
|
||||||
|
import { ServerBlocklistModel } from '../server/server-blocklist'
|
||||||
|
|
||||||
export enum ScopeNames {
|
export enum ScopeNames {
|
||||||
SUMMARY = 'SUMMARY'
|
SUMMARY = 'SUMMARY'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SummaryOptions = {
|
||||||
|
whereActor?: WhereOptions
|
||||||
|
withAccountBlockerIds?: number[]
|
||||||
|
}
|
||||||
|
|
||||||
@DefaultScope(() => ({
|
@DefaultScope(() => ({
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
|
@ -42,8 +49,16 @@ export enum ScopeNames {
|
||||||
]
|
]
|
||||||
}))
|
}))
|
||||||
@Scopes(() => ({
|
@Scopes(() => ({
|
||||||
[ ScopeNames.SUMMARY ]: (whereActor?: WhereOptions) => {
|
[ ScopeNames.SUMMARY ]: (options: SummaryOptions = {}) => {
|
||||||
return {
|
const whereActor = options.whereActor || undefined
|
||||||
|
|
||||||
|
const serverInclude: IncludeOptions = {
|
||||||
|
attributes: [ 'host' ],
|
||||||
|
model: ServerModel.unscoped(),
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
|
||||||
|
const query: FindOptions = {
|
||||||
attributes: [ 'id', 'name' ],
|
attributes: [ 'id', 'name' ],
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
|
@ -52,11 +67,8 @@ export enum ScopeNames {
|
||||||
required: true,
|
required: true,
|
||||||
where: whereActor,
|
where: whereActor,
|
||||||
include: [
|
include: [
|
||||||
{
|
serverInclude,
|
||||||
attributes: [ 'host' ],
|
|
||||||
model: ServerModel.unscoped(),
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
model: AvatarModel.unscoped(),
|
model: AvatarModel.unscoped(),
|
||||||
required: false
|
required: false
|
||||||
|
@ -65,6 +77,35 @@ export enum ScopeNames {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.withAccountBlockerIds) {
|
||||||
|
query.include.push({
|
||||||
|
attributes: [ 'id' ],
|
||||||
|
model: AccountBlocklistModel.unscoped(),
|
||||||
|
as: 'BlockedAccounts',
|
||||||
|
required: false,
|
||||||
|
where: {
|
||||||
|
accountId: {
|
||||||
|
[Op.in]: options.withAccountBlockerIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
serverInclude.include = [
|
||||||
|
{
|
||||||
|
attributes: [ 'id' ],
|
||||||
|
model: ServerBlocklistModel.unscoped(),
|
||||||
|
required: false,
|
||||||
|
where: {
|
||||||
|
accountId: {
|
||||||
|
[Op.in]: options.withAccountBlockerIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
@Table({
|
@Table({
|
||||||
|
@ -163,6 +204,16 @@ export class AccountModel extends Model<AccountModel> {
|
||||||
})
|
})
|
||||||
VideoComments: VideoCommentModel[]
|
VideoComments: VideoCommentModel[]
|
||||||
|
|
||||||
|
@HasMany(() => AccountBlocklistModel, {
|
||||||
|
foreignKey: {
|
||||||
|
name: 'targetAccountId',
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
as: 'BlockedAccounts',
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
BlockedAccounts: AccountBlocklistModel[]
|
||||||
|
|
||||||
@BeforeDestroy
|
@BeforeDestroy
|
||||||
static async sendDeleteIfOwned (instance: AccountModel, options) {
|
static async sendDeleteIfOwned (instance: AccountModel, options) {
|
||||||
if (!instance.Actor) {
|
if (!instance.Actor) {
|
||||||
|
@ -343,4 +394,8 @@ export class AccountModel extends Model<AccountModel> {
|
||||||
getDisplayName () {
|
getDisplayName () {
|
||||||
return this.name
|
return this.name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isBlocked () {
|
||||||
|
return this.BlockedAccounts && this.BlockedAccounts.length !== 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,6 @@ export class ServerBlocklistModel extends Model<ServerBlocklistModel> {
|
||||||
|
|
||||||
@BelongsTo(() => ServerModel, {
|
@BelongsTo(() => ServerModel, {
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'targetServerId',
|
|
||||||
allowNull: false
|
allowNull: false
|
||||||
},
|
},
|
||||||
onDelete: 'CASCADE'
|
onDelete: 'CASCADE'
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, Updat
|
||||||
import { isHostValid } from '../../helpers/custom-validators/servers'
|
import { isHostValid } from '../../helpers/custom-validators/servers'
|
||||||
import { ActorModel } from '../activitypub/actor'
|
import { ActorModel } from '../activitypub/actor'
|
||||||
import { throwIfNotValid } from '../utils'
|
import { throwIfNotValid } from '../utils'
|
||||||
|
import { AccountBlocklistModel } from '../account/account-blocklist'
|
||||||
|
import { ServerBlocklistModel } from './server-blocklist'
|
||||||
|
|
||||||
@Table({
|
@Table({
|
||||||
tableName: 'server',
|
tableName: 'server',
|
||||||
|
@ -40,6 +42,14 @@ export class ServerModel extends Model<ServerModel> {
|
||||||
})
|
})
|
||||||
Actors: ActorModel[]
|
Actors: ActorModel[]
|
||||||
|
|
||||||
|
@HasMany(() => ServerBlocklistModel, {
|
||||||
|
foreignKey: {
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
|
onDelete: 'CASCADE'
|
||||||
|
})
|
||||||
|
BlockedByAccounts: ServerBlocklistModel[]
|
||||||
|
|
||||||
static loadByHost (host: string) {
|
static loadByHost (host: string) {
|
||||||
const query = {
|
const query = {
|
||||||
where: {
|
where: {
|
||||||
|
@ -50,6 +60,10 @@ export class ServerModel extends Model<ServerModel> {
|
||||||
return ServerModel.findOne(query)
|
return ServerModel.findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isBlocked () {
|
||||||
|
return this.BlockedByAccounts && this.BlockedByAccounts.length !== 0
|
||||||
|
}
|
||||||
|
|
||||||
toFormattedJSON () {
|
toFormattedJSON () {
|
||||||
return {
|
return {
|
||||||
host: this.host
|
host: this.host
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
|
||||||
import { getSortOnModel, SortType, throwIfNotValid } from '../utils'
|
import { getSortOnModel, SortType, throwIfNotValid } from '../utils'
|
||||||
import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
|
import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
|
||||||
import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
|
import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
|
||||||
import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
|
import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
|
||||||
import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
|
import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
|
import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
|
||||||
|
@ -71,7 +71,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
|
||||||
required: true,
|
required: true,
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }),
|
model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, { withAccount: true } as SummaryOptions ] }),
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,7 +24,7 @@ import {
|
||||||
isVideoChannelSupportValid
|
isVideoChannelSupportValid
|
||||||
} from '../../helpers/custom-validators/video-channels'
|
} from '../../helpers/custom-validators/video-channels'
|
||||||
import { sendDeleteActor } from '../../lib/activitypub/send'
|
import { sendDeleteActor } from '../../lib/activitypub/send'
|
||||||
import { AccountModel, ScopeNames as AccountModelScopeNames } from '../account/account'
|
import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
|
||||||
import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
|
import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
|
||||||
import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
|
import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
|
||||||
import { VideoModel } from './video'
|
import { VideoModel } from './video'
|
||||||
|
@ -58,6 +58,11 @@ type AvailableForListOptions = {
|
||||||
actorId: number
|
actorId: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SummaryOptions = {
|
||||||
|
withAccount?: boolean // Default: false
|
||||||
|
withAccountBlockerIds?: number[]
|
||||||
|
}
|
||||||
|
|
||||||
@DefaultScope(() => ({
|
@DefaultScope(() => ({
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
|
@ -67,7 +72,7 @@ type AvailableForListOptions = {
|
||||||
]
|
]
|
||||||
}))
|
}))
|
||||||
@Scopes(() => ({
|
@Scopes(() => ({
|
||||||
[ScopeNames.SUMMARY]: (withAccount = false) => {
|
[ScopeNames.SUMMARY]: (options: SummaryOptions = {}) => {
|
||||||
const base: FindOptions = {
|
const base: FindOptions = {
|
||||||
attributes: [ 'name', 'description', 'id', 'actorId' ],
|
attributes: [ 'name', 'description', 'id', 'actorId' ],
|
||||||
include: [
|
include: [
|
||||||
|
@ -90,9 +95,11 @@ type AvailableForListOptions = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withAccount === true) {
|
if (options.withAccount === true) {
|
||||||
base.include.push({
|
base.include.push({
|
||||||
model: AccountModel.scope(AccountModelScopeNames.SUMMARY),
|
model: AccountModel.scope({
|
||||||
|
method: [ AccountModelScopeNames.SUMMARY, { withAccountBlockerIds: options.withAccountBlockerIds } as AccountSummaryOptions ]
|
||||||
|
}),
|
||||||
required: true
|
required: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ export type VideoFormattingJSONOptions = {
|
||||||
waitTranscoding?: boolean,
|
waitTranscoding?: boolean,
|
||||||
scheduledUpdate?: boolean,
|
scheduledUpdate?: boolean,
|
||||||
blacklistInfo?: boolean
|
blacklistInfo?: boolean
|
||||||
playlistInfo?: boolean
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video {
|
function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormattingJSONOptions): Video {
|
||||||
|
@ -98,17 +97,6 @@ function videoModelToFormattedJSON (video: VideoModel, options?: VideoFormatting
|
||||||
videoObject.blacklisted = !!video.VideoBlacklist
|
videoObject.blacklisted = !!video.VideoBlacklist
|
||||||
videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null
|
videoObject.blacklistedReason = video.VideoBlacklist ? video.VideoBlacklist.reason : null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.additionalAttributes.playlistInfo === true) {
|
|
||||||
// We filtered on a specific videoId/videoPlaylistId, that is unique
|
|
||||||
const playlistElement = video.VideoPlaylistElements[0]
|
|
||||||
|
|
||||||
videoObject.playlistElement = {
|
|
||||||
position: playlistElement.position,
|
|
||||||
startTimestamp: playlistElement.startTimestamp,
|
|
||||||
stopTimestamp: playlistElement.stopTimestamp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return videoObject
|
return videoObject
|
||||||
|
|
|
@ -13,14 +13,18 @@ import {
|
||||||
Table,
|
Table,
|
||||||
UpdatedAt
|
UpdatedAt
|
||||||
} from 'sequelize-typescript'
|
} from 'sequelize-typescript'
|
||||||
import { VideoModel } from './video'
|
import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
|
||||||
import { VideoPlaylistModel } from './video-playlist'
|
import { VideoPlaylistModel } from './video-playlist'
|
||||||
import { getSort, throwIfNotValid } from '../utils'
|
import { getSort, throwIfNotValid } from '../utils'
|
||||||
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
|
||||||
import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
|
import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
|
||||||
import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
|
import { PlaylistElementObject } from '../../../shared/models/activitypub/objects/playlist-element-object'
|
||||||
import * as validator from 'validator'
|
import * as validator from 'validator'
|
||||||
import { AggregateOptions, Op, Sequelize, Transaction } from 'sequelize'
|
import { AggregateOptions, Op, ScopeOptions, Sequelize, Transaction } from 'sequelize'
|
||||||
|
import { UserModel } from '../account/user'
|
||||||
|
import { VideoPlaylistElement, VideoPlaylistElementType } from '../../../shared/models/videos/playlist/video-playlist-element.model'
|
||||||
|
import { AccountModel } from '../account/account'
|
||||||
|
import { VideoPrivacy } from '../../../shared/models/videos'
|
||||||
|
|
||||||
@Table({
|
@Table({
|
||||||
tableName: 'videoPlaylistElement',
|
tableName: 'videoPlaylistElement',
|
||||||
|
@ -90,9 +94,9 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
|
||||||
|
|
||||||
@BelongsTo(() => VideoModel, {
|
@BelongsTo(() => VideoModel, {
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
allowNull: false
|
allowNull: true
|
||||||
},
|
},
|
||||||
onDelete: 'CASCADE'
|
onDelete: 'set null'
|
||||||
})
|
})
|
||||||
Video: VideoModel
|
Video: VideoModel
|
||||||
|
|
||||||
|
@ -107,6 +111,57 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
|
||||||
return VideoPlaylistElementModel.destroy(query)
|
return VideoPlaylistElementModel.destroy(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static listForApi (options: {
|
||||||
|
start: number,
|
||||||
|
count: number,
|
||||||
|
videoPlaylistId: number,
|
||||||
|
serverAccount: AccountModel,
|
||||||
|
user?: UserModel
|
||||||
|
}) {
|
||||||
|
const accountIds = [ options.serverAccount.id ]
|
||||||
|
const videoScope: (ScopeOptions | string)[] = [
|
||||||
|
VideoScopeNames.WITH_BLACKLISTED
|
||||||
|
]
|
||||||
|
|
||||||
|
if (options.user) {
|
||||||
|
accountIds.push(options.user.Account.id)
|
||||||
|
videoScope.push({ method: [ VideoScopeNames.WITH_USER_HISTORY, options.user.id ] })
|
||||||
|
}
|
||||||
|
|
||||||
|
const forApiOptions: ForAPIOptions = { withAccountBlockerIds: accountIds }
|
||||||
|
videoScope.push({
|
||||||
|
method: [
|
||||||
|
VideoScopeNames.FOR_API, forApiOptions
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const findQuery = {
|
||||||
|
offset: options.start,
|
||||||
|
limit: options.count,
|
||||||
|
order: getSort('position'),
|
||||||
|
where: {
|
||||||
|
videoPlaylistId: options.videoPlaylistId
|
||||||
|
},
|
||||||
|
include: [
|
||||||
|
{
|
||||||
|
model: VideoModel.scope(videoScope),
|
||||||
|
required: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
const countQuery = {
|
||||||
|
where: {
|
||||||
|
videoPlaylistId: options.videoPlaylistId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all([
|
||||||
|
VideoPlaylistElementModel.count(countQuery),
|
||||||
|
VideoPlaylistElementModel.findAll(findQuery)
|
||||||
|
]).then(([ total, data ]) => ({ total, data }))
|
||||||
|
}
|
||||||
|
|
||||||
static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) {
|
static loadByPlaylistAndVideo (videoPlaylistId: number, videoId: number) {
|
||||||
const query = {
|
const query = {
|
||||||
where: {
|
where: {
|
||||||
|
@ -118,6 +173,10 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
|
||||||
return VideoPlaylistElementModel.findOne(query)
|
return VideoPlaylistElementModel.findOne(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static loadById (playlistElementId: number) {
|
||||||
|
return VideoPlaylistElementModel.findByPk(playlistElementId)
|
||||||
|
}
|
||||||
|
|
||||||
static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) {
|
static loadByPlaylistAndVideoForAP (playlistId: number | string, videoId: number | string) {
|
||||||
const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId }
|
const playlistWhere = validator.isUUID('' + playlistId) ? { uuid: playlistId } : { id: playlistId }
|
||||||
const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId }
|
const videoWhere = validator.isUUID('' + videoId) ? { uuid: videoId } : { id: videoId }
|
||||||
|
@ -213,6 +272,42 @@ export class VideoPlaylistElementModel extends Model<VideoPlaylistElementModel>
|
||||||
return VideoPlaylistElementModel.increment({ position: by }, query)
|
return VideoPlaylistElementModel.increment({ position: by }, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getType (displayNSFW?: boolean, accountId?: number) {
|
||||||
|
const video = this.Video
|
||||||
|
|
||||||
|
if (!video) return VideoPlaylistElementType.DELETED
|
||||||
|
|
||||||
|
// Owned video, don't filter it
|
||||||
|
if (accountId && video.VideoChannel.Account.id === accountId) return VideoPlaylistElementType.REGULAR
|
||||||
|
|
||||||
|
if (video.privacy === VideoPrivacy.PRIVATE) return VideoPlaylistElementType.PRIVATE
|
||||||
|
|
||||||
|
if (video.isBlacklisted() || video.isBlocked()) return VideoPlaylistElementType.UNAVAILABLE
|
||||||
|
if (video.nsfw === true && displayNSFW === false) return VideoPlaylistElementType.UNAVAILABLE
|
||||||
|
|
||||||
|
return VideoPlaylistElementType.REGULAR
|
||||||
|
}
|
||||||
|
|
||||||
|
getVideoElement (displayNSFW?: boolean, accountId?: number) {
|
||||||
|
if (!this.Video) return null
|
||||||
|
if (this.getType(displayNSFW, accountId) !== VideoPlaylistElementType.REGULAR) return null
|
||||||
|
|
||||||
|
return this.Video.toFormattedJSON()
|
||||||
|
}
|
||||||
|
|
||||||
|
toFormattedJSON (options: { displayNSFW?: boolean, accountId?: number } = {}): VideoPlaylistElement {
|
||||||
|
return {
|
||||||
|
id: this.id,
|
||||||
|
position: this.position,
|
||||||
|
startTimestamp: this.startTimestamp,
|
||||||
|
stopTimestamp: this.stopTimestamp,
|
||||||
|
|
||||||
|
type: this.getType(options.displayNSFW, options.accountId),
|
||||||
|
|
||||||
|
video: this.getVideoElement(options.displayNSFW, options.accountId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toActivityPubObject (): PlaylistElementObject {
|
toActivityPubObject (): PlaylistElementObject {
|
||||||
const base: PlaylistElementObject = {
|
const base: PlaylistElementObject = {
|
||||||
id: this.url,
|
id: this.url,
|
||||||
|
|
|
@ -33,7 +33,7 @@ import {
|
||||||
WEBSERVER
|
WEBSERVER
|
||||||
} from '../../initializers/constants'
|
} from '../../initializers/constants'
|
||||||
import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model'
|
import { VideoPlaylist } from '../../../shared/models/videos/playlist/video-playlist.model'
|
||||||
import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
|
import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account'
|
||||||
import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
|
import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { VideoPlaylistElementModel } from './video-playlist-element'
|
import { VideoPlaylistElementModel } from './video-playlist-element'
|
||||||
|
@ -115,7 +115,7 @@ type AvailableForListOptions = {
|
||||||
[ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => {
|
[ ScopeNames.AVAILABLE_FOR_LIST ]: (options: AvailableForListOptions) => {
|
||||||
// Only list local playlists OR playlists that are on an instance followed by actorId
|
// Only list local playlists OR playlists that are on an instance followed by actorId
|
||||||
const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
|
const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
|
||||||
const actorWhere = {
|
const whereActor = {
|
||||||
[ Op.or ]: [
|
[ Op.or ]: [
|
||||||
{
|
{
|
||||||
serverId: null
|
serverId: null
|
||||||
|
@ -159,7 +159,7 @@ type AvailableForListOptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const accountScope = {
|
const accountScope = {
|
||||||
method: [ AccountScopeNames.SUMMARY, actorWhere ]
|
method: [ AccountScopeNames.SUMMARY, { whereActor } as SummaryOptions ]
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -341,7 +341,7 @@ export class VideoPlaylistModel extends Model<VideoPlaylistModel> {
|
||||||
},
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
attributes: [ 'videoId', 'startTimestamp', 'stopTimestamp' ],
|
attributes: [ 'id', 'videoId', 'startTimestamp', 'stopTimestamp' ],
|
||||||
model: VideoPlaylistElementModel.unscoped(),
|
model: VideoPlaylistElementModel.unscoped(),
|
||||||
where: {
|
where: {
|
||||||
videoId: {
|
videoId: {
|
||||||
|
|
|
@ -91,7 +91,7 @@ import {
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { TagModel } from './tag'
|
import { TagModel } from './tag'
|
||||||
import { VideoAbuseModel } from './video-abuse'
|
import { VideoAbuseModel } from './video-abuse'
|
||||||
import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
|
import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
|
||||||
import { VideoCommentModel } from './video-comment'
|
import { VideoCommentModel } from './video-comment'
|
||||||
import { VideoFileModel } from './video-file'
|
import { VideoFileModel } from './video-file'
|
||||||
import { VideoShareModel } from './video-share'
|
import { VideoShareModel } from './video-share'
|
||||||
|
@ -190,26 +190,29 @@ export enum ScopeNames {
|
||||||
WITH_FILES = 'WITH_FILES',
|
WITH_FILES = 'WITH_FILES',
|
||||||
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
|
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
|
||||||
WITH_BLACKLISTED = 'WITH_BLACKLISTED',
|
WITH_BLACKLISTED = 'WITH_BLACKLISTED',
|
||||||
|
WITH_BLOCKLIST = 'WITH_BLOCKLIST',
|
||||||
WITH_USER_HISTORY = 'WITH_USER_HISTORY',
|
WITH_USER_HISTORY = 'WITH_USER_HISTORY',
|
||||||
WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
|
WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
|
||||||
WITH_USER_ID = 'WITH_USER_ID',
|
WITH_USER_ID = 'WITH_USER_ID',
|
||||||
WITH_THUMBNAILS = 'WITH_THUMBNAILS'
|
WITH_THUMBNAILS = 'WITH_THUMBNAILS'
|
||||||
}
|
}
|
||||||
|
|
||||||
type ForAPIOptions = {
|
export type ForAPIOptions = {
|
||||||
ids: number[]
|
ids?: number[]
|
||||||
|
|
||||||
videoPlaylistId?: number
|
videoPlaylistId?: number
|
||||||
|
|
||||||
withFiles?: boolean
|
withFiles?: boolean
|
||||||
|
|
||||||
|
withAccountBlockerIds?: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
type AvailableForListIDsOptions = {
|
export type AvailableForListIDsOptions = {
|
||||||
serverAccountId: number
|
serverAccountId: number
|
||||||
followerActorId: number
|
followerActorId: number
|
||||||
includeLocalVideos: boolean
|
includeLocalVideos: boolean
|
||||||
|
|
||||||
withoutId?: boolean
|
attributesType?: 'none' | 'id' | 'all'
|
||||||
|
|
||||||
filter?: VideoFilter
|
filter?: VideoFilter
|
||||||
categoryOneOf?: number[]
|
categoryOneOf?: number[]
|
||||||
|
@ -236,14 +239,16 @@ type AvailableForListIDsOptions = {
|
||||||
@Scopes(() => ({
|
@Scopes(() => ({
|
||||||
[ ScopeNames.FOR_API ]: (options: ForAPIOptions) => {
|
[ ScopeNames.FOR_API ]: (options: ForAPIOptions) => {
|
||||||
const query: FindOptions = {
|
const query: FindOptions = {
|
||||||
where: {
|
|
||||||
id: {
|
|
||||||
[ Op.in ]: options.ids // FIXME: sequelize ANY seems broken
|
|
||||||
}
|
|
||||||
},
|
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: VideoChannelModel.scope({ method: [ VideoChannelScopeNames.SUMMARY, true ] }),
|
model: VideoChannelModel.scope({
|
||||||
|
method: [
|
||||||
|
VideoChannelScopeNames.SUMMARY, {
|
||||||
|
withAccount: true,
|
||||||
|
withAccountBlockerIds: options.withAccountBlockerIds
|
||||||
|
} as SummaryOptions
|
||||||
|
]
|
||||||
|
}),
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -254,6 +259,14 @@ type AvailableForListIDsOptions = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.ids) {
|
||||||
|
query.where = {
|
||||||
|
id: {
|
||||||
|
[ Op.in ]: options.ids // FIXME: sequelize ANY seems broken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (options.withFiles === true) {
|
if (options.withFiles === true) {
|
||||||
query.include.push({
|
query.include.push({
|
||||||
model: VideoFileModel.unscoped(),
|
model: VideoFileModel.unscoped(),
|
||||||
|
@ -278,10 +291,14 @@ type AvailableForListIDsOptions = {
|
||||||
|
|
||||||
const query: FindOptions = {
|
const query: FindOptions = {
|
||||||
raw: true,
|
raw: true,
|
||||||
attributes: options.withoutId === true ? [] : [ 'id' ],
|
|
||||||
include: []
|
include: []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const attributesType = options.attributesType || 'id'
|
||||||
|
|
||||||
|
if (attributesType === 'id') query.attributes = [ 'id' ]
|
||||||
|
else if (attributesType === 'none') query.attributes = [ ]
|
||||||
|
|
||||||
whereAnd.push({
|
whereAnd.push({
|
||||||
id: {
|
id: {
|
||||||
[ Op.notIn ]: Sequelize.literal(
|
[ Op.notIn ]: Sequelize.literal(
|
||||||
|
@ -290,17 +307,19 @@ type AvailableForListIDsOptions = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
whereAnd.push({
|
if (options.serverAccountId) {
|
||||||
channelId: {
|
whereAnd.push({
|
||||||
[ Op.notIn ]: Sequelize.literal(
|
channelId: {
|
||||||
'(' +
|
[ Op.notIn ]: Sequelize.literal(
|
||||||
'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
|
'(' +
|
||||||
buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
|
'SELECT id FROM "videoChannel" WHERE "accountId" IN (' +
|
||||||
')' +
|
buildBlockedAccountSQL(options.serverAccountId, options.user ? options.user.Account.id : undefined) +
|
||||||
')'
|
')' +
|
||||||
)
|
')'
|
||||||
}
|
)
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Only list public/published videos
|
// Only list public/published videos
|
||||||
if (!options.filter || options.filter !== 'all-local') {
|
if (!options.filter || options.filter !== 'all-local') {
|
||||||
|
@ -527,6 +546,9 @@ type AvailableForListIDsOptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
return query
|
return query
|
||||||
|
},
|
||||||
|
[ScopeNames.WITH_BLOCKLIST]: {
|
||||||
|
|
||||||
},
|
},
|
||||||
[ ScopeNames.WITH_THUMBNAILS ]: {
|
[ ScopeNames.WITH_THUMBNAILS ]: {
|
||||||
include: [
|
include: [
|
||||||
|
@ -845,9 +867,9 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
@HasMany(() => VideoPlaylistElementModel, {
|
@HasMany(() => VideoPlaylistElementModel, {
|
||||||
foreignKey: {
|
foreignKey: {
|
||||||
name: 'videoId',
|
name: 'videoId',
|
||||||
allowNull: false
|
allowNull: true
|
||||||
},
|
},
|
||||||
onDelete: 'cascade'
|
onDelete: 'set null'
|
||||||
})
|
})
|
||||||
VideoPlaylistElements: VideoPlaylistElementModel[]
|
VideoPlaylistElements: VideoPlaylistElementModel[]
|
||||||
|
|
||||||
|
@ -1586,7 +1608,7 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
serverAccountId: serverActor.Account.id,
|
serverAccountId: serverActor.Account.id,
|
||||||
followerActorId,
|
followerActorId,
|
||||||
includeLocalVideos: true,
|
includeLocalVideos: true,
|
||||||
withoutId: true // Don't break aggregation
|
attributesType: 'none' // Don't break aggregation
|
||||||
}
|
}
|
||||||
|
|
||||||
const query: FindOptions = {
|
const query: FindOptions = {
|
||||||
|
@ -1719,6 +1741,11 @@ export class VideoModel extends Model<VideoModel> {
|
||||||
return !!this.VideoBlacklist
|
return !!this.VideoBlacklist
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isBlocked () {
|
||||||
|
return (this.VideoChannel.Account.Actor.Server && this.VideoChannel.Account.Actor.Server.isBlocked()) ||
|
||||||
|
this.VideoChannel.Account.isBlocked()
|
||||||
|
}
|
||||||
|
|
||||||
getOriginalFile () {
|
getOriginalFile () {
|
||||||
if (Array.isArray(this.VideoFiles) === false) return undefined
|
if (Array.isArray(this.VideoFiles) === false) return undefined
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ describe('Test video playlists API validator', function () {
|
||||||
let watchLaterPlaylistId: number
|
let watchLaterPlaylistId: number
|
||||||
let videoId: number
|
let videoId: number
|
||||||
let videoId2: number
|
let videoId2: number
|
||||||
|
let playlistElementId: number
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
// ---------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -132,18 +133,18 @@ describe('Test video playlists API validator', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When listing videos of a playlist', function () {
|
describe('When listing videos of a playlist', function () {
|
||||||
const path = '/api/v1/video-playlists'
|
const path = '/api/v1/video-playlists/'
|
||||||
|
|
||||||
it('Should fail with a bad start pagination', async function () {
|
it('Should fail with a bad start pagination', async function () {
|
||||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
await checkBadStartPagination(server.url, path + playlistUUID + '/videos', server.accessToken)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with a bad count pagination', async function () {
|
it('Should fail with a bad count pagination', async function () {
|
||||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
await checkBadCountPagination(server.url, path + playlistUUID + '/videos', server.accessToken)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with a bad filter', async function () {
|
it('Should success with the correct parameters', async function () {
|
||||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
await makeGetRequest({ url: server.url, path: path + playlistUUID + '/videos', statusCodeExpected: 200 })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -296,7 +297,7 @@ describe('Test video playlists API validator', function () {
|
||||||
token: server.accessToken,
|
token: server.accessToken,
|
||||||
playlistId: playlistUUID,
|
playlistId: playlistUUID,
|
||||||
elementAttrs: Object.assign({
|
elementAttrs: Object.assign({
|
||||||
videoId: videoId,
|
videoId,
|
||||||
startTimestamp: 2,
|
startTimestamp: 2,
|
||||||
stopTimestamp: 3
|
stopTimestamp: 3
|
||||||
}, elementAttrs)
|
}, elementAttrs)
|
||||||
|
@ -344,7 +345,8 @@ describe('Test video playlists API validator', function () {
|
||||||
|
|
||||||
it('Succeed with the correct params', async function () {
|
it('Succeed with the correct params', async function () {
|
||||||
const params = getBase({}, { expectedStatus: 200 })
|
const params = getBase({}, { expectedStatus: 200 })
|
||||||
await addVideoInPlaylist(params)
|
const res = await addVideoInPlaylist(params)
|
||||||
|
playlistElementId = res.body.videoPlaylistElement.id
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail if the video was already added in the playlist', async function () {
|
it('Should fail if the video was already added in the playlist', async function () {
|
||||||
|
@ -362,7 +364,7 @@ describe('Test video playlists API validator', function () {
|
||||||
startTimestamp: 1,
|
startTimestamp: 1,
|
||||||
stopTimestamp: 2
|
stopTimestamp: 2
|
||||||
}, elementAttrs),
|
}, elementAttrs),
|
||||||
videoId: videoId,
|
playlistElementId,
|
||||||
playlistId: playlistUUID,
|
playlistId: playlistUUID,
|
||||||
expectedStatus: 400
|
expectedStatus: 400
|
||||||
}, wrapper)
|
}, wrapper)
|
||||||
|
@ -390,14 +392,14 @@ describe('Test video playlists API validator', function () {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with an unknown or incorrect video id', async function () {
|
it('Should fail with an unknown or incorrect playlistElement id', async function () {
|
||||||
{
|
{
|
||||||
const params = getBase({}, { videoId: 'toto' })
|
const params = getBase({}, { playlistElementId: 'toto' })
|
||||||
await updateVideoPlaylistElement(params)
|
await updateVideoPlaylistElement(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const params = getBase({}, { videoId: 42, expectedStatus: 404 })
|
const params = getBase({}, { playlistElementId: 42, expectedStatus: 404 })
|
||||||
await updateVideoPlaylistElement(params)
|
await updateVideoPlaylistElement(params)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -415,7 +417,7 @@ describe('Test video playlists API validator', function () {
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with an unknown element', async function () {
|
it('Should fail with an unknown element', async function () {
|
||||||
const params = getBase({}, { videoId: videoId2, expectedStatus: 404 })
|
const params = getBase({}, { playlistElementId: 888, expectedStatus: 404 })
|
||||||
await updateVideoPlaylistElement(params)
|
await updateVideoPlaylistElement(params)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -587,7 +589,7 @@ describe('Test video playlists API validator', function () {
|
||||||
return Object.assign({
|
return Object.assign({
|
||||||
url: server.url,
|
url: server.url,
|
||||||
token: server.accessToken,
|
token: server.accessToken,
|
||||||
videoId: videoId,
|
playlistElementId,
|
||||||
playlistId: playlistUUID,
|
playlistId: playlistUUID,
|
||||||
expectedStatus: 400
|
expectedStatus: 400
|
||||||
}, wrapper)
|
}, wrapper)
|
||||||
|
@ -617,18 +619,18 @@ describe('Test video playlists API validator', function () {
|
||||||
|
|
||||||
it('Should fail with an unknown or incorrect video id', async function () {
|
it('Should fail with an unknown or incorrect video id', async function () {
|
||||||
{
|
{
|
||||||
const params = getBase({ videoId: 'toto' })
|
const params = getBase({ playlistElementId: 'toto' })
|
||||||
await removeVideoFromPlaylist(params)
|
await removeVideoFromPlaylist(params)
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
const params = getBase({ videoId: 42, expectedStatus: 404 })
|
const params = getBase({ playlistElementId: 42, expectedStatus: 404 })
|
||||||
await removeVideoFromPlaylist(params)
|
await removeVideoFromPlaylist(params)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail with an unknown element', async function () {
|
it('Should fail with an unknown element', async function () {
|
||||||
const params = getBase({ videoId: videoId2, expectedStatus: 404 })
|
const params = getBase({ playlistElementId: 888, expectedStatus: 404 })
|
||||||
await removeVideoFromPlaylist(params)
|
await removeVideoFromPlaylist(params)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,12 @@ import {
|
||||||
import { UserRole } from '../../../../shared/models/users'
|
import { UserRole } from '../../../../shared/models/users'
|
||||||
import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
|
import { VideoPlaylistPrivacy } from '../../../../shared/models/videos/playlist/video-playlist-privacy.model'
|
||||||
|
|
||||||
async function testEndpoints (server: ServerInfo, token: string, filter: string, playlistUUID: string, statusCodeExpected: number) {
|
async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: number) {
|
||||||
const paths = [
|
const paths = [
|
||||||
'/api/v1/video-channels/root_channel/videos',
|
'/api/v1/video-channels/root_channel/videos',
|
||||||
'/api/v1/accounts/root/videos',
|
'/api/v1/accounts/root/videos',
|
||||||
'/api/v1/videos',
|
'/api/v1/videos',
|
||||||
'/api/v1/search/videos',
|
'/api/v1/search/videos'
|
||||||
'/api/v1/video-playlists/' + playlistUUID + '/videos'
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const path of paths) {
|
for (const path of paths) {
|
||||||
|
@ -70,39 +69,28 @@ describe('Test videos filters', function () {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
moderatorAccessToken = await userLogin(server, moderator)
|
moderatorAccessToken = await userLogin(server, moderator)
|
||||||
|
|
||||||
const res = await createVideoPlaylist({
|
|
||||||
url: server.url,
|
|
||||||
token: server.accessToken,
|
|
||||||
playlistAttrs: {
|
|
||||||
displayName: 'super playlist',
|
|
||||||
privacy: VideoPlaylistPrivacy.PUBLIC,
|
|
||||||
videoChannelId: server.videoChannel.id
|
|
||||||
}
|
|
||||||
})
|
|
||||||
playlistUUID = res.body.videoPlaylist.uuid
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When setting a video filter', function () {
|
describe('When setting a video filter', function () {
|
||||||
|
|
||||||
it('Should fail with a bad filter', async function () {
|
it('Should fail with a bad filter', async function () {
|
||||||
await testEndpoints(server, server.accessToken, 'bad-filter', playlistUUID, 400)
|
await testEndpoints(server, server.accessToken, 'bad-filter', 400)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should succeed with a good filter', async function () {
|
it('Should succeed with a good filter', async function () {
|
||||||
await testEndpoints(server, server.accessToken,'local', playlistUUID, 200)
|
await testEndpoints(server, server.accessToken,'local', 200)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should fail to list all-local with a simple user', async function () {
|
it('Should fail to list all-local with a simple user', async function () {
|
||||||
await testEndpoints(server, userAccessToken, 'all-local', playlistUUID, 401)
|
await testEndpoints(server, userAccessToken, 'all-local', 401)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should succeed to list all-local with a moderator', async function () {
|
it('Should succeed to list all-local with a moderator', async function () {
|
||||||
await testEndpoints(server, moderatorAccessToken, 'all-local', playlistUUID, 200)
|
await testEndpoints(server, moderatorAccessToken, 'all-local', 200)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Should succeed to list all-local with an admin', async function () {
|
it('Should succeed to list all-local with an admin', async function () {
|
||||||
await testEndpoints(server, server.accessToken, 'all-local', playlistUUID, 200)
|
await testEndpoints(server, server.accessToken, 'all-local', 200)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Because we cannot authenticate the user on the RSS endpoint
|
// Because we cannot authenticate the user on the RSS endpoint
|
||||||
|
|
|
@ -62,7 +62,7 @@ describe('Test videos API validator', function () {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('When listing a video', function () {
|
describe('When listing videos', function () {
|
||||||
it('Should fail with a bad start pagination', async function () {
|
it('Should fail with a bad start pagination', async function () {
|
||||||
await checkBadStartPagination(server.url, path)
|
await checkBadStartPagination(server.url, path)
|
||||||
})
|
})
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,6 @@ import * as request from 'supertest'
|
||||||
import { Job, JobState } from '../../models'
|
import { Job, JobState } from '../../models'
|
||||||
import { wait } from '../miscs/miscs'
|
import { wait } from '../miscs/miscs'
|
||||||
import { ServerInfo } from './servers'
|
import { ServerInfo } from './servers'
|
||||||
import { inspect } from 'util'
|
|
||||||
|
|
||||||
function getJobsList (url: string, accessToken: string, state: JobState) {
|
function getJobsList (url: string, accessToken: string, state: JobState) {
|
||||||
const path = '/api/v1/jobs/' + state
|
const path = '/api/v1/jobs/' + state
|
||||||
|
@ -37,11 +36,10 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
|
||||||
else servers = serversArg as ServerInfo[]
|
else servers = serversArg as ServerInfo[]
|
||||||
|
|
||||||
const states: JobState[] = [ 'waiting', 'active', 'delayed' ]
|
const states: JobState[] = [ 'waiting', 'active', 'delayed' ]
|
||||||
let pendingRequests = false
|
let pendingRequests: boolean
|
||||||
|
|
||||||
function tasksBuilder () {
|
function tasksBuilder () {
|
||||||
const tasks: Promise<any>[] = []
|
const tasks: Promise<any>[] = []
|
||||||
pendingRequests = false
|
|
||||||
|
|
||||||
// Check if each server has pending request
|
// Check if each server has pending request
|
||||||
for (const server of servers) {
|
for (const server of servers) {
|
||||||
|
@ -62,6 +60,7 @@ async function waitJobs (serversArg: ServerInfo[] | ServerInfo) {
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
|
pendingRequests = false
|
||||||
await Promise.all(tasksBuilder())
|
await Promise.all(tasksBuilder())
|
||||||
|
|
||||||
// Retry, in case of new jobs were created
|
// Retry, in case of new jobs were created
|
||||||
|
|
|
@ -196,11 +196,11 @@ function updateVideoPlaylistElement (options: {
|
||||||
url: string,
|
url: string,
|
||||||
token: string,
|
token: string,
|
||||||
playlistId: number | string,
|
playlistId: number | string,
|
||||||
videoId: number | string,
|
playlistElementId: number | string,
|
||||||
elementAttrs: VideoPlaylistElementUpdate,
|
elementAttrs: VideoPlaylistElementUpdate,
|
||||||
expectedStatus?: number
|
expectedStatus?: number
|
||||||
}) {
|
}) {
|
||||||
const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.videoId
|
const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId
|
||||||
|
|
||||||
return makePutBodyRequest({
|
return makePutBodyRequest({
|
||||||
url: options.url,
|
url: options.url,
|
||||||
|
@ -215,10 +215,10 @@ function removeVideoFromPlaylist (options: {
|
||||||
url: string,
|
url: string,
|
||||||
token: string,
|
token: string,
|
||||||
playlistId: number | string,
|
playlistId: number | string,
|
||||||
videoId: number | string,
|
playlistElementId: number,
|
||||||
expectedStatus?: number
|
expectedStatus?: number
|
||||||
}) {
|
}) {
|
||||||
const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.videoId
|
const path = '/api/v1/video-playlists/' + options.playlistId + '/videos/' + options.playlistElementId
|
||||||
|
|
||||||
return makeDeleteRequest({
|
return makeDeleteRequest({
|
||||||
url: options.url,
|
url: options.url,
|
||||||
|
|
|
@ -19,6 +19,7 @@ export * from './playlist/video-playlist-privacy.model'
|
||||||
export * from './playlist/video-playlist-type.model'
|
export * from './playlist/video-playlist-type.model'
|
||||||
export * from './playlist/video-playlist-update.model'
|
export * from './playlist/video-playlist-update.model'
|
||||||
export * from './playlist/video-playlist.model'
|
export * from './playlist/video-playlist.model'
|
||||||
|
export * from './playlist/video-playlist-element.model'
|
||||||
export * from './video-change-ownership.model'
|
export * from './video-change-ownership.model'
|
||||||
export * from './video-change-ownership-create.model'
|
export * from './video-change-ownership-create.model'
|
||||||
export * from './video-create.model'
|
export * from './video-create.model'
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
export type VideoExistInPlaylist = {
|
export type VideoExistInPlaylist = {
|
||||||
[videoId: number ]: {
|
[videoId: number ]: {
|
||||||
|
playlistElementId: number
|
||||||
playlistId: number
|
playlistId: number
|
||||||
startTimestamp?: number
|
startTimestamp?: number
|
||||||
stopTimestamp?: number
|
stopTimestamp?: number
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Video } from '../video.model'
|
||||||
|
|
||||||
|
export enum VideoPlaylistElementType {
|
||||||
|
REGULAR = 0,
|
||||||
|
DELETED = 1,
|
||||||
|
PRIVATE = 2,
|
||||||
|
UNAVAILABLE = 3 // Blacklisted, blocked by the user/instance, NSFW...
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VideoPlaylistElement {
|
||||||
|
id: number
|
||||||
|
position: number
|
||||||
|
startTimestamp: number
|
||||||
|
stopTimestamp: number
|
||||||
|
|
||||||
|
type: VideoPlaylistElementType
|
||||||
|
|
||||||
|
video?: Video
|
||||||
|
}
|
|
@ -17,12 +17,6 @@ export interface VideoFile {
|
||||||
fps: number
|
fps: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlaylistElement {
|
|
||||||
position: number
|
|
||||||
startTimestamp: number
|
|
||||||
stopTimestamp: number
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Video {
|
export interface Video {
|
||||||
id: number
|
id: number
|
||||||
uuid: string
|
uuid: string
|
||||||
|
@ -59,8 +53,6 @@ export interface Video {
|
||||||
userHistory?: {
|
userHistory?: {
|
||||||
currentTime: number
|
currentTime: number
|
||||||
}
|
}
|
||||||
|
|
||||||
playlistElement?: PlaylistElement
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface VideoDetails extends Video {
|
export interface VideoDetails extends Video {
|
||||||
|
|
|
@ -1922,6 +1922,9 @@ components:
|
||||||
type: number
|
type: number
|
||||||
stopTimestamp:
|
stopTimestamp:
|
||||||
type: number
|
type: number
|
||||||
|
video:
|
||||||
|
nullable: true
|
||||||
|
$ref: '#/components/schemas/Video'
|
||||||
VideoFile:
|
VideoFile:
|
||||||
properties:
|
properties:
|
||||||
magnetUri:
|
magnetUri:
|
||||||
|
@ -2029,9 +2032,6 @@ components:
|
||||||
properties:
|
properties:
|
||||||
currentTime:
|
currentTime:
|
||||||
type: number
|
type: number
|
||||||
playlistElement:
|
|
||||||
nullable: true
|
|
||||||
$ref: '#/components/schemas/PlaylistElement'
|
|
||||||
VideoDetails:
|
VideoDetails:
|
||||||
allOf:
|
allOf:
|
||||||
- $ref: '#/components/schemas/Video'
|
- $ref: '#/components/schemas/Video'
|
||||||
|
|
Loading…
Reference in New Issue
Block a user