mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2025-12-05 01:10:36 +00:00
Compare commits
11 Commits
08029076f9
...
25f0ba7bd5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25f0ba7bd5 | ||
|
|
3405ebd2e0 | ||
|
|
e5351a0eb0 | ||
|
|
639f73f414 | ||
|
|
4cd6dbf253 | ||
|
|
ff758863cf | ||
|
|
bd11b153fb | ||
|
|
4f8edcc095 | ||
|
|
0edf927e03 | ||
|
|
09d90b8152 | ||
|
|
dbc47e0397 |
@@ -5,7 +5,6 @@ import { BulkService } from '@app/shared/shared-moderation/bulk.service'
|
||||
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
|
||||
import { UserSubscriptionService } from '@app/shared/shared-user-subscription/user-subscription.service'
|
||||
import { UserAdminService } from '@app/shared/shared-users/user-admin.service'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { AccountVideoChannelsComponent } from './account-video-channels/account-video-channels.component'
|
||||
import { AccountVideosComponent } from './account-videos/account-videos.component'
|
||||
import { AccountsComponent } from './accounts.component'
|
||||
@@ -21,7 +20,6 @@ export default [
|
||||
providers: [
|
||||
UserSubscriptionService,
|
||||
BlocklistService,
|
||||
VideoPlaylistService,
|
||||
VideoBlockService,
|
||||
AbuseService,
|
||||
UserAdminService,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component } from '@angular/core'
|
||||
|
||||
import { WatchedWordsListAdminOwnerComponent } from '@app/shared/standalone-watched-words/watched-words-list-admin-owner.component'
|
||||
import { WatchedWordsListAdminOwnerComponent } from '@app/shared/shared-watched-words/watched-words-list-admin-owner.component'
|
||||
|
||||
@Component({
|
||||
templateUrl: './watched-words-list-admin.component.html',
|
||||
|
||||
@@ -16,8 +16,7 @@ import { SearchService } from '@app/shared/shared-search/search.service'
|
||||
import { TwoFactorService } from '@app/shared/shared-users/two-factor.service'
|
||||
import { UserAdminService } from '@app/shared/shared-users/user-admin.service'
|
||||
import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { WatchedWordsListService } from '@app/shared/standalone-watched-words/watched-words-list.service'
|
||||
import { WatchedWordsListService } from '@app/shared/shared-watched-words/watched-words-list.service'
|
||||
import { AdminModerationComponent } from './admin-moderation.component'
|
||||
import { AdminOverviewComponent } from './admin-overview.component'
|
||||
import { AdminSettingsComponent } from './admin-settings.component'
|
||||
@@ -52,7 +51,6 @@ const commonConfig = {
|
||||
DynamicElementService,
|
||||
FindInBulkService,
|
||||
SearchService,
|
||||
VideoPlaylistService,
|
||||
WatchedWordsListService
|
||||
]
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { BlocklistService } from '@app/shared/shared-moderation/blocklist.servic
|
||||
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
|
||||
import { FindInBulkService } from '@app/shared/shared-search/find-in-bulk.service'
|
||||
import { SearchService } from '@app/shared/shared-search/search.service'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { AbuseService } from '@app/shared/shared-moderation/abuse.service'
|
||||
|
||||
export default [
|
||||
@@ -18,7 +17,6 @@ export default [
|
||||
CustomPageService,
|
||||
FindInBulkService,
|
||||
SearchService,
|
||||
VideoPlaylistService,
|
||||
CustomMarkupService,
|
||||
DynamicElementService,
|
||||
BlocklistService,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpStatusCode, UserImport, UserImportState } from '@peertube/peertube-models'
|
||||
import { UploadState, UploaderX, UploadxService } from 'ngx-uploadx'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { UploadProgressComponent } from '../../shared/standalone-upload/upload-progress.component'
|
||||
import { UploadProgressComponent } from '../../shared/shared-upload/upload-progress.component'
|
||||
import { UserImportExportService } from './user-import-export.service'
|
||||
|
||||
@Component({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component, viewChild } from '@angular/core'
|
||||
import { UserNotificationsComponent } from '@app/shared/standalone-notifications/user-notifications.component'
|
||||
import { UserNotificationsComponent } from '@app/shared/shared-notifications/user-notifications.component'
|
||||
import { NgIf } from '@angular/common'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { GlobalIconComponent } from '../../shared/shared-icons/global-icon.component'
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<div>
|
||||
<h2 class="form-title">
|
||||
<my-global-icon iconName="calendar"></my-global-icon>
|
||||
|
||||
<ng-container i18n>Activity</ng-container>
|
||||
</h2>
|
||||
|
||||
<div class="no-results" *ngIf="pagination.totalItems === 0" i18n>
|
||||
No activity in the channel yet
|
||||
</div>
|
||||
|
||||
<div class="activities" myInfiniteScroller (nearOfBottom)="onNearOfBottom()" [dataObservable]="onDataSubject.asObservable()">
|
||||
@for (activity of activities; track activity.id; let even = $even) {
|
||||
<my-date-group-label [date]="activity.createdAt" [store]="groupByDateStore"></my-date-group-label>
|
||||
|
||||
<div class="activity-container" [ngClass]="{ even: even }">
|
||||
<my-video-channel-activity [activity]="activity"></my-video-channel-activity>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -0,0 +1,53 @@
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
|
||||
.activity-container {
|
||||
margin-bottom: 0.75rem;
|
||||
border-radius: 24px;
|
||||
padding: 1rem 1.5rem;
|
||||
background-color: pvar(--bg-secondary-350);
|
||||
|
||||
&.even {
|
||||
background-color: pvar(--bg-secondary-400);
|
||||
}
|
||||
}
|
||||
|
||||
my-date-group-label:not(.date-displayed) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
my-date-group-label.date-displayed {
|
||||
color: pvar(--fg-300);
|
||||
font-weight: $font-bold;
|
||||
display: block;
|
||||
|
||||
@include padding-bottom(1.5rem);
|
||||
@include font-size(18px);
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid pvar(--bg-secondary-500);
|
||||
|
||||
@include padding-top(1.5rem);
|
||||
@include margin-top(1.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
my-date-group-label:not(.date-displayed) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
my-date-group-label.date-displayed {
|
||||
color: pvar(--fg-300);
|
||||
font-weight: $font-bold;
|
||||
display: block;
|
||||
|
||||
@include padding-bottom(1.5rem);
|
||||
@include font-size(18px);
|
||||
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid pvar(--bg-secondary-500);
|
||||
|
||||
@include padding-top(1.5rem);
|
||||
@include margin-top(1.5rem);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ComponentPagination, hasMoreItems, Notifier, resetCurrentPage } from '@app/core'
|
||||
import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.component'
|
||||
import { VideoChannelService } from '@app/shared/shared-main/channel/video-channel.service'
|
||||
import { InfiniteScrollerDirective } from '@app/shared/shared-main/common/infinite-scroller.directive'
|
||||
import { VideoChannelActivity } from '@peertube/peertube-models'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { VideoChannelEditControllerService } from '../video-channel-edit-controller.service'
|
||||
import { VideoChannelEdit } from '../video-channel-edit.model'
|
||||
import { VideoChannelActivityComponent } from './video-channel-activity.component'
|
||||
import { DateGroupLabelComponent } from '@app/shared/shared-main/date/date-group-label.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-channel-activities',
|
||||
templateUrl: './video-channel-activities.component.html',
|
||||
styleUrls: [ './video-channel-activities.component.scss' ],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
GlobalIconComponent,
|
||||
InfiniteScrollerDirective,
|
||||
VideoChannelActivityComponent,
|
||||
DateGroupLabelComponent
|
||||
]
|
||||
})
|
||||
export class VideoChannelActivitiesComponent implements OnInit, OnDestroy {
|
||||
private notifier = inject(Notifier)
|
||||
private channelService = inject(VideoChannelService)
|
||||
private editController = inject(VideoChannelEditControllerService)
|
||||
|
||||
pagination: ComponentPagination = {
|
||||
currentPage: 1,
|
||||
itemsPerPage: 20,
|
||||
totalItems: null
|
||||
}
|
||||
|
||||
videoChannelEdit: VideoChannelEdit
|
||||
|
||||
activities: VideoChannelActivity[] = []
|
||||
|
||||
onDataSubject = new Subject<any[]>()
|
||||
|
||||
groupByDateStore = new Set<number>()
|
||||
|
||||
private storeSub: Subscription
|
||||
|
||||
ngOnInit () {
|
||||
this.videoChannelEdit = this.editController.getStore()
|
||||
this.reload()
|
||||
|
||||
this.storeSub = this.editController.getStoreChangesObs()
|
||||
.subscribe(() => {
|
||||
this.videoChannelEdit = this.editController.getStore()
|
||||
|
||||
this.reload()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.storeSub?.unsubscribe()
|
||||
}
|
||||
|
||||
private reload () {
|
||||
this.activities = []
|
||||
resetCurrentPage(this.pagination)
|
||||
|
||||
this.loadMoreActivities()
|
||||
}
|
||||
|
||||
onNearOfBottom () {
|
||||
if (!hasMoreItems(this.pagination)) return
|
||||
|
||||
this.pagination.currentPage += 1
|
||||
|
||||
this.loadMoreActivities()
|
||||
}
|
||||
|
||||
private loadMoreActivities () {
|
||||
this.channelService.listActivities({
|
||||
channelName: this.videoChannelEdit.channel.name,
|
||||
componentPagination: this.pagination
|
||||
}).subscribe({
|
||||
next: res => {
|
||||
this.activities = this.activities.concat(res.data)
|
||||
this.pagination.totalItems = res.total
|
||||
|
||||
this.onDataSubject.next(res.data)
|
||||
},
|
||||
|
||||
error: err => this.notifier.error(err.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<div class="root">
|
||||
<div class="message">
|
||||
@switch (a.action.id) {
|
||||
@case (1) { <!-- VideoChannelActivityAction.CREATE) -->
|
||||
|
||||
@switch (a.targetType.id) {
|
||||
@case (1) { <!-- VideoChannelActivityTargetType.VIDEO) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> created video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@case (2) { <!-- VideoChannelActivityTargetType.PLAYLIST) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> created playlist <a [routerLink]="[ '/my-library/video-playlists/', a.playlist.shortUUID ]">{{ a.playlist.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<!-- VideoChannelActivityTargetType.CHANNEL) - Case cannot exist -->
|
||||
|
||||
@case (4) { <!-- VideoChannelActivityTargetType.CHANNEL_SYNC) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> created <a routerLink="/my-library/video-channel-syncs">a channel synchronization</a>: {{ a.channelSync.externalChannelUrl }}
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@case (5) { <!-- VideoChannelActivityTargetType.VIDEO_IMPORT) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> imported video <a [routerLink]="[ '/w', a.videoImport.uuid ]">{{ a.videoImport.name }}</a>: {{ a.videoImport.targetUrl }}
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@case (2) { <!-- VideoChannelActivityAction.UPDATE) -->
|
||||
@switch (a.targetType.id) {
|
||||
@case (1) { <!-- VideoChannelActivityTargetType.VIDEO) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> updated video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@case (2) { <!-- VideoChannelActivityTargetType.PLAYLIST) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> updated playlist <a [routerLink]="[ '/my-library/video-playlists/', a.playlist.shortUUID ]">{{ a.playlist.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@case (3) { <!-- VideoChannelActivityTargetType.CHANNEL) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> updated the channel
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
|
||||
<!-- VideoChannelActivityTargetType.CHANNEL_SYNC) - Case cannot exist -->
|
||||
|
||||
<!-- VideoChannelActivityTargetType.CHANNEL_SYNC) - Case cannot exist -->
|
||||
}
|
||||
}
|
||||
|
||||
@case (3) { <!-- VideoChannelActivityAction.DELETE) -->
|
||||
@switch (a.targetType.id) {
|
||||
@case (1) { <!-- VideoChannelActivityTargetType.VIDEO) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> deleted video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@case (2) { <!-- VideoChannelActivityTargetType.PLAYLIST) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> deleted playlist <a [routerLink]="[ '/my-library/video-playlists/', a.playlist.shortUUID ]">{{ a.playlist.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<!-- VideoChannelActivityTargetType.CHANNEL) - Case cannot exist -->
|
||||
|
||||
@case (4) { <!-- VideoChannelActivityTargetType.CHANNEL_SYNC) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> delete channel synchronization {{ a.channelSync.externalChannelUrl }}
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<!-- VideoChannelActivityTargetType.VIDEO_IMPORT) - Case cannot exist -->
|
||||
}
|
||||
}
|
||||
|
||||
@case (4) { <!-- VideoChannelActivityAction.UPDATE_CAPTIONS) -->
|
||||
@switch (a.targetType.id) {
|
||||
@case (1) { <!-- VideoChannelActivityTargetType.VIDEO) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> updated captions of video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@case (5) { <!-- VideoChannelActivityAction.UPDATE_CHAPTERS) -->
|
||||
@switch (a.targetType.id) {
|
||||
@case (1) { <!-- VideoChannelActivityTargetType.VIDEO) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> updated chapters of video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@case (6) { <!-- VideoChannelActivityAction.UPDATE_PASSWORDS) -->
|
||||
@switch (a.targetType.id) {
|
||||
@case (1) { <!-- VideoChannelActivityTargetType.VIDEO) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> updated passwords of video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@case (7) { <!-- VideoChannelActivityAction.CREATE_STUDIO_TASKS) -->
|
||||
@switch (a.targetType.id) {
|
||||
@case (1) { <!-- VideoChannelActivityTargetType.VIDEO) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> created studio tasks of video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@case (8) { <!-- VideoChannelActivityAction.UPDATE_SOURCE_FILE) -->
|
||||
@switch (a.targetType.id) {
|
||||
@case (1) { <!-- VideoChannelActivityTargetType.VIDEO) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> updated source file of video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@case (9) { <!-- VideoChannelActivityAction.UPDATE_ELEMENTS) -->
|
||||
@switch (a.targetType.id) {
|
||||
@case (2) { <!-- VideoChannelActivityTargetType.PLAYLIST) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> updated elements of playlist <a [routerLink]="[ '/my-library/video-playlists/', a.playlist.shortUUID ]">{{ a.playlist.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@case (10) { <!-- VideoChannelActivityAction.REMOVE_CHANNEL_OWNERSHIP) -->
|
||||
@switch (a.targetType.id) {
|
||||
@case (1) { <!-- VideoChannelActivityTargetType.VIDEO) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> removed video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a> by updating its channel
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@case (2) { <!-- VideoChannelActivityTargetType.PLAYLIST) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> removed playlist <a [routerLink]="[ '/my-library/video-playlists/', a.playlist.shortUUID ]">{{ a.playlist.name }}</a> by updating its channel
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@case (11) { <!-- VideoChannelActivityAction.CREATE_CHANNEL_OWNERSHIP) -->
|
||||
@switch (a.targetType.id) {
|
||||
@case (1) { <!-- VideoChannelActivityTargetType.VIDEO) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> created video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a> by updating its channel
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@case (2) { <!-- VideoChannelActivityTargetType.PLAYLIST) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> created playlist <a [routerLink]="[ '/my-library/video-playlists/', a.playlist.shortUUID ]">{{ a.playlist.name }}</a> by updating its channel
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="date">
|
||||
{{ a.createdAt | myFromNow }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,20 @@
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.message {
|
||||
color: pvar(--fg-300);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.date {
|
||||
color: pvar(--fg-200);
|
||||
|
||||
@include font-size(14px);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, input, OnInit } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { FromNowPipe } from '@app/shared/shared-main/date/from-now.pipe'
|
||||
import { VideoChannelActivity } from '@peertube/peertube-models'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-channel-activity',
|
||||
templateUrl: './video-channel-activity.component.html',
|
||||
styleUrls: [ './video-channel-activity.component.scss' ],
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
FromNowPipe
|
||||
]
|
||||
})
|
||||
export class VideoChannelActivityComponent implements OnInit {
|
||||
activity = input<VideoChannelActivity>()
|
||||
|
||||
a: VideoChannelActivity
|
||||
account: string
|
||||
|
||||
ngOnInit () {
|
||||
this.a = this.activity()
|
||||
|
||||
this.account = this.a.account
|
||||
? this.a.account.displayName
|
||||
: $localize`Deleted account`
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, OnInit } from '@angular/core'
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import {
|
||||
BuildFormArgumentTyped,
|
||||
@@ -53,7 +53,7 @@ type Form = {
|
||||
GlobalIconComponent
|
||||
]
|
||||
})
|
||||
export class VideoChannelEditGeneralComponent implements OnInit {
|
||||
export class VideoChannelEditGeneralComponent implements OnInit, OnDestroy {
|
||||
private formReactiveService = inject(FormReactiveService)
|
||||
private editController = inject(VideoChannelEditControllerService)
|
||||
|
||||
@@ -64,6 +64,7 @@ export class VideoChannelEditGeneralComponent implements OnInit {
|
||||
videoChannelEdit: VideoChannelEdit
|
||||
|
||||
private formSub: Subscription
|
||||
private storeSub: Subscription
|
||||
|
||||
get instanceHost () {
|
||||
return window.location.host
|
||||
@@ -75,7 +76,7 @@ export class VideoChannelEditGeneralComponent implements OnInit {
|
||||
this.videoChannelEdit = this.editController.getStore()
|
||||
this.buildForm()
|
||||
|
||||
this.editController.getStoreChangesObs()
|
||||
this.storeSub = this.editController.getStoreChangesObs()
|
||||
.subscribe(() => {
|
||||
this.videoChannelEdit = this.editController.getStore()
|
||||
|
||||
@@ -89,6 +90,11 @@ export class VideoChannelEditGeneralComponent implements OnInit {
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.storeSub?.unsubscribe()
|
||||
this.editController.unregisterSaveHook()
|
||||
}
|
||||
|
||||
private buildForm () {
|
||||
this.formSub?.unsubscribe()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, OnInit } from '@angular/core'
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
import { AuthService, ConfirmService, Notifier } from '@app/core'
|
||||
@@ -12,6 +12,7 @@ import { CollaboratorStateComponent } from '@app/shared/shared-main/channel/coll
|
||||
import { VideoChannelService } from '@app/shared/shared-main/channel/video-channel.service'
|
||||
import { VideoChannelCollaborator, VideoChannelCollaboratorState } from '@peertube/peertube-models'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { VideoChannelEditControllerService } from '../video-channel-edit-controller.service'
|
||||
import { VideoChannelEdit } from '../video-channel-edit.model'
|
||||
|
||||
@@ -31,7 +32,7 @@ import { VideoChannelEdit } from '../video-channel-edit.model'
|
||||
CollaboratorStateComponent
|
||||
]
|
||||
})
|
||||
export class VideoChannelEditEditorsComponent implements OnInit {
|
||||
export class VideoChannelEditEditorsComponent implements OnInit, OnDestroy {
|
||||
private confirmService = inject(ConfirmService)
|
||||
private notifier = inject(Notifier)
|
||||
private channelService = inject(VideoChannelService)
|
||||
@@ -44,12 +45,23 @@ export class VideoChannelEditEditorsComponent implements OnInit {
|
||||
newEditorUsername: string
|
||||
collaboratorActions: DropdownAction<VideoChannelCollaborator>[] = []
|
||||
|
||||
private storeSub: Subscription
|
||||
|
||||
ngOnInit () {
|
||||
this.videoChannelEdit = this.editController.getStore()
|
||||
|
||||
this.storeSub = this.editController.getStoreChangesObs()
|
||||
.subscribe(() => {
|
||||
this.videoChannelEdit = this.editController.getStore()
|
||||
})
|
||||
|
||||
this.buildActions()
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
this.storeSub?.unsubscribe()
|
||||
}
|
||||
|
||||
private buildActions () {
|
||||
this.collaboratorActions = [
|
||||
{
|
||||
|
||||
@@ -57,7 +57,7 @@ h2 {
|
||||
}
|
||||
|
||||
@include on-small-main-col {
|
||||
.watch-button-label {
|
||||
.go-public-page {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,18 @@ export class VideoChannelEditComponent implements OnInit, OnDestroy {
|
||||
label: $localize`Editors`,
|
||||
routerLink: 'editors',
|
||||
isDisplayed: () => this.mode === 'update'
|
||||
},
|
||||
|
||||
{
|
||||
type: 'separator'
|
||||
},
|
||||
|
||||
{
|
||||
type: 'link',
|
||||
icon: 'calendar',
|
||||
label: $localize`Activity`,
|
||||
routerLink: 'activities',
|
||||
isDisplayed: () => this.mode === 'update'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Routes } from '@angular/router'
|
||||
import { VideoChannelActivitiesComponent } from './pages/video-channel-activities.component'
|
||||
import { VideoChannelEditGeneralComponent } from './pages/video-channel-edit-general.component'
|
||||
import { VideoChannelEditEditorsComponent } from './pages/video-channel-editors.component'
|
||||
|
||||
@@ -25,5 +26,14 @@ export const videoChannelEditRoutes: Routes = [
|
||||
title: $localize`Channel editors`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'activities',
|
||||
component: VideoChannelActivitiesComponent,
|
||||
data: {
|
||||
meta: {
|
||||
title: $localize`Channel activities`
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<my-privacy-badge [playlist]="playlist"></my-privacy-badge>
|
||||
</td>
|
||||
|
||||
<td *ngIf="table.isColumnDisplayed('updated')">
|
||||
<td *ngIf="table.isColumnDisplayed('updatedAt')">
|
||||
{{ playlist.updatedAt | ptDate: 'short' }}
|
||||
</td>
|
||||
</ng-template>
|
||||
|
||||
@@ -23,9 +23,9 @@ import { PTDatePipe } from '../../shared/shared-main/common/date.pipe'
|
||||
import { NumberFormatterPipe } from '../../shared/shared-main/common/number-formatter.pipe'
|
||||
import { VideoPlaylistMiniatureComponent } from '../../shared/shared-video-playlist/video-playlist-miniature.component'
|
||||
import { PrivacyBadgeComponent } from '../../shared/shared-video/privacy-badge.component'
|
||||
import { ChannelToggleComponent } from '../../shared/standalone-channels/channel-toggle.component'
|
||||
import { ChannelToggleComponent } from '../../shared/shared-channels/channel-toggle.component'
|
||||
|
||||
type ColumnName = 'videoChannelPosition' | 'videos' | 'name' | 'privacy' | 'updated'
|
||||
type ColumnName = 'videoChannelPosition' | 'videos' | 'name' | 'privacy' | 'updatedAt'
|
||||
|
||||
type QueryParams = TableQueryParams & {
|
||||
channelName?: string
|
||||
@@ -104,7 +104,7 @@ export class MyVideoPlaylistsComponent implements OnInit, OnDestroy {
|
||||
{ id: 'videos', label: $localize`Videos`, selected: true, sortable: false },
|
||||
{ id: 'name', label: $localize`Name`, selected: true, sortable: true },
|
||||
{ id: 'privacy', label: $localize`Privacy`, selected: true, sortable: false },
|
||||
{ id: 'updated', label: $localize`Updated`, selected: true, sortable: false }
|
||||
{ id: 'updatedAt', label: $localize`Updated`, selected: true, sortable: true }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Video } from '@app/shared/shared-main/video/video.model'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { TableColumnInfo, TableComponent, TableQueryParams } from '@app/shared/shared-tables/table.component'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { ChannelToggleComponent } from '@app/shared/standalone-channels/channel-toggle.component'
|
||||
import { ChannelToggleComponent } from '@app/shared/shared-channels/channel-toggle.component'
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { arrayify, pick } from '@peertube/peertube-core-utils'
|
||||
import { VideoChannel, VideoExistInPlaylist, VideoPrivacy, VideoPrivacyType, VideosExistInPlaylists } from '@peertube/peertube-models'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
|
||||
import { WatchedWordsListAdminOwnerComponent } from '@app/shared/standalone-watched-words/watched-words-list-admin-owner.component'
|
||||
import { WatchedWordsListAdminOwnerComponent } from '@app/shared/shared-watched-words/watched-words-list-admin-owner.component'
|
||||
|
||||
@Component({
|
||||
templateUrl: './my-watched-words-list.component.html',
|
||||
|
||||
@@ -7,8 +7,7 @@ import { VideoBlockService } from '@app/shared/shared-moderation/video-block.ser
|
||||
import { UserSubscriptionService } from '@app/shared/shared-user-subscription/user-subscription.service'
|
||||
import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { WatchedWordsListService } from '@app/shared/standalone-watched-words/watched-words-list.service'
|
||||
import { WatchedWordsListService } from '@app/shared/shared-watched-words/watched-words-list.service'
|
||||
import { LoginGuard } from '../core'
|
||||
import { CommentsOnMyVideosComponent } from './comments-on-my-videos/comments-on-my-videos.component'
|
||||
import { AutomaticTagService } from './my-auto-tag-policies/automatic-tag.service'
|
||||
@@ -33,7 +32,6 @@ import { MyWatchedWordsListComponent } from './my-watched-words-list/my-watched-
|
||||
const commonConfig = {
|
||||
path: '',
|
||||
providers: [
|
||||
VideoPlaylistService,
|
||||
BlocklistService,
|
||||
VideoBlockService,
|
||||
AbuseService,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { LoginGuard } from '@app/core'
|
||||
import { RemoteInteractionComponent } from './remote-interaction.component'
|
||||
import { FindInBulkService } from '@app/shared/shared-search/find-in-bulk.service'
|
||||
import { SearchService } from '@app/shared/shared-search/search.service'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -11,8 +10,7 @@ export default [
|
||||
component: RemoteInteractionComponent,
|
||||
providers: [
|
||||
FindInBulkService,
|
||||
SearchService,
|
||||
VideoPlaylistService
|
||||
SearchService
|
||||
],
|
||||
canActivate: [ LoginGuard ],
|
||||
data: {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { SearchComponent } from './search.component'
|
||||
import { ChannelLazyLoadResolver, PlaylistLazyLoadResolver, VideoLazyLoadResolver } from './shared'
|
||||
import { UserSubscriptionService } from '../shared/shared-user-subscription/user-subscription.service'
|
||||
import { SearchService } from '@app/shared/shared-search/search.service'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
|
||||
import { BlocklistService } from '@app/shared/shared-moderation/blocklist.service'
|
||||
import { AbuseService } from '@app/shared/shared-moderation/abuse.service'
|
||||
@@ -19,7 +18,6 @@ export default [
|
||||
},
|
||||
providers: [
|
||||
SearchService,
|
||||
VideoPlaylistService,
|
||||
UserSubscriptionService,
|
||||
BlocklistService,
|
||||
VideoBlockService,
|
||||
|
||||
@@ -7,14 +7,12 @@ import { BlocklistService } from '@app/shared/shared-moderation/blocklist.servic
|
||||
import { BulkService } from '@app/shared/shared-moderation/bulk.service'
|
||||
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
|
||||
import { UserSubscriptionService } from '@app/shared/shared-user-subscription/user-subscription.service'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
|
||||
export default [
|
||||
{
|
||||
path: ':videoChannelName',
|
||||
component: VideoChannelsComponent,
|
||||
providers: [
|
||||
VideoPlaylistService,
|
||||
UserSubscriptionService,
|
||||
BlocklistService,
|
||||
BulkService,
|
||||
|
||||
@@ -5,10 +5,10 @@ import { AbuseService } from '@app/shared/shared-moderation/abuse.service'
|
||||
import { BlocklistService } from '@app/shared/shared-moderation/blocklist.service'
|
||||
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
|
||||
import { UserSubscriptionService } from '@app/shared/shared-user-subscription/user-subscription.service'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { OverviewService, VideosListAllComponent } from '.'
|
||||
import { OverviewService } from './overview'
|
||||
import { VideoOverviewComponent } from './overview/video-overview.component'
|
||||
import { VideoUserSubscriptionsComponent } from './video-user-subscriptions.component'
|
||||
import { VideosListAllComponent } from './videos-list-all.component'
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -16,7 +16,6 @@ export default [
|
||||
providers: [
|
||||
OverviewService,
|
||||
UserSubscriptionService,
|
||||
VideoPlaylistService,
|
||||
BlocklistService,
|
||||
VideoBlockService,
|
||||
AbuseService
|
||||
|
||||
@@ -8,7 +8,6 @@ import { UserSubscriptionService } from '@app/shared/shared-user-subscription/us
|
||||
import { UserAdminService } from '@app/shared/shared-users/user-admin.service'
|
||||
import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { VideoPlaylistService } from '@app/shared/shared-video-playlist/video-playlist.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { VideoStateMessageService } from '@app/shared/shared-video/video-state-message.service'
|
||||
import { OverviewService } from '../+video-list'
|
||||
@@ -21,7 +20,6 @@ export default [
|
||||
providers: [
|
||||
OverviewService,
|
||||
UserSubscriptionService,
|
||||
VideoPlaylistService,
|
||||
BlocklistService,
|
||||
VideoBlockService,
|
||||
LiveVideoService,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Video } from '@app/shared/shared-main/video/video.model'
|
||||
import { VideoStateMessageService } from '@app/shared/shared-video/video-state-message.service'
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ButtonComponent } from '../../shared/shared-main/buttons/button.component'
|
||||
import { UploadProgressComponent } from '../../shared/standalone-upload/upload-progress.component'
|
||||
import { UploadProgressComponent } from '../../shared/shared-upload/upload-progress.component'
|
||||
import { ManageErrorsComponent } from './common/manage-errors.component'
|
||||
import { VideoEdit } from './common/video-edit.model'
|
||||
import { VideoManageController } from './video-manage-controller.service'
|
||||
|
||||
@@ -6,7 +6,7 @@ import { GlobalIconComponent } from '@app/shared/shared-icons/global-icon.compon
|
||||
import { ButtonComponent } from '@app/shared/shared-main/buttons/button.component'
|
||||
import { LoaderComponent } from '@app/shared/shared-main/common/loader.component'
|
||||
import { UserNotificationService } from '@app/shared/shared-main/users/user-notification.service'
|
||||
import { UserNotificationsComponent } from '@app/shared/standalone-notifications/user-notifications.component'
|
||||
import { UserNotificationsComponent } from '@app/shared/shared-notifications/user-notifications.component'
|
||||
import { NgbDropdown, NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { filter } from 'rxjs/operators'
|
||||
|
||||
@@ -99,6 +99,7 @@ const icons = {
|
||||
'user-x': require('../../../assets/images/feather/user-x.svg'),
|
||||
'user': require('../../../assets/images/feather/user.svg'),
|
||||
'grip-horizontal': require('../../../assets/images/feather/grip-horizontal.svg'),
|
||||
'calendar': require('../../../assets/images/feather/calendar.svg'),
|
||||
'users': require('../../../assets/images/feather/users.svg')
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ComponentPaginationLight, RestExtractor, RestService, ServerService } f
|
||||
import {
|
||||
ActorImage,
|
||||
ResultList,
|
||||
VideoChannelActivity,
|
||||
VideoChannelCollaborator,
|
||||
VideoChannelCreate,
|
||||
VideoChannel as VideoChannelServer,
|
||||
@@ -162,4 +163,22 @@ export class VideoChannelService {
|
||||
return this.authHttp.delete(url)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
listActivities (options: {
|
||||
channelName: string
|
||||
componentPagination: ComponentPaginationLight
|
||||
}) {
|
||||
const { channelName, componentPagination } = options
|
||||
|
||||
const pagination = this.restService.componentToRestPagination(componentPagination)
|
||||
|
||||
let params = new HttpParams()
|
||||
params = this.restService.addRestGetParams(params, pagination)
|
||||
|
||||
const url = VideoChannelService.BASE_VIDEO_CHANNEL_URL + channelName + '/activities'
|
||||
return this.authHttp.get<ResultList<VideoChannelActivity>>(url, { params })
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
@if (text) {
|
||||
{{ text }}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { ChangeDetectionStrategy, Component, HostBinding, input, OnInit } from '@angular/core'
|
||||
import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@peertube/peertube-core-utils'
|
||||
|
||||
export enum GroupDate {
|
||||
TODAY = 1,
|
||||
YESTERDAY = 2,
|
||||
THIS_WEEK = 3,
|
||||
THIS_MONTH = 4,
|
||||
LAST_MONTH = 5,
|
||||
OLDER = 6
|
||||
}
|
||||
|
||||
export type GroupDateLabels = { [id in GroupDate]: string }
|
||||
|
||||
@Component({
|
||||
selector: 'my-date-group-label',
|
||||
templateUrl: 'date-group-label.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class DateGroupLabelComponent implements OnInit {
|
||||
store = input.required<Set<number>>()
|
||||
date = input.required<Date | string>()
|
||||
|
||||
groupedDateLabels = input<GroupDateLabels>({
|
||||
[GroupDate.TODAY]: $localize`Today`,
|
||||
[GroupDate.YESTERDAY]: $localize`Yesterday`,
|
||||
[GroupDate.THIS_WEEK]: $localize`This week`,
|
||||
[GroupDate.THIS_MONTH]: $localize`This month`,
|
||||
[GroupDate.LAST_MONTH]: $localize`Last month`,
|
||||
[GroupDate.OLDER]: $localize`Older`
|
||||
})
|
||||
|
||||
text: string
|
||||
|
||||
@HostBinding('class.date-displayed')
|
||||
dateDisplayed = false
|
||||
|
||||
ngOnInit (): void {
|
||||
const periods = [
|
||||
{
|
||||
value: GroupDate.TODAY,
|
||||
validator: (d: Date) => isToday(d)
|
||||
},
|
||||
{
|
||||
value: GroupDate.YESTERDAY,
|
||||
validator: (d: Date) => isYesterday(d)
|
||||
},
|
||||
{
|
||||
value: GroupDate.THIS_WEEK,
|
||||
validator: (d: Date) => isLastWeek(d)
|
||||
},
|
||||
{
|
||||
value: GroupDate.THIS_MONTH,
|
||||
validator: (d: Date) => isThisMonth(d)
|
||||
},
|
||||
{
|
||||
value: GroupDate.LAST_MONTH,
|
||||
validator: (d: Date) => isLastMonth(d)
|
||||
},
|
||||
{
|
||||
value: GroupDate.OLDER,
|
||||
validator: () => true
|
||||
}
|
||||
]
|
||||
|
||||
for (const period of periods) {
|
||||
if (period.validator(new Date(this.date()))) {
|
||||
if (this.store().has(period.value) === true) break
|
||||
|
||||
// Only "Older" period, no need to display anything
|
||||
if (period.value === GroupDate.OLDER && this.store().size === 0) break
|
||||
|
||||
this.store().add(period.value)
|
||||
this.text = this.groupedDateLabels()[period.value]
|
||||
this.dateDisplayed = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Component, OnChanges, inject, input, model } from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component, OnChanges, inject, input, model } from '@angular/core'
|
||||
import { FromNowPipe } from './from-now.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'my-date-toggle',
|
||||
templateUrl: './date-toggle.component.html',
|
||||
styleUrls: [ './date-toggle.component.scss' ],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true
|
||||
})
|
||||
export class DateToggleComponent implements OnChanges {
|
||||
@@ -40,6 +41,6 @@ export class DateToggleComponent implements OnChanges {
|
||||
|
||||
private updateDates () {
|
||||
this.dateRelative = this.fromNowPipe.transform(this.date())
|
||||
this.dateAbsolute = this.date().toLocaleDateString()
|
||||
this.dateAbsolute = this.date().toLocaleString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,11 @@ import { formatICU } from '@app/helpers'
|
||||
standalone: true
|
||||
})
|
||||
export class FromNowPipe implements PipeTransform {
|
||||
|
||||
transform (arg: number | Date | string) {
|
||||
const argDate = new Date(arg)
|
||||
const seconds = Math.trunc((Date.now() - argDate.getTime()) / 1000)
|
||||
const seconds = Math.round((Date.now() - argDate.getTime()) / 1000)
|
||||
|
||||
let interval = Math.trunc(seconds / 31536000)
|
||||
let interval = Math.round(seconds / 31536000)
|
||||
if (interval >= 1) {
|
||||
return formatICU($localize`{interval, plural, =1 {1 year ago} other {{interval} years ago}}`, { interval })
|
||||
}
|
||||
@@ -20,7 +19,7 @@ export class FromNowPipe implements PipeTransform {
|
||||
return formatICU($localize`{interval, plural, =1 {in 1 year} other {in {interval} years}}`, { interval: -interval })
|
||||
}
|
||||
|
||||
interval = Math.trunc(seconds / 2419200)
|
||||
interval = Math.round(seconds / 2419200)
|
||||
// 12 months = 360 days, but a year ~ 365 days
|
||||
// Display "1 year ago" rather than "12 months ago"
|
||||
if (interval >= 12) return $localize`1 year ago`
|
||||
@@ -33,7 +32,7 @@ export class FromNowPipe implements PipeTransform {
|
||||
return formatICU($localize`{interval, plural, =1 {in 1 month} other {in {interval} months}}`, { interval: -interval })
|
||||
}
|
||||
|
||||
interval = Math.trunc(seconds / 604800)
|
||||
interval = Math.round(seconds / 604800)
|
||||
// 4 weeks ~ 28 days, but our month is 30 days
|
||||
// Display "1 month ago" rather than "4 weeks ago"
|
||||
if (interval >= 4) return $localize`1 month ago`
|
||||
@@ -46,7 +45,7 @@ export class FromNowPipe implements PipeTransform {
|
||||
return formatICU($localize`{interval, plural, =1 {in 1 week} other {in {interval} weeks}}`, { interval: -interval })
|
||||
}
|
||||
|
||||
interval = Math.trunc(seconds / 86400)
|
||||
interval = Math.round(seconds / 86400)
|
||||
if (interval >= 1) {
|
||||
return formatICU($localize`{interval, plural, =1 {1 day ago} other {{interval} days ago}}`, { interval })
|
||||
}
|
||||
@@ -54,7 +53,7 @@ export class FromNowPipe implements PipeTransform {
|
||||
return formatICU($localize`{interval, plural, =1 {in 1 day} other {in {interval} days}}`, { interval: -interval })
|
||||
}
|
||||
|
||||
interval = Math.trunc(seconds / 3600)
|
||||
interval = Math.round(seconds / 3600)
|
||||
if (interval >= 1) {
|
||||
return formatICU($localize`{interval, plural, =1 {1 hour ago} other {{interval} hours ago}}`, { interval })
|
||||
}
|
||||
@@ -62,9 +61,9 @@ export class FromNowPipe implements PipeTransform {
|
||||
return formatICU($localize`{interval, plural, =1 {in 1 hour} other {in {interval} hours}}`, { interval: -interval })
|
||||
}
|
||||
|
||||
interval = Math.trunc(seconds / 60)
|
||||
interval = Math.round(seconds / 60)
|
||||
if (interval >= 1) return $localize`${interval} min ago`
|
||||
if (interval <= -1) return $localize`in ${ -interval } min`
|
||||
if (interval <= -1) return $localize`in ${-interval} min`
|
||||
|
||||
return $localize`just now`
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { VideoService } from '../shared-main/video/video.service'
|
||||
import { VideoThumbnailComponent } from '../shared-thumbnail/video-thumbnail.component'
|
||||
import { VideoPlaylistService } from '../shared-video-playlist/video-playlist.service'
|
||||
import { VideoViewsCounterComponent } from '../shared-video/video-views-counter.component'
|
||||
import { ActorHostComponent } from '../standalone-actor/actor-host.component'
|
||||
import { ActorHostComponent } from '../shared-actor/actor-host.component'
|
||||
import { VideoActionsDisplayType, VideoActionsDropdownComponent } from './video-actions-dropdown.component'
|
||||
|
||||
export type MiniatureDisplayOptions = {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
class="videos" [ngClass]="{ 'display-as-row': displayAsRow() }"
|
||||
>
|
||||
<ng-container *ngIf="highlightedLives.length !== 0">
|
||||
<h2 class="date-title">
|
||||
<h2 class="videos-section-title">
|
||||
<my-global-icon class="pt-icon me-1 top--1px" iconName="live"></my-global-icon>
|
||||
<ng-container i18n>Lives</ng-container>
|
||||
</h2>
|
||||
@@ -37,15 +37,17 @@
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<h2 *ngIf="!groupByDate()" class="date-title">
|
||||
<my-global-icon class="pt-icon me-1" iconName="videos"></my-global-icon> Videos
|
||||
<h2 class="date-title" [ngClass]="{ 'visually-hidden': groupByDate() }">
|
||||
<my-global-icon class="pt-icon me-1" iconName="videos"></my-global-icon>
|
||||
|
||||
<ng-container i18n>Videos</ng-container>
|
||||
</h2>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngFor="let video of videos; trackBy: videoById;">
|
||||
<h2 class="date-title" *ngIf="getCurrentGroupedDateLabel(video)">
|
||||
{{ getCurrentGroupedDateLabel(video) }}
|
||||
</h2>
|
||||
@if (groupByDate()) {
|
||||
<my-date-group-label [date]="video.publishedAt" [store]="groupByDateStore" [groupedDateLabels]="groupedDateLabels"></my-date-group-label>
|
||||
}
|
||||
|
||||
<div class="video-wrapper">
|
||||
<my-video-miniature
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
@use '_bootstrap-variables';
|
||||
@use '_variables' as *;
|
||||
@use '_mixins' as *;
|
||||
@use '_miniature' as *;
|
||||
@use "_bootstrap-variables";
|
||||
@use "_variables" as *;
|
||||
@use "_mixins" as *;
|
||||
@use "_miniature" as *;
|
||||
|
||||
// Cannot set margin top to videos-header because of the main header fixed position
|
||||
$margin-top: 2rem;
|
||||
@@ -21,7 +21,12 @@ $margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.date-title {
|
||||
my-date-group-label:not(.date-displayed) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
my-date-group-label.date-displayed,
|
||||
.videos-section-title {
|
||||
font-weight: $font-semibold;
|
||||
margin-bottom: 20px;
|
||||
font-size: 1rem;
|
||||
@@ -30,7 +35,7 @@ $margin-top: 2rem;
|
||||
grid-column: 1 / -1;
|
||||
|
||||
&:not(:first-child) {
|
||||
margin-top: .5rem;
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid $separator-border-color;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
updatePaginationOnDelete
|
||||
} from '@app/core'
|
||||
import { GlobalIconComponent, GlobalIconName } from '@app/shared/shared-icons/global-icon.component'
|
||||
import { isLastMonth, isLastWeek, isThisMonth, isToday, isYesterday } from '@peertube/peertube-core-utils'
|
||||
import { ResultList, VideoSortField } from '@peertube/peertube-models'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import debug from 'debug'
|
||||
@@ -21,6 +20,7 @@ import { Observable, Subject, Subscription, forkJoin, fromEvent, of } from 'rxjs
|
||||
import { concatMap, debounceTime, map, switchMap } from 'rxjs/operators'
|
||||
import { ButtonComponent } from '../shared-main/buttons/button.component'
|
||||
import { InfiniteScrollerDirective } from '../shared-main/common/infinite-scroller.directive'
|
||||
import { DateGroupLabelComponent, GroupDate, GroupDateLabels } from '../shared-main/date/date-group-label.component'
|
||||
import { Syndication } from '../shared-main/feeds/syndication.model'
|
||||
import { Video } from '../shared-main/video/video.model'
|
||||
import { VideoFiltersHeaderComponent } from './video-filters-header.component'
|
||||
@@ -35,16 +35,6 @@ export type HeaderAction = {
|
||||
routerLink?: string
|
||||
}
|
||||
|
||||
enum GroupDate {
|
||||
UNKNOWN = 0,
|
||||
TODAY = 1,
|
||||
YESTERDAY = 2,
|
||||
THIS_WEEK = 3,
|
||||
THIS_MONTH = 4,
|
||||
LAST_MONTH = 5,
|
||||
OLDER = 6
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'my-videos-list',
|
||||
templateUrl: './videos-list.component.html',
|
||||
@@ -56,7 +46,8 @@ enum GroupDate {
|
||||
VideoFiltersHeaderComponent,
|
||||
InfiniteScrollerDirective,
|
||||
VideoMiniatureComponent,
|
||||
GlobalIconComponent
|
||||
GlobalIconComponent,
|
||||
DateGroupLabelComponent
|
||||
]
|
||||
})
|
||||
export class VideosListComponent implements OnInit, OnDestroy {
|
||||
@@ -111,6 +102,16 @@ export class VideosListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
displayModerationBlock = true
|
||||
|
||||
groupByDateStore = new Set<number>()
|
||||
groupedDateLabels: GroupDateLabels = {
|
||||
[GroupDate.TODAY]: $localize`Today's videos`,
|
||||
[GroupDate.YESTERDAY]: $localize`Yesterday's videos`,
|
||||
[GroupDate.THIS_WEEK]: $localize`This week's videos`,
|
||||
[GroupDate.THIS_MONTH]: $localize`This month's videos`,
|
||||
[GroupDate.LAST_MONTH]: $localize`Last month's videos`,
|
||||
[GroupDate.OLDER]: $localize`Older videos`
|
||||
}
|
||||
|
||||
private routeSub: Subscription
|
||||
private userSub: Subscription
|
||||
private resizeSub: Subscription
|
||||
@@ -121,17 +122,6 @@ export class VideosListComponent implements OnInit, OnDestroy {
|
||||
totalItems: null
|
||||
}
|
||||
|
||||
private groupedDateLabels: { [id in GroupDate]: string } = {
|
||||
[GroupDate.UNKNOWN]: null,
|
||||
[GroupDate.TODAY]: $localize`Today's videos`,
|
||||
[GroupDate.YESTERDAY]: $localize`Yesterday's videos`,
|
||||
[GroupDate.THIS_WEEK]: $localize`This week's videos`,
|
||||
[GroupDate.THIS_MONTH]: $localize`This month's videos`,
|
||||
[GroupDate.LAST_MONTH]: $localize`Last month's videos`,
|
||||
[GroupDate.OLDER]: $localize`Older videos`
|
||||
}
|
||||
private groupedDates: { [id: number]: GroupDate } = {}
|
||||
|
||||
private lastQueryLength: number
|
||||
|
||||
private videoRequests = new Subject<{
|
||||
@@ -226,6 +216,7 @@ export class VideosListComponent implements OnInit, OnDestroy {
|
||||
this.hasDoneFirstQuery = false
|
||||
this.videos = []
|
||||
this.highlightedLives = []
|
||||
this.groupByDateStore.clear()
|
||||
|
||||
if (this.highlightLives() && (!this.filters.live || this.filters.live === 'both')) {
|
||||
liveFilters = this.filters.clone()
|
||||
@@ -350,8 +341,6 @@ export class VideosListComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.videos = this.videos.concat(videos)
|
||||
|
||||
if (this.groupByDate()) this.buildGroupedDateLabels()
|
||||
|
||||
this.onVideosDataSubject.next(videos)
|
||||
this.videosLoaded.emit(this.videos)
|
||||
},
|
||||
@@ -364,65 +353,4 @@ export class VideosListComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
private buildGroupedDateLabels () {
|
||||
let currentGroupedDate: GroupDate = GroupDate.UNKNOWN
|
||||
|
||||
const periods = [
|
||||
{
|
||||
value: GroupDate.TODAY,
|
||||
validator: (d: Date) => isToday(d)
|
||||
},
|
||||
{
|
||||
value: GroupDate.YESTERDAY,
|
||||
validator: (d: Date) => isYesterday(d)
|
||||
},
|
||||
{
|
||||
value: GroupDate.THIS_WEEK,
|
||||
validator: (d: Date) => isLastWeek(d)
|
||||
},
|
||||
{
|
||||
value: GroupDate.THIS_MONTH,
|
||||
validator: (d: Date) => isThisMonth(d)
|
||||
},
|
||||
{
|
||||
value: GroupDate.LAST_MONTH,
|
||||
validator: (d: Date) => isLastMonth(d)
|
||||
},
|
||||
{
|
||||
value: GroupDate.OLDER,
|
||||
validator: () => true
|
||||
}
|
||||
]
|
||||
|
||||
let onlyOlderPeriod = true
|
||||
|
||||
for (const video of this.videos) {
|
||||
const publishedDate = video.publishedAt
|
||||
|
||||
for (const period of periods) {
|
||||
if (currentGroupedDate <= period.value && period.validator(publishedDate)) {
|
||||
if (currentGroupedDate !== period.value) {
|
||||
if (period.value !== GroupDate.OLDER) onlyOlderPeriod = false
|
||||
|
||||
currentGroupedDate = period.value
|
||||
this.groupedDates[video.id] = currentGroupedDate
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No need to group by date, there is only "Older" period available
|
||||
if (onlyOlderPeriod) this.groupedDates = {}
|
||||
}
|
||||
|
||||
getCurrentGroupedDateLabel (video: Video) {
|
||||
if (this.groupByDate() === false) return undefined
|
||||
|
||||
return this.groupedDateLabels[this.groupedDates[video.id]]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
||||
|
||||
private disabled = false
|
||||
|
||||
private listenToPlaylistChangeSub: Subscription
|
||||
private listenToVideoPlaylistChangeSub: Subscription
|
||||
private listenToAccountPlaylistsChangeSub: Subscription
|
||||
private playlistsData: CachedPlaylist[] = []
|
||||
|
||||
private pendingAddId: number
|
||||
@@ -101,7 +102,7 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
||||
displayName: VIDEO_PLAYLIST_DISPLAY_NAME_VALIDATOR
|
||||
})
|
||||
|
||||
this.videoPlaylistService.listenToMyAccountPlaylistsChange()
|
||||
this.listenToAccountPlaylistsChangeSub = this.videoPlaylistService.listenToMyAccountPlaylistsChange()
|
||||
.subscribe(result => {
|
||||
this.playlistsData = result.data
|
||||
|
||||
@@ -121,6 +122,8 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
||||
|
||||
ngOnDestroy () {
|
||||
this.unsubscribePlaylistChanges()
|
||||
|
||||
this.listenToAccountPlaylistsChangeSub?.unsubscribe()
|
||||
}
|
||||
|
||||
disableForReuse () {
|
||||
@@ -338,15 +341,15 @@ export class VideoAddToPlaylistComponent extends FormReactive implements OnInit,
|
||||
private listenToVideoPlaylistChange () {
|
||||
this.unsubscribePlaylistChanges()
|
||||
|
||||
this.listenToPlaylistChangeSub = this.videoPlaylistService.listenToVideoPlaylistChange(this.video().id)
|
||||
this.listenToVideoPlaylistChangeSub = this.videoPlaylistService.listenToVideoPlaylistChange(this.video().id)
|
||||
.pipe(filter(() => this.disabled === false))
|
||||
.subscribe(existResult => this.rebuildPlaylists(existResult))
|
||||
}
|
||||
|
||||
private unsubscribePlaylistChanges () {
|
||||
if (this.listenToPlaylistChangeSub) {
|
||||
this.listenToPlaylistChangeSub.unsubscribe()
|
||||
this.listenToPlaylistChangeSub = undefined
|
||||
if (this.listenToVideoPlaylistChangeSub) {
|
||||
this.listenToVideoPlaylistChangeSub.unsubscribe()
|
||||
this.listenToVideoPlaylistChangeSub = undefined
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<div class="miniature" [ngClass]="{ 'no-videos': playlist().videosLength === 0, 'to-manage': toManage(), 'display-as-row': displayAsRow() }">
|
||||
<my-link
|
||||
[internalLink]="playlistRouterLink" [href]="playlistHref" [target]="playlistTarget" inheritParentStyle="true" inheritParentDimension="true"
|
||||
[title]="playlist().description" class="miniature-thumbnail" tabindex="-1"
|
||||
[title]="playlist().displayName" class="miniature-thumbnail" tabindex="-1"
|
||||
>
|
||||
<img alt="" [attr.aria-label]="playlist().displayName" [attr.src]="playlist().thumbnailUrl" />
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<div class="miniature-info">
|
||||
<my-link
|
||||
[internalLink]="playlistRouterLink" [href]="playlistHref" [target]="playlistTarget" inheritParentStyle="true" inheritParentDimension="true"
|
||||
[title]="playlist().description" class="miniature-name" className="ellipsis-multiline-2"
|
||||
[title]="playlist().displayName" class="miniature-name" className="ellipsis-multiline-2"
|
||||
>
|
||||
{{ playlist().displayName }}
|
||||
</my-link>
|
||||
|
||||
@@ -33,7 +33,7 @@ const debugLogger = debug('peertube:playlists:VideoPlaylistService')
|
||||
|
||||
export type CachedPlaylist = VideoPlaylist | { id: number, displayName: string }
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class VideoPlaylistService {
|
||||
private authHttp = inject(HttpClient)
|
||||
private auth = inject(AuthService)
|
||||
|
||||
1
client/src/assets/images/feather/calendar.svg
Normal file
1
client/src/assets/images/feather/calendar.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-calendar-search-icon lucide-calendar-search"><path d="M16 2v4"/><path d="M21 11.75V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h7.25"/><path d="m22 22-1.875-1.875"/><path d="M3 10h18"/><path d="M8 2v4"/><circle cx="18" cy="18" r="3"/></svg>
|
||||
|
After Width: | Height: | Size: 447 B |
@@ -7,6 +7,7 @@
|
||||
line-height: 1;
|
||||
|
||||
@include font-size(2rem);
|
||||
@include rfs(2rem, margin-top);
|
||||
@include rfs(2rem, margin-bottom);
|
||||
|
||||
my-global-icon {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Jsonify } from 'type-fest'
|
||||
|
||||
export function pick<O extends object, K extends keyof O> (object: O, keys: K[]): Pick<O, K> {
|
||||
// Forbid _attributes key to prevent pick on a sequelize model, that doesn't work for attributes
|
||||
export function pick<O extends (object & { _attributes?: never }), K extends keyof O> (object: O, keys: K[]): Pick<O, K> {
|
||||
const result: any = {}
|
||||
|
||||
for (const key of keys) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './video-channel-activity.model.js'
|
||||
export * from './video-channel-collaborator.model.js'
|
||||
export * from './video-channel-create-result.model.js'
|
||||
export * from './video-channel-create.model.js'
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { AccountSummary } from '../../actors/account.model.js'
|
||||
|
||||
export const VideoChannelActivityAction = {
|
||||
CREATE: 1,
|
||||
UPDATE: 2,
|
||||
DELETE: 3,
|
||||
UPDATE_CAPTIONS: 4,
|
||||
UPDATE_CHAPTERS: 5,
|
||||
UPDATE_PASSWORDS: 6,
|
||||
CREATE_STUDIO_TASKS: 7,
|
||||
UPDATE_SOURCE_FILE: 8,
|
||||
UPDATE_ELEMENTS: 9,
|
||||
REMOVE_CHANNEL_OWNERSHIP: 10,
|
||||
CREATE_CHANNEL_OWNERSHIP: 11
|
||||
} as const
|
||||
|
||||
export type VideoChannelActivityActionType = typeof VideoChannelActivityAction[keyof typeof VideoChannelActivityAction]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const VideoChannelActivityTarget = {
|
||||
VIDEO: 1,
|
||||
PLAYLIST: 2,
|
||||
CHANNEL: 3,
|
||||
CHANNEL_SYNC: 4,
|
||||
VIDEO_IMPORT: 5
|
||||
} as const
|
||||
|
||||
export type VideoChannelActivityTargetType = typeof VideoChannelActivityTarget[keyof typeof VideoChannelActivityTarget]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// To add update diff later
|
||||
export interface VideoChannelActivityDetails {
|
||||
}
|
||||
|
||||
export interface VideoChannelActivity {
|
||||
id: number
|
||||
|
||||
// The account may have been deleted
|
||||
account?: AccountSummary
|
||||
|
||||
action: {
|
||||
id: VideoChannelActivityActionType
|
||||
label: string
|
||||
}
|
||||
|
||||
targetType: {
|
||||
id: VideoChannelActivityTargetType
|
||||
label: string
|
||||
}
|
||||
|
||||
details: VideoChannelActivityDetails
|
||||
|
||||
createdAt: Date
|
||||
|
||||
channel: {
|
||||
id: number
|
||||
name: string
|
||||
displayName: string
|
||||
url: string
|
||||
}
|
||||
|
||||
video?: {
|
||||
id: number
|
||||
name: string
|
||||
uuid: string
|
||||
shortUUID: string
|
||||
url: string
|
||||
isLive: boolean
|
||||
}
|
||||
|
||||
videoImport?: {
|
||||
id: number
|
||||
name: string
|
||||
uuid: string
|
||||
shortUUID: string
|
||||
url: string
|
||||
targetUrl: string
|
||||
}
|
||||
|
||||
playlist?: {
|
||||
id: number
|
||||
name: string
|
||||
uuid: string
|
||||
shortUUID: string
|
||||
url: string
|
||||
}
|
||||
|
||||
channelSync?: {
|
||||
id: number
|
||||
externalChannelUrl: string
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,15 @@ import { buildAbsoluteFixturePath } from '@peertube/peertube-node-utils'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class CaptionsCommand extends AbstractCommand {
|
||||
|
||||
add (options: OverrideCommandOptions & {
|
||||
videoId: string | number
|
||||
language: string
|
||||
fixture: string
|
||||
mimeType?: string
|
||||
}) {
|
||||
const { videoId, language, fixture, mimeType } = options
|
||||
add (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: string | number
|
||||
language: string
|
||||
fixture?: string
|
||||
mimeType?: string
|
||||
}
|
||||
) {
|
||||
const { videoId, language, fixture = 'subtitle-good2.vtt', mimeType } = options
|
||||
|
||||
const path = '/api/v1/videos/' + videoId + '/captions/' + language
|
||||
|
||||
@@ -33,10 +34,12 @@ export class CaptionsCommand extends AbstractCommand {
|
||||
})
|
||||
}
|
||||
|
||||
runGenerate (options: OverrideCommandOptions & {
|
||||
videoId: string | number
|
||||
forceTranscription?: boolean
|
||||
}) {
|
||||
runGenerate (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: string | number
|
||||
forceTranscription?: boolean
|
||||
}
|
||||
) {
|
||||
const { videoId } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/captions/generate'
|
||||
|
||||
@@ -50,10 +53,12 @@ export class CaptionsCommand extends AbstractCommand {
|
||||
})
|
||||
}
|
||||
|
||||
list (options: OverrideCommandOptions & {
|
||||
videoId: string | number
|
||||
videoPassword?: string
|
||||
}) {
|
||||
list (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: string | number
|
||||
videoPassword?: string
|
||||
}
|
||||
) {
|
||||
const { videoId, videoPassword } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/captions'
|
||||
|
||||
@@ -67,10 +72,12 @@ export class CaptionsCommand extends AbstractCommand {
|
||||
})
|
||||
}
|
||||
|
||||
delete (options: OverrideCommandOptions & {
|
||||
videoId: string | number
|
||||
language: string
|
||||
}) {
|
||||
delete (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: string | number
|
||||
language: string
|
||||
}
|
||||
) {
|
||||
const { videoId, language } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/captions/' + language
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
HttpStatusCode,
|
||||
ResultList,
|
||||
VideoChannel,
|
||||
VideoChannelActivity,
|
||||
VideoChannelCreate,
|
||||
VideoChannelCreateResult,
|
||||
VideoChannelUpdate,
|
||||
@@ -155,12 +156,19 @@ export class ChannelsCommand extends AbstractCommand {
|
||||
|
||||
updateImage (
|
||||
options: OverrideCommandOptions & {
|
||||
fixture: string
|
||||
fixture?: string
|
||||
channelName: string | number
|
||||
type: 'avatar' | 'banner'
|
||||
}
|
||||
) {
|
||||
const { channelName, fixture, type } = options
|
||||
const { channelName, type } = options
|
||||
|
||||
let fixture = options.fixture
|
||||
|
||||
if (!fixture) {
|
||||
if (type === 'avatar') fixture = 'avatar.png'
|
||||
else fixture = 'banner.jpg'
|
||||
}
|
||||
|
||||
const path = `/api/v1/video-channels/${channelName}/${type}/pick`
|
||||
|
||||
@@ -195,6 +203,8 @@ export class ChannelsCommand extends AbstractCommand {
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
listFollowers (
|
||||
options: OverrideCommandOptions & {
|
||||
channelName: string
|
||||
@@ -219,6 +229,29 @@ export class ChannelsCommand extends AbstractCommand {
|
||||
})
|
||||
}
|
||||
|
||||
listActivities (
|
||||
options: OverrideCommandOptions & {
|
||||
channelName: string
|
||||
start?: number
|
||||
count?: number
|
||||
sort?: string
|
||||
}
|
||||
) {
|
||||
const { channelName, start, count, sort } = options
|
||||
const path = '/api/v1/video-channels/' + channelName + '/activities'
|
||||
|
||||
const query = { start, count, sort }
|
||||
|
||||
return this.getRequestBody<ResultList<VideoChannelActivity>>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
importVideos (
|
||||
options: OverrideCommandOptions & VideosImportInChannelCreate & {
|
||||
channelName: string
|
||||
|
||||
82
packages/tests/src/api/check-params/channel-activities.ts
Normal file
82
packages/tests/src/api/check-params/channel-activities.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import {
|
||||
createSingleServer,
|
||||
killallServers,
|
||||
makeGetRequest,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '@tests/shared/checks.js'
|
||||
|
||||
describe('Test video channel activities API validator', () => {
|
||||
const path = '/api/v1/video-channels/root_channel/activities'
|
||||
let server: PeerTubeServer
|
||||
let userToken: string
|
||||
let editorToken: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(30000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
|
||||
userToken = await server.users.generateUserAndToken('user')
|
||||
editorToken = await server.channelCollaborators.createEditor('editor', 'root_channel')
|
||||
})
|
||||
|
||||
describe('When listing channel activities', function () {
|
||||
it('Should fail with a bad start pagination', async function () {
|
||||
await checkBadStartPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with a bad count pagination', async function () {
|
||||
await checkBadCountPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail with an incorrect sort', async function () {
|
||||
await checkBadSortPagination(server.url, path, server.accessToken)
|
||||
})
|
||||
|
||||
it('Should fail without authentication', async function () {
|
||||
await server.channels.listActivities({ token: null, channelName: 'invalid', expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
})
|
||||
|
||||
it('Should fail with a bad channel', async function () {
|
||||
await server.channels.listActivities({ channelName: 'invalid', expectedStatus: HttpStatusCode.NOT_FOUND_404 })
|
||||
})
|
||||
|
||||
it('Should fail with a non owned channel', async function () {
|
||||
await server.channels.listActivities({ token: userToken, channelName: 'root_channel', expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
})
|
||||
|
||||
it('Should succeed with correct parameters', async function () {
|
||||
await makeGetRequest({
|
||||
url: server.url,
|
||||
path,
|
||||
token: server.accessToken,
|
||||
expectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
|
||||
await server.channels.listActivities({
|
||||
channelName: 'root_channel',
|
||||
start: 0,
|
||||
count: 10,
|
||||
sort: '-createdAt'
|
||||
})
|
||||
|
||||
await server.channels.listActivities({
|
||||
token: editorToken,
|
||||
channelName: 'root_channel',
|
||||
start: 0,
|
||||
count: 10,
|
||||
sort: '-createdAt'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await killallServers([ server ])
|
||||
})
|
||||
})
|
||||
@@ -3,6 +3,7 @@ import './accounts.js'
|
||||
import './auto-tags.js'
|
||||
import './blocklist.js'
|
||||
import './bulk.js'
|
||||
import './channel-activities.js'
|
||||
import './channel-import-videos.js'
|
||||
import './config.js'
|
||||
import './contact-form.js'
|
||||
|
||||
779
packages/tests/src/api/videos/channel-activities.ts
Normal file
779
packages/tests/src/api/videos/channel-activities.ts
Normal file
@@ -0,0 +1,779 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import {
|
||||
LiveVideoLatencyMode,
|
||||
VideoChannelActivityAction,
|
||||
VideoChannelActivityTarget,
|
||||
VideoCreateResult,
|
||||
VideoImport,
|
||||
VideoPlaylistCreateResult,
|
||||
VideoPlaylistElementCreateResult,
|
||||
VideoPlaylistPrivacy,
|
||||
VideoPrivacy
|
||||
} from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultAccountAvatar,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { FIXTURE_URLS } from '@tests/shared/fixture-urls.js'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test channel activities', function () {
|
||||
let server: PeerTubeServer
|
||||
let editorToken: string
|
||||
let channelId: number
|
||||
let channelId2: number
|
||||
|
||||
async function getActivityAfterAction (action: () => Promise<any>) {
|
||||
const before = new Date()
|
||||
await action()
|
||||
|
||||
const { data } = await server.channels.listActivities({ channelName: 'eminem_channel', sort: '-createdAt' })
|
||||
|
||||
const activity = data[0]
|
||||
|
||||
expect(new Date(activity.createdAt).getTime()).to.be.greaterThan(before.getTime())
|
||||
|
||||
if (data.length >= 2) {
|
||||
expect(new Date(data[1].createdAt).getTime()).to.be.below(before.getTime())
|
||||
}
|
||||
|
||||
return data[0]
|
||||
}
|
||||
|
||||
before(async function () {
|
||||
server = await createSingleServer(1)
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultAccountAvatar([ server ])
|
||||
|
||||
{
|
||||
const channel = await server.channels.create({ attributes: { name: 'eminem_channel', displayName: 'Eminem' } })
|
||||
channelId = channel.id
|
||||
}
|
||||
|
||||
{
|
||||
const channel = await server.channels.create({ attributes: { name: 'mero', displayName: 'Mero' } })
|
||||
channelId2 = channel.id
|
||||
}
|
||||
|
||||
editorToken = await server.channelCollaborators.createEditor('editor', 'eminem_channel')
|
||||
})
|
||||
|
||||
describe('Channel', function () {
|
||||
it('Should have an empty activities list', async function () {
|
||||
const { data, total } = await server.channels.listActivities({ channelName: 'eminem_channel' })
|
||||
expect(data).to.have.lengthOf(0)
|
||||
expect(total).to.equal(0)
|
||||
})
|
||||
|
||||
it('Should update a channel', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.channels.update({
|
||||
channelName: 'eminem_channel',
|
||||
|
||||
attributes: {
|
||||
displayName: 'Updated Eminem',
|
||||
description: 'This is the new description'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.id).to.exist
|
||||
|
||||
expect(a.account.id).to.exist
|
||||
expect(a.account.avatars).to.have.length.greaterThan(3)
|
||||
expect(a.account.displayName).to.equal('root')
|
||||
expect(a.account.host).to.exist
|
||||
expect(a.account.url).to.exist
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE)
|
||||
expect(a.action.label).to.equal('Update')
|
||||
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.CHANNEL)
|
||||
expect(a.targetType.label).to.equal('Channel')
|
||||
|
||||
expect(a.channel.id).to.exist
|
||||
expect(a.channel.displayName).to.equal('Updated Eminem')
|
||||
expect(a.channel.name).to.equal('eminem_channel')
|
||||
expect(a.channel.url).to.exist
|
||||
|
||||
expect(a.details).to.be.null
|
||||
})
|
||||
|
||||
it('Should update channel avatar', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.channels.updateImage({
|
||||
channelName: 'eminem_channel',
|
||||
type: 'avatar'
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.CHANNEL)
|
||||
expect(a.channel.name).to.equal('eminem_channel')
|
||||
})
|
||||
|
||||
it('Should update channel banner', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.channels.updateImage({
|
||||
channelName: 'eminem_channel',
|
||||
type: 'banner'
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.CHANNEL)
|
||||
expect(a.channel.name).to.equal('eminem_channel')
|
||||
})
|
||||
|
||||
it('Should delete channel avatar by editor', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.channels.deleteImage({
|
||||
token: editorToken,
|
||||
channelName: 'eminem_channel',
|
||||
type: 'avatar'
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.account.displayName).to.equal('editor')
|
||||
expect(a.account.avatars).to.have.lengthOf(0)
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.CHANNEL)
|
||||
expect(a.channel.name).to.equal('eminem_channel')
|
||||
})
|
||||
|
||||
it('Should delete channel banner', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.channels.deleteImage({
|
||||
channelName: 'eminem_channel',
|
||||
type: 'avatar'
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.CHANNEL)
|
||||
expect(a.channel.name).to.equal('eminem_channel')
|
||||
})
|
||||
|
||||
it('Should update player settings', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.playerSettings.updateForChannel({
|
||||
channelHandle: 'eminem_channel',
|
||||
theme: 'lucide'
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.CHANNEL)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Videos', function () {
|
||||
let video: VideoCreateResult
|
||||
|
||||
before(async function () {
|
||||
await server.config.updateExistingConfig({
|
||||
newConfig: {
|
||||
videoFile: {
|
||||
update: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
await server.config.enableMinimumTranscoding()
|
||||
})
|
||||
|
||||
it('Should upload a video', async function () {
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
video = await server.videos.quickUpload({
|
||||
name: 'uploaded video',
|
||||
channelId
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.CREATE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
|
||||
expect(a.video.id).to.exist
|
||||
expect(a.video.name).to.equal('uploaded video')
|
||||
expect(a.video.uuid).to.exist
|
||||
expect(a.video.shortUUID).to.exist
|
||||
expect(a.video.url).to.exist
|
||||
expect(a.video.isLive).to.be.false
|
||||
})
|
||||
|
||||
it('Should update the video', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.videos.update({
|
||||
id: video.id,
|
||||
attributes: {
|
||||
name: 'Updated video name'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
expect(a.video.name).to.equal('Updated video name')
|
||||
})
|
||||
|
||||
it('Should add captions', async function () {
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
await server.captions.add({
|
||||
videoId: video.id,
|
||||
language: 'fr'
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE_CAPTIONS)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
})
|
||||
|
||||
it('Should remove captions by editor', async function () {
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
await server.captions.delete({ videoId: video.id, language: 'fr', token: editorToken })
|
||||
})
|
||||
|
||||
expect(a.account.name).to.equal('editor')
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE_CAPTIONS)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
})
|
||||
|
||||
it('Should add a password', async function () {
|
||||
await server.videos.update({
|
||||
id: video.id,
|
||||
attributes: { privacy: VideoPrivacy.PASSWORD_PROTECTED, videoPasswords: [ 'password 1' ] }
|
||||
})
|
||||
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
await server.videoPasswords.updateAll({
|
||||
videoId: video.id,
|
||||
passwords: [ 'mypassword1', 'mypassword2' ]
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE_PASSWORDS)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
})
|
||||
|
||||
it('Should delete a password', async function () {
|
||||
const { data } = await server.videoPasswords.list({ videoId: video.id })
|
||||
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
await server.videoPasswords.remove({ id: data[0].id, videoId: video.id, token: editorToken })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE_PASSWORDS)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
|
||||
await server.videos.update({ id: video.id, attributes: { privacy: VideoPrivacy.PUBLIC } })
|
||||
})
|
||||
|
||||
it('Should add chapters', async function () {
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
await server.chapters.update({
|
||||
videoId: video.id,
|
||||
|
||||
chapters: [
|
||||
{ timecode: 0, title: 'Intro' }
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE_CHAPTERS)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
})
|
||||
|
||||
it('Should add a source file', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
const video = await server.videos.quickUpload({ name: 'video with source file', channelId })
|
||||
await waitJobs([ server ])
|
||||
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
await server.videos.replaceSourceFile({
|
||||
token: editorToken,
|
||||
videoId: video.id,
|
||||
fixture: 'video_short.mp4'
|
||||
})
|
||||
})
|
||||
|
||||
await waitJobs([ server ])
|
||||
|
||||
expect(a.account.name).to.equal('editor')
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE_SOURCE_FILE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
})
|
||||
|
||||
it('Should delete a source file', async function () {
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
await server.videos.deleteSource({ id: video.id })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE_SOURCE_FILE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
})
|
||||
|
||||
it('Should create a studio task', async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
await server.config.enableStudio()
|
||||
|
||||
const video = await server.videos.quickUpload({ name: 'video for studio', channelId })
|
||||
await waitJobs([ server ])
|
||||
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
await server.videoStudio.createEditionTasks({
|
||||
videoId: video.id,
|
||||
token: editorToken,
|
||||
tasks: [
|
||||
{
|
||||
name: 'cut',
|
||||
options: {
|
||||
start: 1,
|
||||
end: 3
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.account.name).to.equal('editor')
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.CREATE_STUDIO_TASKS)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
})
|
||||
|
||||
it('Should delete the video', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.videos.remove({ id: video.id })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.DELETE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
|
||||
expect(a.video.id).to.exist
|
||||
expect(a.video.name).to.equal('Updated video name')
|
||||
expect(a.video.uuid).to.exist
|
||||
expect(a.video.shortUUID).to.exist
|
||||
expect(a.video.url).to.exist
|
||||
expect(a.video.isLive).to.be.false
|
||||
})
|
||||
|
||||
it('Should update the channel of a video', async function () {
|
||||
const video = await server.videos.quickUpload({ name: 'video to change channel', channelId })
|
||||
|
||||
{
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.videos.update({ id: video.id, attributes: { channelId: channelId2 } })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.REMOVE_CHANNEL_OWNERSHIP)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
expect(a.video.name).to.equal('video to change channel')
|
||||
}
|
||||
|
||||
{
|
||||
const { data } = await server.channels.listActivities({ channelName: 'mero', sort: '-createdAt' })
|
||||
const a2 = data[0]
|
||||
|
||||
expect(a2.action.id).to.equal(VideoChannelActivityAction.CREATE_CHANNEL_OWNERSHIP)
|
||||
expect(a2.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
expect(a2.video.name).to.equal('video to change channel')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Lives', async function () {
|
||||
let liveVideo: VideoCreateResult
|
||||
|
||||
before(async function () {
|
||||
await server.config.enableLive()
|
||||
})
|
||||
|
||||
it('Should create a live', async function () {
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
liveVideo = await server.live.create({
|
||||
fields: {
|
||||
name: 'live',
|
||||
privacy: VideoPrivacy.PUBLIC,
|
||||
channelId
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.CREATE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
expect(a.video.id).to.exist
|
||||
expect(a.video.name).to.equal('live')
|
||||
expect(a.video.uuid).to.exist
|
||||
expect(a.video.shortUUID).to.exist
|
||||
expect(a.video.url).to.exist
|
||||
expect(a.video.isLive).to.be.true
|
||||
})
|
||||
|
||||
it('Should update the live', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.live.update({
|
||||
videoId: liveVideo.id,
|
||||
fields: {
|
||||
latencyMode: LiveVideoLatencyMode.HIGH_LATENCY
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
})
|
||||
|
||||
it('Should delete the live', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.videos.remove({ id: liveVideo.id })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.DELETE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
expect(a.video.name).to.equal('live')
|
||||
expect(a.video.isLive).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
describe('Video imports', function () {
|
||||
let videoImport: VideoImport
|
||||
|
||||
it('Should import a video', async function () {
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
videoImport = await server.videoImports.importVideo({ attributes: { targetUrl: FIXTURE_URLS.goodVideo, channelId } })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.CREATE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO_IMPORT)
|
||||
|
||||
expect(a.videoImport.id).to.exist
|
||||
expect(a.videoImport.name).to.equal('good_video')
|
||||
expect(a.videoImport.targetUrl).to.equal(FIXTURE_URLS.goodVideo)
|
||||
expect(a.videoImport.uuid).to.exist
|
||||
expect(a.videoImport.shortUUID).to.exist
|
||||
expect(a.videoImport.url).to.exist
|
||||
})
|
||||
|
||||
it('Should delete the import video', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.videos.remove({ id: videoImport.video.id })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.DELETE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
|
||||
const { data } = await server.channels.listActivities({ channelName: 'eminem_channel', sort: '-createdAt' })
|
||||
const importRow = data[1]
|
||||
|
||||
expect(importRow.videoImport.id).to.exist
|
||||
expect(importRow.videoImport.name).to.equal('good_video')
|
||||
expect(importRow.videoImport.targetUrl).to.equal(FIXTURE_URLS.goodVideo)
|
||||
expect(importRow.videoImport.uuid).to.exist
|
||||
expect(importRow.videoImport.shortUUID).to.exist
|
||||
expect(importRow.videoImport.url).to.exist
|
||||
})
|
||||
})
|
||||
|
||||
describe('Playlists', function () {
|
||||
let playlist: VideoPlaylistCreateResult
|
||||
let playlistElement: VideoPlaylistElementCreateResult
|
||||
|
||||
it('Should create a playlist', async function () {
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
playlist = await server.playlists.create({
|
||||
attributes: {
|
||||
displayName: 'My playlist',
|
||||
videoChannelId: channelId,
|
||||
privacy: VideoPlaylistPrivacy.PRIVATE
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.CREATE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.PLAYLIST)
|
||||
|
||||
expect(a.playlist.id).to.exist
|
||||
expect(a.playlist.name).to.equal('My playlist')
|
||||
expect(a.playlist.uuid).exist
|
||||
expect(a.playlist.url).to.exist
|
||||
})
|
||||
|
||||
it('Should update the playlist', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.playlists.update({
|
||||
playlistId: playlist.id,
|
||||
attributes: {
|
||||
displayName: 'My playlist updated',
|
||||
videoChannelId: channelId
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.PLAYLIST)
|
||||
expect(a.playlist.name).to.equal('My playlist updated')
|
||||
})
|
||||
|
||||
it('Should add elements in a playlist', async function () {
|
||||
const video = await server.videos.quickUpload({ name: 'playlist video 1', channelId })
|
||||
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
playlistElement = await server.playlists.addElement({
|
||||
playlistId: playlist.id,
|
||||
attributes: {
|
||||
videoId: video.id
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE_ELEMENTS)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.PLAYLIST)
|
||||
})
|
||||
|
||||
it('Should update elements in a playlist', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.playlists.updateElement({
|
||||
playlistId: playlist.id,
|
||||
elementId: playlistElement.id,
|
||||
attributes: {
|
||||
startTimestamp: 10
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE_ELEMENTS)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.PLAYLIST)
|
||||
})
|
||||
|
||||
it('Should delete elements in a playlist', async function () {
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
await server.playlists.removeElement({
|
||||
playlistId: playlist.id,
|
||||
elementId: playlistElement.id
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE_ELEMENTS)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.PLAYLIST)
|
||||
})
|
||||
|
||||
it('Should delete the playlist', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.playlists.delete({ playlistId: playlist.id })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.DELETE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.PLAYLIST)
|
||||
expect(a.playlist.name).to.equal('My playlist updated')
|
||||
})
|
||||
|
||||
it('Should assign a new channel to a playlist without channel', async function () {
|
||||
const playlist = await server.playlists.create({
|
||||
attributes: {
|
||||
displayName: 'With a new channel',
|
||||
privacy: VideoPlaylistPrivacy.PRIVATE
|
||||
}
|
||||
})
|
||||
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.playlists.update({ playlistId: playlist.id, attributes: { videoChannelId: channelId } })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.CREATE_CHANNEL_OWNERSHIP)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.PLAYLIST)
|
||||
expect(a.playlist.name).to.equal('With a new channel')
|
||||
})
|
||||
|
||||
it('Should delete the channel of a playlist', async function () {
|
||||
const playlist = await server.playlists.create({
|
||||
attributes: {
|
||||
displayName: 'Playlist channel deleted',
|
||||
videoChannelId: channelId,
|
||||
privacy: VideoPlaylistPrivacy.UNLISTED
|
||||
}
|
||||
})
|
||||
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.playlists.update({ playlistId: playlist.id, attributes: { videoChannelId: 'null' as any } })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.REMOVE_CHANNEL_OWNERSHIP)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.PLAYLIST)
|
||||
expect(a.playlist.name).to.equal('Playlist channel deleted')
|
||||
})
|
||||
|
||||
it('Should update the channel of a playlist', async function () {
|
||||
{
|
||||
const playlist = await server.playlists.create({
|
||||
attributes: {
|
||||
displayName: 'Playlist channel updated',
|
||||
videoChannelId: channelId,
|
||||
privacy: VideoPlaylistPrivacy.PUBLIC
|
||||
}
|
||||
})
|
||||
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.playlists.update({ playlistId: playlist.id, attributes: { videoChannelId: channelId2 } })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.REMOVE_CHANNEL_OWNERSHIP)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.PLAYLIST)
|
||||
expect(a.playlist.name).to.equal('Playlist channel updated')
|
||||
}
|
||||
|
||||
{
|
||||
const { data } = await server.channels.listActivities({ channelName: 'mero', sort: '-createdAt' })
|
||||
const a2 = data[0]
|
||||
|
||||
expect(a2.action.id).to.equal(VideoChannelActivityAction.CREATE_CHANNEL_OWNERSHIP)
|
||||
expect(a2.targetType.id).to.equal(VideoChannelActivityTarget.PLAYLIST)
|
||||
expect(a2.playlist.name).to.equal('Playlist channel updated')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Channel sync', function () {
|
||||
let syncId: number
|
||||
|
||||
before(async function () {
|
||||
await server.config.enableChannelSync()
|
||||
})
|
||||
|
||||
it('Should create a channel sync', async function () {
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
const { videoChannelSync } = await server.channelSyncs.create({
|
||||
attributes: {
|
||||
externalChannelUrl: FIXTURE_URLS.youtubePlaylist,
|
||||
videoChannelId: channelId
|
||||
}
|
||||
})
|
||||
|
||||
syncId = videoChannelSync.id
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.CREATE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.CHANNEL_SYNC)
|
||||
|
||||
expect(a.channelSync.id).to.exist
|
||||
expect(a.channelSync.externalChannelUrl).to.equal(FIXTURE_URLS.youtubePlaylist)
|
||||
})
|
||||
|
||||
it('Should delete a channel sync', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.channelSyncs.delete({ channelSyncId: syncId })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.DELETE)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.CHANNEL_SYNC)
|
||||
expect(a.channelSync.externalChannelUrl).to.equal(FIXTURE_URLS.youtubePlaylist)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Common', function () {
|
||||
let channelId: number
|
||||
|
||||
before(async function () {
|
||||
await server.channels.create({ attributes: { name: 'soprano_channel', displayName: 'Soprano' } })
|
||||
|
||||
await server.channels.update({
|
||||
channelName: 'soprano_channel',
|
||||
attributes: {
|
||||
description: 'This is the new description'
|
||||
}
|
||||
})
|
||||
|
||||
channelId = await server.channels.getIdOf({ channelName: 'soprano_channel' })
|
||||
|
||||
const { uuid } = await server.videos.quickUpload({
|
||||
name: 'Inaya',
|
||||
channelId
|
||||
})
|
||||
|
||||
await server.videos.remove({ id: uuid })
|
||||
})
|
||||
|
||||
it('Should correctly paginate activities', async function () {
|
||||
const { data, total } = await server.channels.listActivities({ channelName: 'soprano_channel', start: 1, count: 1 })
|
||||
|
||||
expect(total).to.equal(3)
|
||||
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
expect(data[0].action.id).to.equal(VideoChannelActivityAction.CREATE)
|
||||
|
||||
{
|
||||
const { data, total } = await server.channels.listActivities({ channelName: 'soprano_channel', start: 1, count: 2 })
|
||||
|
||||
expect(total).to.equal(3)
|
||||
expect(data).to.have.lengthOf(2)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should correctly sort activities', async function () {
|
||||
{
|
||||
const { data, total } = await server.channels.listActivities({
|
||||
channelName: 'soprano_channel',
|
||||
sort: 'createdAt',
|
||||
start: 0,
|
||||
count: 1
|
||||
})
|
||||
expect(total).to.equal(3)
|
||||
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].targetType.id).to.equal(VideoChannelActivityTarget.CHANNEL)
|
||||
expect(data[0].action.id).to.equal(VideoChannelActivityAction.UPDATE)
|
||||
}
|
||||
|
||||
{
|
||||
const { data, total } = await server.channels.listActivities({
|
||||
channelName: 'soprano_channel',
|
||||
sort: '-createdAt',
|
||||
start: 0,
|
||||
count: 1
|
||||
})
|
||||
|
||||
expect(total).to.equal(3)
|
||||
|
||||
expect(data).to.have.lengthOf(1)
|
||||
expect(data[0].targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
expect(data[0].action.id).to.equal(VideoChannelActivityAction.DELETE)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should still display activities if the account is deleted', async function () {
|
||||
const editor2Token = await server.channelCollaborators.createEditor('editor2', 'soprano_channel')
|
||||
|
||||
await server.videos.quickUpload({ name: 'deleted account', channelId, token: editor2Token })
|
||||
await server.users.deleteMe({ token: editor2Token })
|
||||
|
||||
const { data } = await server.channels.listActivities({
|
||||
channelName: 'soprano_channel',
|
||||
sort: '-createdAt',
|
||||
start: 0,
|
||||
count: 1
|
||||
})
|
||||
|
||||
expect(data).to.have.lengthOf(1)
|
||||
const a = data[0]
|
||||
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.CREATE)
|
||||
|
||||
expect(a.account).to.not.exist
|
||||
expect(a.video.name).to.equal('deleted account')
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import './channel-activities.js'
|
||||
import './channel-import-videos.js'
|
||||
import './generate-download.js'
|
||||
import './multiple-servers.js'
|
||||
|
||||
@@ -135,7 +135,7 @@ describe('Test video playlists', function () {
|
||||
|
||||
await commands[0].create({
|
||||
attributes: {
|
||||
displayName: 'my super playlist',
|
||||
displayName: 'my normal playlist',
|
||||
privacy: VideoPlaylistPrivacy.PUBLIC,
|
||||
description: 'my super description',
|
||||
thumbnailfile: 'custom-thumbnail.jpg',
|
||||
@@ -176,7 +176,7 @@ describe('Test video playlists', function () {
|
||||
expect(body.data).to.have.lengthOf(1)
|
||||
|
||||
playlist = body.data[0]
|
||||
expect(playlist.displayName).to.equal('my super playlist')
|
||||
expect(playlist.displayName).to.equal('my normal playlist')
|
||||
expect(playlist.privacy.id).to.equal(VideoPlaylistPrivacy.PUBLIC)
|
||||
expect(playlist.type.id).to.equal(VideoPlaylistType.REGULAR)
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('Official plugin auth-openid-connect', function () {
|
||||
const peertubeRes = await getOpenIdUrl(openIdLoginUrl)
|
||||
const kcRes = await loginOnKeycloak({ loginPageUrl: extractLocation(peertubeRes) })
|
||||
|
||||
const ptBypassPath = await sendBackKeycloakCode({ peertubeRes, kcRes })
|
||||
const ptBypassPath = await sendBackKeycloakCode({ peertubeRes, kcRes, success: true })
|
||||
const externalAuthToken = new URL(ptBypassPath, server.url).searchParams.get('externalAuthToken')
|
||||
|
||||
const { body } = await server.login.loginUsingExternalToken({ username: 'myuser_example.com', externalAuthToken })
|
||||
@@ -75,7 +75,7 @@ describe('Official plugin auth-openid-connect', function () {
|
||||
const peertubeRes = await getOpenIdUrl(openIdLoginUrl, 'http://example.com')
|
||||
const kcRes = await loginOnKeycloak({ loginPageUrl: extractLocation(peertubeRes) })
|
||||
|
||||
const ptBypassPath = await sendBackKeycloakCode({ peertubeRes, kcRes, redirectUrl: 'http://example.com' })
|
||||
const ptBypassPath = await sendBackKeycloakCode({ peertubeRes, kcRes, redirectUrl: 'http://example.com', success: true })
|
||||
const externalAuthToken = new URL(ptBypassPath, server.url).searchParams.get('externalAuthToken')
|
||||
|
||||
const { body } = await server.login.loginUsingExternalToken({ username: 'myuser_example.com', externalAuthToken })
|
||||
@@ -102,7 +102,8 @@ describe('Official plugin auth-openid-connect', function () {
|
||||
|
||||
const peertubeRes = await getOpenIdUrl(openIdLoginUrl)
|
||||
const kcRes = await loginOnKeycloak({ loginPageUrl: extractLocation(peertubeRes), username: 'user_group1' })
|
||||
await sendBackKeycloakCode({ peertubeRes, kcRes, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
const redirectUrl = await sendBackKeycloakCode({ peertubeRes, kcRes, success: false })
|
||||
expect(redirectUrl).to.equal('/login?externalAuthError=true')
|
||||
}
|
||||
|
||||
{
|
||||
@@ -113,7 +114,7 @@ describe('Official plugin auth-openid-connect', function () {
|
||||
|
||||
const peertubeRes = await getOpenIdUrl(openIdLoginUrl)
|
||||
const kcRes = await loginOnKeycloak({ loginPageUrl: extractLocation(peertubeRes), username: 'user_group1' })
|
||||
await sendBackKeycloakCode({ peertubeRes, kcRes, expectedStatus: HttpStatusCode.FOUND_302, username: 'user_group1_example.com' })
|
||||
await sendBackKeycloakCode({ peertubeRes, kcRes, username: 'user_group1_example.com', success: true })
|
||||
}
|
||||
|
||||
{
|
||||
@@ -124,7 +125,8 @@ describe('Official plugin auth-openid-connect', function () {
|
||||
|
||||
const peertubeRes = await getOpenIdUrl(openIdLoginUrl)
|
||||
const kcRes = await loginOnKeycloak({ loginPageUrl: extractLocation(peertubeRes), username: 'user_group1' })
|
||||
await sendBackKeycloakCode({ peertubeRes, kcRes, expectedStatus: HttpStatusCode.FORBIDDEN_403 })
|
||||
const redirectUrl = await sendBackKeycloakCode({ peertubeRes, kcRes, success: false })
|
||||
expect(redirectUrl).to.equal('/login?externalAuthError=true')
|
||||
}
|
||||
})
|
||||
|
||||
@@ -134,7 +136,7 @@ describe('Official plugin auth-openid-connect', function () {
|
||||
{
|
||||
const peertubeRes = await getOpenIdUrl(openIdLoginUrl)
|
||||
const kcRes = await loginOnKeycloak({ loginPageUrl: extractLocation(peertubeRes), username: 'moderator' })
|
||||
const ptBypassPath = await sendBackKeycloakCode({ peertubeRes, kcRes, username: 'moderator_example.com' })
|
||||
const ptBypassPath = await sendBackKeycloakCode({ peertubeRes, kcRes, username: 'moderator_example.com', success: true })
|
||||
|
||||
const externalAuthToken = new URL(ptBypassPath, server.url).searchParams.get('externalAuthToken')
|
||||
|
||||
@@ -146,7 +148,7 @@ describe('Official plugin auth-openid-connect', function () {
|
||||
{
|
||||
const peertubeRes = await getOpenIdUrl(openIdLoginUrl)
|
||||
const kcRes = await loginOnKeycloak({ loginPageUrl: extractLocation(peertubeRes), username: 'user_group2' })
|
||||
const ptBypassPath = await sendBackKeycloakCode({ peertubeRes, kcRes, username: 'user_group2_example.com' })
|
||||
const ptBypassPath = await sendBackKeycloakCode({ peertubeRes, kcRes, username: 'user_group2_example.com', success: true })
|
||||
|
||||
const externalAuthToken = new URL(ptBypassPath, server.url).searchParams.get('externalAuthToken')
|
||||
|
||||
@@ -240,11 +242,11 @@ async function loginOnKeycloak (options: {
|
||||
async function sendBackKeycloakCode (options: {
|
||||
peertubeRes: Response
|
||||
kcRes: Response
|
||||
success: boolean
|
||||
redirectUrl?: string
|
||||
expectedStatus?: HttpStatusCodeType
|
||||
username?: string
|
||||
}) {
|
||||
const { peertubeRes, kcRes, redirectUrl = '/login', expectedStatus = HttpStatusCode.FOUND_302, username = 'myuser_example.com' } = options
|
||||
const { peertubeRes, kcRes, redirectUrl = '/login', success, username = 'myuser_example.com' } = options
|
||||
|
||||
const kcText = kcRes.text
|
||||
|
||||
@@ -261,15 +263,16 @@ async function sendBackKeycloakCode (options: {
|
||||
state: extractInputValue(kcText, 'state'),
|
||||
session_state: extractInputValue(kcText, 'session_state')
|
||||
},
|
||||
expectedStatus
|
||||
expectedStatus: HttpStatusCode.FOUND_302
|
||||
})
|
||||
|
||||
if (expectedStatus !== HttpStatusCode.FOUND_302) return undefined
|
||||
|
||||
const ptBypassPath = res.headers['location']
|
||||
expect(ptBypassPath).to.include(redirectUrl)
|
||||
expect(ptBypassPath).to.include('?externalAuthToken=')
|
||||
expect(ptBypassPath).to.include('username=' + username)
|
||||
|
||||
if (success) {
|
||||
expect(ptBypassPath).to.include(redirectUrl)
|
||||
expect(ptBypassPath).to.include('?externalAuthToken=')
|
||||
expect(ptBypassPath).to.include('username=' + username)
|
||||
}
|
||||
|
||||
return ptBypassPath
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ async function updateVideoPlayerSettings (req: express.Request, res: express.Res
|
||||
const body: PlayerVideoSettingsUpdate = req.body
|
||||
const video = res.locals.videoAll
|
||||
|
||||
const setting = await upsertPlayerSettings({ settings: body, channel: undefined, video })
|
||||
const setting = await upsertPlayerSettings({ user: res.locals.oauth.token.User, settings: body, channel: undefined, video })
|
||||
|
||||
await sendUpdateVideoPlayerSettings(video, setting, undefined)
|
||||
|
||||
@@ -104,7 +104,7 @@ async function updateChannelPlayerSettings (req: express.Request, res: express.R
|
||||
const body: PlayerChannelSettingsUpdate = req.body
|
||||
const channel = res.locals.videoChannel
|
||||
|
||||
const settings = await upsertPlayerSettings({ settings: body, channel, video: undefined })
|
||||
const settings = await upsertPlayerSettings({ user: res.locals.oauth.token.User, settings: body, channel, video: undefined })
|
||||
|
||||
await sendUpdateChannelPlayerSettings(channel, settings, undefined)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HttpStatusCode, VideoChannelSyncState } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, VideoChannelActivityAction, VideoChannelSyncState } from '@peertube/peertube-models'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelSyncAuditView } from '@server/helpers/audit-logger.js'
|
||||
import { logger } from '@server/helpers/logger.js'
|
||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
import {
|
||||
apiRateLimiter,
|
||||
asyncMiddleware,
|
||||
@@ -10,6 +11,7 @@ import {
|
||||
ensureSyncIsEnabled,
|
||||
videoChannelSyncValidator
|
||||
} from '@server/middlewares/index.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
|
||||
import { MChannelSyncFormattable } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
@@ -45,8 +47,18 @@ async function createVideoChannelSync (req: express.Request, res: express.Respon
|
||||
state: VideoChannelSyncState.WAITING_FIRST_RUN
|
||||
})
|
||||
|
||||
await syncCreated.save()
|
||||
syncCreated.VideoChannel = res.locals.videoChannel
|
||||
await sequelizeTypescript.transaction(async transaction => {
|
||||
await syncCreated.save({ transaction })
|
||||
syncCreated.VideoChannel = res.locals.videoChannel
|
||||
|
||||
await VideoChannelActivityModel.addChannelSyncActivity({
|
||||
action: VideoChannelActivityAction.CREATE,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: res.locals.videoChannel,
|
||||
sync: syncCreated,
|
||||
transaction
|
||||
})
|
||||
})
|
||||
|
||||
auditLogger.create(getAuditIdFromRes(res), new VideoChannelSyncAuditView(syncCreated.toFormattedJSON()))
|
||||
|
||||
@@ -64,7 +76,17 @@ async function createVideoChannelSync (req: express.Request, res: express.Respon
|
||||
async function removeVideoChannelSync (req: express.Request, res: express.Response) {
|
||||
const syncInstance = res.locals.videoChannelSync
|
||||
|
||||
await syncInstance.destroy()
|
||||
await sequelizeTypescript.transaction(async transaction => {
|
||||
await syncInstance.destroy({ transaction })
|
||||
|
||||
await VideoChannelActivityModel.addChannelSyncActivity({
|
||||
action: VideoChannelActivityAction.DELETE,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: res.locals.videoChannel,
|
||||
sync: syncInstance,
|
||||
transaction
|
||||
})
|
||||
})
|
||||
|
||||
auditLogger.delete(getAuditIdFromRes(res), new VideoChannelSyncAuditView(syncInstance.toFormattedJSON()))
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {
|
||||
ActorImageType,
|
||||
HttpStatusCode,
|
||||
VideoChannelActivityAction,
|
||||
VideoChannelCreate,
|
||||
VideoChannelUpdate,
|
||||
VideoPlaylistForAccountListQuery,
|
||||
@@ -12,18 +12,17 @@ import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { reorderPlaylistOrElementsPosition, sendPlaylistPositionUpdateOfChannel } from '@server/lib/video-playlist.js'
|
||||
import { ActorFollowModel } from '@server/models/actor/actor-follow.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
import { MChannelBannerAccountDefault } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../../helpers/audit-logger.js'
|
||||
import { resetSequelizeInstance } from '../../../helpers/database-utils.js'
|
||||
import { buildNSFWFilters, createReqFiles, getCountVideos, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils.js'
|
||||
import { buildNSFWFilters, getCountVideos, isUserAbleToSearchRemoteURI } from '../../../helpers/express-utils.js'
|
||||
import { logger } from '../../../helpers/logger.js'
|
||||
import { getFormattedObjects } from '../../../helpers/utils.js'
|
||||
import { MIMETYPES } from '../../../initializers/constants.js'
|
||||
import { sequelizeTypescript } from '../../../initializers/database.js'
|
||||
import { sendUpdateActor } from '../../../lib/activitypub/send/index.js'
|
||||
import { JobQueue } from '../../../lib/job-queue/index.js'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../../lib/local-actor.js'
|
||||
import { createLocalVideoChannelWithoutKeys, federateAllVideosOfChannel } from '../../../lib/video-channel.js'
|
||||
import {
|
||||
apiRateLimiter,
|
||||
@@ -42,11 +41,10 @@ import {
|
||||
videoChannelsUpdateValidator,
|
||||
videoPlaylistsSortValidator
|
||||
} from '../../../middlewares/index.js'
|
||||
import { updateAvatarValidator, updateBannerValidator } from '../../../middlewares/validators/actor-image.js'
|
||||
import {
|
||||
ensureChannelOwnerCanUpload,
|
||||
videoChannelActivitiesSortValidator,
|
||||
videoChannelFollowersSortValidator,
|
||||
videoChannelImportVideosValidator,
|
||||
videoChannelsFollowersSortValidator,
|
||||
videoChannelsHandleValidatorFactory,
|
||||
videoChannelsListValidator,
|
||||
videosSortValidator
|
||||
@@ -61,15 +59,15 @@ import { VideoChannelModel } from '../../../models/video/video-channel.js'
|
||||
import { VideoPlaylistModel } from '../../../models/video/video-playlist.js'
|
||||
import { VideoModel } from '../../../models/video/video.js'
|
||||
import { channelCollaborators } from './video-channel-collaborators.js'
|
||||
import { videoChannelLogosRouter } from './video-channel-logos.js'
|
||||
|
||||
const auditLogger = auditLoggerFactory('channels')
|
||||
const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
|
||||
const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
|
||||
|
||||
const videoChannelRouter = express.Router()
|
||||
|
||||
videoChannelRouter.use(apiRateLimiter)
|
||||
videoChannelRouter.use(channelCollaborators)
|
||||
videoChannelRouter.use(videoChannelLogosRouter)
|
||||
|
||||
videoChannelRouter.get(
|
||||
'/',
|
||||
@@ -83,38 +81,6 @@ videoChannelRouter.get(
|
||||
|
||||
videoChannelRouter.post('/', authenticate, asyncMiddleware(videoChannelsAddValidator), asyncRetryTransactionMiddleware(createVideoChannel))
|
||||
|
||||
videoChannelRouter.post(
|
||||
'/:handle/avatar/pick',
|
||||
authenticate,
|
||||
reqAvatarFile,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
updateAvatarValidator,
|
||||
asyncMiddleware(updateVideoChannelAvatar)
|
||||
)
|
||||
|
||||
videoChannelRouter.post(
|
||||
'/:handle/banner/pick',
|
||||
authenticate,
|
||||
reqBannerFile,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
updateBannerValidator,
|
||||
asyncMiddleware(updateVideoChannelBanner)
|
||||
)
|
||||
|
||||
videoChannelRouter.delete(
|
||||
'/:handle/avatar',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
asyncMiddleware(deleteVideoChannelAvatar)
|
||||
)
|
||||
|
||||
videoChannelRouter.delete(
|
||||
'/:handle/banner',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
asyncMiddleware(deleteVideoChannelBanner)
|
||||
)
|
||||
|
||||
videoChannelRouter.put(
|
||||
'/:handle',
|
||||
authenticate,
|
||||
@@ -178,18 +144,28 @@ videoChannelRouter.get(
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: false, checkCanManage: true, checkIsOwner: false })),
|
||||
paginationValidator,
|
||||
videoChannelsFollowersSortValidator,
|
||||
videoChannelFollowersSortValidator,
|
||||
setDefaultSort,
|
||||
setDefaultPagination,
|
||||
asyncMiddleware(listVideoChannelFollowers)
|
||||
)
|
||||
|
||||
videoChannelRouter.get(
|
||||
'/:handle/activities',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
paginationValidator,
|
||||
videoChannelActivitiesSortValidator,
|
||||
setDefaultSort,
|
||||
setDefaultPagination,
|
||||
asyncMiddleware(listVideoChannelActivities)
|
||||
)
|
||||
|
||||
videoChannelRouter.post(
|
||||
'/:handle/import-videos',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
asyncMiddleware(videoChannelImportVideosValidator),
|
||||
asyncMiddleware(ensureChannelOwnerCanUpload),
|
||||
asyncMiddleware(importVideosInChannel)
|
||||
)
|
||||
|
||||
@@ -220,60 +196,6 @@ async function listVideoChannels (req: express.Request, res: express.Response) {
|
||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||
}
|
||||
|
||||
async function updateVideoChannelBanner (req: express.Request, res: express.Response) {
|
||||
const bannerPhysicalFile = req.files['bannerfile'][0]
|
||||
const videoChannel = res.locals.videoChannel
|
||||
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
|
||||
|
||||
const banners = await updateLocalActorImageFiles({
|
||||
accountOrChannel: videoChannel,
|
||||
imagePhysicalFile: bannerPhysicalFile,
|
||||
type: ActorImageType.BANNER,
|
||||
sendActorUpdate: true
|
||||
})
|
||||
|
||||
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
|
||||
|
||||
return res.json({
|
||||
banners: banners.map(b => b.toFormattedJSON())
|
||||
})
|
||||
}
|
||||
|
||||
async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
|
||||
const avatarPhysicalFile = req.files['avatarfile'][0]
|
||||
const videoChannel = res.locals.videoChannel
|
||||
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
|
||||
|
||||
const avatars = await updateLocalActorImageFiles({
|
||||
accountOrChannel: videoChannel,
|
||||
imagePhysicalFile: avatarPhysicalFile,
|
||||
type: ActorImageType.AVATAR,
|
||||
sendActorUpdate: true
|
||||
})
|
||||
|
||||
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
|
||||
|
||||
return res.json({
|
||||
avatars: avatars.map(a => a.toFormattedJSON())
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) {
|
||||
const videoChannel = res.locals.videoChannel
|
||||
|
||||
await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR)
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
}
|
||||
|
||||
async function deleteVideoChannelBanner (req: express.Request, res: express.Response) {
|
||||
const videoChannel = res.locals.videoChannel
|
||||
|
||||
await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER)
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
}
|
||||
|
||||
async function createVideoChannel (req: express.Request, res: express.Response) {
|
||||
const videoChannelInfo: VideoChannelCreate = req.body
|
||||
|
||||
@@ -330,6 +252,13 @@ async function updateVideoChannel (req: express.Request, res: express.Response)
|
||||
oldVideoChannelAuditKeys
|
||||
)
|
||||
|
||||
await VideoChannelActivityModel.addChannelActivity({
|
||||
action: VideoChannelActivityAction.UPDATE,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: videoChannelInstanceUpdated,
|
||||
transaction: t
|
||||
})
|
||||
|
||||
Hooks.runAction('action:api.video-channel.updated', { videoChannel: videoChannelInstanceUpdated, req, res })
|
||||
|
||||
logger.info('Video channel %s updated.', videoChannelInstance.Actor.url)
|
||||
@@ -499,9 +428,24 @@ async function listVideoChannelFollowers (req: express.Request, res: express.Res
|
||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||
}
|
||||
|
||||
async function listVideoChannelActivities (req: express.Request, res: express.Response) {
|
||||
const channel = res.locals.videoChannel
|
||||
|
||||
const resultList = await VideoChannelActivityModel.listForAPI({
|
||||
channelId: channel.id,
|
||||
start: req.query.start,
|
||||
count: req.query.count,
|
||||
sort: req.query.sort
|
||||
})
|
||||
|
||||
return res.json(getFormattedObjects(resultList.data, resultList.total))
|
||||
}
|
||||
|
||||
async function importVideosInChannel (req: express.Request, res: express.Response) {
|
||||
const { externalChannelUrl } = req.body as VideosImportInChannelCreate
|
||||
|
||||
// TODO: add channel activity for this endpoint
|
||||
|
||||
await JobQueue.Instance.createJob({
|
||||
type: 'video-channel-import',
|
||||
payload: {
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { ActorImageType, HttpStatusCode, VideoChannelActivityAction } from '@peertube/peertube-models'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
import express from 'express'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, VideoChannelAuditView } from '../../../helpers/audit-logger.js'
|
||||
import { createReqFiles } from '../../../helpers/express-utils.js'
|
||||
import { MIMETYPES } from '../../../initializers/constants.js'
|
||||
import { deleteLocalActorImageFile, updateLocalActorImageFiles } from '../../../lib/local-actor.js'
|
||||
import { asyncMiddleware, authenticate } from '../../../middlewares/index.js'
|
||||
import { updateAvatarValidator, updateBannerValidator } from '../../../middlewares/validators/actor-image.js'
|
||||
import { videoChannelsHandleValidatorFactory } from '../../../middlewares/validators/index.js'
|
||||
|
||||
const auditLogger = auditLoggerFactory('channels')
|
||||
const reqAvatarFile = createReqFiles([ 'avatarfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
|
||||
const reqBannerFile = createReqFiles([ 'bannerfile' ], MIMETYPES.IMAGE.MIMETYPE_EXT)
|
||||
|
||||
const videoChannelLogosRouter = express.Router()
|
||||
|
||||
videoChannelLogosRouter.post(
|
||||
'/:handle/avatar/pick',
|
||||
authenticate,
|
||||
reqAvatarFile,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
updateAvatarValidator,
|
||||
asyncMiddleware(updateVideoChannelAvatar)
|
||||
)
|
||||
|
||||
videoChannelLogosRouter.post(
|
||||
'/:handle/banner/pick',
|
||||
authenticate,
|
||||
reqBannerFile,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
updateBannerValidator,
|
||||
asyncMiddleware(updateVideoChannelBanner)
|
||||
)
|
||||
|
||||
videoChannelLogosRouter.delete(
|
||||
'/:handle/avatar',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
asyncMiddleware(deleteVideoChannelAvatar)
|
||||
)
|
||||
|
||||
videoChannelLogosRouter.delete(
|
||||
'/:handle/banner',
|
||||
authenticate,
|
||||
asyncMiddleware(videoChannelsHandleValidatorFactory({ checkIsLocal: true, checkCanManage: true, checkIsOwner: false })),
|
||||
asyncMiddleware(deleteVideoChannelBanner)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoChannelLogosRouter
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function updateVideoChannelBanner (req: express.Request, res: express.Response) {
|
||||
const bannerPhysicalFile = req.files['bannerfile'][0]
|
||||
const videoChannel = res.locals.videoChannel
|
||||
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
|
||||
|
||||
const banners = await updateLocalActorImageFiles({
|
||||
accountOrChannel: videoChannel,
|
||||
imagePhysicalFile: bannerPhysicalFile,
|
||||
type: ActorImageType.BANNER,
|
||||
sendActorUpdate: true
|
||||
})
|
||||
|
||||
await VideoChannelActivityModel.addChannelActivity({
|
||||
action: VideoChannelActivityAction.UPDATE,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: videoChannel,
|
||||
transaction: undefined
|
||||
})
|
||||
|
||||
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
|
||||
|
||||
return res.json({
|
||||
banners: banners.map(b => b.toFormattedJSON())
|
||||
})
|
||||
}
|
||||
|
||||
async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
|
||||
const avatarPhysicalFile = req.files['avatarfile'][0]
|
||||
const videoChannel = res.locals.videoChannel
|
||||
const oldVideoChannelAuditKeys = new VideoChannelAuditView(videoChannel.toFormattedJSON())
|
||||
|
||||
const avatars = await updateLocalActorImageFiles({
|
||||
accountOrChannel: videoChannel,
|
||||
imagePhysicalFile: avatarPhysicalFile,
|
||||
type: ActorImageType.AVATAR,
|
||||
sendActorUpdate: true
|
||||
})
|
||||
|
||||
await VideoChannelActivityModel.addChannelActivity({
|
||||
action: VideoChannelActivityAction.UPDATE,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: videoChannel,
|
||||
transaction: undefined
|
||||
})
|
||||
|
||||
auditLogger.update(getAuditIdFromRes(res), new VideoChannelAuditView(videoChannel.toFormattedJSON()), oldVideoChannelAuditKeys)
|
||||
|
||||
return res.json({
|
||||
avatars: avatars.map(a => a.toFormattedJSON())
|
||||
})
|
||||
}
|
||||
|
||||
async function deleteVideoChannelAvatar (req: express.Request, res: express.Response) {
|
||||
const videoChannel = res.locals.videoChannel
|
||||
|
||||
await deleteLocalActorImageFile(videoChannel, ActorImageType.AVATAR)
|
||||
|
||||
await VideoChannelActivityModel.addChannelActivity({
|
||||
action: VideoChannelActivityAction.UPDATE,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: videoChannel,
|
||||
transaction: undefined
|
||||
})
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
}
|
||||
|
||||
async function deleteVideoChannelBanner (req: express.Request, res: express.Response) {
|
||||
const videoChannel = res.locals.videoChannel
|
||||
|
||||
await deleteLocalActorImageFile(videoChannel, ActorImageType.BANNER)
|
||||
|
||||
await VideoChannelActivityModel.addChannelActivity({
|
||||
action: VideoChannelActivityAction.UPDATE,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: videoChannel,
|
||||
transaction: undefined
|
||||
})
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { forceNumber } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
VideoChannelActivityAction,
|
||||
VideoPlaylistCreate,
|
||||
VideoPlaylistCreateResult,
|
||||
VideoPlaylistElementCreate,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
sendPlaylistPositionUpdateOfChannel
|
||||
} from '@server/lib/video-playlist.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
import { MVideoPlaylistFull, MVideoPlaylistThumbnail } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
import { resetSequelizeInstance, retryTransactionWrapper } from '../../helpers/database-utils.js'
|
||||
@@ -186,9 +188,9 @@ async function createVideoPlaylist (req: express.Request, res: express.Response)
|
||||
|
||||
videoPlaylist.url = getLocalVideoPlaylistActivityPubUrl(videoPlaylist) // We use the UUID, so set the URL after building the object
|
||||
|
||||
if (videoPlaylistInfo.videoChannelId) {
|
||||
const videoChannel = res.locals.videoChannel
|
||||
const videoChannel = res.locals.videoChannel
|
||||
|
||||
if (videoChannel && videoPlaylistInfo.videoChannelId) {
|
||||
videoPlaylist.videoChannelId = videoChannel.id
|
||||
videoPlaylist.VideoChannel = videoChannel
|
||||
}
|
||||
@@ -221,6 +223,16 @@ async function createVideoPlaylist (req: express.Request, res: express.Response)
|
||||
videoPlaylistCreated.OwnerAccount = await AccountModel.load(user.Account.id, t)
|
||||
await sendCreateVideoPlaylist(videoPlaylistCreated, t)
|
||||
|
||||
if (videoChannel) {
|
||||
await VideoChannelActivityModel.addPlaylistActivity({
|
||||
action: VideoChannelActivityAction.CREATE,
|
||||
user,
|
||||
channel: videoChannel,
|
||||
playlist: videoPlaylistCreated,
|
||||
transaction: t
|
||||
})
|
||||
}
|
||||
|
||||
return videoPlaylistCreated
|
||||
})
|
||||
})
|
||||
@@ -238,7 +250,7 @@ async function createVideoPlaylist (req: express.Request, res: express.Response)
|
||||
|
||||
async function updateVideoPlaylist (req: express.Request, res: express.Response) {
|
||||
const playlist = res.locals.videoPlaylistFull
|
||||
const videoPlaylistInfoToUpdate = req.body as VideoPlaylistUpdate
|
||||
const body = req.body as VideoPlaylistUpdate
|
||||
|
||||
const wasPrivatePlaylist = playlist.privacy === VideoPlaylistPrivacy.PRIVATE
|
||||
const wasNotPrivatePlaylist = playlist.privacy !== VideoPlaylistPrivacy.PRIVATE
|
||||
@@ -256,39 +268,59 @@ async function updateVideoPlaylist (req: express.Request, res: express.Response)
|
||||
|
||||
try {
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
if (videoPlaylistInfoToUpdate.videoChannelId !== undefined) {
|
||||
if (videoPlaylistInfoToUpdate.videoChannelId === null) {
|
||||
removedFromChannel = {
|
||||
id: playlist.videoChannelId,
|
||||
position: playlist.videoChannelPosition
|
||||
}
|
||||
const newChannel = res.locals.videoChannel
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
playlist.videoChannelId = null
|
||||
} else {
|
||||
const videoChannel = res.locals.videoChannel
|
||||
// Had a channel, but the user changed it (to null or another channel)
|
||||
if (playlist.videoChannelId && body.videoChannelId !== undefined && body.videoChannelId !== playlist.videoChannelId) {
|
||||
await VideoChannelActivityModel.addPlaylistActivity({
|
||||
action: VideoChannelActivityAction.REMOVE_CHANNEL_OWNERSHIP,
|
||||
user,
|
||||
channel: playlist.VideoChannel,
|
||||
playlist,
|
||||
transaction: t
|
||||
})
|
||||
|
||||
if (playlist.videoChannelId !== videoPlaylistInfoToUpdate.videoChannelId) {
|
||||
removedFromChannel = {
|
||||
id: playlist.videoChannelId,
|
||||
position: playlist.videoChannelPosition
|
||||
}
|
||||
|
||||
playlist.videoChannelPosition = await VideoPlaylistModel.getNextPositionOf({
|
||||
videoChannelId: videoChannel.id,
|
||||
transaction: t
|
||||
})
|
||||
}
|
||||
|
||||
playlist.videoChannelId = videoChannel.id
|
||||
playlist.VideoChannel = videoChannel
|
||||
removedFromChannel = {
|
||||
id: playlist.videoChannelId,
|
||||
position: playlist.videoChannelPosition
|
||||
}
|
||||
|
||||
playlist.videoChannelId = null
|
||||
playlist.VideoChannel = null
|
||||
}
|
||||
|
||||
if (videoPlaylistInfoToUpdate.displayName !== undefined) playlist.name = videoPlaylistInfoToUpdate.displayName
|
||||
if (videoPlaylistInfoToUpdate.description !== undefined) playlist.description = videoPlaylistInfoToUpdate.description
|
||||
if (newChannel && newChannel.id !== playlist.videoChannelId) {
|
||||
await VideoChannelActivityModel.addPlaylistActivity({
|
||||
action: VideoChannelActivityAction.CREATE_CHANNEL_OWNERSHIP,
|
||||
user,
|
||||
channel: newChannel,
|
||||
playlist,
|
||||
transaction: t
|
||||
})
|
||||
|
||||
if (videoPlaylistInfoToUpdate.privacy !== undefined) {
|
||||
playlist.privacy = forceNumber(videoPlaylistInfoToUpdate.privacy) as VideoPlaylistPrivacyType
|
||||
playlist.videoChannelPosition = await VideoPlaylistModel.getNextPositionOf({
|
||||
videoChannelId: newChannel.id,
|
||||
transaction: t
|
||||
})
|
||||
|
||||
playlist.videoChannelId = newChannel.id
|
||||
playlist.VideoChannel = newChannel
|
||||
} else if (newChannel) {
|
||||
await VideoChannelActivityModel.addPlaylistActivity({
|
||||
action: VideoChannelActivityAction.UPDATE,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: newChannel,
|
||||
playlist,
|
||||
transaction: t
|
||||
})
|
||||
}
|
||||
|
||||
if (body.displayName !== undefined) playlist.name = body.displayName
|
||||
if (body.description !== undefined) playlist.description = body.description
|
||||
|
||||
if (body.privacy !== undefined) {
|
||||
playlist.privacy = forceNumber(body.privacy) as VideoPlaylistPrivacyType
|
||||
|
||||
if (wasNotPrivatePlaylist === true && playlist.privacy === VideoPlaylistPrivacy.PRIVATE) {
|
||||
await sendDeleteVideoPlaylist(playlist, t)
|
||||
@@ -360,6 +392,14 @@ async function removeVideoPlaylist (req: express.Request, res: express.Response)
|
||||
|
||||
if (videoPlaylistInstance.videoChannelId) {
|
||||
await sendPlaylistPositionUpdateOfChannel(videoPlaylistInstance.videoChannelId, t)
|
||||
|
||||
await VideoChannelActivityModel.addPlaylistActivity({
|
||||
action: VideoChannelActivityAction.DELETE,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: videoPlaylistInstance.VideoChannel,
|
||||
playlist: videoPlaylistInstance,
|
||||
transaction: t
|
||||
})
|
||||
}
|
||||
|
||||
logger.info('Video playlist %s deleted.', videoPlaylistInstance.uuid)
|
||||
@@ -394,6 +434,16 @@ async function addVideoInPlaylist (req: express.Request, res: express.Response)
|
||||
videoPlaylist.changed('updatedAt', true)
|
||||
await videoPlaylist.save({ transaction: t })
|
||||
|
||||
if (videoPlaylist.VideoChannel) {
|
||||
await VideoChannelActivityModel.addPlaylistActivity({
|
||||
action: VideoChannelActivityAction.UPDATE_ELEMENTS,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: videoPlaylist.VideoChannel,
|
||||
playlist: videoPlaylist,
|
||||
transaction: t
|
||||
})
|
||||
}
|
||||
|
||||
return playlistElement
|
||||
})
|
||||
|
||||
@@ -432,6 +482,16 @@ async function updateVideoPlaylistElement (req: express.Request, res: express.Re
|
||||
|
||||
await sendUpdateVideoPlaylist(videoPlaylist, t)
|
||||
|
||||
if (videoPlaylist.VideoChannel) {
|
||||
await VideoChannelActivityModel.addPlaylistActivity({
|
||||
action: VideoChannelActivityAction.UPDATE_ELEMENTS,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: videoPlaylist.VideoChannel,
|
||||
playlist: videoPlaylist,
|
||||
transaction: t
|
||||
})
|
||||
}
|
||||
|
||||
return element
|
||||
})
|
||||
|
||||
@@ -459,6 +519,16 @@ async function removeVideoFromPlaylist (req: express.Request, res: express.Respo
|
||||
videoPlaylist.changed('updatedAt', true)
|
||||
await videoPlaylist.save({ transaction: t })
|
||||
|
||||
if (videoPlaylist.VideoChannel) {
|
||||
await VideoChannelActivityModel.addPlaylistActivity({
|
||||
action: VideoChannelActivityAction.UPDATE_ELEMENTS,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: videoPlaylist.VideoChannel,
|
||||
playlist: videoPlaylist,
|
||||
transaction: t
|
||||
})
|
||||
}
|
||||
|
||||
logger.info('Video playlist element %d of playlist %s deleted.', videoPlaylistElement.position, videoPlaylist.uuid)
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HttpStatusCode, VideoCaptionGenerate } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, VideoCaptionGenerate, VideoChannelActivityAction } from '@peertube/peertube-models'
|
||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { createLocalCaption, createTranscriptionTaskIfNeeded, updateHLSMasterOnCaptionChangeIfNeeded } from '@server/lib/video-captions.js'
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
listVideoCaptionsValidator
|
||||
} from '../../../middlewares/validators/index.js'
|
||||
import { VideoCaptionModel } from '../../../models/video/video-caption.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
|
||||
const lTags = loggerTagsFactory('api', 'video-caption')
|
||||
|
||||
@@ -94,7 +95,17 @@ async function createVideoCaption (req: express.Request, res: express.Response)
|
||||
}
|
||||
|
||||
await retryTransactionWrapper(() => {
|
||||
return sequelizeTypescript.transaction(t => federateVideoIfNeeded(video, false, t))
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.UPDATE_CAPTIONS,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: video.VideoChannel,
|
||||
video,
|
||||
transaction: t
|
||||
})
|
||||
|
||||
return federateVideoIfNeeded(video, false, t)
|
||||
})
|
||||
})
|
||||
|
||||
Hooks.runAction('action:api.video-caption.created', { caption: videoCaption, req, res })
|
||||
@@ -116,7 +127,17 @@ async function deleteVideoCaption (req: express.Request, res: express.Response)
|
||||
}
|
||||
|
||||
await retryTransactionWrapper(() => {
|
||||
return sequelizeTypescript.transaction(t => federateVideoIfNeeded(video, false, t))
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.UPDATE_CAPTIONS,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: video.VideoChannel,
|
||||
video,
|
||||
transaction: t
|
||||
})
|
||||
|
||||
return federateVideoIfNeeded(video, false, t)
|
||||
})
|
||||
})
|
||||
|
||||
logger.info('Video caption %s of video %s deleted.', videoCaption.language, video.uuid, lTags(video.uuid))
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import { HttpStatusCode, VideoChannelActivityAction, VideoChapterUpdate } from '@peertube/peertube-models'
|
||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/federate.js'
|
||||
import { replaceChapters } from '@server/lib/video-chapters.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
||||
import express from 'express'
|
||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate } from '../../../middlewares/index.js'
|
||||
import { updateVideoChaptersValidator, videosCustomGetValidator } from '../../../middlewares/validators/index.js'
|
||||
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
||||
import { HttpStatusCode, VideoChapterUpdate } from '@peertube/peertube-models'
|
||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/federate.js'
|
||||
import { replaceChapters } from '@server/lib/video-chapters.js'
|
||||
|
||||
const videoChaptersRouter = express.Router()
|
||||
|
||||
videoChaptersRouter.get('/:id/chapters',
|
||||
videoChaptersRouter.get(
|
||||
'/:id/chapters',
|
||||
asyncMiddleware(videosCustomGetValidator('only-video-and-blacklist')),
|
||||
asyncMiddleware(listVideoChapters)
|
||||
)
|
||||
|
||||
videoChaptersRouter.put('/:videoId/chapters',
|
||||
videoChaptersRouter.put(
|
||||
'/:videoId/chapters',
|
||||
authenticate,
|
||||
asyncMiddleware(updateVideoChaptersValidator),
|
||||
asyncRetryTransactionMiddleware(replaceVideoChapters)
|
||||
@@ -43,6 +46,14 @@ async function replaceVideoChapters (req: express.Request, res: express.Response
|
||||
return sequelizeTypescript.transaction(async t => {
|
||||
await replaceChapters({ video, chapters: body.chapters, transaction: t })
|
||||
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.UPDATE_CHAPTERS,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: video.VideoChannel,
|
||||
video,
|
||||
transaction: t
|
||||
})
|
||||
|
||||
await federateVideoIfNeeded(video, false, t)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, VideoChannelActivityAction } from '@peertube/peertube-models'
|
||||
import { pickCommonVideoQuery } from '@server/helpers/query.js'
|
||||
import { openapiOperationDoc } from '@server/middlewares/doc.js'
|
||||
import { getServerActor } from '@server/models/application/application.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
import express from 'express'
|
||||
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger.js'
|
||||
import { buildNSFWFilters, getCountVideos } from '../../../helpers/express-utils.js'
|
||||
@@ -178,6 +179,14 @@ async function removeVideo (req: express.Request, res: express.Response) {
|
||||
|
||||
await sequelizeTypescript.transaction(async t => {
|
||||
await videoInstance.destroy({ transaction: t })
|
||||
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.DELETE,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: videoInstance.VideoChannel,
|
||||
video: videoInstance,
|
||||
transaction: t
|
||||
})
|
||||
})
|
||||
|
||||
auditLogger.delete(getAuditIdFromRes(res), new VideoAuditView(videoInstance.toFormattedDetailsJSON()))
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { pick } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, LiveVideoCreate, LiveVideoUpdate, ThumbnailType, UserRight, VideoState } from '@peertube/peertube-models'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
LiveVideoCreate,
|
||||
LiveVideoUpdate,
|
||||
ThumbnailType,
|
||||
UserRight,
|
||||
VideoChannelActivityAction,
|
||||
VideoState
|
||||
} from '@peertube/peertube-models'
|
||||
import { uuidToShort } from '@peertube/peertube-node-utils'
|
||||
import { exists, isArray } from '@server/helpers/custom-validators/misc.js'
|
||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||
@@ -26,6 +34,7 @@ import express from 'express'
|
||||
import { Transaction } from 'sequelize'
|
||||
import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
|
||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares/index.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
|
||||
const lTags = loggerTagsFactory('api', 'live')
|
||||
|
||||
@@ -134,11 +143,19 @@ async function updateLiveVideo (req: express.Request, res: express.Response) {
|
||||
videoLive.LiveSchedules = await VideoLiveScheduleModel.addToLiveId(videoLive.id, body.schedules.map(s => s.startAt), t)
|
||||
}
|
||||
}
|
||||
|
||||
video.VideoLive = await videoLive.save({ transaction: t })
|
||||
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.UPDATE,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: video.VideoChannel,
|
||||
video,
|
||||
transaction: t
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
video.VideoLive = await videoLive.save()
|
||||
|
||||
await federateVideoIfNeeded(video, false)
|
||||
|
||||
return res.status(HttpStatusCode.NO_CONTENT_204).end()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { HttpStatusCode, VideoChannelActivityAction } from '@peertube/peertube-models'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||
import express from 'express'
|
||||
import { Transaction } from 'sequelize'
|
||||
import { HttpStatusCode } from '@peertube/peertube-models'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
import { getVideoWithAttributes } from '@server/helpers/video.js'
|
||||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||
import { getFormattedObjects } from '../../../helpers/utils.js'
|
||||
import {
|
||||
asyncMiddleware,
|
||||
@@ -23,7 +23,8 @@ import {
|
||||
const lTags = loggerTagsFactory('api', 'video')
|
||||
const videoPasswordRouter = express.Router()
|
||||
|
||||
videoPasswordRouter.get('/:videoId/passwords',
|
||||
videoPasswordRouter.get(
|
||||
'/:videoId/passwords',
|
||||
authenticate,
|
||||
paginationValidator,
|
||||
videoPasswordsSortValidator,
|
||||
@@ -33,13 +34,15 @@ videoPasswordRouter.get('/:videoId/passwords',
|
||||
asyncMiddleware(listVideoPasswords)
|
||||
)
|
||||
|
||||
videoPasswordRouter.put('/:videoId/passwords',
|
||||
videoPasswordRouter.put(
|
||||
'/:videoId/passwords',
|
||||
authenticate,
|
||||
asyncMiddleware(updateVideoPasswordListValidator),
|
||||
asyncMiddleware(updateVideoPasswordList)
|
||||
)
|
||||
|
||||
videoPasswordRouter.delete('/:videoId/passwords/:passwordId',
|
||||
videoPasswordRouter.delete(
|
||||
'/:videoId/passwords/:passwordId',
|
||||
authenticate,
|
||||
asyncMiddleware(removeVideoPasswordValidator),
|
||||
asyncRetryTransactionMiddleware(removeVideoPassword)
|
||||
@@ -67,7 +70,7 @@ async function listVideoPasswords (req: express.Request, res: express.Response)
|
||||
}
|
||||
|
||||
async function updateVideoPasswordList (req: express.Request, res: express.Response) {
|
||||
const videoInstance = getVideoWithAttributes(res)
|
||||
const videoInstance = res.locals.videoAll
|
||||
const videoId = videoInstance.id
|
||||
|
||||
const passwordArray = req.body.passwords as string[]
|
||||
@@ -75,6 +78,14 @@ async function updateVideoPasswordList (req: express.Request, res: express.Respo
|
||||
await VideoPasswordModel.sequelize.transaction(async (t: Transaction) => {
|
||||
await VideoPasswordModel.deleteAllPasswords(videoId, t)
|
||||
await VideoPasswordModel.addPasswords(passwordArray, videoId, t)
|
||||
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.UPDATE_PASSWORDS,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: videoInstance.VideoChannel,
|
||||
video: videoInstance,
|
||||
transaction: t
|
||||
})
|
||||
})
|
||||
|
||||
logger.info(
|
||||
@@ -88,10 +99,19 @@ async function updateVideoPasswordList (req: express.Request, res: express.Respo
|
||||
}
|
||||
|
||||
async function removeVideoPassword (req: express.Request, res: express.Response) {
|
||||
const videoInstance = getVideoWithAttributes(res)
|
||||
const videoInstance = res.locals.videoAll
|
||||
const password = res.locals.videoPassword
|
||||
|
||||
await VideoPasswordModel.deletePassword(password.id)
|
||||
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.UPDATE_PASSWORDS,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: videoInstance.VideoChannel,
|
||||
video: videoInstance,
|
||||
transaction: null
|
||||
})
|
||||
|
||||
logger.info(
|
||||
'Password with id %d of video named %s and uuid %s has been deleted.',
|
||||
password.id,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { buildAspectRatio } from '@peertube/peertube-core-utils'
|
||||
import { HttpStatusCode, VideoState } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, VideoChannelActivityAction, VideoState } from '@peertube/peertube-models'
|
||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
import { CreateJobArgument, CreateJobOptions, JobQueue } from '@server/lib/job-queue/index.js'
|
||||
import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
@@ -12,6 +12,7 @@ import { addRemoteStoryboardJobIfNeeded, buildLocalStoryboardJobIfNeeded, buildM
|
||||
import { VideoPathManager } from '@server/lib/video-path-manager.js'
|
||||
import { buildNextVideoState } from '@server/lib/video-state.js'
|
||||
import { openapiOperationDoc } from '@server/middlewares/doc.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MStreamingPlaylistFiles, MVideo, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
||||
import express from 'express'
|
||||
@@ -73,6 +74,14 @@ async function deleteVideoLatestSourceFile (req: express.Request, res: express.R
|
||||
|
||||
await videoSource.save()
|
||||
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.UPDATE_SOURCE_FILE,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: video.VideoChannel,
|
||||
video,
|
||||
transaction: null
|
||||
})
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
@@ -132,6 +141,14 @@ async function replaceVideoSourceResumable (req: express.Request, res: express.R
|
||||
transaction
|
||||
})
|
||||
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.UPDATE_SOURCE_FILE,
|
||||
user,
|
||||
channel: video.VideoChannel,
|
||||
video,
|
||||
transaction
|
||||
})
|
||||
|
||||
return video
|
||||
})
|
||||
|
||||
@@ -147,6 +164,7 @@ async function replaceVideoSourceResumable (req: express.Request, res: express.R
|
||||
|
||||
await regenerateMiniaturesIfNeeded(video, res.locals.ffprobe)
|
||||
await video.VideoChannel.setAsUpdated()
|
||||
|
||||
await addVideoJobsAfterUpload(video, videoFile.withVideoOrPlaylist(video))
|
||||
|
||||
logger.info('Replaced video file of video %s with uuid %s.', video.name, video.uuid, lTags(video.uuid))
|
||||
|
||||
@@ -7,6 +7,7 @@ import { MIMETYPES, VIDEO_FILTERS } from '@server/initializers/constants.js'
|
||||
import { buildTaskFileFieldname, createVideoStudioJob, getStudioTaskFilePath, getTaskFileFromReq } from '@server/lib/video-studio.js'
|
||||
import {
|
||||
HttpStatusCode,
|
||||
VideoChannelActivityAction,
|
||||
VideoState,
|
||||
VideoStudioCreateEdition,
|
||||
VideoStudioTask,
|
||||
@@ -17,6 +18,7 @@ import {
|
||||
VideoStudioTaskWatermark
|
||||
} from '@peertube/peertube-models'
|
||||
import { asyncMiddleware, authenticate, videoStudioAddEditionValidator } from '../../../middlewares/index.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
|
||||
const studioRouter = express.Router()
|
||||
|
||||
@@ -45,7 +47,8 @@ const tasksFiles = createAnyReqFiles(
|
||||
}
|
||||
)
|
||||
|
||||
studioRouter.post('/:videoId/studio/edit',
|
||||
studioRouter.post(
|
||||
'/:videoId/studio/edit',
|
||||
authenticate,
|
||||
tasksFiles,
|
||||
asyncMiddleware(videoStudioAddEditionValidator),
|
||||
@@ -73,12 +76,22 @@ async function createEditionTasks (req: express.Request, res: express.Response)
|
||||
tasks: await Bluebird.mapSeries(body.tasks, (t, i) => buildTaskPayload(t, i, files))
|
||||
}
|
||||
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
await createVideoStudioJob({
|
||||
user: res.locals.oauth.token.User,
|
||||
user,
|
||||
payload,
|
||||
video
|
||||
})
|
||||
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.CREATE_STUDIO_TASKS,
|
||||
user,
|
||||
channel: video.VideoChannel,
|
||||
video,
|
||||
transaction: null
|
||||
})
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
HttpStatusCode,
|
||||
NSFWFlag,
|
||||
ThumbnailType,
|
||||
VideoChannelActivityAction,
|
||||
VideoCommentPolicy,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType,
|
||||
@@ -36,6 +37,7 @@ import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist.js'
|
||||
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares/index.js'
|
||||
import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update.js'
|
||||
import { VideoModel } from '../../../models/video/video.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
|
||||
const lTags = loggerTagsFactory('api', 'video')
|
||||
const auditLogger = auditLoggerFactory('videos')
|
||||
@@ -64,6 +66,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
|
||||
const videoFromReq = res.locals.videoAll
|
||||
const oldVideoAuditView = new VideoAuditView(videoFromReq.toFormattedDetailsJSON())
|
||||
const body: VideoUpdate = req.body
|
||||
const user = res.locals.oauth.token.User
|
||||
|
||||
const hadPrivacyForFederation = isPrivacyForFederation(videoFromReq.privacy)
|
||||
const oldPrivacy = videoFromReq.privacy
|
||||
@@ -148,13 +151,40 @@ async function updateVideo (req: express.Request, res: express.Response) {
|
||||
}
|
||||
|
||||
// Video channel update?
|
||||
if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
|
||||
await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
|
||||
videoInstanceUpdated.VideoChannel = res.locals.videoChannel
|
||||
const newChannel = res.locals.videoChannel
|
||||
if (newChannel && videoInstanceUpdated.channelId !== newChannel.id) {
|
||||
const oldChannel = videoInstanceUpdated.VideoChannel
|
||||
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.REMOVE_CHANNEL_OWNERSHIP,
|
||||
user,
|
||||
channel: oldChannel,
|
||||
video: videoInstanceUpdated,
|
||||
transaction: t
|
||||
})
|
||||
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.CREATE_CHANNEL_OWNERSHIP,
|
||||
user,
|
||||
channel: newChannel,
|
||||
video: videoInstanceUpdated,
|
||||
transaction: t
|
||||
})
|
||||
|
||||
await videoInstanceUpdated.$set('VideoChannel', newChannel, { transaction: t })
|
||||
videoInstanceUpdated.VideoChannel = newChannel
|
||||
|
||||
if (hadPrivacyForFederation === true) {
|
||||
await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
|
||||
}
|
||||
} else {
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.UPDATE,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: videoInstanceUpdated.VideoChannel,
|
||||
video: videoInstanceUpdated,
|
||||
transaction: t
|
||||
})
|
||||
}
|
||||
|
||||
// Schedule an update in the future?
|
||||
@@ -176,7 +206,7 @@ async function updateVideo (req: express.Request, res: express.Response) {
|
||||
|
||||
await autoBlacklistVideoIfNeeded({
|
||||
video: videoInstanceUpdated,
|
||||
user: res.locals.oauth.token.User,
|
||||
user,
|
||||
isRemote: false,
|
||||
isNew: false,
|
||||
isNewFile: false,
|
||||
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
UserImportStateType,
|
||||
UserRegistrationState,
|
||||
UserRegistrationStateType,
|
||||
VideoChannelActivityAction,
|
||||
VideoChannelActivityActionType,
|
||||
VideoChannelActivityTarget,
|
||||
VideoChannelActivityTargetType,
|
||||
VideoChannelCollaboratorState,
|
||||
VideoChannelCollaboratorStateType,
|
||||
VideoChannelSyncState,
|
||||
@@ -54,7 +58,7 @@ import { CONFIG, registerConfigChangedHandler } from './config.js'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const LAST_MIGRATION_VERSION = 940
|
||||
export const LAST_MIGRATION_VERSION = 945
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -157,7 +161,9 @@ export const SORTABLE_COLUMNS = {
|
||||
|
||||
AVAILABLE_PLUGINS: [ 'npmName', 'popularity', 'trending' ],
|
||||
|
||||
VIDEO_REDUNDANCIES: [ 'name' ]
|
||||
VIDEO_REDUNDANCIES: [ 'name' ],
|
||||
|
||||
VIDEO_CHANNEL_ACTIVITIES: [ 'createdAt' ]
|
||||
}
|
||||
|
||||
export const ROUTE_CACHE_LIFETIME = {
|
||||
@@ -698,6 +704,28 @@ export const CHANNEL_COLLABORATOR_STATE: { [id in VideoChannelCollaboratorStateT
|
||||
[VideoChannelCollaboratorState.REJECTED]: 'Rejected'
|
||||
}
|
||||
|
||||
export const VIDEO_CHANNEL_ACTIVITY_ACTIONS: { [id in VideoChannelActivityActionType]: string } = {
|
||||
[VideoChannelActivityAction.CREATE]: 'Create',
|
||||
[VideoChannelActivityAction.UPDATE]: 'Update',
|
||||
[VideoChannelActivityAction.DELETE]: 'Delete',
|
||||
[VideoChannelActivityAction.UPDATE_CAPTIONS]: 'Update captions',
|
||||
[VideoChannelActivityAction.UPDATE_CHAPTERS]: 'Update chapters',
|
||||
[VideoChannelActivityAction.UPDATE_PASSWORDS]: 'Update passwords',
|
||||
[VideoChannelActivityAction.CREATE_STUDIO_TASKS]: 'Create studio tasks',
|
||||
[VideoChannelActivityAction.UPDATE_SOURCE_FILE]: 'Update source file',
|
||||
[VideoChannelActivityAction.UPDATE_ELEMENTS]: 'Update elements',
|
||||
[VideoChannelActivityAction.REMOVE_CHANNEL_OWNERSHIP]: 'Remove channel ownership',
|
||||
[VideoChannelActivityAction.CREATE_CHANNEL_OWNERSHIP]: 'Create channel ownership'
|
||||
}
|
||||
|
||||
export const VIDEO_CHANNEL_ACTIVITY_TARGETS: { [id in VideoChannelActivityTargetType]: string } = {
|
||||
[VideoChannelActivityTarget.CHANNEL]: 'Channel',
|
||||
[VideoChannelActivityTarget.CHANNEL_SYNC]: 'Channel synchronization',
|
||||
[VideoChannelActivityTarget.PLAYLIST]: 'Playlist',
|
||||
[VideoChannelActivityTarget.VIDEO]: 'Video',
|
||||
[VideoChannelActivityTarget.VIDEO_IMPORT]: 'Video import'
|
||||
}
|
||||
|
||||
export const MIMETYPES = {
|
||||
AUDIO: {
|
||||
MIMETYPE_EXT: {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { UserVideoHistoryModel } from '@server/models/user/user-video-history.js
|
||||
import { UserModel } from '@server/models/user/user.js'
|
||||
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
|
||||
import { StoryboardModel } from '@server/models/video/storyboard.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
import { VideoChannelCollaboratorModel } from '@server/models/video/video-channel-collaborator.js'
|
||||
import { VideoChannelSyncModel } from '@server/models/video/video-channel-sync.js'
|
||||
import { VideoChapterModel } from '@server/models/video/video-chapter.js'
|
||||
@@ -148,6 +149,7 @@ export async function initDatabaseModels (silent: boolean) {
|
||||
VideoShareModel,
|
||||
VideoFileModel,
|
||||
VideoSourceModel,
|
||||
VideoChannelActivityModel,
|
||||
VideoChapterModel,
|
||||
VideoCaptionModel,
|
||||
VideoBlacklistModel,
|
||||
|
||||
48
server/core/initializers/migrations/0945-channel-activity.ts
Normal file
48
server/core/initializers/migrations/0945-channel-activity.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as Sequelize from 'sequelize'
|
||||
|
||||
async function up (utils: {
|
||||
transaction: Sequelize.Transaction
|
||||
queryInterface: Sequelize.QueryInterface
|
||||
sequelize: Sequelize.Sequelize
|
||||
}): Promise<void> {
|
||||
{
|
||||
await utils.sequelize.query(
|
||||
`CREATE TABLE IF NOT EXISTS "videoChannelActivity" (
|
||||
"id" SERIAL,
|
||||
"action" INTEGER NOT NULL,
|
||||
"targetType" INTEGER NOT NULL,
|
||||
"data" JSONB NOT NULL,
|
||||
"details" JSONB,
|
||||
"videoChannelId" INTEGER NOT NULL REFERENCES "videoChannel" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
"accountId" INTEGER REFERENCES "account" ("id") ON DELETE
|
||||
SET
|
||||
NULL ON UPDATE CASCADE,
|
||||
"videoId" INTEGER REFERENCES "video" ("id") ON DELETE
|
||||
SET
|
||||
NULL ON UPDATE CASCADE,
|
||||
"videoPlaylistId" INTEGER REFERENCES "videoPlaylist" ("id") ON DELETE
|
||||
SET
|
||||
NULL ON UPDATE CASCADE,
|
||||
"videoChannelSyncId" INTEGER REFERENCES "videoChannelSync" ("id") ON DELETE
|
||||
SET
|
||||
NULL ON UPDATE CASCADE,
|
||||
"videoImportId" INTEGER REFERENCES "videoImport" ("id") ON DELETE
|
||||
SET
|
||||
NULL ON UPDATE CASCADE,
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
"updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
PRIMARY KEY ("id")
|
||||
);`,
|
||||
{ transaction: utils.transaction }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function down (options) {
|
||||
throw new Error('Not implemented.')
|
||||
}
|
||||
|
||||
export {
|
||||
down,
|
||||
up
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { PlayerSettingsObject } from '@peertube/peertube-models'
|
||||
import { sanitizeAndCheckPlayerSettingsObject } from '@server/helpers/custom-validators/activitypub/player-settings.js'
|
||||
import { MChannelId, MChannelUrl, MVideoIdUrl } from '../../types/models/index.js'
|
||||
import { MChannelDefault, MVideoIdUrl } from '../../types/models/index.js'
|
||||
import { upsertPlayerSettings } from '../player-settings.js'
|
||||
import { fetchAPObjectIfNeeded } from './activity.js'
|
||||
import { checkUrlsSameHost } from './url.js'
|
||||
|
||||
export async function upsertAPPlayerSettings (options: {
|
||||
video: MVideoIdUrl
|
||||
channel: MChannelUrl & MChannelId
|
||||
channel: MChannelDefault
|
||||
settingsObject: PlayerSettingsObject | string
|
||||
contextUrl: string
|
||||
}) {
|
||||
@@ -30,7 +30,7 @@ export async function upsertAPPlayerSettings (options: {
|
||||
throw new Error(`Player settings ${settingsObject.id} object is not on the same host as context URL ${contextUrl}`)
|
||||
}
|
||||
|
||||
await upsertPlayerSettings({ settings: getPlayerSettingsAttributesFromObject(settingsObject), channel, video })
|
||||
await upsertPlayerSettings({ user: null, settings: getPlayerSettingsAttributesFromObject(settingsObject), channel, video })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -18,7 +18,7 @@ import { logger } from '../../../helpers/logger.js'
|
||||
import { sequelizeTypescript } from '../../../initializers/database.js'
|
||||
import { ActorModel } from '../../../models/actor/actor.js'
|
||||
import { APProcessorOptions } from '../../../types/activitypub-processor.model.js'
|
||||
import { MActorAccountChannelId, MActorFull, MActorSignature } from '../../../types/models/index.js'
|
||||
import { MActorFull, MActorSignature } from '../../../types/models/index.js'
|
||||
import { fetchAPObjectIfNeeded } from '../activity.js'
|
||||
import { getOrCreateAPActor } from '../actors/get.js'
|
||||
import { APActorUpdater } from '../actors/updater.js'
|
||||
@@ -142,13 +142,13 @@ async function processUpdatePlayerSettings (
|
||||
byActor: MActorSignature,
|
||||
settingsObject: PlayerSettingsObject
|
||||
) {
|
||||
let actor: MActorAccountChannelId
|
||||
let actor: MActorFull
|
||||
|
||||
const { video } = await maybeGetOrCreateAPVideo({ videoObject: settingsObject.object })
|
||||
|
||||
if (!video) {
|
||||
try {
|
||||
actor = await getOrCreateAPActor(settingsObject.object)
|
||||
actor = await getOrCreateAPActor(settingsObject.object, 'all')
|
||||
} catch {
|
||||
actor = undefined
|
||||
}
|
||||
|
||||
@@ -232,11 +232,9 @@ async function handleRefreshGrant (options: {
|
||||
lastActivityIP: options.ip,
|
||||
lastActivityDate: new Date(),
|
||||
|
||||
...pick(refreshToken.token, [
|
||||
'loginDevice',
|
||||
'loginIP',
|
||||
'loginDate'
|
||||
])
|
||||
loginIP: refreshToken.token.loginIP,
|
||||
loginDate: refreshToken.token.loginDate,
|
||||
loginDevice: refreshToken.token.loginDevice
|
||||
})
|
||||
|
||||
return saveToken(token, client, refreshToken.user, { refreshTokenAuthName })
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
PeerTubeError,
|
||||
ThumbnailType,
|
||||
ThumbnailType_Type,
|
||||
VideoChannelActivityAction,
|
||||
VideoCreate,
|
||||
VideoPrivacy,
|
||||
VideoStateType
|
||||
@@ -16,12 +17,13 @@ import { LoggerTagsFn, logger } from '@server/helpers/logger.js'
|
||||
import { CONFIG } from '@server/initializers/config.js'
|
||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
import { ScheduleVideoUpdateModel } from '@server/models/video/schedule-video-update.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
import { VideoLiveReplaySettingModel } from '@server/models/video/video-live-replay-setting.js'
|
||||
import { VideoLiveScheduleModel } from '@server/models/video/video-live-schedule.js'
|
||||
import { VideoLiveModel } from '@server/models/video/video-live.js'
|
||||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import { MChannel, MChannelAccountLight, MThumbnail, MUser, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
||||
import { MChannel, MChannelAccountLight, MThumbnail, MUserAccountId, MVideoFile, MVideoFullLight } from '@server/types/models/index.js'
|
||||
import { FilteredModelAttributes } from '@server/types/sequelize.js'
|
||||
import { FfprobeData } from 'fluent-ffmpeg'
|
||||
import { move } from 'fs-extra/esm'
|
||||
@@ -92,7 +94,7 @@ export class LocalVideoCreator {
|
||||
liveAttributes: LiveAttributes
|
||||
|
||||
channel: MChannelAccountLight
|
||||
user: MUser
|
||||
user: MUserAccountId
|
||||
videoAttributeResultHook: VideoAttributeHookFilter
|
||||
thumbnails: ThumbnailOptions
|
||||
|
||||
@@ -139,6 +141,14 @@ export class LocalVideoCreator {
|
||||
return sequelizeTypescript.transaction(async transaction => {
|
||||
await this.video.save({ transaction })
|
||||
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.CREATE,
|
||||
user: this.options.user,
|
||||
channel: this.channel,
|
||||
video: this.video,
|
||||
transaction
|
||||
})
|
||||
|
||||
for (const thumbnail of thumbnails) {
|
||||
await this.video.addAndSaveThumbnail(thumbnail, transaction)
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { PlayerChannelSettings, PlayerVideoSettings } from '@peertube/peertube-models'
|
||||
import { PlayerChannelSettings, PlayerVideoSettings, VideoChannelActivityAction } from '@peertube/peertube-models'
|
||||
import { retryTransactionWrapper } from '@server/helpers/database-utils.js'
|
||||
import { sequelizeTypescript } from '@server/initializers/database.js'
|
||||
import { PlayerSettingModel } from '@server/models/video/player-setting.js'
|
||||
import { MChannelId, MVideoId } from '@server/types/models/index.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
import { MChannelDefault, MUserAccountId, MVideoId } from '@server/types/models/index.js'
|
||||
|
||||
export async function upsertPlayerSettings (options: {
|
||||
user: MUserAccountId
|
||||
settings: PlayerVideoSettings | PlayerChannelSettings
|
||||
channel: MChannelId
|
||||
channel: MChannelDefault
|
||||
video: MVideoId
|
||||
}) {
|
||||
const { settings, channel, video } = options
|
||||
const { user, settings, channel, video } = options
|
||||
|
||||
if (!channel && !video) throw new Error('channel or video must be specified')
|
||||
|
||||
@@ -21,11 +23,22 @@ export async function upsertPlayerSettings (options: {
|
||||
|
||||
if (setting) await setting.destroy({ transaction })
|
||||
|
||||
return PlayerSettingModel.create({
|
||||
const playerSettings = await PlayerSettingModel.create({
|
||||
theme: settings.theme,
|
||||
channelId: channel?.id,
|
||||
videoId: video?.id
|
||||
}, { transaction })
|
||||
|
||||
if (user && channel) {
|
||||
await VideoChannelActivityModel.addChannelActivity({
|
||||
action: VideoChannelActivityAction.UPDATE,
|
||||
user,
|
||||
channel,
|
||||
transaction
|
||||
})
|
||||
}
|
||||
|
||||
return playerSettings
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
NSFWFlag,
|
||||
ThumbnailType,
|
||||
ThumbnailType_Type,
|
||||
VideoChannelActivityAction,
|
||||
VideoImportCreate,
|
||||
VideoImportPayload,
|
||||
VideoImportState,
|
||||
@@ -19,6 +20,7 @@ import { Hooks } from '@server/lib/plugins/hooks.js'
|
||||
import { ServerConfigManager } from '@server/lib/server-config-manager.js'
|
||||
import { autoBlacklistVideoIfNeeded } from '@server/lib/video-blacklist.js'
|
||||
import { buildCommentsPolicy, setVideoTags } from '@server/lib/video.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
import { VideoImportModel } from '@server/models/video/video-import.js'
|
||||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
@@ -27,7 +29,7 @@ import {
|
||||
MChannelAccountDefault,
|
||||
MChannelSync,
|
||||
MThumbnail,
|
||||
MUser,
|
||||
MUserAccountId,
|
||||
MVideo,
|
||||
MVideoAccountDefault,
|
||||
MVideoImportFormattable,
|
||||
@@ -73,7 +75,7 @@ async function insertFromImportIntoDB (parameters: {
|
||||
videoChannel: MChannelAccountDefault
|
||||
tags: string[]
|
||||
videoImportAttributes: FilteredModelAttributes<VideoImportModel>
|
||||
user: MUser
|
||||
user: MUserAccountId
|
||||
videoPasswords?: string[]
|
||||
}): Promise<MVideoImportFormattable> {
|
||||
const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user, videoPasswords } = parameters
|
||||
@@ -113,6 +115,15 @@ async function insertFromImportIntoDB (parameters: {
|
||||
) as MVideoImportFormattable
|
||||
videoImport.Video = videoCreated
|
||||
|
||||
await VideoChannelActivityModel.addVideoImportActivity({
|
||||
action: VideoChannelActivityAction.CREATE,
|
||||
channel: videoChannel,
|
||||
videoImport,
|
||||
video: videoCreated,
|
||||
user,
|
||||
transaction: t
|
||||
})
|
||||
|
||||
return videoImport
|
||||
})
|
||||
|
||||
@@ -164,7 +175,7 @@ async function buildVideoFromImport ({ channelId, importData, importDataOverride
|
||||
async function buildYoutubeDLImport (options: {
|
||||
targetUrl: string
|
||||
channel: MChannelAccountDefault
|
||||
user: MUser
|
||||
user: MUserAccountId
|
||||
channelSync?: MChannelSync
|
||||
importDataOverride?: Partial<VideoImportCreate>
|
||||
thumbnailFilePath?: string
|
||||
|
||||
@@ -33,7 +33,8 @@ export const videoPasswordsSortValidator = checkSortFactory(SORTABLE_COLUMNS.VID
|
||||
export const watchedWordsListsSortValidator = checkSortFactory(SORTABLE_COLUMNS.WATCHED_WORDS_LISTS)
|
||||
|
||||
export const accountsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.ACCOUNT_FOLLOWERS)
|
||||
export const videoChannelsFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS)
|
||||
export const videoChannelFollowersSortValidator = checkSortFactory(SORTABLE_COLUMNS.CHANNEL_FOLLOWERS)
|
||||
export const videoChannelActivitiesSortValidator = checkSortFactory(SORTABLE_COLUMNS.VIDEO_CHANNEL_ACTIVITIES)
|
||||
|
||||
export const userRegistrationsSortValidator = checkSortFactory(SORTABLE_COLUMNS.USER_REGISTRATIONS)
|
||||
|
||||
|
||||
@@ -35,14 +35,17 @@ export const videoChannelsAddValidator = [
|
||||
if (actor) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Another actor (account/channel) with this name on this instance already exists or has already existed.'
|
||||
message: req.t(
|
||||
'Another actor (account/channel) with name {name} on this instance already exists or has already existed.',
|
||||
{ name: req.body.name }
|
||||
)
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
const count = await VideoChannelModel.countByAccount(res.locals.oauth.token.User.Account.id)
|
||||
if (count >= CONFIG.VIDEO_CHANNELS.MAX_PER_USER) {
|
||||
res.fail({ message: `You cannot create more than ${CONFIG.VIDEO_CHANNELS.MAX_PER_USER} channels` })
|
||||
res.fail({ message: req.t('You cannot create more than {count} channels', { count: CONFIG.VIDEO_CHANNELS.MAX_PER_USER }) })
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -73,7 +76,7 @@ export const videoChannelsUpdateValidator = [
|
||||
|
||||
export const videoChannelsRemoveValidator = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
if (!await checkVideoChannelIsNotTheLastOne(res.locals.videoChannel, res)) return
|
||||
if (!await checkVideoChannelIsNotTheLastOne(res.locals.videoChannel, req, res)) return
|
||||
|
||||
return next()
|
||||
}
|
||||
@@ -100,17 +103,6 @@ export const videoChannelsHandleValidatorFactory = (options: {
|
||||
]
|
||||
}
|
||||
|
||||
export const ensureChannelOwnerCanUpload = [
|
||||
async (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
const channel = res.locals.videoChannel
|
||||
const user = { id: channel.Account.userId }
|
||||
|
||||
if (!await checkUserQuota({ user, videoFileSize: 1, req, res })) return
|
||||
|
||||
next()
|
||||
}
|
||||
]
|
||||
|
||||
export const listAccountChannelsValidator = [
|
||||
query('withStats')
|
||||
.optional()
|
||||
@@ -155,7 +147,7 @@ export const videoChannelImportVideosValidator = [
|
||||
if (!CONFIG.IMPORT.VIDEOS.HTTP.ENABLED) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'Channel import is impossible as video upload via HTTP is not enabled on the server'
|
||||
message: req.t('Channel import is impossible as video upload via HTTP is not enabled on the server')
|
||||
})
|
||||
}
|
||||
|
||||
@@ -164,23 +156,26 @@ export const videoChannelImportVideosValidator = [
|
||||
if (res.locals.videoChannelSync && res.locals.videoChannelSync.videoChannelId !== res.locals.videoChannel.id) {
|
||||
return res.fail({
|
||||
status: HttpStatusCode.FORBIDDEN_403,
|
||||
message: 'This channel sync is not owned by this channel'
|
||||
message: req.t('This channel sync is not owned by this channel')
|
||||
})
|
||||
}
|
||||
|
||||
const user = { id: res.locals.videoChannel.Account.userId }
|
||||
if (!await checkUserQuota({ user, videoFileSize: 1, req, res })) return
|
||||
|
||||
return next()
|
||||
}
|
||||
]
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function checkVideoChannelIsNotTheLastOne (videoChannel: MChannelAccountDefault, res: express.Response) {
|
||||
async function checkVideoChannelIsNotTheLastOne (videoChannel: MChannelAccountDefault, req: express.Request, res: express.Response) {
|
||||
const count = await VideoChannelModel.countByAccount(videoChannel.Account.id)
|
||||
|
||||
if (count <= 1) {
|
||||
res.fail({
|
||||
status: HttpStatusCode.CONFLICT_409,
|
||||
message: 'Cannot remove the last channel of this user'
|
||||
message: req.t('Cannot remove the last channel of this user')
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user