Merge branch 'Chocobozzz:develop' into feature/Remember-user-table-pagination-in-admin

This commit is contained in:
Wicklow 2023-10-16 07:18:39 +00:00 committed by GitHub
commit 6cd05dc963
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
100 changed files with 44156 additions and 42411 deletions

View File

@ -1,7 +1,10 @@
<ng-template #modal> <ng-template #modal>
<div class="modal-header"> <div class="modal-header">
<h1 i18n class="modal-title">Contact the administrator(s)<p class="modal-subtitle">{{ instanceName }}</p></h1> <h1 i18n class="modal-title">Contact the administrator(s)<p class="modal-subtitle">{{ instanceName }}</p></h1>
<my-global-icon iconName="cross" aria-label="Close" tabindex="0" role="button" (click)="hide()" (keydown.enter)="hide()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -2,7 +2,9 @@
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Follow</h4> <h4 i18n class="modal-title">Follow</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -5,7 +5,9 @@
<ng-container *ngIf="isReject()">Reject {{ registration.username }} registration</ng-container> <ng-container *ngIf="isReject()">Reject {{ registration.username }} registration</ng-container>
</h4> </h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<form novalidate [formGroup]="form" (ngSubmit)="processRegistration()"> <form novalidate [formGroup]="form" (ngSubmit)="processRegistration()">

View File

@ -112,11 +112,13 @@
<li *ngFor="let file of video.files"> <li *ngFor="let file of video.files">
<a target="_blank" rel="noopener noreferrer" [href]="file.fileUrl">{{ file.resolution.label }}</a>: {{ file.size | bytes: 1 }} <a target="_blank" rel="noopener noreferrer" [href]="file.fileUrl">{{ file.resolution.label }}</a>: {{ file.size | bytes: 1 }}
<my-global-icon <button
*ngIf="canRemoveOneFile(video)" *ngIf="canRemoveOneFile(video)" class="border-0 p-0"
i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button" i18n-title title="Delete this file"
(click)="removeVideoFile(video, file, 'web-videos')" (click)="removeVideoFile(video, file, 'web-videos')"
></my-global-icon> >
<my-global-icon iconName="delete"></my-global-icon>
</button>
</li> </li>
</ul> </ul>
</div> </div>
@ -128,11 +130,13 @@
<li *ngFor="let file of video.streamingPlaylists[0].files"> <li *ngFor="let file of video.streamingPlaylists[0].files">
<a target="_blank" rel="noopener noreferrer" [href]="file.fileUrl">{{ file.resolution.label }}</a>: {{ file.size | bytes: 1 }} <a target="_blank" rel="noopener noreferrer" [href]="file.fileUrl">{{ file.resolution.label }}</a>: {{ file.size | bytes: 1 }}
<my-global-icon <button
*ngIf="canRemoveOneFile(video)" *ngIf="canRemoveOneFile(video)" class="border-0 p-0"
i18n-ngbTooltip ngbTooltip="Delete this file" iconName="delete" role="button" i18n-title title="Delete this file"
(click)="removeVideoFile(video, file, 'hls')" (click)="removeVideoFile(video, file, 'hls')"
></my-global-icon> >
<my-global-icon iconName="delete"></my-global-icon>
</button>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -1,7 +1,7 @@
<h1 i18n class="title-page-v2"> <h1 class="title-page-v2">
<strong class="underline-orange">{{ instanceName }}</strong> <strong class="underline-orange">{{ instanceName }}</strong>
>
Login <ng-container i18n>Login</ng-container>
</h1> </h1>
<div class="margin-content"> <div class="margin-content">
@ -120,7 +120,9 @@
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Forgot your password</h4> <h4 i18n class="modal-title">Forgot your password</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideForgotPasswordModal()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hideForgotPasswordModal()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body text-start"> <div class="modal-body text-start">

View File

@ -2,7 +2,9 @@
<div class="modal-header"> <div class="modal-header">
<h1 i18n class="modal-title">Accept ownership</h1> <h1 i18n class="modal-title">Accept ownership</h1>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="dismiss()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body" [formGroup]="form"> <div class="modal-body" [formGroup]="form">

View File

@ -2,14 +2,20 @@
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Change ownership</h4> <h4 i18n class="modal-title">Change ownership</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="dismiss()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body" [formGroup]="form"> <div class="modal-body" [formGroup]="form">
<div class="form-group"> <div class="form-group">
<label i18n for="next-ownership-username">Select the next owner</label> <label i18n for="next-ownership-username">Select the next owner</label>
<p-autoComplete formControlName="username" [suggestions]="usernamePropositions"
(completeMethod)="search($event)" id="next-ownership-username"></p-autoComplete> <p-autoComplete
formControlName="username" [suggestions]="usernamePropositions"
(completeMethod)="search($event)" id="next-ownership-username"
></p-autoComplete>
<div *ngIf="formErrors.username" class="form-error" role="alert"> <div *ngIf="formErrors.username" class="form-error" role="alert">
{{ formErrors.username }} {{ formErrors.username }}
</div> </div>

View File

@ -3,7 +3,9 @@
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Add caption</h4> <h4 i18n class="modal-title">Add caption</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -1,7 +1,9 @@
<ng-container [formGroup]="form"> <ng-container [formGroup]="form">
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Edit caption</h4> <h4 i18n class="modal-title">Edit caption</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -3,11 +3,13 @@
<my-actor-avatar [actor]="user?.account" [actorType]="getAvatarActorType()" size="25"></my-actor-avatar> <my-actor-avatar [actor]="user?.account" [actorType]="getAvatarActorType()" size="25"></my-actor-avatar>
<div class="textarea-wrapper"> <div class="textarea-wrapper">
<textarea i18n-placeholder placeholder="Add comment..." myAutoResize <textarea
i18n-placeholder placeholder="Add comment..." myAutoResize
[readonly]="(user === null) ? true : false" [readonly]="(user === null) ? true : false"
(click)="openVisitorModal($event)" (click)="openVisitorModal($event)"
formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }" formControlName="text" [ngClass]="{ 'input-error': formErrors['text'] }"
(keyup.control.enter)="onValidKey()" (keyup.meta.enter)="onValidKey()" #textarea> (keyup.control.enter)="onValidKey()" (keyup.meta.enter)="onValidKey()" #textarea
>
</textarea> </textarea>
<my-help <my-help
@ -57,7 +59,10 @@
<ng-template #visitorModal let-modal> <ng-template #visitorModal let-modal>
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title" i18n>You are one step away from commenting</h4> <h4 class="modal-title" id="modal-basic-title" i18n>You are one step away from commenting</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideModals()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hideModals()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@ -81,8 +86,12 @@
<ng-template #emojiModal> <ng-template #emojiModal>
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title" id="modal-basic-title" i18n>Markdown Emoji List</h4> <h4 class="modal-title" id="modal-basic-title" i18n>Markdown Emoji List</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hideModals()"></my-global-icon>
<button class="border-0 p-0" title="Close this modal" i18n-title (click)="hideModals()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="emoji-flex"> <div class="emoji-flex">
<div class="emoji-flex-item" *ngFor="let emojiMarkup of getEmojiMarkupList()"> <div class="emoji-flex-item" *ngFor="let emojiMarkup of getEmojiMarkupList()">

View File

@ -19,23 +19,21 @@
</div> </div>
<div class="playlist-controls"> <div class="playlist-controls">
<my-global-icon <button
iconName="videos" class="border-0 p-0 me-2" [ngClass]="{ active: autoPlayNextVideoPlaylist }" (click)="switchAutoPlayNextVideoPlaylist()"
[class.active]="autoPlayNextVideoPlaylist" [ngbTooltip]="autoPlayNextVideoPlaylistSwitchText" [ariaLabel]="autoPlayNextVideoPlaylistSwitchText"
(click)="switchAutoPlayNextVideoPlaylist()" placement="bottom auto" container="body"
[ngbTooltip]="autoPlayNextVideoPlaylistSwitchText" >
placement="bottom auto" <my-global-icon iconName="videos"></my-global-icon>
container="body" </button>
></my-global-icon>
<my-global-icon <button
iconName="repeat" class="border-0 p-0" [ngClass]="{ active: loopPlaylist }" (click)="switchLoopPlaylist()"
[class.active]="loopPlaylist" [ngbTooltip]="loopPlaylistSwitchText" [ariaLabel]="loopPlaylistSwitchText"
(click)="switchLoopPlaylist()" placement="bottom auto" container="body"
[ngbTooltip]="loopPlaylistSwitchText" >
placement="bottom auto" <my-global-icon iconName="repeat"></my-global-icon>
container="body" </button>
></my-global-icon>
</div> </div>
</div> </div>

View File

@ -46,19 +46,9 @@
display: flex; display: flex;
margin: 10px 0; margin: 10px 0;
my-global-icon:not(:last-child) { button:not(.active) {
@include margin-right(.5rem);
}
my-global-icon {
&:not(.active) {
opacity: .5; opacity: .5;
} }
::ng-deep {
cursor: pointer;
}
}
} }
my-video-playlist-element-miniature { my-video-playlist-element-miniature {

View File

@ -40,7 +40,9 @@
<div [innerHTML]="broadcastMessage.message"></div> <div [innerHTML]="broadcastMessage.message"></div>
<button <button
*ngIf="broadcastMessage.dismissable" (click)="hideBroadcastMessage()" class="border-0" title="Close this message" i18n-title> *ngIf="broadcastMessage.dismissable" (click)="hideBroadcastMessage()"
class="border-0" title="Close this message" i18n-title
>
<my-global-icon iconName="cross"></my-global-icon> <my-global-icon iconName="cross"></my-global-icon>
</button> </button>
</div> </div>

View File

@ -1,7 +1,9 @@
<ng-template #modal let-hide="close"> <ng-template #modal let-hide="close">
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Change the language</h4> <h4 i18n class="modal-title">Change the language</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>

View File

@ -1,7 +1,9 @@
<ng-template #modal let-hide="close"> <ng-template #modal let-hide="close">
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Welcome to {{ instanceName }}, dear user!</h4> <h4 i18n class="modal-title">Welcome to {{ instanceName }}, dear user!</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -1,7 +1,9 @@
<ng-template #modal let-hide="close"> <ng-template #modal let-hide="close">
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Welcome to PeerTube, dear administrator!</h4> <h4 i18n class="modal-title">Welcome to PeerTube, dear administrator!</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -3,7 +3,9 @@
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">{{ title }}</h4> <h4 class="modal-title">{{ title }}</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="dismiss()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body" > <div class="modal-body" >

View File

@ -1,7 +1,10 @@
<ng-template #modal let-hide="close"> <ng-template #modal let-hide="close">
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">{{title}}</h4> <h4 class="modal-title">{{title}}</h4>
<my-global-icon *ngIf="close" iconName="cross" aria-label="Close" role="button" (click)="onCloseClick()"></my-global-icon>
<button *ngIf="close" class="border-0 p-0" title="Close this modal" i18n-title (click)="onCloseClick()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body" [innerHTML]="content"></div> <div class="modal-body" [innerHTML]="content"></div>

View File

@ -1,7 +1,9 @@
<ng-template #modal let-hide="close"> <ng-template #modal let-hide="close">
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Configuration warning!</h4> <h4 i18n class="modal-title">Configuration warning!</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -1,7 +1,9 @@
<ng-template #modal let-hide="close"> <ng-template #modal let-hide="close">
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">My settings</h4> <h4 i18n class="modal-title">My settings</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -5,7 +5,9 @@
<ng-container i18n *ngIf="!isAdminView">Messages with the moderation team</ng-container> <ng-container i18n *ngIf="!isAdminView">Messages with the moderation team</ng-container>
</h4> </h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -2,7 +2,9 @@
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Moderation comment</h4> <h4 i18n class="modal-title">Moderation comment</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -1,12 +1,12 @@
<button class="feed border-0 p-0" *ngIf="syndicationItems && syndicationItems.length !== 0"> <button
<my-global-icon *ngIf="syndicationItems && syndicationItems.length !== 0"
role="button" aria-label="Open syndication dropdown" i18n-aria-label [ngbPopover]="feedsList" [autoClose]="true" placement="bottom left auto"
*ngIf="syndicationItems.length !== 0" [ngbPopover]="feedsList" [autoClose]="true" placement="bottom left auto" class="feed border-0 p-0"
class="icon-syndication" iconName="syndication" title="Open syndication dropdown" i18n-title
> >
</my-global-icon> <my-global-icon iconName="syndication"></my-global-icon>
<ng-template #feedsList> <ng-template #feedsList>
<a *ngFor="let item of syndicationItems" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a> <a *ngFor="let item of syndicationItems" class="feed-link" [href]="item.url" target="_blank" rel="noopener noreferrer">{{ item.label }}</a>
</ng-template> </ng-template>
</button> </button>

View File

@ -4,8 +4,9 @@
.feed { .feed {
width: 100%; width: 100%;
color: inherit; color: inherit;
}
a { .feed-link {
color: pvar(--mainForegroundColor); color: pvar(--mainForegroundColor);
display: block; display: block;
min-width: 100px; min-width: 100px;
@ -13,7 +14,6 @@
&:hover { &:hover {
text-decoration: underline; text-decoration: underline;
} }
}
} }
my-global-icon { my-global-icon {

View File

@ -2,7 +2,9 @@
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">{{ action }}</h4> <h4 class="modal-title">{{ action }}</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -1,7 +1,9 @@
<ng-template #modal> <ng-template #modal>
<div class="modal-header"> <div class="modal-header">
<h4 class="modal-title">{{ modalTitle }}</h4> <h4 class="modal-title">{{ modalTitle }}</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -1,7 +1,9 @@
<ng-template #modal> <ng-template #modal>
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Report video "{{ video.name }}"</h4> <h4 i18n class="modal-title">Report video "{{ video.name }}"</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -2,7 +2,9 @@
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">{{ getModalTitle() }}</h4> <h4 i18n class="modal-title">{{ getModalTitle() }}</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -9,7 +9,9 @@
<h4 i18n class="modal-title" *ngIf="getSingleVideo().isLive">Block live "{{ getSingleVideo().name }}"</h4> <h4 i18n class="modal-title" *ngIf="getSingleVideo().isLive">Block live "{{ getSingleVideo().name }}"</h4>
</ng-container> </ng-container>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -1,7 +1,9 @@
<ng-template #modal let-hide="close"> <ng-template #modal let-hide="close">
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Support {{ displayName }}</h4> <h4 i18n class="modal-title">Support {{ displayName }}</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body" [innerHTML]="htmlSupport"></div> <div class="modal-body" [innerHTML]="htmlSupport"></div>

View File

@ -2,7 +2,9 @@
<div class="modal-header"> <div class="modal-header">
<h4 i18n class="modal-title">Live information</h4> <h4 i18n class="modal-title">Live information</h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="dismiss()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="dismiss()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body" *ngIf="live"> <div class="modal-body" *ngIf="live">

View File

@ -11,7 +11,9 @@
</div> </div>
</h4> </h4>
<my-global-icon iconName="cross" aria-label="Close" role="button" (click)="hide()"></my-global-icon> <button class="border-0 p-0" title="Close this modal" i18n-title (click)="hide()">
<my-global-icon iconName="cross"></my-global-icon>
</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">

View File

@ -1,6 +1,6 @@
<div class="margin-content"> <div class="margin-content">
<div class="videos-header"> <div class="videos-header pt-4 mb-4">
<h1 *ngIf="displayTitle" class="title" placement="bottom" [ngbTooltip]="titleTooltip" container="body"> <h1 *ngIf="displayTitle" class="title mb-1" placement="bottom" [ngbTooltip]="titleTooltip" container="body">
{{ title }} {{ title }}
</h1> </h1>
@ -10,7 +10,7 @@
<my-feed [syndicationItems]="syndicationItems"></my-feed> <my-feed [syndicationItems]="syndicationItems"></my-feed>
</div> </div>
<div class="action-block"> <div *ngIf="headerActions.length !== 0" class="action-block mt-3">
<ng-container *ngFor="let action of headerActions"> <ng-container *ngFor="let action of headerActions">
<a *ngIf="action.routerLink" class="ms-2" [routerLink]="action.routerLink" routerLinkActive="active"> <a *ngIf="action.routerLink" class="ms-2" [routerLink]="action.routerLink" routerLinkActive="active">
<ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container> <ng-container *ngTemplateOutlet="actionContent; context:{ $implicit: action }"></ng-container>

View File

@ -4,12 +4,11 @@
@use '_miniature' as *; @use '_miniature' as *;
// Cannot set margin top to videos-header because of the main header fixed position // Cannot set margin top to videos-header because of the main header fixed position
$margin-top: 30px; $margin-top: 2rem;
.videos-header { .videos-header {
display: grid; display: grid;
grid-template-columns: auto 1fr auto; grid-template-columns: auto 1fr auto;
margin-bottom: 30px;
.title, .title,
.title-subscription { .title-subscription {
@ -21,9 +20,6 @@ $margin-top: 30px;
color: pvar(--mainForegroundColor); color: pvar(--mainForegroundColor);
display: inline-block; display: inline-block;
font-weight: $font-semibold; font-weight: $font-semibold;
margin-top: $margin-top;
margin-bottom: 0;
} }
.title-subscription { .title-subscription {
@ -39,7 +35,6 @@ $margin-top: 30px;
.action-block { .action-block {
grid-column: 3; grid-column: 3;
grid-row: 1/3; grid-row: 1/3;
margin-top: $margin-top;
} }
my-feed { my-feed {
@ -77,15 +72,15 @@ $margin-top: 30px;
@include margin-right(pvar(--horizontalMarginContent)); @include margin-right(pvar(--horizontalMarginContent));
.video-wrapper { .video-wrapper {
margin-bottom: 15px; margin-bottom: 1rem;
} }
} }
@media screen and (max-width: $mobile-view) { @media screen and (max-width: $mobile-view) {
.videos-header, .videos-header,
my-video-filters-header { my-video-filters-header {
@include margin-left(15px); @include margin-left(1rem);
@include margin-right(15px); @include margin-right(1rem);
display: inline-block; display: inline-block;
} }
@ -95,9 +90,8 @@ $margin-top: 30px;
} }
.videos-header { .videos-header {
flex-direction: column; text-align: center;
align-items: center; width: 100%;
height: auto; margin-bottom: 1rem;
margin-bottom: 10px;
} }
} }

View File

@ -53,10 +53,13 @@
<my-edit-button *ngIf="owned && touchScreenEditButton" [ptRouterLink]="[ '/my-library', 'video-playlists', playlist.uuid ]"></my-edit-button> <my-edit-button *ngIf="owned && touchScreenEditButton" [ptRouterLink]="[ '/my-library', 'video-playlists', playlist.uuid ]"></my-edit-button>
<div *ngIf="owned" class="more dropdown-root" ngbDropdown #moreDropdown="ngbDropdown" placement="left auto" <div
*ngIf="owned" class="more dropdown-root" ngbDropdown #moreDropdown="ngbDropdown" placement="left auto"
(openChange)="onDropdownOpenChange()" autoClose="outside" container="body" (openChange)="onDropdownOpenChange()" autoClose="outside" container="body"
> >
<my-global-icon iconName="more-vertical" ngbDropdownToggle role="button" class="icon-more" (click)="$event.preventDefault()"></my-global-icon> <button class="border-0 p-0 more-button" (click)="$event.preventDefault()" ngbDropdownToggle>
<my-global-icon iconName="more-vertical"></my-global-icon>
</button>
<div ngbDropdownMenu> <div ngbDropdownMenu>
<ng-container *ngIf="playlistElement.video"> <ng-container *ngIf="playlistElement.video">

View File

@ -29,24 +29,6 @@ my-video-thumbnail,
padding: 10px; padding: 10px;
border-bottom: 1px solid $separator-border-color; border-bottom: 1px solid $separator-border-color;
.more {
display: flex;
}
&:hover {
background-color: rgba(0, 0, 0, 0.05);
.more {
opacity: 1;
}
}
@media not all and (hover: hover) and (pointer: fine) {
.more {
opacity: 1 !important;
}
}
&.playing { &.playing {
background-color: rgba(0, 0, 0, 0.02); background-color: rgba(0, 0, 0, 0.02);
} }
@ -87,20 +69,38 @@ my-video-thumbnail,
} }
.more { .more {
opacity: 0; display: flex;
align-items: center;
&.show {
opacity: 1;
} }
.icon-more { .more-button {
@include apply-svg-color(pvar(--greyForegroundColor)); opacity: 0;
display: flex;
&::after { &::after {
border: 0; border: 0;
} }
my-global-icon {
@include apply-svg-color(pvar(--greyForegroundColor));
}
}
&:hover,
&:focus {
background-color: rgba(0, 0, 0, 0.05);
}
&:hover,
&:focus-within,
.show {
.more-button {
opacity: 1;
}
}
@media not all and (hover: hover) and (pointer: fine) {
.more-button {
opacity: 1 !important;
} }
} }
} }
@ -183,7 +183,7 @@ my-video-thumbnail,
my-edit-button { my-edit-button {
display: none; display: none;
+ .more { + .more-button {
display: inline-flex; display: inline-flex;
} }
} }
@ -204,7 +204,7 @@ my-video-thumbnail,
} }
} }
my-edit-button + .more { my-edit-button + .more-button {
display: none; display: none;
} }
} }

View File

@ -17,7 +17,6 @@ class ChaptersPlugin extends Plugin {
this.player.ready(() => { this.player.ready(() => {
player.addClass('vjs-chapters') player.addClass('vjs-chapters')
this.player.one('durationchange', () => {
for (const chapter of this.chapters) { for (const chapter of this.chapters) {
if (chapter.timecode === 0) continue if (chapter.timecode === 0) continue
@ -27,13 +26,14 @@ class ChaptersPlugin extends Plugin {
this.getSeekBar().addChild(marker) this.getSeekBar().addChild(marker)
} }
}) })
})
} }
dispose () { dispose () {
for (const marker of this.markers) { for (const marker of this.markers) {
this.getSeekBar().removeChild(marker) this.getSeekBar().removeChild(marker)
} }
super.dispose()
} }
getChapter (timecode: number) { getChapter (timecode: number) {

View File

@ -9,16 +9,25 @@ export class ProgressBarMarkerComponent extends Component {
// eslint-disable-next-line @typescript-eslint/no-useless-constructor // eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor (player: videojs.Player, options?: ProgressBarMarkerComponentOptions & videojs.ComponentOptions) { constructor (player: videojs.Player, options?: ProgressBarMarkerComponentOptions & videojs.ComponentOptions) {
super(player, options) super(player, options)
const updateMarker = () => {
(this.el() as HTMLElement).style.setProperty('left', this.buildLeftStyle())
}
this.player().on('durationchange', updateMarker)
this.one('dispose', () => this.player().off('durationchange', updateMarker))
} }
createEl () { createEl () {
const left = (this.options_.timecode / this.player().duration()) * 100
return videojs.dom.createEl('span', { return videojs.dom.createEl('span', {
className: 'vjs-marker', className: 'vjs-marker',
style: `left: ${left}%` style: `left: ${this.buildLeftStyle()}`
}) as HTMLButtonElement }) as HTMLButtonElement
} }
private buildLeftStyle () {
return `${(this.options_.timecode / this.player().duration()) * 100}%`
}
} }
videojs.registerComponent('ProgressBarMarkerComponent', ProgressBarMarkerComponent) videojs.registerComponent('ProgressBarMarkerComponent', ProgressBarMarkerComponent)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -115,7 +115,7 @@
"bencode": "^4.0.0", "bencode": "^4.0.0",
"bittorrent-tracker": "^10.0.12", "bittorrent-tracker": "^10.0.12",
"bluebird": "^3.5.0", "bluebird": "^3.5.0",
"bullmq": "^3.6.6", "bullmq": "^4.12.3",
"bytes": "^3.0.0", "bytes": "^3.0.0",
"chokidar": "^3.4.2", "chokidar": "^3.4.2",
"commander": "^11.0.0", "commander": "^11.0.0",

View File

@ -172,15 +172,21 @@ async function getVideoStream (path: string, existingProbe?: FfprobeData) {
// Chapters // Chapters
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function getChaptersFromContainer (path: string, existingProbe?: FfprobeData) { async function getChaptersFromContainer (options: {
const metadata = existingProbe || await ffprobePromise(path) path: string
maxTitleLength: number
ffprobe?: FfprobeData
}) {
const { path, maxTitleLength, ffprobe } = options
const metadata = ffprobe || await ffprobePromise(path)
if (!Array.isArray(metadata?.chapters)) return [] if (!Array.isArray(metadata?.chapters)) return []
return metadata.chapters return metadata.chapters
.map(c => ({ .map(c => ({
timecode: c.start_time, timecode: Math.round(c.start_time),
title: c['TAG:title'] title: (c['TAG:title'] || '').slice(0, maxTitleLength)
})) }))
} }

View File

@ -3,7 +3,7 @@ import { VideoStateType } from '../videos/index.js'
import { VideoStudioTaskCut } from '../videos/studio/index.js' import { VideoStudioTaskCut } from '../videos/studio/index.js'
import { SendEmailOptions } from './emailer.model.js' import { SendEmailOptions } from './emailer.model.js'
export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' | 'paused' | 'waiting-children' export type JobState = 'active' | 'completed' | 'failed' | 'waiting' | 'delayed' | 'paused' | 'waiting-children' | 'prioritized'
export type JobType = export type JobType =
| 'activitypub-cleaner' | 'activitypub-cleaner'

View File

@ -19,7 +19,7 @@ describe('Test videos chapters API validator', function () {
// --------------------------------------------------------------- // ---------------------------------------------------------------
before(async function () { before(async function () {
this.timeout(30000) this.timeout(60000)
server = await createSingleServer(1) server = await createSingleServer(1)

View File

@ -199,7 +199,7 @@ describe('Test video studio API validator', function () {
}) })
it('Should fail with a video that is already waiting for edition', async function () { it('Should fail with a video that is already waiting for edition', async function () {
this.timeout(120000) this.timeout(360000)
await command.createEditionTasks({ await command.createEditionTasks({
videoId: videoUUID, videoId: videoUUID,
@ -257,7 +257,7 @@ describe('Test video studio API validator', function () {
}) })
it('Should succeed with the correct params', async function () { it('Should succeed with the correct params', async function () {
this.timeout(120000) this.timeout(360000)
await cut(0, 2, HttpStatusCode.NO_CONTENT_204) await cut(0, 2, HttpStatusCode.NO_CONTENT_204)
@ -291,7 +291,7 @@ describe('Test video studio API validator', function () {
}) })
it('Should succeed with the correct params', async function () { it('Should succeed with the correct params', async function () {
this.timeout(120000) this.timeout(360000)
await addWatermark('custom-thumbnail.jpg', HttpStatusCode.NO_CONTENT_204) await addWatermark('custom-thumbnail.jpg', HttpStatusCode.NO_CONTENT_204)
@ -337,7 +337,7 @@ describe('Test video studio API validator', function () {
}) })
it('Should succeed with the correct params', async function () { it('Should succeed with the correct params', async function () {
this.timeout(120000) this.timeout(360000)
await addIntroOutro('add-intro', 'video_very_short_240p.mp4', HttpStatusCode.NO_CONTENT_204) await addIntroOutro('add-intro', 'video_very_short_240p.mp4', HttpStatusCode.NO_CONTENT_204)
await waitJobs([ server ]) await waitJobs([ server ])
@ -347,7 +347,7 @@ describe('Test video studio API validator', function () {
}) })
it('Should check total quota when creating the task', async function () { it('Should check total quota when creating the task', async function () {
this.timeout(120000) this.timeout(360000)
const user = await server.users.create({ username: 'user_quota_1' }) const user = await server.users.create({ username: 'user_quota_1' })
const token = await server.login.getAccessToken('user_quota_1') const token = await server.login.getAccessToken('user_quota_1')

View File

@ -66,7 +66,7 @@ describe('Test admin notifications', function () {
joinPeerTubeServer.setLatestVersion('1.4.2') joinPeerTubeServer.setLatestVersion('1.4.2')
await wait(3000) await wait(4500)
await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '1.4.2', checkType: 'absence' }) await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '1.4.2', checkType: 'absence' })
}) })
@ -75,14 +75,14 @@ describe('Test admin notifications', function () {
joinPeerTubeServer.setLatestVersion('15.4.2') joinPeerTubeServer.setLatestVersion('15.4.2')
await wait(3000) await wait(4500)
await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.2', checkType: 'presence' }) await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.2', checkType: 'presence' })
}) })
it('Should not send the same notification to admins', async function () { it('Should not send the same notification to admins', async function () {
this.timeout(30000) this.timeout(30000)
await wait(3000) await wait(4500)
expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(1) expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(1)
}) })
@ -97,7 +97,7 @@ describe('Test admin notifications', function () {
joinPeerTubeServer.setLatestVersion('15.4.3') joinPeerTubeServer.setLatestVersion('15.4.3')
await wait(3000) await wait(4500)
await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.3', checkType: 'presence' }) await checkNewPeerTubeVersion({ ...baseParams, latestVersion: '15.4.3', checkType: 'presence' })
expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2) expect(adminNotifications.filter(n => n.type === UserNotificationType.NEW_PEERTUBE_VERSION)).to.have.lengthOf(2)
}) })

View File

@ -39,7 +39,7 @@ describe('Test comments notifications', function () {
}) })
it('Should not send a new comment notification after a comment on another video', async function () { it('Should not send a new comment notification after a comment on another video', async function () {
this.timeout(30000) this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } }) const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
@ -51,7 +51,7 @@ describe('Test comments notifications', function () {
}) })
it('Should not send a new comment notification if I comment my own video', async function () { it('Should not send a new comment notification if I comment my own video', async function () {
this.timeout(30000) this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } }) const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
@ -63,7 +63,7 @@ describe('Test comments notifications', function () {
}) })
it('Should not send a new comment notification if the account is muted', async function () { it('Should not send a new comment notification if the account is muted', async function () {
this.timeout(30000) this.timeout(60000)
await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' }) await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' })
@ -79,7 +79,7 @@ describe('Test comments notifications', function () {
}) })
it('Should send a new comment notification after a local comment on my video', async function () { it('Should send a new comment notification after a local comment on my video', async function () {
this.timeout(30000) this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } }) const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
@ -91,7 +91,7 @@ describe('Test comments notifications', function () {
}) })
it('Should send a new comment notification after a remote comment on my video', async function () { it('Should send a new comment notification after a remote comment on my video', async function () {
this.timeout(30000) this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } }) const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
@ -109,7 +109,7 @@ describe('Test comments notifications', function () {
}) })
it('Should send a new comment notification after a local reply on my video', async function () { it('Should send a new comment notification after a local reply on my video', async function () {
this.timeout(30000) this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } }) const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
@ -122,7 +122,7 @@ describe('Test comments notifications', function () {
}) })
it('Should send a new comment notification after a remote reply on my video', async function () { it('Should send a new comment notification after a remote reply on my video', async function () {
this.timeout(30000) this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } }) const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
await waitJobs(servers) await waitJobs(servers)
@ -148,7 +148,7 @@ describe('Test comments notifications', function () {
}) })
it('Should convert markdown in comment to html', async function () { it('Should convert markdown in comment to html', async function () {
this.timeout(30000) this.timeout(60000)
const { uuid } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'cool video' } }) const { uuid } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'cool video' } })
@ -178,7 +178,7 @@ describe('Test comments notifications', function () {
}) })
it('Should not send a new mention comment notification if I mention the video owner', async function () { it('Should not send a new mention comment notification if I mention the video owner', async function () {
this.timeout(30000) this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } }) const { uuid, shortUUID } = await servers[0].videos.upload({ token: userToken, attributes: { name: 'super video' } })
@ -189,7 +189,7 @@ describe('Test comments notifications', function () {
}) })
it('Should not send a new mention comment notification if I mention myself', async function () { it('Should not send a new mention comment notification if I mention myself', async function () {
this.timeout(30000) this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } }) const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
@ -200,7 +200,7 @@ describe('Test comments notifications', function () {
}) })
it('Should not send a new mention notification if the account is muted', async function () { it('Should not send a new mention notification if the account is muted', async function () {
this.timeout(30000) this.timeout(60000)
await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' }) await servers[0].blocklist.addToMyBlocklist({ token: userToken, account: 'root' })
@ -215,7 +215,7 @@ describe('Test comments notifications', function () {
}) })
it('Should not send a new mention notification if the remote account mention a local account', async function () { it('Should not send a new mention notification if the remote account mention a local account', async function () {
this.timeout(30000) this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } }) const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
@ -229,7 +229,7 @@ describe('Test comments notifications', function () {
}) })
it('Should send a new mention notification after local comments', async function () { it('Should send a new mention notification after local comments', async function () {
this.timeout(30000) this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } }) const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
@ -245,7 +245,7 @@ describe('Test comments notifications', function () {
}) })
it('Should send a new mention notification after remote comments', async function () { it('Should send a new mention notification after remote comments', async function () {
this.timeout(30000) this.timeout(60000)
const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } }) const { uuid, shortUUID } = await servers[0].videos.upload({ attributes: { name: 'super video' } })
@ -277,7 +277,7 @@ describe('Test comments notifications', function () {
}) })
it('Should convert markdown in comment to html', async function () { it('Should convert markdown in comment to html', async function () {
this.timeout(30000) this.timeout(60000)
const { uuid } = await servers[0].videos.upload({ attributes: { name: 'super video' } }) const { uuid } = await servers[0].videos.upload({ attributes: { name: 'super video' } })

View File

@ -233,7 +233,7 @@ describe('Test user notifications', function () {
}) })
it('Should not send a notification if the wait transcoding is false', async function () { it('Should not send a notification if the wait transcoding is false', async function () {
this.timeout(100_000) this.timeout(240000)
await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: false }) await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: false })
await waitJobs(servers) await waitJobs(servers)
@ -245,7 +245,7 @@ describe('Test user notifications', function () {
}) })
it('Should send a notification even if the video is not transcoded in other resolutions', async function () { it('Should send a notification even if the video is not transcoded in other resolutions', async function () {
this.timeout(100_000) this.timeout(240000)
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true, fixture: 'video_short_240p.mp4' }) const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true, fixture: 'video_short_240p.mp4' })
await waitJobs(servers) await waitJobs(servers)
@ -254,7 +254,7 @@ describe('Test user notifications', function () {
}) })
it('Should send a notification with a transcoded video', async function () { it('Should send a notification with a transcoded video', async function () {
this.timeout(100_000) this.timeout(240000)
const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true }) const { name, shortUUID } = await uploadRandomVideoOnServers(servers, 2, { waitTranscoding: true })
await waitJobs(servers) await waitJobs(servers)
@ -263,7 +263,7 @@ describe('Test user notifications', function () {
}) })
it('Should send a notification when an imported video is transcoded', async function () { it('Should send a notification when an imported video is transcoded', async function () {
this.timeout(120000) this.timeout(240000)
const name = 'video import ' + buildUUID() const name = 'video import ' + buildUUID()

View File

@ -232,7 +232,7 @@ describe('Test stats (excluding redundancy)', function () {
}) })
it('Should have the correct AP stats', async function () { it('Should have the correct AP stats', async function () {
this.timeout(120000) this.timeout(240000)
await servers[0].config.disableTranscoding() await servers[0].config.disableTranscoding()

View File

@ -489,7 +489,7 @@ describe('Test video transcoding', function () {
}) })
it('Should downscale to the closest divisor standard framerate', async function () { it('Should downscale to the closest divisor standard framerate', async function () {
this.timeout(200_000) this.timeout(360_000)
let tempFixturePath: string let tempFixturePath: string

View File

@ -74,7 +74,7 @@ describe('Test video playlists', function () {
let commands: PlaylistsCommand[] let commands: PlaylistsCommand[]
before(async function () { before(async function () {
this.timeout(240000) this.timeout(360000)
servers = await createMultipleServers(3) servers = await createMultipleServers(3)

View File

@ -19,7 +19,7 @@ import { HttpStatusCode, VideoCreate, VideoPrivacy, VideoState } from '@peertube
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger.js' import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger.js'
import { createReqFiles } from '../../../helpers/express-utils.js' import { createReqFiles } from '../../../helpers/express-utils.js'
import { logger, loggerTagsFactory } from '../../../helpers/logger.js' import { logger, loggerTagsFactory } from '../../../helpers/logger.js'
import { MIMETYPES } from '../../../initializers/constants.js' import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../../initializers/constants.js'
import { sequelizeTypescript } from '../../../initializers/database.js' import { sequelizeTypescript } from '../../../initializers/database.js'
import { Hooks } from '../../../lib/plugins/hooks.js' import { Hooks } from '../../../lib/plugins/hooks.js'
import { generateLocalVideoMiniature } from '../../../lib/thumbnail.js' import { generateLocalVideoMiniature } from '../../../lib/thumbnail.js'
@ -145,7 +145,10 @@ async function addVideo (options: {
const videoFile = await buildNewFile({ path: videoPhysicalFile.path, mode: 'web-video' }) const videoFile = await buildNewFile({ path: videoPhysicalFile.path, mode: 'web-video' })
const originalFilename = videoPhysicalFile.originalname const originalFilename = videoPhysicalFile.originalname
const containerChapters = await getChaptersFromContainer(videoPhysicalFile.path) const containerChapters = await getChaptersFromContainer({
path: videoPhysicalFile.path,
maxTitleLength: CONSTRAINTS_FIELDS.VIDEO_CHAPTERS.TITLE.max
})
logger.debug(`Got ${containerChapters.length} chapters from video "${video.name}" container`, { containerChapters, ...lTags(video.uuid) }) logger.debug(`Got ${containerChapters.length} chapters from video "${video.name}" container`, { containerChapters, ...lTags(video.uuid) })
// Move physical file // Move physical file

View File

@ -2,10 +2,10 @@ import { JobState } from '@peertube/peertube-models'
import { jobTypes } from '@server/lib/job-queue/job-queue.js' import { jobTypes } from '@server/lib/job-queue/job-queue.js'
import { exists } from './misc.js' import { exists } from './misc.js'
const jobStates: JobState[] = [ 'active', 'completed', 'failed', 'waiting', 'delayed', 'paused', 'waiting-children' ] const jobStates = new Set<JobState>([ 'active', 'completed', 'failed', 'waiting', 'delayed', 'paused', 'waiting-children', 'prioritized' ])
function isValidJobState (value: JobState) { function isValidJobState (value: JobState) {
return exists(value) && jobStates.includes(value) return exists(value) && jobStates.has(value)
} }
function isValidJobType (value: any) { function isValidJobType (value: any) {

View File

@ -41,7 +41,7 @@ import {
import { logger } from '../../../helpers/logger.js' import { logger } from '../../../helpers/logger.js'
import { getSecureTorrentName } from '../../../helpers/utils.js' import { getSecureTorrentName } from '../../../helpers/utils.js'
import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent.js' import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent.js'
import { JOB_TTL } from '../../../initializers/constants.js' import { CONSTRAINTS_FIELDS, JOB_TTL } from '../../../initializers/constants.js'
import { sequelizeTypescript } from '../../../initializers/database.js' import { sequelizeTypescript } from '../../../initializers/database.js'
import { VideoFileModel } from '../../../models/video/video-file.js' import { VideoFileModel } from '../../../models/video/video-file.js'
import { VideoImportModel } from '../../../models/video/video-import.js' import { VideoImportModel } from '../../../models/video/video-import.js'
@ -143,16 +143,20 @@ async function processFile (downloader: () => Promise<string>, videoImport: MVid
throw new Error('The user video quota is exceeded with this video to import.') throw new Error('The user video quota is exceeded with this video to import.')
} }
const probe = await ffprobePromise(tempVideoPath) const ffprobe = await ffprobePromise(tempVideoPath)
const { resolution } = await isAudioFile(tempVideoPath, probe) const { resolution } = await isAudioFile(tempVideoPath, ffprobe)
? { resolution: VideoResolution.H_NOVIDEO } ? { resolution: VideoResolution.H_NOVIDEO }
: await getVideoStreamDimensionsInfo(tempVideoPath, probe) : await getVideoStreamDimensionsInfo(tempVideoPath, ffprobe)
const fps = await getVideoStreamFPS(tempVideoPath, probe) const fps = await getVideoStreamFPS(tempVideoPath, ffprobe)
const duration = await getVideoStreamDuration(tempVideoPath, probe) const duration = await getVideoStreamDuration(tempVideoPath, ffprobe)
const containerChapters = await getChaptersFromContainer(tempVideoPath, probe) const containerChapters = await getChaptersFromContainer({
path: tempVideoPath,
maxTitleLength: CONSTRAINTS_FIELDS.VIDEO_CHAPTERS.TITLE.max,
ffprobe
})
// Prepare video file object for creation in database // Prepare video file object for creation in database
const fileExt = getLowercaseExtension(tempVideoPath) const fileExt = getLowercaseExtension(tempVideoPath)

View File

@ -257,6 +257,9 @@ class JobQueue {
queue.on('error', err => { logger.error('Error in job queue %s.', handlerName, { err }) }) queue.on('error', err => { logger.error('Error in job queue %s.', handlerName, { err }) })
this.queues[handlerName] = queue this.queues[handlerName] = queue
queue.removeDeprecatedPriorityKey()
.catch(err => logger.error('Cannot remove bullmq deprecated priority keys of ' + handlerName, { err }))
} }
private buildQueueEvent (handlerName: JobType) { private buildQueueEvent (handlerName: JobType) {
@ -455,12 +458,15 @@ class JobQueue {
} }
private buildStateFilter (state?: JobState) { private buildStateFilter (state?: JobState) {
if (!state) return jobStates if (!state) return Array.from(jobStates)
const states = [ state ] const states = [ state ]
// Include parent if filtering on waiting // Include parent and prioritized if filtering on waiting
if (state === 'waiting') states.push('waiting-children') if (state === 'waiting') {
states.push('waiting-children')
states.push('prioritized')
}
return states return states
} }

View File

@ -1,4 +1,4 @@
import { buildVideoEmbedPath, buildVideoWatchPath, pick } from '@peertube/peertube-core-utils' import { buildVideoEmbedPath, buildVideoWatchPath, pick, wait } from '@peertube/peertube-core-utils'
import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream } from '@peertube/peertube-ffmpeg' import { ffprobePromise, getAudioStream, getVideoStreamDimensionsInfo, getVideoStreamFPS, hasAudioStream } from '@peertube/peertube-ffmpeg'
import { import {
ResultList, ResultList,
@ -1925,7 +1925,20 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
? getHLSRedundancyDirectory(this) ? getHLSRedundancyDirectory(this)
: getHLSDirectory(this) : getHLSDirectory(this)
try {
await remove(directoryPath) await remove(directoryPath)
} catch (err) {
// If it's a live, ffmpeg may have added another file while fs-extra is removing the directory
// So wait a little bit and retry
if (err.code === 'ENOTEMPTY') {
await wait(1000)
await remove(directoryPath)
return
}
throw err
}
if (isRedundancy !== true) { if (isRedundancy !== true) {
const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo

View File

@ -3572,17 +3572,18 @@ builtins@^5.0.1:
dependencies: dependencies:
semver "^7.0.0" semver "^7.0.0"
bullmq@^3.6.6: bullmq@^4.12.3:
version "3.15.8" version "4.12.3"
resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-3.15.8.tgz#e8ec5b46b0b7d7ce57e509280d03745109411e05" resolved "https://registry.yarnpkg.com/bullmq/-/bullmq-4.12.3.tgz#0c649b9a5e48227519c526ee9edd96b982eee22d"
integrity sha512-k3uimHGhl5svqD7SEak+iI6c5DxeLOaOXzCufI9Ic0ST3nJr69v71TGR4cXCTXdgCff3tLec5HgoBnfyWjgn5A== integrity sha512-4uPp4NQTALFF+eFK7g8VJM+rt0aiduQdzBomgiEO1OK4OE+TdgC6cjGXooKI/asuB8iDhSZ+pSnGYy5Xyr6qRA==
dependencies: dependencies:
cron-parser "^4.6.0" cron-parser "^4.6.0"
glob "^8.0.3" glob "^8.0.3"
ioredis "^5.3.2" ioredis "^5.3.2"
lodash "^4.17.21" lodash "^4.17.21"
msgpackr "^1.6.2" msgpackr "^1.6.2"
semver "^7.3.7" node-abort-controller "^3.1.1"
semver "^7.5.4"
tslib "^2.0.0" tslib "^2.0.0"
uuid "^9.0.0" uuid "^9.0.0"
@ -7458,6 +7459,11 @@ nice-try@^1.0.4:
resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366"
integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==
node-abort-controller@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/node-abort-controller/-/node-abort-controller-3.1.1.tgz#a94377e964a9a37ac3976d848cb5c765833b8548"
integrity sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==
node-addon-api@^3.0.0: node-addon-api@^3.0.0:
version "3.2.1" version "3.2.1"
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
@ -8911,7 +8917,7 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.3.1:
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
semver@^7.0.0, semver@^7.3.2, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.1, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4: semver@^7.0.0, semver@^7.3.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.1, semver@^7.5.2, semver@^7.5.3, semver@^7.5.4:
version "7.5.4" version "7.5.4"
resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e"
integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==