mirror of
https://github.com/Chocobozzz/PeerTube.git
synced 2026-09-03 20:53:09 -05:00
Add video embed domain privacy
This commit is contained in:
+13
-12
@@ -136,6 +136,16 @@ More detailed documentation is available:
|
||||
* [Server code/architecture](https://docs.joinpeertube.org/contribute/architecture#server)
|
||||
* [Server development (adding a new feature...)](/support/doc/development/server.md)
|
||||
|
||||
### Embed only
|
||||
|
||||
The embed is a standalone application built using Vite.
|
||||
The generated files (HTML entrypoint and multiple JS and CSS files) are served by the Vite server (behind `localhost:5173/videos/embed/:videoUUID` or `localhost:5173/video-playlists/embed/:playlistUUID`).
|
||||
The following command will compile embed files and run the PeerTube server:
|
||||
|
||||
```sh
|
||||
npm run dev:embed
|
||||
```
|
||||
|
||||
### Client side
|
||||
|
||||
To develop on the client side:
|
||||
@@ -145,16 +155,17 @@ npm run dev:client
|
||||
```
|
||||
|
||||
The API will listen on `localhost:9000` and the frontend on `localhost:3000`.
|
||||
Embed is also available on `localhost:5173`.
|
||||
Client files are automatically compiled on change, and the web browser will
|
||||
reload them automatically thanks to hot module replacement.
|
||||
|
||||
More detailed documentation is available:
|
||||
* [Client code/architecture](https://docs.joinpeertube.org/contribute/architecture#client)
|
||||
|
||||
|
||||
### Client and server side
|
||||
### Client with embed and server side
|
||||
|
||||
The API will listen on `localhost:9000` and the frontend on `localhost:3000`.
|
||||
Embed is also available on `localhost:5173`.
|
||||
File changes are automatically recompiled, injected in the web browser (no need to refresh manually)
|
||||
and the web server is automatically restarted.
|
||||
|
||||
@@ -162,16 +173,6 @@ and the web server is automatically restarted.
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Embed
|
||||
|
||||
The embed is a standalone application built using Vite.
|
||||
The generated files (HTML entrypoint and multiple JS and CSS files) are served by the Vite server (behind `localhost:5173/videos/embed/:videoUUID` or `localhost:5173/video-playlists/embed/:playlistUUID`).
|
||||
The following command will compile embed files and run the PeerTube server:
|
||||
|
||||
```sh
|
||||
npm run dev:embed
|
||||
```
|
||||
|
||||
### RTL layout
|
||||
|
||||
To test RTL (right-to-left) layout using `ar` locale:
|
||||
|
||||
@@ -2,7 +2,7 @@ import { inject } from '@angular/core'
|
||||
import { ActivatedRouteSnapshot, ResolveFn, RouterStateSnapshot, Routes } from '@angular/router'
|
||||
import { CanDeactivateGuard, ServerService, UserRightGuard } from '@app/core'
|
||||
import { CustomPageService } from '@app/shared/shared-main/custom-page/custom-page.service'
|
||||
import { CustomConfig, UserRight, VideoCommentPolicyType, VideoConstant, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import { CustomConfig, UserRight, VideoCommentPolicyType, ConstantLabel, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import { map } from 'rxjs'
|
||||
import { AdminConfigComponent } from './admin-config.component'
|
||||
import {
|
||||
@@ -28,26 +28,26 @@ export const homepageResolver: ResolveFn<string> = (_route: ActivatedRouteSnapsh
|
||||
.pipe(map(({ content }) => content))
|
||||
}
|
||||
|
||||
export const categoriesResolver: ResolveFn<VideoConstant<number>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
export const categoriesResolver: ResolveFn<ConstantLabel<number>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(ServerService).getVideoCategories()
|
||||
}
|
||||
|
||||
export const languagesResolver: ResolveFn<VideoConstant<string>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
export const languagesResolver: ResolveFn<ConstantLabel<string>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(ServerService).getVideoLanguages()
|
||||
}
|
||||
|
||||
export const licencesResolver: ResolveFn<VideoConstant<number>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
export const licencesResolver: ResolveFn<ConstantLabel<number>[]> = (_route: ActivatedRouteSnapshot, _state: RouterStateSnapshot) => {
|
||||
return inject(ServerService).getVideoLicences()
|
||||
}
|
||||
|
||||
export const privaciesResolver: ResolveFn<VideoConstant<VideoPrivacyType>[]> = (
|
||||
export const privaciesResolver: ResolveFn<ConstantLabel<VideoPrivacyType>[]> = (
|
||||
_route: ActivatedRouteSnapshot,
|
||||
_state: RouterStateSnapshot
|
||||
) => {
|
||||
return inject(ServerService).getVideoPrivacies()
|
||||
}
|
||||
|
||||
export const commentPoliciesResolver: ResolveFn<VideoConstant<VideoCommentPolicyType>[]> = (
|
||||
export const commentPoliciesResolver: ResolveFn<ConstantLabel<VideoCommentPolicyType>[]> = (
|
||||
_route: ActivatedRouteSnapshot,
|
||||
_state: RouterStateSnapshot
|
||||
) => {
|
||||
|
||||
@@ -24,7 +24,7 @@ import { USER_VIDEO_QUOTA_DAILY_VALIDATOR, USER_VIDEO_QUOTA_VALIDATOR } from '@a
|
||||
import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { BroadcastMessageLevel, CustomConfig, VideoCommentPolicyType, VideoConstant, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import { BroadcastMessageLevel, CustomConfig, VideoCommentPolicyType, ConstantLabel, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { pairwise } from 'rxjs/operators'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
@@ -264,9 +264,9 @@ export class AdminConfigGeneralComponent implements OnInit, OnDestroy, CanCompon
|
||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||
|
||||
const data = this.route.snapshot.data as {
|
||||
licences: VideoConstant<number>[]
|
||||
privacies: VideoConstant<VideoPrivacyType>[]
|
||||
commentPolicies: VideoConstant<VideoCommentPolicyType>[]
|
||||
licences: ConstantLabel<number>[]
|
||||
privacies: ConstantLabel<VideoPrivacyType>[]
|
||||
commentPolicies: ConstantLabel<VideoCommentPolicyType>[]
|
||||
}
|
||||
|
||||
this.privacyOptions = this.videoService.explainedPrivacyLabels(data.privacies).videoPrivacies
|
||||
|
||||
@@ -20,7 +20,7 @@ import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.serv
|
||||
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||
import { SelectRadioComponent } from '@app/shared/shared-forms/select/select-radio.component'
|
||||
import { getCompleteLocale, I18N_LOCALES } from '@peertube/peertube-core-utils'
|
||||
import { ActorImage, CustomConfig, NSFWPolicyType, VideoConstant } from '@peertube/peertube-models'
|
||||
import { ActorImage, CustomConfig, NSFWPolicyType, ConstantLabel } from '@peertube/peertube-models'
|
||||
import merge from 'lodash-es/merge'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { SelectOptionsItem } from 'src/types/select-options-item.model'
|
||||
@@ -145,8 +145,8 @@ export class AdminConfigInformationComponent implements OnInit, OnDestroy, CanCo
|
||||
this.customConfig = this.route.parent.snapshot.data['customConfig']
|
||||
|
||||
const data = this.route.snapshot.data as {
|
||||
languages: VideoConstant<string>[]
|
||||
categories: VideoConstant<number>[]
|
||||
languages: ConstantLabel<string>[]
|
||||
categories: ConstantLabel<number>[]
|
||||
}
|
||||
|
||||
this.languageItems = data.languages.map(l => ({ label: l.label, id: l.id }))
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="modal-body">
|
||||
<form novalidate [formGroup]="form" (ngSubmit)="submit()">
|
||||
<div class="form-group">
|
||||
<label i18n for="hostsOrHandles">1 host (without "http://"), account handle or channel handle per line</label>
|
||||
<label i18n for="hostsOrHandles">1 domain (without "http://"), account handle or channel handle per line</label>
|
||||
|
||||
<textarea
|
||||
[placeholder]="placeholder" formControlName="hostsOrHandles" type="text" id="hostsOrHandles" name="hostsOrHandles"
|
||||
|
||||
+24
@@ -176,6 +176,30 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@case (12) { <!-- VideoChannelActivityAction.SEND_OWNERSHIP_REQUEST) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> sent an ownership request to {{ a.targetAccount.displayName }} for video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@case (13) { <!-- VideoChannelActivityAction.ACCEPT_OWNERSHIP_REQUEST) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> accepted an ownership request for video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@case (14) { <!-- VideoChannelActivityAction.REFUSE_OWNERSHIP_REQUEST) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> refused an ownership request for video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
@case (15) { <!-- VideoChannelActivityAction.UPDATE_EMBED_POLICY) -->
|
||||
<ng-container i18n>
|
||||
<strong>{{ account }}</strong> updated embed policy of video <a [routerLink]="[ '/w', a.video.shortUUID ]">{{ a.video.name }}</a>
|
||||
</ng-container>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { FormReactive } from '@app/shared/shared-forms/form-reactive'
|
||||
import { VideoPlaylist } from '@app/shared/shared-video-playlist/video-playlist.model'
|
||||
import { VideoConstant, VideoPlaylistPrivacyType } from '@peertube/peertube-models'
|
||||
import { ConstantLabel, VideoPlaylistPrivacyType } from '@peertube/peertube-models'
|
||||
import { SelectChannelItem } from '../../../types/select-options-item.model'
|
||||
|
||||
export abstract class MyVideoPlaylistEdit extends FormReactive {
|
||||
// Declare it here to avoid errors in create template
|
||||
videoPlaylistToUpdate: VideoPlaylist
|
||||
userVideoChannels: SelectChannelItem[] = []
|
||||
videoPlaylistPrivacies: VideoConstant<VideoPlaylistPrivacyType>[] = []
|
||||
videoPlaylistPrivacies: ConstantLabel<VideoPlaylistPrivacyType>[] = []
|
||||
|
||||
abstract isCreation (): boolean
|
||||
abstract getFormButtonTitle (): string
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, OnInit, inject, input, output } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ServerService } from '@app/core'
|
||||
import { AdvancedSearch } from '@app/shared/shared-search/advanced-search.model'
|
||||
import { HTMLServerConfig, VideoConstant } from '@peertube/peertube-models'
|
||||
import { HTMLServerConfig, ConstantLabel } from '@peertube/peertube-models'
|
||||
import { SelectTagsComponent } from '../shared/shared-forms/select/select-tags.component'
|
||||
|
||||
type FormOption = { id: string, label: string }
|
||||
@@ -19,9 +19,9 @@ export class SearchFiltersComponent implements OnInit {
|
||||
advancedSearch = input<AdvancedSearch>(new AdvancedSearch())
|
||||
filtered = output<AdvancedSearch>()
|
||||
|
||||
videoCategories: VideoConstant<number>[] = []
|
||||
videoLicences: VideoConstant<number>[] = []
|
||||
videoLanguages: VideoConstant<string>[] = []
|
||||
videoCategories: ConstantLabel<number>[] = []
|
||||
videoLicences: ConstantLabel<number>[] = []
|
||||
videoLanguages: ConstantLabel<string>[] = []
|
||||
|
||||
publishedDateRanges: FormOption[] = []
|
||||
sorts: FormOption[] = []
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Video } from '@app/shared/shared-main/video/video.model'
|
||||
import { VideoChannelSummary, VideoConstant, VideosOverview as VideosOverviewServer } from '@peertube/peertube-models'
|
||||
import { VideoChannelSummary, ConstantLabel, VideosOverview as VideosOverviewServer } from '@peertube/peertube-models'
|
||||
|
||||
export class VideosOverview implements VideosOverviewServer {
|
||||
channels: {
|
||||
@@ -8,7 +8,7 @@ export class VideosOverview implements VideosOverviewServer {
|
||||
}[]
|
||||
|
||||
categories: {
|
||||
category: VideoConstant<number>
|
||||
category: ConstantLabel<number>
|
||||
videos: Video[]
|
||||
}[]
|
||||
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Routes } from '@angular/router'
|
||||
import { CanDeactivateGuard, LoginGuard } from '@app/core'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { VideoEmbedPrivacyService } from '@app/shared/shared-video/video-embed-privacy.service'
|
||||
import { VideoStateMessageService } from '@app/shared/shared-video/video-state-message.service'
|
||||
import { I18nPrimengCalendarService } from '../shared-manage/common/i18n-primeng-calendar.service'
|
||||
import { VideoUploadService } from '../shared-manage/common/video-upload.service'
|
||||
import { manageRoutes } from '../shared-manage/routes'
|
||||
import { VideoStudioService } from '../shared-manage/studio/video-studio.service'
|
||||
import { VideoManageController } from '../shared-manage/video-manage-controller.service'
|
||||
import { VideoManageComponent } from './video-manage.component'
|
||||
import { VideoManageResolver } from './video-manage.resolver'
|
||||
import { VideoManageController } from '../shared-manage/video-manage-controller.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -25,7 +26,8 @@ export default [
|
||||
VideoUploadService,
|
||||
VideoStudioService,
|
||||
VideoStateMessageService,
|
||||
PlayerSettingsService
|
||||
PlayerSettingsService,
|
||||
VideoEmbedPrivacyService
|
||||
],
|
||||
resolve: {
|
||||
resolverData: VideoManageResolver
|
||||
|
||||
@@ -9,13 +9,15 @@ import { VideoPasswordService } from '@app/shared/shared-main/video/video-passwo
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { VideoEmbedPrivacyService } from '@app/shared/shared-video/video-embed-privacy.service'
|
||||
import {
|
||||
ConstantLabel,
|
||||
LiveVideo,
|
||||
PlayerVideoSettings,
|
||||
UserVideoQuota,
|
||||
VideoCaption,
|
||||
VideoChapter,
|
||||
VideoConstant,
|
||||
VideoEmbedPrivacy,
|
||||
VideoPassword,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType,
|
||||
@@ -36,9 +38,10 @@ export type VideoManageResolverData = {
|
||||
live: LiveVideo
|
||||
videoPasswords: VideoPassword[]
|
||||
userQuota: UserVideoQuota
|
||||
privacies: VideoConstant<VideoPrivacyType>[]
|
||||
privacies: ConstantLabel<VideoPrivacyType>[]
|
||||
videoEdit: VideoEdit
|
||||
playerSettings: PlayerVideoSettings
|
||||
embedPrivacy: VideoEmbedPrivacy
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
@@ -53,6 +56,7 @@ export class VideoManageResolver {
|
||||
private userService = inject(UserService)
|
||||
private serverService = inject(ServerService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
private videoEmbedPrivacyService = inject(VideoEmbedPrivacyService)
|
||||
|
||||
resolve (route: ActivatedRouteSnapshot) {
|
||||
const uuid: string = route.params['uuid']
|
||||
@@ -71,7 +75,8 @@ export class VideoManageResolver {
|
||||
videoPasswords,
|
||||
userQuota,
|
||||
privacies,
|
||||
playerSettings
|
||||
playerSettings,
|
||||
embedPrivacy
|
||||
]) => {
|
||||
const videoEdit = await VideoEdit.createFromAPI(this.serverService.getHTMLConfig(), {
|
||||
video,
|
||||
@@ -80,7 +85,8 @@ export class VideoManageResolver {
|
||||
live,
|
||||
videoSource,
|
||||
playerSettings,
|
||||
videoPasswords: videoPasswords.map(p => p.password)
|
||||
videoPasswords: videoPasswords.map(p => p.password),
|
||||
embedPrivacy
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -94,7 +100,8 @@ export class VideoManageResolver {
|
||||
userQuota,
|
||||
privacies,
|
||||
videoEdit,
|
||||
playerSettings
|
||||
playerSettings,
|
||||
embedPrivacy
|
||||
} satisfies VideoManageResolverData
|
||||
}
|
||||
),
|
||||
@@ -140,7 +147,9 @@ export class VideoManageResolver {
|
||||
|
||||
this.serverService.getVideoPrivacies(),
|
||||
|
||||
this.playerSettingsService.getVideoSettings({ videoId: video.uuid, raw: true })
|
||||
this.playerSettingsService.getVideoSettings({ videoId: video.uuid, raw: true }),
|
||||
|
||||
this.videoEmbedPrivacyService.getPrivacy({ videoId: video.uuid })
|
||||
] as const
|
||||
}
|
||||
}
|
||||
|
||||
+7
-4
@@ -7,6 +7,8 @@ import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerServ
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { VideoImportService } from '@app/shared/shared-main/video/video-import.service'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { VideoEmbedPrivacyService } from '@app/shared/shared-video/video-embed-privacy.service'
|
||||
import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||
import { PeerTubeProblemDocument, ServerErrorCode, UserVideoQuota, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
@@ -18,7 +20,6 @@ import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.co
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { VideoManageContainerComponent } from '../../shared-manage/video-manage-container.component'
|
||||
import { DragDropDirective } from '../shared/drag-drop.directive'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
|
||||
const debugLogger = debug('peertube:video-publish')
|
||||
|
||||
@@ -52,6 +53,7 @@ export class VideoImportTorrentComponent implements OnInit, AfterViewInit, CanCo
|
||||
private serverService = inject(ServerService)
|
||||
private manageController = inject(VideoManageController)
|
||||
private route = inject(ActivatedRoute)
|
||||
private videoEmbedPrivacyService = inject(VideoEmbedPrivacyService)
|
||||
|
||||
readonly userChannels = input.required<SelectChannelItem[]>()
|
||||
readonly userQuota = input.required<UserVideoQuota>()
|
||||
@@ -136,12 +138,13 @@ export class VideoImportTorrentComponent implements OnInit, AfterViewInit, CanCo
|
||||
.pipe(switchMap(({ video }) => {
|
||||
return forkJoin([
|
||||
this.videoService.getVideo({ videoId: video.uuid }),
|
||||
this.playerSettingsService.getVideoSettings({ videoId: video.uuid, raw: true })
|
||||
this.playerSettingsService.getVideoSettings({ videoId: video.uuid, raw: true }),
|
||||
this.videoEmbedPrivacyService.getPrivacy({ videoId: video.uuid })
|
||||
])
|
||||
}))
|
||||
.subscribe({
|
||||
next: async ([ video, playerSettings ]) => {
|
||||
await videoEdit.loadFromAPI({ video, playerSettings, loadPrivacy: false })
|
||||
next: async ([ video, playerSettings, embedPrivacy ]) => {
|
||||
await videoEdit.loadFromAPI({ video, playerSettings, embedPrivacy, loadPrivacy: false })
|
||||
|
||||
this.loadingBar.useRef('import-video').complete()
|
||||
|
||||
|
||||
+16
-5
@@ -9,6 +9,8 @@ import { VideoCaptionService } from '@app/shared/shared-main/video-caption/video
|
||||
import { VideoChapterService } from '@app/shared/shared-main/video/video-chapter.service'
|
||||
import { VideoImportService } from '@app/shared/shared-main/video/video-import.service'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { VideoEmbedPrivacyService } from '@app/shared/shared-video/video-embed-privacy.service'
|
||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||
import { UserVideoQuota, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import debug from 'debug'
|
||||
@@ -19,7 +21,6 @@ import { SelectChannelComponent } from '../../../shared/shared-forms/select/sele
|
||||
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||
import { HelpComponent } from '../../../shared/shared-main/buttons/help.component'
|
||||
import { VideoManageContainerComponent } from '../../shared-manage/video-manage-container.component'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
|
||||
const debugLogger = debug('peertube:video-publish')
|
||||
|
||||
@@ -51,6 +52,7 @@ export class VideoImportUrlComponent implements OnInit, AfterViewInit, CanCompon
|
||||
private chapterService = inject(VideoChapterService)
|
||||
private captionService = inject(VideoCaptionService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
private videoEmbedPrivacyService = inject(VideoEmbedPrivacyService)
|
||||
|
||||
readonly userChannels = input.required<SelectChannelItem[]>()
|
||||
readonly userQuota = input.required<UserVideoQuota>()
|
||||
@@ -127,13 +129,22 @@ export class VideoImportUrlComponent implements OnInit, AfterViewInit, CanCompon
|
||||
this.captionService.listCaptions(video.uuid),
|
||||
this.chapterService.getChapters({ videoId: video.uuid }),
|
||||
this.playerSettingsService.getVideoSettings({ videoId: video.uuid, raw: true }),
|
||||
this.videoService.getVideo({ videoId: video.uuid })
|
||||
]).pipe(map(([ { data: captions }, { chapters }, playerSettings, video ]) => ({ captions, chapters, playerSettings, video })))
|
||||
this.videoService.getVideo({ videoId: video.uuid }),
|
||||
this.videoEmbedPrivacyService.getPrivacy({ videoId: video.uuid })
|
||||
]).pipe(
|
||||
map(([ { data: captions }, { chapters }, playerSettings, video, embedPrivacy ]) => ({
|
||||
captions,
|
||||
chapters,
|
||||
playerSettings,
|
||||
video,
|
||||
embedPrivacy
|
||||
}))
|
||||
)
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: async ({ video, playerSettings, captions, chapters }) => {
|
||||
await videoEdit.loadFromAPI({ video, captions, playerSettings, chapters, loadPrivacy: false })
|
||||
next: async ({ video, playerSettings, captions, chapters, embedPrivacy }) => {
|
||||
await videoEdit.loadFromAPI({ video, captions, playerSettings, chapters, embedPrivacy, loadPrivacy: false })
|
||||
|
||||
this.loadingBar.useRef('import-video').complete()
|
||||
|
||||
|
||||
+7
-4
@@ -6,6 +6,7 @@ import { VideoManageController } from '@app/+videos-publish-manage/shared-manage
|
||||
import { AuthService, CanComponentDeactivate, HooksService, Notifier, ServerService } from '@app/core'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { VideoEmbedPrivacyService } from '@app/shared/shared-video/video-embed-privacy.service'
|
||||
import { LiveVideoLatencyMode, PeerTubeProblemDocument, ServerErrorCode, UserVideoQuota, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import debug from 'debug'
|
||||
import { forkJoin, map, switchMap } from 'rxjs'
|
||||
@@ -40,6 +41,7 @@ export class VideoGoLiveComponent implements OnInit, AfterViewInit, CanComponent
|
||||
private manageController = inject(VideoManageController)
|
||||
private route = inject(ActivatedRoute)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
private videoEmbedPrivacyService = inject(VideoEmbedPrivacyService)
|
||||
|
||||
readonly userChannels = input.required<SelectChannelItem[]>()
|
||||
readonly userQuota = input.required<UserVideoQuota>()
|
||||
@@ -104,14 +106,15 @@ export class VideoGoLiveComponent implements OnInit, AfterViewInit, CanComponent
|
||||
switchMap(({ video }) => {
|
||||
return forkJoin([
|
||||
this.liveVideoService.getVideoLive(video.uuid),
|
||||
this.playerSettingsService.getVideoSettings({ videoId: video.uuid, raw: true })
|
||||
]).pipe(map(([ live, playerSettings ]) => ({ live, playerSettings, video })))
|
||||
this.playerSettingsService.getVideoSettings({ videoId: video.uuid, raw: true }),
|
||||
this.videoEmbedPrivacyService.getPrivacy({ videoId: video.uuid })
|
||||
]).pipe(map(([ live, playerSettings, embedPrivacy ]) => ({ live, playerSettings, embedPrivacy, video })))
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: async ({ video: { id, uuid, shortUUID }, live, playerSettings }) => {
|
||||
next: async ({ video: { id, uuid, shortUUID }, live, playerSettings, embedPrivacy }) => {
|
||||
videoEdit.loadAfterPublish({ video: { id, uuid, shortUUID } })
|
||||
await videoEdit.loadFromAPI({ live, playerSettings, loadPrivacy: false })
|
||||
await videoEdit.loadFromAPI({ live, playerSettings, embedPrivacy, loadPrivacy: false })
|
||||
|
||||
debugLogger(`Live published`)
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { VideoStudioService } from '../shared-manage/studio/video-studio.service
|
||||
import { VideoManageController } from '../shared-manage/video-manage-controller.service'
|
||||
import { VideoPublishComponent } from './video-publish.component'
|
||||
import { VideoPublishResolver } from './video-publish.resolver'
|
||||
import { VideoEmbedPrivacyService } from '@app/shared/shared-video/video-embed-privacy.service'
|
||||
|
||||
const debugLogger = debug('peertube:video-publish')
|
||||
|
||||
@@ -49,7 +50,8 @@ export default [
|
||||
LiveVideoService,
|
||||
I18nPrimengCalendarService,
|
||||
VideoUploadService,
|
||||
VideoStudioService
|
||||
VideoStudioService,
|
||||
VideoEmbedPrivacyService
|
||||
],
|
||||
resolve: {
|
||||
resolverData: VideoPublishResolver
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AuthService, AuthUser, CanComponentDeactivate, CanDeactivateGuard, Hook
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { NgbNav, NgbNavContent, NgbNavItem, NgbNavLink, NgbNavLinkBase, NgbNavOutlet } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HTMLServerConfig, UserVideoQuota, VideoConstant, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import { HTMLServerConfig, UserVideoQuota, ConstantLabel, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import { SelectChannelItem } from 'src/types'
|
||||
import { HelpComponent } from '../../shared/shared-main/buttons/help.component'
|
||||
import { ChannelsSetupMessageComponent } from '../../shared/shared-main/channel/channels-setup-message.component'
|
||||
@@ -79,7 +79,7 @@ export class VideoPublishComponent implements OnInit, CanComponentDeactivate {
|
||||
|
||||
userChannels: SelectChannelItem[]
|
||||
userQuota: UserVideoQuota
|
||||
privacies: VideoConstant<VideoPrivacyType>[]
|
||||
privacies: ConstantLabel<VideoPrivacyType>[]
|
||||
|
||||
private publishedIdQuery: string
|
||||
private uploadingQuery: string
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Injectable, inject } from '@angular/core'
|
||||
import { AuthService, ServerService, UserService } from '@app/core'
|
||||
import { listUserChannelsForSelect } from '@app/helpers'
|
||||
import { UserVideoQuota, VideoConstant, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import { UserVideoQuota, ConstantLabel, VideoPrivacyType } from '@peertube/peertube-models'
|
||||
import { forkJoin } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import { SelectChannelItem } from '../../../types'
|
||||
@@ -9,7 +9,7 @@ import { SelectChannelItem } from '../../../types'
|
||||
export type VideoPublishResolverData = {
|
||||
videoChannels: SelectChannelItem[]
|
||||
userQuota: UserVideoQuota
|
||||
privacies: VideoConstant<VideoPrivacyType>[]
|
||||
privacies: ConstantLabel<VideoPrivacyType>[]
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import { FormReactiveService } from '@app/shared/shared-forms/form-reactive.serv
|
||||
import { SelectOptionsComponent } from '@app/shared/shared-forms/select/select-options.component'
|
||||
import { VideoCaptionEdit } from '@app/+videos-publish-manage/shared-manage/common/video-caption-edit.model'
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HTMLServerConfig, VideoConstant } from '@peertube/peertube-models'
|
||||
import { HTMLServerConfig, ConstantLabel } from '@peertube/peertube-models'
|
||||
import { ReactiveFileComponent } from '../../../shared/shared-forms/reactive-file.component'
|
||||
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||
|
||||
@@ -29,7 +29,7 @@ export class VideoCaptionAddModalComponent extends FormReactive implements OnIni
|
||||
|
||||
readonly modal = viewChild<ElementRef>('modal')
|
||||
|
||||
videoCaptionLanguages: VideoConstant<string>[] = []
|
||||
videoCaptionLanguages: ConstantLabel<string>[] = []
|
||||
|
||||
private openedModal: NgbModalRef
|
||||
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ import { VideoCaptionService } from '@app/shared/shared-main/video-caption/video
|
||||
import { EmbedComponent } from '@app/shared/shared-main/video/embed.component'
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { millisecondsToVttTime, sortBy, timeToInt } from '@peertube/peertube-core-utils'
|
||||
import { HTMLServerConfig, VideoConstant } from '@peertube/peertube-models'
|
||||
import { HTMLServerConfig, ConstantLabel } from '@peertube/peertube-models'
|
||||
import { parse } from '@plussub/srt-vtt-parser'
|
||||
import { PeerTubePlayer } from '../../../../standalone/embed-player-api/player'
|
||||
import { ConfirmService, Notifier, ServerService } from '../../../core'
|
||||
@@ -74,7 +74,7 @@ export class VideoCaptionEditModalComponent extends FormReactive implements OnIn
|
||||
|
||||
activeSegment: Segment
|
||||
|
||||
videoCaptionLanguages: VideoConstant<string>[] = []
|
||||
videoCaptionLanguages: ConstantLabel<string>[] = []
|
||||
|
||||
timestampParser = this.webvttToMS.bind(this)
|
||||
timestampFormatter = millisecondsToVttTime
|
||||
|
||||
+2
-3
@@ -1,4 +1,3 @@
|
||||
|
||||
import { Component, OnInit, inject, viewChild } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { VideoCaptionEdit, VideoCaptionWithPathEdit } from '@app/+videos-publish-manage/shared-manage/common/video-caption-edit.model'
|
||||
@@ -6,7 +5,7 @@ import { ServerService } from '@app/core'
|
||||
import { removeElementFromArray } from '@app/helpers'
|
||||
import { AlertComponent } from '@app/shared/shared-main/common/alert.component'
|
||||
import { PTDatePipe } from '@app/shared/shared-main/common/date.pipe'
|
||||
import { HTMLServerConfig, VideoConstant } from '@peertube/peertube-models'
|
||||
import { HTMLServerConfig, ConstantLabel } from '@peertube/peertube-models'
|
||||
import debug from 'debug'
|
||||
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||
import { ButtonComponent } from '../../../shared/shared-main/buttons/button.component'
|
||||
@@ -51,7 +50,7 @@ export class VideoCaptionsComponent implements OnInit {
|
||||
displayTranscriptionInfo: boolean
|
||||
videoEdit: VideoEdit
|
||||
|
||||
videoLanguages: VideoConstant<string>[] = []
|
||||
videoLanguages: ConstantLabel<string>[] = []
|
||||
|
||||
private initialVideoCaptions: string[] = []
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ import {
|
||||
VideoChapter,
|
||||
VideoCreate,
|
||||
VideoDetails,
|
||||
VideoEmbedPrivacy,
|
||||
VideoEmbedPrivacyPolicy,
|
||||
VideoEmbedPrivacyUpdate,
|
||||
VideoImportCreate,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType,
|
||||
@@ -28,6 +31,7 @@ import debug from 'debug'
|
||||
import { Jsonify, SharedUnionFieldsDeep } from 'type-fest'
|
||||
import { VideoCaptionWithPathEdit } from './video-caption-edit.model'
|
||||
import { VideoChaptersEdit } from './video-chapters-edit.model'
|
||||
import { splitAndGetNotEmpty } from '@root-helpers/string'
|
||||
|
||||
const debugLogger = debug('peertube:video-manage:video-edit')
|
||||
|
||||
@@ -69,6 +73,11 @@ type StudioForm = {
|
||||
|
||||
type PlayerSettingsForm = PlayerVideoSettingsUpdate
|
||||
|
||||
type EmbedPrivacyForm = {
|
||||
videoPrivacyEmbedEnableAllowlist?: boolean
|
||||
videoPrivacyEmbedAllowlistDomains?: string
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type LoadFromPublishOptions = Required<Pick<VideoCreate, 'channelId' | 'support'>> & Partial<Pick<VideoCreate, 'name'>> & {
|
||||
@@ -123,6 +132,7 @@ type UpdateFromAPIOptions = {
|
||||
videoPasswords?: string[]
|
||||
videoSource?: VideoSource
|
||||
playerSettings: PlayerVideoSettings
|
||||
embedPrivacy: VideoEmbedPrivacy
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -151,7 +161,8 @@ export class VideoEdit {
|
||||
private live: LiveUpdate
|
||||
private replaceFile: File
|
||||
private studioTasks: VideoStudioTask[] = []
|
||||
private playerSettings: PlayerVideoSettings
|
||||
private playerSettings: PlayerVideoSettingsUpdate
|
||||
private embedPrivacy: VideoEmbedPrivacyUpdate
|
||||
|
||||
private videoImport: Pick<VideoImportCreate, 'magnetUri' | 'torrentfile' | 'targetUrl'>
|
||||
|
||||
@@ -202,7 +213,9 @@ export class VideoEdit {
|
||||
thumbnailfile?: { size: number }
|
||||
|
||||
live?: LiveUpdate
|
||||
playerSettings?: PlayerVideoSettings
|
||||
playerSettings?: PlayerVideoSettingsUpdate
|
||||
|
||||
embedPrivacy?: VideoEmbedPrivacyUpdate
|
||||
|
||||
pluginData?: any
|
||||
pluginDefaults?: Record<string, string | boolean>
|
||||
@@ -307,6 +320,7 @@ export class VideoEdit {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Build a new VideoEdit model based on data coming from the API
|
||||
static async createFromAPI (serverConfig: HTMLServerConfig, options: UpdateFromAPIOptions) {
|
||||
const videoEdit = new VideoEdit(serverConfig)
|
||||
await videoEdit.loadFromAPI(options)
|
||||
@@ -315,13 +329,14 @@ export class VideoEdit {
|
||||
}
|
||||
|
||||
async loadFromAPI (options: UpdateFromAPIOptions & { loadPrivacy?: boolean }) {
|
||||
const { video, videoPasswords, live, chapters, captions, videoSource, playerSettings, loadPrivacy = true } = options
|
||||
const { video, videoPasswords, live, chapters, captions, videoSource, playerSettings, embedPrivacy, loadPrivacy = true } = options
|
||||
|
||||
debugLogger('Load from API', options)
|
||||
|
||||
this.loadVideo({ video, videoPasswords, saveInStore: true, loadPrivacy })
|
||||
this.loadLive(live)
|
||||
this.loadPlayerSettings(playerSettings)
|
||||
this.loadEmbedPrivacy(embedPrivacy)
|
||||
|
||||
if (captions !== undefined) {
|
||||
this.captions = captions
|
||||
@@ -488,6 +503,18 @@ export class VideoEdit {
|
||||
this.saveStore.playerSettings = buildObj()
|
||||
}
|
||||
|
||||
private loadEmbedPrivacy (embedPrivacy: UpdateFromAPIOptions['embedPrivacy']) {
|
||||
const buildObj = () => {
|
||||
return {
|
||||
policy: embedPrivacy.policy.id,
|
||||
domains: embedPrivacy.domains ?? []
|
||||
}
|
||||
}
|
||||
|
||||
this.embedPrivacy = buildObj()
|
||||
this.saveStore.embedPrivacy = buildObj()
|
||||
}
|
||||
|
||||
loadAfterPublish (options: {
|
||||
video: Pick<VideoDetails, 'id' | 'uuid' | 'shortUUID'>
|
||||
}) {
|
||||
@@ -845,16 +872,36 @@ export class VideoEdit {
|
||||
}
|
||||
}
|
||||
|
||||
toPlayerSettingsUpdate (): PlayerVideoSettingsUpdate {
|
||||
if (!this.playerSettings) return undefined
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
loadFromEmbedPrivacyForm (value: EmbedPrivacyForm) {
|
||||
this.embedPrivacy = {
|
||||
policy: value.videoPrivacyEmbedEnableAllowlist
|
||||
? VideoEmbedPrivacyPolicy.ALLOWLIST
|
||||
: VideoEmbedPrivacyPolicy.ALL_ALLOWED,
|
||||
|
||||
domains: splitAndGetNotEmpty(value.videoPrivacyEmbedAllowlistDomains)
|
||||
}
|
||||
}
|
||||
|
||||
toEmbedPrivacyFormPatch (): Required<EmbedPrivacyForm> {
|
||||
if (!this.embedPrivacy) {
|
||||
return {
|
||||
videoPrivacyEmbedEnableAllowlist: false,
|
||||
videoPrivacyEmbedAllowlistDomains: ''
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
theme: this.playerSettings.theme
|
||||
videoPrivacyEmbedEnableAllowlist: this.embedPrivacy.policy === VideoEmbedPrivacyPolicy.ALLOWLIST,
|
||||
videoPrivacyEmbedAllowlistDomains: this.embedPrivacy.domains.join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getVideoSource () {
|
||||
return this.metadata.videoSource
|
||||
}
|
||||
@@ -887,6 +934,10 @@ export class VideoEdit {
|
||||
return this.playerSettings
|
||||
}
|
||||
|
||||
getEmbedPrivacy () {
|
||||
return this.embedPrivacy
|
||||
}
|
||||
|
||||
getStudioTasksSummary () {
|
||||
return this.getStudioTasks().map(t => {
|
||||
if (t.name === 'add-intro') {
|
||||
@@ -1018,6 +1069,21 @@ export class VideoEdit {
|
||||
return changes
|
||||
}
|
||||
|
||||
hasEmbedPrivacyChanges () {
|
||||
if (!this.embedPrivacy) return false
|
||||
if (!this.saveStore.embedPrivacy) return true
|
||||
|
||||
const changes = !this.areSameObjects(this.embedPrivacy, this.saveStore.embedPrivacy)
|
||||
|
||||
debugLogger('Check if embed privacy has changes', {
|
||||
embedPrivacy: this.embedPrivacy,
|
||||
saveEmbedPrivacy: this.saveStore.embedPrivacy,
|
||||
changes
|
||||
})
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
hasPendingChanges () {
|
||||
@@ -1028,7 +1094,8 @@ export class VideoEdit {
|
||||
this.hasChaptersChanges() ||
|
||||
this.hasCommonChanges() ||
|
||||
this.hasPluginDataChanges() ||
|
||||
this.hasPlayerSettingsChanges()
|
||||
this.hasPlayerSettingsChanges() ||
|
||||
this.hasEmbedPrivacyChanges()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+4
@@ -23,6 +23,10 @@ input[type="text"] {
|
||||
@include peertube-input-text(100%);
|
||||
}
|
||||
|
||||
textarea {
|
||||
@include peertube-textarea(100%, 150px);
|
||||
}
|
||||
|
||||
p-calendar {
|
||||
display: block;
|
||||
|
||||
|
||||
+2
-2
@@ -53,8 +53,6 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<my-peertube-checkbox inputName="downloadEnabled" formControlName="downloadEnabled" i18n-labelText labelText="Enable download"></my-peertube-checkbox>
|
||||
|
||||
<div class="form-group" formGroupName="playerSettings">
|
||||
<label i18n for="playerSettingsTheme">Player Theme</label>
|
||||
<div class="form-group-description" i18n>Only used by the web player</div>
|
||||
@@ -62,6 +60,8 @@
|
||||
<my-select-player-theme formControlName="theme" inputId="playerSettingsTheme" mode="video" [channel]="videoChannel">
|
||||
</my-select-player-theme>
|
||||
</div>
|
||||
|
||||
<my-peertube-checkbox inputName="downloadEnabled" formControlName="downloadEnabled" i18n-labelText labelText="Enable download"></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -10,7 +10,7 @@ import {
|
||||
HTMLServerConfig,
|
||||
LiveVideoLatencyMode,
|
||||
LiveVideoLatencyModeType,
|
||||
VideoConstant,
|
||||
ConstantLabel,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType,
|
||||
VideoState
|
||||
@@ -85,7 +85,7 @@ export class VideoLiveSettingsComponent implements OnInit, OnDestroy {
|
||||
calendarDateFormat: string
|
||||
myYearRange: string
|
||||
|
||||
replayPrivacies: VideoConstant<VideoPrivacyType>[] = []
|
||||
replayPrivacies: ConstantLabel<VideoPrivacyType>[] = []
|
||||
|
||||
latencyModes: SelectOptionsItem[] = [
|
||||
{
|
||||
|
||||
+5
-5
@@ -35,7 +35,7 @@ import {
|
||||
HTMLServerConfig,
|
||||
RegisterClientFormFieldOptions,
|
||||
RegisterClientVideoFieldOptions,
|
||||
VideoConstant,
|
||||
ConstantLabel,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType
|
||||
} from '@peertube/peertube-models'
|
||||
@@ -132,10 +132,10 @@ export class VideoMainInfoComponent implements OnInit, OnDestroy {
|
||||
forbidScheduledPublication: boolean
|
||||
hideWaitTranscoding: boolean
|
||||
|
||||
videoPrivacies: VideoConstant<VideoEditPrivacyType>[] = []
|
||||
videoCategories: VideoConstant<number>[] = []
|
||||
videoLicences: VideoConstant<number>[] = []
|
||||
videoLanguages: VideoConstant<string>[] = []
|
||||
videoPrivacies: ConstantLabel<VideoEditPrivacyType>[] = []
|
||||
videoCategories: ConstantLabel<number>[] = []
|
||||
videoLicences: ConstantLabel<number>[] = []
|
||||
videoLanguages: ConstantLabel<string>[] = []
|
||||
|
||||
pluginDataFormGroup: FormGroup
|
||||
|
||||
|
||||
+44
-16
@@ -5,22 +5,8 @@
|
||||
</h2>
|
||||
|
||||
<form [formGroup]="form" id="video-manage-form">
|
||||
|
||||
<div class="form-columns">
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<my-select-radio
|
||||
i18n-label label="Comments policy"
|
||||
[items]="commentPolicies"
|
||||
inputId="commentsPolicy"
|
||||
formControlName="commentsPolicy"
|
||||
>
|
||||
<div class="form-group-description" i18n>
|
||||
You can require comments to be approved depending on <a routerLink="/my-account/auto-tag-policies" target="_blank">your auto-tags policies</a>
|
||||
</div>
|
||||
</my-select-radio>
|
||||
</div>
|
||||
|
||||
<my-peertube-checkbox inputName="nsfw" formControlName="nsfw">
|
||||
<ng-template ptTemplate="label">
|
||||
<ng-container i18n>Your video contains sensitive content</ng-container>
|
||||
@@ -49,11 +35,53 @@
|
||||
<ng-container i18n>Potentially sexually explicit content</ng-container>
|
||||
</ng-template>
|
||||
</my-peertube-checkbox>
|
||||
|
||||
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
|
||||
<div class="form-group">
|
||||
<my-select-radio i18n-label label="Comments policy" [items]="commentPolicies" inputId="commentsPolicy" formControlName="commentsPolicy">
|
||||
<div class="form-group-description" i18n>
|
||||
You can require comments to be approved depending on <a routerLink="/my-account/auto-tag-policies" target="_blank">your auto-tags policies</a>
|
||||
</div>
|
||||
</my-select-radio>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox inputName="videoPrivacyEmbedEnableAllowlist" formControlName="videoPrivacyEmbedEnableAllowlist">
|
||||
<ng-template ptTemplate="label">
|
||||
<ng-container i18n>Restrict embed to the following domains</ng-container>
|
||||
</ng-template>
|
||||
|
||||
<ng-container ngProjectAs="extra">
|
||||
<div class="form-group">
|
||||
<label i18n for="videoPrivacyEmbedAllowlistDomains">Allowed domains</label>
|
||||
<div class="form-group-description">1 domain (without "https://") per line</div>
|
||||
|
||||
<textarea
|
||||
placeholder="example.com"
|
||||
formControlName="videoPrivacyEmbedAllowlistDomains"
|
||||
type="text"
|
||||
id="videoPrivacyEmbedAllowlistDomains"
|
||||
name="videoPrivacyEmbedAllowlistDomains"
|
||||
class="form-control"
|
||||
[ngClass]="{ 'input-error': formErrors['videoPrivacyEmbedAllowlistDomains'] }"
|
||||
></textarea>
|
||||
|
||||
@if (formErrors.videoPrivacyEmbedAllowlistDomains) {
|
||||
<div class="form-error" role="alert">
|
||||
{{ formErrors.videoPrivacyEmbedAllowlistDomains }}
|
||||
|
||||
@if (form.controls['videoPrivacyEmbedAllowlistDomains'].errors.validHosts) {
|
||||
<div>{{ form.controls['videoPrivacyEmbedAllowlistDomains'].errors.validHosts.value }}</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-container>
|
||||
</my-peertube-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
+38
-7
@@ -6,7 +6,7 @@ import { ServerService } from '@app/core'
|
||||
import { BuildFormArgument } from '@app/shared/form-validators/form-validator.model'
|
||||
import { VIDEO_NSFW_SUMMARY_VALIDATOR } from '@app/shared/form-validators/video-validators'
|
||||
import { FormReactiveErrors, FormReactiveService, FormReactiveMessages } from '@app/shared/shared-forms/form-reactive.service'
|
||||
import { HTMLServerConfig, VideoCommentPolicyType, VideoConstant } from '@peertube/peertube-models'
|
||||
import { HTMLServerConfig, VideoCommentPolicyType, ConstantLabel } from '@peertube/peertube-models'
|
||||
import debug from 'debug'
|
||||
import { Subscription } from 'rxjs'
|
||||
import { PeertubeCheckboxComponent } from '../../../shared/shared-forms/peertube-checkbox.component'
|
||||
@@ -14,6 +14,7 @@ import { SelectRadioComponent } from '../../../shared/shared-forms/select/select
|
||||
import { GlobalIconComponent } from '../../../shared/shared-icons/global-icon.component'
|
||||
import { PeerTubeTemplateDirective } from '../../../shared/shared-main/common/peertube-template.directive'
|
||||
import { VideoManageController } from '../video-manage-controller.service'
|
||||
import { UNIQUE_HOSTS_VALIDATOR } from '@app/shared/form-validators/host-validators'
|
||||
|
||||
const debugLogger = debug('peertube:video-manage')
|
||||
|
||||
@@ -25,6 +26,9 @@ type Form = {
|
||||
nsfwSummary: FormControl<string>
|
||||
|
||||
commentPolicies: FormControl<VideoCommentPolicyType>
|
||||
|
||||
videoPrivacyEmbedEnableAllowlist: FormControl<boolean>
|
||||
videoPrivacyEmbedAllowlistDomains: FormControl<string>
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -53,7 +57,7 @@ export class VideoModerationComponent implements OnInit, OnDestroy {
|
||||
formErrors: FormReactiveErrors = {}
|
||||
validationMessages: FormReactiveMessages = {}
|
||||
|
||||
commentPolicies: VideoConstant<VideoCommentPolicyType>[] = []
|
||||
commentPolicies: ConstantLabel<VideoCommentPolicyType>[] = []
|
||||
serverConfig: HTMLServerConfig
|
||||
|
||||
private updatedSub: Subscription
|
||||
@@ -74,13 +78,15 @@ export class VideoModerationComponent implements OnInit, OnDestroy {
|
||||
private buildForm () {
|
||||
const videoEdit = this.manageController.getStore().videoEdit
|
||||
|
||||
const defaultValues = videoEdit.toCommonFormPatch()
|
||||
const defaultValues = { ...videoEdit.toCommonFormPatch(), ...videoEdit.toEmbedPrivacyFormPatch() }
|
||||
const obj: BuildFormArgument = {
|
||||
commentsPolicy: null,
|
||||
nsfw: null,
|
||||
nsfwFlagViolent: null,
|
||||
nsfwFlagSex: null,
|
||||
nsfwSummary: VIDEO_NSFW_SUMMARY_VALIDATOR
|
||||
nsfwSummary: VIDEO_NSFW_SUMMARY_VALIDATOR,
|
||||
videoPrivacyEmbedEnableAllowlist: null,
|
||||
videoPrivacyEmbedAllowlistDomains: UNIQUE_HOSTS_VALIDATOR
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -100,22 +106,29 @@ export class VideoModerationComponent implements OnInit, OnDestroy {
|
||||
debugLogger('Updating form values', formValues)
|
||||
|
||||
videoEdit.loadFromCommonForm(formValues)
|
||||
videoEdit.loadFromEmbedPrivacyForm(formValues)
|
||||
})
|
||||
|
||||
this.formReactiveService.markAllAsDirty(this.form.controls)
|
||||
|
||||
this.updatedSub = this.manageController.getUpdatedObs().subscribe(() => {
|
||||
this.form.patchValue(videoEdit.toCommonFormPatch())
|
||||
this.form.patchValue({ ...videoEdit.toCommonFormPatch(), ...videoEdit.toEmbedPrivacyFormPatch() })
|
||||
})
|
||||
|
||||
this.updateNSFWControls(videoEdit.toCommonFormPatch().nsfw)
|
||||
this.trackNSFWChange()
|
||||
this.updateAllowedDomainsControls(videoEdit.toEmbedPrivacyFormPatch().videoPrivacyEmbedEnableAllowlist)
|
||||
|
||||
this.trackControlsChange()
|
||||
}
|
||||
|
||||
private trackNSFWChange () {
|
||||
private trackControlsChange () {
|
||||
this.form.controls.nsfw
|
||||
.valueChanges
|
||||
.subscribe(newNSFW => this.updateNSFWControls(newNSFW))
|
||||
|
||||
this.form.controls.videoPrivacyEmbedEnableAllowlist
|
||||
.valueChanges
|
||||
.subscribe(newEnableAllowlist => this.updateAllowedDomainsControls(newEnableAllowlist))
|
||||
}
|
||||
|
||||
private updateNSFWControls (nsfw: boolean) {
|
||||
@@ -137,4 +150,22 @@ export class VideoModerationComponent implements OnInit, OnDestroy {
|
||||
control.enable()
|
||||
}
|
||||
}
|
||||
|
||||
private updateAllowedDomainsControls (enableAllowlist: boolean) {
|
||||
const controls = [
|
||||
this.form.controls.videoPrivacyEmbedAllowlistDomains
|
||||
]
|
||||
|
||||
if (!enableAllowlist) {
|
||||
for (const control of controls) {
|
||||
control.disable()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (const control of controls) {
|
||||
control.enable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-15
@@ -10,12 +10,14 @@ import { VideoPasswordService } from '@app/shared/shared-main/video/video-passwo
|
||||
import { VideoService } from '@app/shared/shared-main/video/video.service'
|
||||
import { LiveVideoService } from '@app/shared/shared-video-live/live-video.service'
|
||||
import { PlayerSettingsService } from '@app/shared/shared-video/player-settings.service'
|
||||
import { VideoEmbedPrivacyService } from '@app/shared/shared-video/video-embed-privacy.service'
|
||||
import { LoadingBarService } from '@ngx-loading-bar/core'
|
||||
import {
|
||||
ConstantLabel,
|
||||
HTMLServerConfig,
|
||||
HttpStatusCode,
|
||||
LiveVideo,
|
||||
UserVideoQuota,
|
||||
VideoConstant,
|
||||
VideoPassword,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType,
|
||||
@@ -49,11 +51,12 @@ export class VideoManageController implements OnDestroy {
|
||||
private videoStudio = inject(VideoStudioService)
|
||||
private peertubeRouter = inject(PeerTubeRouterService)
|
||||
private playerSettingsService = inject(PlayerSettingsService)
|
||||
private videoEmbedPrivacyService = inject(VideoEmbedPrivacyService)
|
||||
|
||||
private videoEdit: VideoEdit
|
||||
private userChannels: SelectChannelItem[]
|
||||
private userQuota: UserVideoQuota
|
||||
private privacies: VideoConstant<VideoPrivacyType>[]
|
||||
private privacies: ConstantLabel<VideoPrivacyType>[]
|
||||
|
||||
private manageType: VideoManageType
|
||||
private serverConfig: HTMLServerConfig
|
||||
@@ -138,7 +141,7 @@ export class VideoManageController implements OnDestroy {
|
||||
videoEdit: VideoEdit
|
||||
userChannels: SelectChannelItem[]
|
||||
userQuota: UserVideoQuota
|
||||
privacies: VideoConstant<VideoPrivacyType>[]
|
||||
privacies: ConstantLabel<VideoPrivacyType>[]
|
||||
}) {
|
||||
this.videoEdit = store.videoEdit
|
||||
this.userChannels = store.userChannels
|
||||
@@ -264,6 +267,13 @@ export class VideoManageController implements OnDestroy {
|
||||
|
||||
return this.liveVideoService.updateLive(videoAttributes.uuid, this.videoEdit.toLiveUpdate())
|
||||
}),
|
||||
switchMap(() => {
|
||||
if (!this.videoEdit.hasEmbedPrivacyChanges()) return of(true)
|
||||
|
||||
debugLogger('Update embed privacy')
|
||||
|
||||
return this.videoEmbedPrivacyService.updatePrivacy({ videoId: videoAttributes.uuid, settings: this.videoEdit.getEmbedPrivacy() })
|
||||
}),
|
||||
switchMap(() => {
|
||||
debugLogger('Update video')
|
||||
|
||||
@@ -276,38 +286,42 @@ export class VideoManageController implements OnDestroy {
|
||||
|
||||
return this.videoStudio.editVideo(videoAttributes.uuid, this.videoEdit.getStudioTasks())
|
||||
.pipe(tap(() => this.videoEdit.resetStudio()))
|
||||
}),
|
||||
})
|
||||
).pipe( // https://stackoverflow.com/questions/69260751/angular-observable-pipe-limit
|
||||
switchMap(() => {
|
||||
return forkJoin([
|
||||
this.videoService.getVideo({ videoId: videoAttributes.uuid }),
|
||||
return forkJoin({
|
||||
video: this.videoService.getVideo({ videoId: videoAttributes.uuid }),
|
||||
|
||||
this.videoEdit.getVideoAttributes().privacy === VideoPrivacy.PASSWORD_PROTECTED
|
||||
videoPasswords: this.videoEdit.getVideoAttributes().privacy === VideoPrivacy.PASSWORD_PROTECTED
|
||||
? this.videoPasswordService.getVideoPasswords({ videoUUID: videoAttributes.uuid })
|
||||
: of([] as VideoPassword[]),
|
||||
|
||||
isLive
|
||||
live: isLive
|
||||
? this.liveVideoService.getVideoLive(videoAttributes.uuid)
|
||||
: of(undefined),
|
||||
: of(undefined as LiveVideo),
|
||||
|
||||
!isLive
|
||||
chaptersRes: !isLive
|
||||
? this.videoChapterService.getChapters({ videoId: videoAttributes.uuid })
|
||||
: of(undefined),
|
||||
|
||||
!isLive
|
||||
captionsRes: !isLive
|
||||
? this.videoCaptionService.listCaptions(videoAttributes.uuid)
|
||||
: of(undefined),
|
||||
|
||||
this.playerSettingsService.getVideoSettings({ videoId: videoAttributes.uuid, raw: true })
|
||||
])
|
||||
playerSettings: this.playerSettingsService.getVideoSettings({ videoId: videoAttributes.uuid, raw: true }),
|
||||
|
||||
embedPrivacy: this.videoEmbedPrivacyService.getPrivacy({ videoId: videoAttributes.uuid })
|
||||
})
|
||||
}),
|
||||
switchMap(([ video, videoPasswords, live, chaptersRes, captionsRes, playerSettings ]) => {
|
||||
switchMap(({ video, videoPasswords, live, chaptersRes, captionsRes, playerSettings, embedPrivacy }) => {
|
||||
return this.videoEdit.loadFromAPI({
|
||||
video,
|
||||
videoPasswords: videoPasswords.map(p => p.password),
|
||||
live,
|
||||
chapters: chaptersRes?.chapters,
|
||||
captions: captionsRes?.data,
|
||||
playerSettings
|
||||
playerSettings,
|
||||
embedPrivacy
|
||||
})
|
||||
}),
|
||||
first(), // To complete
|
||||
|
||||
@@ -57,13 +57,13 @@ export class AuthUser extends User implements ServerMyUserModel {
|
||||
return this.videoChannelCollaborations.length !== 0
|
||||
}
|
||||
|
||||
isEditorOfChannel (channel: Pick<VideoChannel, 'id'>) {
|
||||
isEditorOfChannel (channel?: Pick<VideoChannel, 'id'>) {
|
||||
if (!channel) return false
|
||||
|
||||
return this.videoChannelCollaborations.some(c => c.id === channel.id)
|
||||
}
|
||||
|
||||
isOwnerOfChannel (channel: Pick<VideoChannel, 'id'>) {
|
||||
isOwnerOfChannel (channel?: Pick<VideoChannel, 'id'>) {
|
||||
if (!channel) return true
|
||||
|
||||
return this.videoChannels.some(c => c.id === channel.id)
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
ServerConfig,
|
||||
ServerStats,
|
||||
VideoCommentPolicy,
|
||||
VideoConstant,
|
||||
ConstantLabel,
|
||||
VideoLicenceType,
|
||||
VideoPlaylistPrivacyType,
|
||||
VideoPrivacyType
|
||||
@@ -31,11 +31,11 @@ export class ServerService {
|
||||
configReloaded = new Subject<ServerConfig>()
|
||||
|
||||
private localeObservable: Observable<any>
|
||||
private videoLicensesObservable: Observable<VideoConstant<VideoLicenceType>[]>
|
||||
private videoCategoriesObservable: Observable<VideoConstant<number>[]>
|
||||
private videoPrivaciesObservable: Observable<VideoConstant<VideoPrivacyType>[]>
|
||||
private videoPlaylistPrivaciesObservable: Observable<VideoConstant<VideoPlaylistPrivacyType>[]>
|
||||
private videoLanguagesObservable: Observable<VideoConstant<string>[]>
|
||||
private videoLicensesObservable: Observable<ConstantLabel<VideoLicenceType>[]>
|
||||
private videoCategoriesObservable: Observable<ConstantLabel<number>[]>
|
||||
private videoPrivaciesObservable: Observable<ConstantLabel<VideoPrivacyType>[]>
|
||||
private videoPlaylistPrivaciesObservable: Observable<ConstantLabel<VideoPlaylistPrivacyType>[]>
|
||||
private videoLanguagesObservable: Observable<ConstantLabel<string>[]>
|
||||
private configObservable: Observable<ServerConfig>
|
||||
|
||||
private configLoaded = false
|
||||
@@ -196,7 +196,7 @@ export class ServerService {
|
||||
.pipe(map(data => ({ data, translations })))
|
||||
}),
|
||||
map(({ data, translations }) => {
|
||||
const hashToPopulate: VideoConstant<T>[] = Object.keys(data)
|
||||
const hashToPopulate: ConstantLabel<T>[] = Object.keys(data)
|
||||
.map(dataKey => {
|
||||
const label = data[dataKey]
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ export function getBackendUrl () {
|
||||
return environment.apiUrl || environment.originServerUrl || window.location.origin
|
||||
}
|
||||
|
||||
export function getEmbedUrl () {
|
||||
return environment.embedUrl || getOriginUrl()
|
||||
}
|
||||
|
||||
export function getBackendHost () {
|
||||
return new URL(getBackendUrl()).host
|
||||
}
|
||||
|
||||
@@ -70,8 +70,8 @@ export const UNIQUE_HOSTS_VALIDATOR: BuildFormValidator = {
|
||||
VALIDATORS: [ Validators.required, validHosts, unique ],
|
||||
MESSAGES: {
|
||||
required: $localize`Domain is required.`,
|
||||
validHosts: $localize`Hosts entered are invalid.`,
|
||||
unique: $localize`Hosts entered contain duplicates.`
|
||||
validHosts: $localize`Domains entered are invalid.`,
|
||||
unique: $localize`Domains entered contain duplicates.`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export const UNIQUE_HOSTS_OR_HANDLE_VALIDATOR: BuildFormValidator = {
|
||||
VALIDATORS: [ Validators.required, validHostsOrHandles, unique ],
|
||||
MESSAGES: {
|
||||
required: $localize`Domain is required.`,
|
||||
validHostsOrHandles: $localize`Hosts or handles are invalid.`,
|
||||
unique: $localize`Hosts or handles contain duplicates.`
|
||||
validHostsOrHandles: $localize`Domains or handles are invalid.`,
|
||||
unique: $localize`Domains or handles contain duplicates.`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Account } from '@app/shared/shared-main/account/account.model'
|
||||
import { VideoChannel } from '@app/shared/shared-main/channel/video-channel.model'
|
||||
import {
|
||||
ConstantLabel,
|
||||
VideoCommentPolicyType,
|
||||
VideoConstant,
|
||||
VideoDetails as VideoDetailsServerModel,
|
||||
VideoEmbedPrivacyPolicy,
|
||||
VideoEmbedPrivacyPolicyType,
|
||||
VideoFile,
|
||||
VideoStateType,
|
||||
VideoStreamingPlaylist,
|
||||
@@ -19,7 +21,7 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
||||
tags: string[]
|
||||
downloadEnabled: boolean
|
||||
|
||||
commentsPolicy: VideoConstant<VideoCommentPolicyType>
|
||||
commentsPolicy: ConstantLabel<VideoCommentPolicyType>
|
||||
|
||||
likesPercent: number
|
||||
dislikesPercent: number
|
||||
@@ -28,11 +30,13 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
||||
|
||||
inputFileUpdatedAt: Date | string
|
||||
|
||||
embedPrivacyPolicy: ConstantLabel<VideoEmbedPrivacyPolicyType>
|
||||
|
||||
// These fields are not optional
|
||||
declare files: VideoFile[]
|
||||
declare streamingPlaylists: VideoStreamingPlaylist[]
|
||||
declare waitTranscoding: boolean
|
||||
declare state: VideoConstant<VideoStateType>
|
||||
declare state: ConstantLabel<VideoStateType>
|
||||
|
||||
constructor (hash: VideoDetailsServerModel, translations = {}) {
|
||||
super(hash, translations)
|
||||
@@ -48,6 +52,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
||||
|
||||
this.trackerUrls = hash.trackerUrls
|
||||
|
||||
this.embedPrivacyPolicy = hash.embedPrivacyPolicy
|
||||
|
||||
this.buildLikeAndDislikePercents()
|
||||
}
|
||||
|
||||
@@ -63,4 +69,8 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
|
||||
hasHlsPlaylist () {
|
||||
return !!this.getHlsPlaylist()
|
||||
}
|
||||
|
||||
hasEmbedRestrictions () {
|
||||
return this.embedPrivacyPolicy.id !== VideoEmbedPrivacyPolicy.ALL_ALLOWED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { AuthUser } from '@app/core'
|
||||
import { User } from '@app/core/users/user.model'
|
||||
import { durationToString, getOriginUrl } from '@app/helpers'
|
||||
import { durationToString, getEmbedUrl } from '@app/helpers'
|
||||
import { Actor } from '@app/shared/shared-main/account/actor.model'
|
||||
import { buildVideoWatchPath, getAllFiles, peertubeTranslate } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
ActorImage,
|
||||
ConstantLabel,
|
||||
HTMLServerConfig,
|
||||
Thumbnail,
|
||||
UserRight,
|
||||
VideoConstant,
|
||||
VideoFile,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType,
|
||||
@@ -31,10 +31,10 @@ export class Video implements VideoServerModel {
|
||||
publishedAt: Date
|
||||
originallyPublishedAt: Date | string
|
||||
|
||||
category: VideoConstant<number>
|
||||
licence: VideoConstant<number>
|
||||
language: VideoConstant<string>
|
||||
privacy: VideoConstant<VideoPrivacyType>
|
||||
category: ConstantLabel<number>
|
||||
licence: ConstantLabel<number>
|
||||
language: ConstantLabel<string>
|
||||
privacy: ConstantLabel<VideoPrivacyType>
|
||||
|
||||
truncatedDescription: string
|
||||
description: string
|
||||
@@ -81,7 +81,7 @@ export class Video implements VideoServerModel {
|
||||
originInstanceHost: string
|
||||
|
||||
waitTranscoding?: boolean
|
||||
state?: VideoConstant<VideoStateType>
|
||||
state?: ConstantLabel<VideoStateType>
|
||||
scheduledUpdate?: VideoScheduleUpdate
|
||||
|
||||
blacklisted?: boolean
|
||||
@@ -168,7 +168,7 @@ export class Video implements VideoServerModel {
|
||||
this.name = hash.name
|
||||
|
||||
this.embedPath = hash.embedPath
|
||||
this.embedUrl = hash.embedUrl || (getOriginUrl() + hash.embedPath)
|
||||
this.embedUrl = hash.embedUrl || (getEmbedUrl() + hash.embedPath)
|
||||
|
||||
this.url = hash.url
|
||||
|
||||
@@ -255,7 +255,11 @@ export class Video implements VideoServerModel {
|
||||
}
|
||||
|
||||
isUpdatableBy (user: AuthUser) {
|
||||
return user && this.isLocal === true && (user.isEditorOfChannel(this.channel) || user.hasRight(UserRight.UPDATE_ANY_VIDEO))
|
||||
return user && this.isLocal === true && (
|
||||
user.isOwnerOfChannel(this.channel) ||
|
||||
user.isEditorOfChannel(this.channel) ||
|
||||
user.hasRight(UserRight.UPDATE_ANY_VIDEO)
|
||||
)
|
||||
}
|
||||
|
||||
isStudioEditableBy (options: {
|
||||
@@ -268,23 +272,15 @@ export class Video implements VideoServerModel {
|
||||
}
|
||||
|
||||
isRemovableBy (user: AuthUser) {
|
||||
return user && this.isLocal === true && (user.isEditorOfChannel(this.channel) || user.hasRight(UserRight.REMOVE_ANY_VIDEO))
|
||||
return user && this.isLocal === true && (
|
||||
user.isOwnerOfChannel(this.channel) ||
|
||||
user.isEditorOfChannel(this.channel) ||
|
||||
user.hasRight(UserRight.REMOVE_ANY_VIDEO)
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
isOwner (user: AuthUser) {
|
||||
return user && this.isLocal === true && this.account.name === user.username
|
||||
}
|
||||
|
||||
hasSeeAllVideosRight (user: AuthUser) {
|
||||
return user?.hasRight(UserRight.SEE_ALL_VIDEOS)
|
||||
}
|
||||
|
||||
isOwnerOrHasSeeAllVideosRight (user: AuthUser) {
|
||||
return this.isOwner(user) || this.hasSeeAllVideosRight(user)
|
||||
}
|
||||
|
||||
canRemoveOneFile (user: AuthUser) {
|
||||
return this.isLocal &&
|
||||
user && user.hasRight(UserRight.MANAGE_VIDEO_FILES) &&
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
UserVideoRateType,
|
||||
UserVideoRateUpdate,
|
||||
VideoChannel as VideoChannelServerModel,
|
||||
VideoConstant,
|
||||
ConstantLabel,
|
||||
VideoDetails as VideoDetailsServerModel,
|
||||
VideoFile,
|
||||
VideoFileMetadata,
|
||||
@@ -534,7 +534,7 @@ export class VideoService {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
explainedPrivacyLabels (serverPrivacies: VideoConstant<VideoPrivacyType>[], defaultPrivacyId: VideoPrivacyType = VideoPrivacy.PUBLIC) {
|
||||
explainedPrivacyLabels (serverPrivacies: ConstantLabel<VideoPrivacyType>[], defaultPrivacyId: VideoPrivacyType = VideoPrivacy.PUBLIC) {
|
||||
const descriptions = {
|
||||
[VideoPrivacy.PRIVATE]: $localize`Only I can see this video`,
|
||||
[VideoPrivacy.UNLISTED]: $localize`Only shareable via a private link`,
|
||||
@@ -557,7 +557,7 @@ export class VideoService {
|
||||
}
|
||||
}
|
||||
|
||||
explainedLicenceLabels (serverLicences: VideoConstant<VideoLicenceType>[]) {
|
||||
explainedLicenceLabels (serverLicences: ConstantLabel<VideoLicenceType>[]) {
|
||||
const descriptions = {
|
||||
[VideoLicence['CC-BY']]: $localize`CC-BY`,
|
||||
[VideoLicence['CC-BY-SA']]: $localize`CC-BY-SA`,
|
||||
@@ -599,7 +599,7 @@ export class VideoService {
|
||||
return $localize`This video contains sensitive content: ${flags.join(' - ')}`
|
||||
}
|
||||
|
||||
getMostPrivatePrivacy (serverPrivacies: VideoConstant<VideoPrivacyType>[]) {
|
||||
getMostPrivatePrivacy (serverPrivacies: ConstantLabel<VideoPrivacyType>[]) {
|
||||
// We do not add a password as this requires additional configuration.
|
||||
const order = [
|
||||
VideoPrivacy.PRIVATE,
|
||||
@@ -611,7 +611,7 @@ export class VideoService {
|
||||
return this.getPrivacyFromOrder(serverPrivacies, order)
|
||||
}
|
||||
|
||||
private getPrivacyFromOrder (serverPrivacies: VideoConstant<VideoPrivacyType>[], order: VideoPrivacyType[]) {
|
||||
private getPrivacyFromOrder (serverPrivacies: ConstantLabel<VideoPrivacyType>[], order: VideoPrivacyType[]) {
|
||||
for (const privacy of order) {
|
||||
if (serverPrivacies.find(p => p.id === privacy)) {
|
||||
return privacy
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="modal-body">
|
||||
<form novalidate [formGroup]="form" (ngSubmit)="submit()">
|
||||
<div class="form-group">
|
||||
<label i18n for="hosts">1 host (without "http://") per line</label>
|
||||
<label i18n for="hosts">1 domain (without "http://") per line</label>
|
||||
|
||||
<textarea
|
||||
[placeholder]="placeholder()" formControlName="hosts" type="text" id="hosts" name="hosts"
|
||||
|
||||
+8
-15
@@ -2,10 +2,6 @@
|
||||
@use "_mixins" as *;
|
||||
@use "_form-mixins" as *;
|
||||
|
||||
my-timestamp-input {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
my-input-text {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -35,6 +31,14 @@ my-input-text {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
my-timestamp-input {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
margin-top: 30px;
|
||||
|
||||
@@ -66,14 +70,3 @@ my-input-text {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.alert-private {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<div class="playlist">
|
||||
@if (video()) {
|
||||
<h5 i18n class="text-center mb-4">Share the playlist</h5>
|
||||
}
|
||||
|
||||
@if (isPrivatePlaylist()) {
|
||||
<my-alert type="primary">
|
||||
<div i18n>This playlist is private so you won't be able to share it with external users</div>
|
||||
|
||||
<a
|
||||
i18n
|
||||
class="peertube-button-link primary-button"
|
||||
[routerLink]="[ '/my-library/video-playlists/update', playlist().shortUUID ]"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Update playlist privacy</a>
|
||||
</my-alert>
|
||||
}
|
||||
|
||||
<div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activePlaylistId">
|
||||
<ng-container ngbNavItem="url">
|
||||
<a ngbNavLink i18n>URL</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="nav-content">
|
||||
<my-input-text
|
||||
inputId="playlist-url"
|
||||
i18n-ariaLabel
|
||||
ariaLabel="Playlist URL"
|
||||
[value]="playlistUrl"
|
||||
[withToggle]="false"
|
||||
[withCopy]="true"
|
||||
[show]="true"
|
||||
[readonly]="true"
|
||||
></my-input-text>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="qrcode">
|
||||
<a ngbNavLink i18n>QR-Code</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="nav-content">
|
||||
<qrcode [qrdata]="playlistUrl" [width]="256" level="Q"></qrcode>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="embed">
|
||||
<a ngbNavLink i18n>Embed</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="nav-content">
|
||||
<my-input-text
|
||||
inputId="playlist-embed-url"
|
||||
i18n-ariaLabel
|
||||
ariaLabel="Playlist embed URL"
|
||||
[value]="customizations().onlyEmbedUrl ? playlistEmbedUrl : playlistEmbedHTML"
|
||||
(change)="onUpdate()"
|
||||
[withToggle]="false"
|
||||
[withCopy]="true"
|
||||
[show]="true"
|
||||
[readonly]="true"
|
||||
></my-input-text>
|
||||
|
||||
@if (notSecure()) {
|
||||
<my-alert i18n type="warning" class="mt-3">
|
||||
The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS
|
||||
websites).
|
||||
</my-alert>
|
||||
}
|
||||
|
||||
<div class="embed" [innerHTML]="playlistEmbedSafeHTML"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div [ngbNavOutlet]="nav"></div>
|
||||
|
||||
<div class="filters">
|
||||
@if (video()) {
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="includeVideoInPlaylist"
|
||||
[(ngModel)]="customizations().includeVideoInPlaylist"
|
||||
i18n-labelText
|
||||
labelText="Share the playlist at this video position"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isInEmbedTab()) {
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="onlyEmbedUrl"
|
||||
[(ngModel)]="customizations().onlyEmbedUrl"
|
||||
i18n-labelText
|
||||
labelText="Only display embed URL"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="responsive"
|
||||
[(ngModel)]="customizations().responsive"
|
||||
i18n-labelText
|
||||
labelText="Responsive embed"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
}
|
||||
|
||||
<my-plugin-placeholder pluginId="share-modal-playlist-settings"></my-plugin-placeholder>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Component, inject, input, OnInit } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { HooksService } from '@app/core'
|
||||
import { NgbNav, NgbNavContent, NgbNavItem, NgbNavLink, NgbNavLinkBase, NgbNavOutlet } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { buildPlaylistLink, decoratePlaylistLink } from '@peertube/peertube-core-utils'
|
||||
import { VideoPlaylistPrivacy } from '@peertube/peertube-models'
|
||||
import { buildVideoOrPlaylistEmbed } from '@root-helpers/video'
|
||||
import { QRCodeComponent } from 'angularx-qrcode'
|
||||
import { InputTextComponent } from '../shared-forms/input-text.component'
|
||||
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
|
||||
import { AlertComponent } from '../shared-main/common/alert.component'
|
||||
import { PluginPlaceholderComponent } from '../shared-main/plugins/plugin-placeholder.component'
|
||||
import { VideoDetails } from '../shared-main/video/video-details.model'
|
||||
import { VideoPlaylist } from '../shared-video-playlist/video-playlist.model'
|
||||
import { Customizations, TabId } from './video-share.model'
|
||||
|
||||
@Component({
|
||||
selector: 'my-share-playlist',
|
||||
templateUrl: './share-playlist.component.html',
|
||||
styleUrls: [ './share-common.component.scss' ],
|
||||
imports: [
|
||||
RouterLink,
|
||||
NgbNav,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
NgbNavLinkBase,
|
||||
NgbNavContent,
|
||||
NgbNavOutlet,
|
||||
InputTextComponent,
|
||||
QRCodeComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
FormsModule,
|
||||
PluginPlaceholderComponent,
|
||||
AlertComponent
|
||||
]
|
||||
})
|
||||
export class SharePlaylistComponent implements OnInit {
|
||||
private sanitizer = inject(DomSanitizer)
|
||||
private hooks = inject(HooksService)
|
||||
|
||||
readonly playlist = input<VideoPlaylist>()
|
||||
readonly playlistPosition = input<number>()
|
||||
readonly video = input<VideoDetails>()
|
||||
readonly customizations = input<Customizations>()
|
||||
|
||||
activePlaylistId: TabId = 'url'
|
||||
|
||||
playlistUrl: string
|
||||
playlistEmbedUrl: string
|
||||
playlistEmbedHTML: string
|
||||
playlistEmbedSafeHTML: SafeHtml
|
||||
|
||||
ngOnInit () {
|
||||
this.onUpdate()
|
||||
}
|
||||
|
||||
async onUpdate () {
|
||||
const playlist = this.playlist()
|
||||
const customizations = this.customizations()
|
||||
|
||||
if (!playlist || !customizations) return
|
||||
|
||||
this.playlistUrl = await this.getPlaylistUrl()
|
||||
this.playlistEmbedUrl = await this.getPlaylistEmbedUrl()
|
||||
this.playlistEmbedHTML = await this.getPlaylistEmbedCode({ responsive: customizations.responsive })
|
||||
this.playlistEmbedSafeHTML = this.sanitizer.bypassSecurityTrustHtml(await this.getPlaylistEmbedCode({ responsive: false }))
|
||||
}
|
||||
|
||||
notSecure () {
|
||||
return window.location.protocol === 'http:'
|
||||
}
|
||||
|
||||
isInEmbedTab () {
|
||||
return this.activePlaylistId === 'embed'
|
||||
}
|
||||
|
||||
isPrivatePlaylist () {
|
||||
return this.playlist().privacy.id === VideoPlaylistPrivacy.PRIVATE
|
||||
}
|
||||
|
||||
private getPlaylistUrl () {
|
||||
const url = buildPlaylistLink(this.playlist())
|
||||
|
||||
return this.hooks.wrapFun(
|
||||
decoratePlaylistLink,
|
||||
{ url, ...this.getPlaylistOptions() },
|
||||
'video-watch',
|
||||
'filter:share.video-playlist-url.build.params',
|
||||
'filter:share.video-playlist-url.build.result'
|
||||
)
|
||||
}
|
||||
|
||||
private getPlaylistEmbedUrl () {
|
||||
return this.hooks.wrapFun(
|
||||
decoratePlaylistLink,
|
||||
{ url: this.playlist().embedUrl, ...this.getPlaylistOptions() },
|
||||
'video-watch',
|
||||
'filter:share.video-playlist-embed-url.build.params',
|
||||
'filter:share.video-playlist-embed-url.build.result'
|
||||
)
|
||||
}
|
||||
|
||||
private async getPlaylistEmbedCode (options: { responsive: boolean }) {
|
||||
const { responsive } = options
|
||||
|
||||
return this.hooks.wrapFun(
|
||||
buildVideoOrPlaylistEmbed,
|
||||
{
|
||||
embedUrl: await this.getPlaylistEmbedUrl(),
|
||||
embedTitle: this.playlist().displayName,
|
||||
responsive,
|
||||
aspectRatio: this.video()?.aspectRatio
|
||||
},
|
||||
'video-watch',
|
||||
'filter:share.video-playlist-embed-code.build.params',
|
||||
'filter:share.video-playlist-embed-code.build.result'
|
||||
)
|
||||
}
|
||||
|
||||
private getPlaylistOptions (baseUrl?: string) {
|
||||
const customizations = this.customizations()
|
||||
|
||||
if (!customizations) return { baseUrl }
|
||||
|
||||
const playlistPosition = this.playlistPosition()
|
||||
|
||||
return {
|
||||
baseUrl,
|
||||
playlistPosition: playlistPosition && customizations.includeVideoInPlaylist
|
||||
? playlistPosition
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
<div class="video">
|
||||
@if (playlist()) {
|
||||
<h5 i18n class="text-center mb-4">Share the video</h5>
|
||||
}
|
||||
|
||||
@if (isPrivateVideo()) {
|
||||
<my-alert type="primary">
|
||||
<div i18n>This video is private so you won't be able to share it with external users</div>
|
||||
|
||||
<a
|
||||
i18n
|
||||
class="peertube-button-link secondary-button mt-3"
|
||||
[routerLink]="[ '/videos/', 'manage', video().shortUUID ]"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Update video privacy</a>
|
||||
</my-alert>
|
||||
} @else if (isPasswordProtectedVideo()) {
|
||||
<my-alert i18n type="primary">
|
||||
This video is password protected, please note that recipients will require the corresponding password to access the content.
|
||||
</my-alert>
|
||||
} @else if (video().hasEmbedRestrictions() && isInEmbedTab()) {
|
||||
<my-alert type="primary">
|
||||
<div i18n>
|
||||
This video has embed restrictions and may not be shareable on external websites depending on the chosen restrictions.
|
||||
</div>
|
||||
|
||||
<a
|
||||
i18n
|
||||
class="peertube-button-link secondary-button mt-3"
|
||||
[routerLink]="[ '/videos/', 'manage', video().shortUUID, 'moderation' ]"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Update embed privacy</a>
|
||||
</my-alert>
|
||||
}
|
||||
|
||||
<div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activeVideoId">
|
||||
<ng-container ngbNavItem="url">
|
||||
<a ngbNavLink i18n>URL</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="nav-content">
|
||||
<my-input-text
|
||||
inputId="video-url"
|
||||
i18n-ariaLabel
|
||||
ariaLabel="Video URL"
|
||||
[value]="videoUrl"
|
||||
[withToggle]="false"
|
||||
[withCopy]="true"
|
||||
[show]="true"
|
||||
[readonly]="true"
|
||||
></my-input-text>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="qrcode">
|
||||
<a ngbNavLink i18n>QR-Code</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="nav-content">
|
||||
<qrcode [qrdata]="videoUrl" [width]="256" level="Q"></qrcode>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="embed">
|
||||
<a ngbNavLink i18n>Embed</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="nav-content">
|
||||
<my-input-text
|
||||
inputId="video-embed-url"
|
||||
i18n-ariaLabel
|
||||
ariaLabel="Video embed URL"
|
||||
[value]="customizations().onlyEmbedUrl ? videoEmbedUrl : videoEmbedHTML"
|
||||
(ngModelChange)="onUpdate()"
|
||||
[withToggle]="false"
|
||||
[withCopy]="true"
|
||||
[show]="true"
|
||||
[readonly]="true"
|
||||
></my-input-text>
|
||||
|
||||
@if (notSecure()) {
|
||||
<my-alert i18n type="warning" class="mt-3">
|
||||
The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS
|
||||
websites).
|
||||
</my-alert>
|
||||
}
|
||||
|
||||
<div class="embed" [innerHTML]="videoEmbedSafeHTML"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div [ngbNavOutlet]="nav"></div>
|
||||
|
||||
<div class="filters">
|
||||
@if (!video().isLive) {
|
||||
<div class="form-group start-at">
|
||||
<my-peertube-checkbox inputName="startAt" [(ngModel)]="customizations().startAtCheckbox" i18n-labelText labelText="Start at"></my-peertube-checkbox>
|
||||
<my-timestamp-input
|
||||
[timestamp]="customizations().startAt"
|
||||
[maxTimestamp]="video().duration"
|
||||
[disabled]="!customizations().startAtCheckbox"
|
||||
[(ngModel)]="customizations().startAt"
|
||||
>
|
||||
</my-timestamp-input>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (videoCaptions().length !== 0) {
|
||||
<div class="form-group video-caption-block">
|
||||
<my-peertube-checkbox
|
||||
inputName="subtitleCheckbox"
|
||||
[(ngModel)]="customizations().subtitleCheckbox"
|
||||
i18n-labelText
|
||||
labelText="Auto select subtitle"
|
||||
></my-peertube-checkbox>
|
||||
|
||||
<div class="peertube-select-container">
|
||||
<select [(ngModel)]="customizations().subtitle" [disabled]="!customizations().subtitleCheckbox" class="form-control">
|
||||
@for (caption of videoCaptions(); track caption) {
|
||||
<option [value]="caption.language.id">{{ caption.language.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isInEmbedTab()) {
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="onlyEmbedUrl"
|
||||
[(ngModel)]="customizations().onlyEmbedUrl"
|
||||
i18n-labelText
|
||||
labelText="Only display embed URL"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="responsive"
|
||||
[(ngModel)]="customizations().responsive"
|
||||
i18n-labelText
|
||||
labelText="Responsive embed"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
}
|
||||
|
||||
<my-plugin-placeholder pluginId="share-modal-video-settings"></my-plugin-placeholder>
|
||||
|
||||
<div class="advanced-filters" [ngbCollapse]="isAdvancedCustomizationCollapsed" [animation]="true">
|
||||
@if (!video().isLive) {
|
||||
<div class="form-group stop-at">
|
||||
<my-peertube-checkbox inputName="stopAt" [(ngModel)]="customizations().stopAtCheckbox" i18n-labelText labelText="Stop at"></my-peertube-checkbox>
|
||||
<my-timestamp-input
|
||||
[timestamp]="customizations().stopAt"
|
||||
[maxTimestamp]="video().duration"
|
||||
[disabled]="!customizations().stopAtCheckbox"
|
||||
[(ngModel)]="customizations().stopAt"
|
||||
>
|
||||
</my-timestamp-input>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox inputName="autoplay" [(ngModel)]="customizations().autoplay" i18n-labelText labelText="Autoplay"></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox inputName="muted" [(ngModel)]="customizations().muted" i18n-labelText labelText="Muted"></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
@if (!video().isLive) {
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox inputName="loop" [(ngModel)]="customizations().loop" i18n-labelText labelText="Loop"></my-peertube-checkbox>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!isLocalVideo() && !isInEmbedTab()) {
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="originUrl"
|
||||
[(ngModel)]="customizations().originUrl"
|
||||
i18n-labelText
|
||||
labelText="Use origin instance URL"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isInEmbedTab()) {
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox inputName="title" [(ngModel)]="customizations().title" i18n-labelText labelText="Display video title"></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox inputName="embedP2P" [(ngModel)]="customizations().embedP2P" i18n-labelText labelText="P2P"></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="warningTitle"
|
||||
[(ngModel)]="customizations().warningTitle"
|
||||
i18n-labelText
|
||||
labelText="Display privacy warning"
|
||||
[disabled]="!customizations().embedP2P"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="controlBar"
|
||||
[(ngModel)]="customizations().controlBar"
|
||||
i18n-labelText
|
||||
labelText="Display player control bar"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="peertubeLink"
|
||||
[(ngModel)]="customizations().peertubeLink"
|
||||
i18n-labelText
|
||||
labelText="Display PeerTube button link"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="border-0 p-0 mt-4 mx-auto fw-semibold d-block"
|
||||
(click)="isAdvancedCustomizationCollapsed = !isAdvancedCustomizationCollapsed"
|
||||
[attr.aria-expanded]="!isAdvancedCustomizationCollapsed"
|
||||
aria-controls="collapseBasic"
|
||||
>
|
||||
@if (isAdvancedCustomizationCollapsed) {
|
||||
<span class="chevron-down"></span>
|
||||
<ng-container i18n>More customization</ng-container>
|
||||
} @else {
|
||||
<span class="chevron-up"></span>
|
||||
<ng-container i18n>Less customization</ng-container>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,170 @@
|
||||
import { Component, inject, input, OnInit } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { AuthService, HooksService, ServerService } from '@app/core'
|
||||
import { NgbCollapse, NgbNav, NgbNavContent, NgbNavItem, NgbNavLink, NgbNavLinkBase, NgbNavOutlet } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { buildVideoLink, decorateVideoLink } from '@peertube/peertube-core-utils'
|
||||
import { VideoCaption, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { buildVideoOrPlaylistEmbed } from '@root-helpers/video'
|
||||
import { QRCodeComponent } from 'angularx-qrcode'
|
||||
import { InputTextComponent } from '../shared-forms/input-text.component'
|
||||
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
|
||||
import { TimestampInputComponent } from '../shared-forms/timestamp-input.component'
|
||||
import { AlertComponent } from '../shared-main/common/alert.component'
|
||||
import { PluginPlaceholderComponent } from '../shared-main/plugins/plugin-placeholder.component'
|
||||
import { VideoDetails } from '../shared-main/video/video-details.model'
|
||||
import { VideoPlaylist } from '../shared-video-playlist/video-playlist.model'
|
||||
import { Customizations, TabId } from './video-share.model'
|
||||
|
||||
@Component({
|
||||
selector: 'my-share-video',
|
||||
templateUrl: './share-video.component.html',
|
||||
styleUrls: [ './share-common.component.scss' ],
|
||||
imports: [
|
||||
RouterLink,
|
||||
NgbNav,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
NgbNavLinkBase,
|
||||
NgbNavContent,
|
||||
NgbNavOutlet,
|
||||
InputTextComponent,
|
||||
QRCodeComponent,
|
||||
PeertubeCheckboxComponent,
|
||||
FormsModule,
|
||||
PluginPlaceholderComponent,
|
||||
TimestampInputComponent,
|
||||
NgbCollapse,
|
||||
AlertComponent
|
||||
]
|
||||
})
|
||||
export class ShareVideoComponent implements OnInit {
|
||||
private sanitizer = inject(DomSanitizer)
|
||||
private server = inject(ServerService)
|
||||
private hooks = inject(HooksService)
|
||||
private authService = inject(AuthService)
|
||||
|
||||
readonly video = input<VideoDetails>(null)
|
||||
readonly videoCaptions = input<VideoCaption[]>([])
|
||||
readonly playlist = input<VideoPlaylist>(null)
|
||||
readonly customizations = input<Customizations>(null)
|
||||
|
||||
activeVideoId: TabId = 'url'
|
||||
isAdvancedCustomizationCollapsed = true
|
||||
|
||||
videoUrl: string
|
||||
videoEmbedUrl: string
|
||||
videoEmbedHTML: string
|
||||
videoEmbedSafeHTML: SafeHtml
|
||||
|
||||
ngOnInit () {
|
||||
this.onUpdate()
|
||||
}
|
||||
|
||||
async onUpdate () {
|
||||
const video = this.video()
|
||||
const customizations = this.customizations()
|
||||
|
||||
if (!video || !customizations) return
|
||||
|
||||
this.videoUrl = await this.getVideoUrl()
|
||||
this.videoEmbedUrl = await this.getVideoEmbedUrl()
|
||||
this.videoEmbedHTML = await this.getVideoEmbedCode({ responsive: customizations.responsive })
|
||||
this.videoEmbedSafeHTML = this.sanitizer.bypassSecurityTrustHtml(await this.getVideoEmbedCode({ responsive: false }))
|
||||
}
|
||||
|
||||
notSecure () {
|
||||
return window.location.protocol === 'http:'
|
||||
}
|
||||
|
||||
isInEmbedTab () {
|
||||
return this.activeVideoId === 'embed'
|
||||
}
|
||||
|
||||
isLocalVideo () {
|
||||
return this.video().isLocal
|
||||
}
|
||||
|
||||
isPrivateVideo () {
|
||||
return this.video().privacy.id === VideoPrivacy.PRIVATE
|
||||
}
|
||||
|
||||
isPasswordProtectedVideo () {
|
||||
return this.video().privacy.id === VideoPrivacy.PASSWORD_PROTECTED
|
||||
}
|
||||
|
||||
private getVideoUrl () {
|
||||
const customizations = this.customizations()
|
||||
const url = customizations?.originUrl
|
||||
? this.video().url
|
||||
: buildVideoLink(this.video(), window.location.origin)
|
||||
|
||||
return this.hooks.wrapFun(
|
||||
decorateVideoLink,
|
||||
{ url, ...this.getVideoOptions(false) },
|
||||
'video-watch',
|
||||
'filter:share.video-url.build.params',
|
||||
'filter:share.video-url.build.result'
|
||||
)
|
||||
}
|
||||
|
||||
private getVideoEmbedUrl () {
|
||||
return this.hooks.wrapFun(
|
||||
decorateVideoLink,
|
||||
{ url: this.video().embedUrl, ...this.getVideoOptions(true) },
|
||||
'video-watch',
|
||||
'filter:share.video-embed-url.build.params',
|
||||
'filter:share.video-embed-url.build.result'
|
||||
)
|
||||
}
|
||||
|
||||
private async getVideoEmbedCode (options: { responsive: boolean }) {
|
||||
const { responsive } = options
|
||||
|
||||
return this.hooks.wrapFun(
|
||||
buildVideoOrPlaylistEmbed,
|
||||
{
|
||||
embedUrl: await this.getVideoEmbedUrl(),
|
||||
embedTitle: this.video().name,
|
||||
responsive,
|
||||
aspectRatio: this.video().aspectRatio
|
||||
},
|
||||
'video-watch',
|
||||
'filter:share.video-embed-code.build.params',
|
||||
'filter:share.video-embed-code.build.result'
|
||||
)
|
||||
}
|
||||
|
||||
private getVideoOptions (forEmbed: boolean) {
|
||||
const customizations = this.customizations()
|
||||
|
||||
if (!customizations) return {}
|
||||
|
||||
const embedOptions = forEmbed
|
||||
? {
|
||||
title: customizations.title,
|
||||
warningTitle: customizations.warningTitle,
|
||||
controlBar: customizations.controlBar,
|
||||
peertubeLink: customizations.peertubeLink,
|
||||
|
||||
p2p: customizations.embedP2P === this.server.getHTMLConfig().defaults.p2p.embed.enabled
|
||||
? undefined
|
||||
: customizations.embedP2P
|
||||
}
|
||||
: {}
|
||||
|
||||
return {
|
||||
startTime: customizations.startAtCheckbox ? customizations.startAt : undefined,
|
||||
stopTime: customizations.stopAtCheckbox ? customizations.stopAt : undefined,
|
||||
|
||||
subtitle: customizations.subtitleCheckbox ? customizations.subtitle : undefined,
|
||||
|
||||
loop: customizations.loop,
|
||||
autoplay: customizations.autoplay,
|
||||
muted: customizations.muted,
|
||||
|
||||
...embedOptions
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,366 +9,21 @@
|
||||
|
||||
<div class="modal-body">
|
||||
@if (playlist()) {
|
||||
<div class="playlist">
|
||||
@if (video()) {
|
||||
<h5 i18n class="text-center mb-4">Share the playlist</h5>
|
||||
}
|
||||
|
||||
@if (isPrivatePlaylist()) {
|
||||
<my-alert class="alert-private" type="warning">
|
||||
<div i18n>This playlist is private so you won't be able to share it with external users</div>
|
||||
|
||||
<a
|
||||
i18n
|
||||
class="peertube-button-link primary-button"
|
||||
[routerLink]="[ '/my-library/video-playlists/update', playlist().shortUUID ]"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Update playlist privacy</a>
|
||||
</my-alert>
|
||||
}
|
||||
|
||||
<div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activePlaylistId">
|
||||
<ng-container ngbNavItem="url">
|
||||
<a ngbNavLink i18n>URL</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="nav-content">
|
||||
<my-input-text
|
||||
inputId="playlist-url"
|
||||
i18n-ariaLabel
|
||||
ariaLabel="Playlist URL"
|
||||
[value]="playlistUrl"
|
||||
[withToggle]="false"
|
||||
[withCopy]="true"
|
||||
[show]="true"
|
||||
[readonly]="true"
|
||||
></my-input-text>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="qrcode">
|
||||
<a ngbNavLink i18n>QR-Code</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="nav-content">
|
||||
<qrcode [qrdata]="playlistUrl" [width]="256" level="Q"></qrcode>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="embed">
|
||||
<a ngbNavLink i18n>Embed</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="nav-content">
|
||||
<my-input-text
|
||||
inputId="playlist-embed-url"
|
||||
i18n-ariaLabel
|
||||
ariaLabel="Playlist embed URL"
|
||||
[value]="customizations.onlyEmbedUrl ? playlistEmbedUrl : playlistEmbedHTML"
|
||||
(change)="onUpdate()"
|
||||
[withToggle]="false"
|
||||
[withCopy]="true"
|
||||
[show]="true"
|
||||
[readonly]="true"
|
||||
></my-input-text>
|
||||
|
||||
@if (notSecure()) {
|
||||
<my-alert i18n type="warning" class="mt-3">
|
||||
The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS
|
||||
websites).
|
||||
</my-alert>
|
||||
}
|
||||
|
||||
<div class="embed" [innerHTML]="playlistEmbedSafeHTML"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div [ngbNavOutlet]="nav"></div>
|
||||
|
||||
<div class="filters">
|
||||
@if (video()) {
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="includeVideoInPlaylist"
|
||||
[(ngModel)]="customizations.includeVideoInPlaylist"
|
||||
i18n-labelText
|
||||
labelText="Share the playlist at this video position"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isInPlaylistEmbedTab()) {
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="onlyEmbedUrl"
|
||||
[(ngModel)]="customizations.onlyEmbedUrl"
|
||||
i18n-labelText
|
||||
labelText="Only display embed URL"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="responsive"
|
||||
[(ngModel)]="customizations.responsive"
|
||||
i18n-labelText
|
||||
labelText="Responsive embed"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
}
|
||||
|
||||
<my-plugin-placeholder pluginId="share-modal-playlist-settings"></my-plugin-placeholder>
|
||||
</div>
|
||||
</div>
|
||||
<my-share-playlist
|
||||
[playlist]="playlist()"
|
||||
[playlistPosition]="playlistPosition()"
|
||||
[video]="video()"
|
||||
[customizations]="customizations"
|
||||
></my-share-playlist>
|
||||
}
|
||||
|
||||
@if (video()) {
|
||||
<div class="video">
|
||||
@if (playlist()) {
|
||||
<h5 i18n class="text-center mb-4">Share the video</h5>
|
||||
}
|
||||
|
||||
@if (isPrivateVideo()) {
|
||||
<my-alert class="alert-private" type="warning">
|
||||
<div i18n>This video is private so you won't be able to share it with external users</div>
|
||||
|
||||
<a
|
||||
i18n
|
||||
class="peertube-button-link primary-button mt-3"
|
||||
[routerLink]="[ '/videos/', 'manage', video().shortUUID ]"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Update video privacy</a>
|
||||
</my-alert>
|
||||
}
|
||||
|
||||
@if (isPasswordProtectedVideo()) {
|
||||
<my-alert i18n class="alert-private" type="warning">
|
||||
This video is password protected, please note that recipients will require the corresponding password to access the content.
|
||||
</my-alert>
|
||||
}
|
||||
|
||||
<div ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="activeVideoId">
|
||||
<ng-container ngbNavItem="url">
|
||||
<a ngbNavLink i18n>URL</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="nav-content">
|
||||
<my-input-text
|
||||
inputId="video-url"
|
||||
i18n-ariaLabel
|
||||
ariaLabel="Video URL"
|
||||
[value]="videoUrl"
|
||||
[withToggle]="false"
|
||||
[withCopy]="true"
|
||||
[show]="true"
|
||||
[readonly]="true"
|
||||
></my-input-text>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="qrcode">
|
||||
<a ngbNavLink i18n>QR-Code</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="nav-content">
|
||||
<qrcode [qrdata]="videoUrl" [width]="256" level="Q"></qrcode>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
|
||||
<ng-container ngbNavItem="embed">
|
||||
<a ngbNavLink i18n>Embed</a>
|
||||
|
||||
<ng-template ngbNavContent>
|
||||
<div class="nav-content">
|
||||
<my-input-text
|
||||
inputId="video-embed-url"
|
||||
i18n-ariaLabel
|
||||
ariaLabel="Video embed URL"
|
||||
[value]="customizations.onlyEmbedUrl ? videoEmbedUrl : videoEmbedHTML"
|
||||
(ngModelChange)="onUpdate()"
|
||||
[withToggle]="false"
|
||||
[withCopy]="true"
|
||||
[show]="true"
|
||||
[readonly]="true"
|
||||
></my-input-text>
|
||||
|
||||
@if (notSecure()) {
|
||||
<my-alert i18n type="warning" class="mt-3">
|
||||
The url is not secured (no HTTPS), so the embed video won't work on HTTPS websites (web browsers block non secured HTTP requests on HTTPS
|
||||
websites).
|
||||
</my-alert>
|
||||
}
|
||||
|
||||
<div class="embed" [innerHTML]="videoEmbedSafeHTML"></div>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div [ngbNavOutlet]="nav"></div>
|
||||
|
||||
<div class="filters">
|
||||
@if (!video().isLive) {
|
||||
<div class="form-group start-at">
|
||||
<my-peertube-checkbox inputName="startAt" [(ngModel)]="customizations.startAtCheckbox" i18n-labelText labelText="Start at"></my-peertube-checkbox>
|
||||
<my-timestamp-input
|
||||
[timestamp]="customizations.startAt"
|
||||
[maxTimestamp]="video().duration"
|
||||
[disabled]="!customizations.startAtCheckbox"
|
||||
[(ngModel)]="customizations.startAt"
|
||||
>
|
||||
</my-timestamp-input>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (videoCaptions().length !== 0) {
|
||||
<div class="form-group video-caption-block">
|
||||
<my-peertube-checkbox
|
||||
inputName="subtitleCheckbox"
|
||||
[(ngModel)]="customizations.subtitleCheckbox"
|
||||
i18n-labelText
|
||||
labelText="Auto select subtitle"
|
||||
></my-peertube-checkbox>
|
||||
|
||||
<div class="peertube-select-container">
|
||||
<select [(ngModel)]="customizations.subtitle" [disabled]="!customizations.subtitleCheckbox" class="form-control">
|
||||
@for (caption of videoCaptions(); track caption) {
|
||||
<option [value]="caption.language.id">{{ caption.language.label }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isInVideoEmbedTab()) {
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="onlyEmbedUrl"
|
||||
[(ngModel)]="customizations.onlyEmbedUrl"
|
||||
i18n-labelText
|
||||
labelText="Only display embed URL"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="responsive"
|
||||
[(ngModel)]="customizations.responsive"
|
||||
i18n-labelText
|
||||
labelText="Responsive embed"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
}
|
||||
|
||||
<my-plugin-placeholder pluginId="share-modal-video-settings"></my-plugin-placeholder>
|
||||
|
||||
<div class="advanced-filters" [ngbCollapse]="isAdvancedCustomizationCollapsed" [animation]="true">
|
||||
@if (!video().isLive) {
|
||||
<div class="form-group stop-at">
|
||||
<my-peertube-checkbox inputName="stopAt" [(ngModel)]="customizations.stopAtCheckbox" i18n-labelText labelText="Stop at"></my-peertube-checkbox>
|
||||
<my-timestamp-input
|
||||
[timestamp]="customizations.stopAt"
|
||||
[maxTimestamp]="video().duration"
|
||||
[disabled]="!customizations.stopAtCheckbox"
|
||||
[(ngModel)]="customizations.stopAt"
|
||||
>
|
||||
</my-timestamp-input>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox inputName="autoplay" [(ngModel)]="customizations.autoplay" i18n-labelText labelText="Autoplay"></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox inputName="muted" [(ngModel)]="customizations.muted" i18n-labelText labelText="Muted"></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
@if (!video().isLive) {
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox inputName="loop" [(ngModel)]="customizations.loop" i18n-labelText labelText="Loop"></my-peertube-checkbox>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!isLocalVideo() && !isInVideoEmbedTab()) {
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="originUrl"
|
||||
[(ngModel)]="customizations.originUrl"
|
||||
i18n-labelText
|
||||
labelText="Use origin instance URL"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isInVideoEmbedTab()) {
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="title"
|
||||
[(ngModel)]="customizations.title"
|
||||
i18n-labelText
|
||||
labelText="Display video title"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox inputName="embedP2P" [(ngModel)]="customizations.embedP2P" i18n-labelText labelText="P2P"></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="warningTitle"
|
||||
[(ngModel)]="customizations.warningTitle"
|
||||
i18n-labelText
|
||||
labelText="Display privacy warning"
|
||||
[disabled]="!customizations.embedP2P"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="controlBar"
|
||||
[(ngModel)]="customizations.controlBar"
|
||||
i18n-labelText
|
||||
labelText="Display player control bar"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<my-peertube-checkbox
|
||||
inputName="peertubeLink"
|
||||
[(ngModel)]="customizations.peertubeLink"
|
||||
i18n-labelText
|
||||
labelText="Display PeerTube button link"
|
||||
></my-peertube-checkbox>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button
|
||||
class="border-0 p-0 mt-4 mx-auto fw-semibold d-block"
|
||||
(click)="isAdvancedCustomizationCollapsed = !isAdvancedCustomizationCollapsed"
|
||||
[attr.aria-expanded]="!isAdvancedCustomizationCollapsed"
|
||||
aria-controls="collapseBasic"
|
||||
>
|
||||
@if (isAdvancedCustomizationCollapsed) {
|
||||
<span class="chevron-down"></span>
|
||||
<ng-container i18n>More customization</ng-container>
|
||||
} @else{
|
||||
<span class="chevron-up"></span>
|
||||
<ng-container i18n>Less customization</ng-container>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<my-share-video
|
||||
[video]="video()"
|
||||
[videoCaptions]="videoCaptions()"
|
||||
[playlist]="playlist()"
|
||||
[customizations]="customizations"
|
||||
></my-share-video>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -1,111 +1,38 @@
|
||||
import { Component, ElementRef, inject, input, model, viewChild } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { HooksService, ServerService } from '@app/core'
|
||||
import { VideoDetails } from '@app/shared/shared-main/video/video-details.model'
|
||||
import {
|
||||
NgbCollapse,
|
||||
NgbModal,
|
||||
NgbNav,
|
||||
NgbNavContent,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
NgbNavLinkBase,
|
||||
NgbNavOutlet
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { buildPlaylistLink, buildVideoLink, decoratePlaylistLink, decorateVideoLink } from '@peertube/peertube-core-utils'
|
||||
import { VideoCaption, VideoPlaylistPrivacy, VideoPrivacy } from '@peertube/peertube-models'
|
||||
import { buildVideoOrPlaylistEmbed } from '@root-helpers/video'
|
||||
import { QRCodeComponent } from 'angularx-qrcode'
|
||||
import { InputTextComponent } from '../shared-forms/input-text.component'
|
||||
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
|
||||
import { TimestampInputComponent } from '../shared-forms/timestamp-input.component'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { VideoCaption } from '@peertube/peertube-models'
|
||||
import { GlobalIconComponent } from '../shared-icons/global-icon.component'
|
||||
import { AlertComponent } from '../shared-main/common/alert.component'
|
||||
import { PluginPlaceholderComponent } from '../shared-main/plugins/plugin-placeholder.component'
|
||||
import { VideoPlaylist } from '../shared-video-playlist/video-playlist.model'
|
||||
|
||||
type Customizations = {
|
||||
startAtCheckbox: boolean
|
||||
startAt: number
|
||||
|
||||
stopAtCheckbox: boolean
|
||||
stopAt: number
|
||||
|
||||
subtitleCheckbox: boolean
|
||||
subtitle: string
|
||||
|
||||
loop: boolean
|
||||
originUrl: boolean
|
||||
autoplay: boolean
|
||||
muted: boolean
|
||||
|
||||
embedP2P: boolean
|
||||
onlyEmbedUrl: boolean
|
||||
title: boolean
|
||||
warningTitle: boolean
|
||||
controlBar: boolean
|
||||
peertubeLink: boolean
|
||||
responsive: boolean
|
||||
|
||||
includeVideoInPlaylist: boolean
|
||||
}
|
||||
|
||||
type TabId = 'url' | 'qrcode' | 'embed'
|
||||
import { SharePlaylistComponent } from './share-playlist.component'
|
||||
import { ShareVideoComponent } from './share-video.component'
|
||||
import { Customizations } from './video-share.model'
|
||||
|
||||
@Component({
|
||||
selector: 'my-video-share',
|
||||
templateUrl: './video-share.component.html',
|
||||
styleUrls: [ './video-share.component.scss' ],
|
||||
imports: [
|
||||
GlobalIconComponent,
|
||||
RouterLink,
|
||||
NgbNav,
|
||||
NgbNavItem,
|
||||
NgbNavLink,
|
||||
NgbNavLinkBase,
|
||||
NgbNavContent,
|
||||
InputTextComponent,
|
||||
QRCodeComponent,
|
||||
NgbNavOutlet,
|
||||
PeertubeCheckboxComponent,
|
||||
FormsModule,
|
||||
PluginPlaceholderComponent,
|
||||
TimestampInputComponent,
|
||||
NgbCollapse,
|
||||
AlertComponent
|
||||
SharePlaylistComponent,
|
||||
ShareVideoComponent
|
||||
]
|
||||
})
|
||||
export class VideoShareComponent {
|
||||
private modalService = inject(NgbModal)
|
||||
private sanitizer = inject(DomSanitizer)
|
||||
private server = inject(ServerService)
|
||||
private hooks = inject(HooksService)
|
||||
|
||||
readonly modal = viewChild<ElementRef>('modal')
|
||||
readonly playlistShare = viewChild(SharePlaylistComponent)
|
||||
readonly videoShare = viewChild(ShareVideoComponent)
|
||||
|
||||
readonly video = input<VideoDetails>(null)
|
||||
readonly videoCaptions = input<VideoCaption[]>([])
|
||||
readonly playlist = input<VideoPlaylist>(null)
|
||||
readonly playlistPosition = model<number>(null)
|
||||
|
||||
activeVideoId: TabId = 'url'
|
||||
activePlaylistId: TabId = 'url'
|
||||
|
||||
customizations: Customizations
|
||||
isAdvancedCustomizationCollapsed = true
|
||||
|
||||
videoUrl: string
|
||||
playlistUrl: string
|
||||
|
||||
videoEmbedUrl: string
|
||||
playlistEmbedUrl: string
|
||||
|
||||
videoEmbedHTML: string
|
||||
videoEmbedSafeHTML: SafeHtml
|
||||
playlistEmbedHTML: string
|
||||
playlistEmbedSafeHTML: SafeHtml
|
||||
|
||||
show (currentVideoTimestamp?: number, currentPlaylistPosition?: number) {
|
||||
let subtitle: string
|
||||
@@ -163,166 +90,8 @@ export class VideoShareComponent {
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getVideoUrl () {
|
||||
const url = this.customizations.originUrl
|
||||
? this.video().url
|
||||
: buildVideoLink(this.video(), window.location.origin)
|
||||
|
||||
return this.hooks.wrapFun(
|
||||
decorateVideoLink,
|
||||
{ url, ...this.getVideoOptions(false) },
|
||||
'video-watch',
|
||||
'filter:share.video-url.build.params',
|
||||
'filter:share.video-url.build.result'
|
||||
)
|
||||
}
|
||||
|
||||
getVideoEmbedUrl () {
|
||||
return this.hooks.wrapFun(
|
||||
decorateVideoLink,
|
||||
{ url: this.video().embedUrl, ...this.getVideoOptions(true) },
|
||||
'video-watch',
|
||||
'filter:share.video-embed-url.build.params',
|
||||
'filter:share.video-embed-url.build.result'
|
||||
)
|
||||
}
|
||||
|
||||
async getVideoEmbedCode (options: { responsive: boolean }) {
|
||||
const { responsive } = options
|
||||
return this.hooks.wrapFun(
|
||||
buildVideoOrPlaylistEmbed,
|
||||
{ embedUrl: await this.getVideoEmbedUrl(), embedTitle: this.video().name, responsive, aspectRatio: this.video().aspectRatio },
|
||||
'video-watch',
|
||||
'filter:share.video-embed-code.build.params',
|
||||
'filter:share.video-embed-code.build.result'
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
getPlaylistUrl () {
|
||||
const url = buildPlaylistLink(this.playlist())
|
||||
|
||||
return this.hooks.wrapFun(
|
||||
decoratePlaylistLink,
|
||||
{ url, ...this.getPlaylistOptions() },
|
||||
'video-watch',
|
||||
'filter:share.video-playlist-url.build.params',
|
||||
'filter:share.video-playlist-url.build.result'
|
||||
)
|
||||
}
|
||||
|
||||
getPlaylistEmbedUrl () {
|
||||
return this.hooks.wrapFun(
|
||||
decoratePlaylistLink,
|
||||
{ url: this.playlist().embedUrl, ...this.getPlaylistOptions() },
|
||||
'video-watch',
|
||||
'filter:share.video-playlist-embed-url.build.params',
|
||||
'filter:share.video-playlist-embed-url.build.result'
|
||||
)
|
||||
}
|
||||
|
||||
async getPlaylistEmbedCode (options: { responsive: boolean }) {
|
||||
const { responsive } = options
|
||||
return this.hooks.wrapFun(
|
||||
buildVideoOrPlaylistEmbed,
|
||||
{
|
||||
embedUrl: await this.getPlaylistEmbedUrl(),
|
||||
embedTitle: this.playlist().displayName,
|
||||
responsive,
|
||||
aspectRatio: this.video()?.aspectRatio
|
||||
},
|
||||
'video-watch',
|
||||
'filter:share.video-playlist-embed-code.build.params',
|
||||
'filter:share.video-playlist-embed-code.build.result'
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async onUpdate () {
|
||||
if (this.playlist()) {
|
||||
this.playlistUrl = await this.getPlaylistUrl()
|
||||
this.playlistEmbedUrl = await this.getPlaylistEmbedUrl()
|
||||
this.playlistEmbedHTML = await this.getPlaylistEmbedCode({ responsive: this.customizations.responsive })
|
||||
this.playlistEmbedSafeHTML = this.sanitizer.bypassSecurityTrustHtml(await this.getPlaylistEmbedCode({ responsive: false }))
|
||||
}
|
||||
|
||||
if (this.video()) {
|
||||
this.videoUrl = await this.getVideoUrl()
|
||||
this.videoEmbedUrl = await this.getVideoEmbedUrl()
|
||||
this.videoEmbedHTML = await this.getVideoEmbedCode({ responsive: this.customizations.responsive })
|
||||
this.videoEmbedSafeHTML = this.sanitizer.bypassSecurityTrustHtml(await this.getVideoEmbedCode({ responsive: false }))
|
||||
}
|
||||
}
|
||||
|
||||
notSecure () {
|
||||
return window.location.protocol === 'http:'
|
||||
}
|
||||
|
||||
isInVideoEmbedTab () {
|
||||
return this.activeVideoId === 'embed'
|
||||
}
|
||||
|
||||
isInPlaylistEmbedTab () {
|
||||
return this.activePlaylistId === 'embed'
|
||||
}
|
||||
|
||||
isLocalVideo () {
|
||||
return this.video().isLocal
|
||||
}
|
||||
|
||||
isPrivateVideo () {
|
||||
return this.video().privacy.id === VideoPrivacy.PRIVATE
|
||||
}
|
||||
|
||||
isPrivatePlaylist () {
|
||||
return this.playlist().privacy.id === VideoPlaylistPrivacy.PRIVATE
|
||||
}
|
||||
|
||||
isPasswordProtectedVideo () {
|
||||
return this.video().privacy.id === VideoPrivacy.PASSWORD_PROTECTED
|
||||
}
|
||||
|
||||
private getPlaylistOptions (baseUrl?: string) {
|
||||
const playlistPosition = this.playlistPosition()
|
||||
return {
|
||||
baseUrl,
|
||||
|
||||
playlistPosition: playlistPosition && this.customizations.includeVideoInPlaylist
|
||||
? playlistPosition
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
|
||||
private getVideoOptions (forEmbed: boolean) {
|
||||
const embedOptions = forEmbed
|
||||
? {
|
||||
title: this.customizations.title,
|
||||
warningTitle: this.customizations.warningTitle,
|
||||
controlBar: this.customizations.controlBar,
|
||||
peertubeLink: this.customizations.peertubeLink,
|
||||
|
||||
// If using default value, we don't need to specify it
|
||||
p2p: this.customizations.embedP2P === this.server.getHTMLConfig().defaults.p2p.embed.enabled
|
||||
? undefined
|
||||
: this.customizations.embedP2P
|
||||
}
|
||||
: {}
|
||||
|
||||
return {
|
||||
startTime: this.customizations.startAtCheckbox ? this.customizations.startAt : undefined,
|
||||
stopTime: this.customizations.stopAtCheckbox ? this.customizations.stopAt : undefined,
|
||||
|
||||
subtitle: this.customizations.subtitleCheckbox ? this.customizations.subtitle : undefined,
|
||||
|
||||
loop: this.customizations.loop,
|
||||
autoplay: this.customizations.autoplay,
|
||||
muted: this.customizations.muted,
|
||||
|
||||
...embedOptions
|
||||
}
|
||||
onUpdate () {
|
||||
this.playlistShare()?.onUpdate()
|
||||
this.videoShare()?.onUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
export type Customizations = {
|
||||
startAtCheckbox: boolean
|
||||
startAt: number
|
||||
|
||||
stopAtCheckbox: boolean
|
||||
stopAt: number
|
||||
|
||||
subtitleCheckbox: boolean
|
||||
subtitle: string
|
||||
|
||||
loop: boolean
|
||||
originUrl: boolean
|
||||
autoplay: boolean
|
||||
muted: boolean
|
||||
|
||||
embedP2P: boolean
|
||||
onlyEmbedUrl: boolean
|
||||
title: boolean
|
||||
warningTitle: boolean
|
||||
controlBar: boolean
|
||||
peertubeLink: boolean
|
||||
responsive: boolean
|
||||
|
||||
includeVideoInPlaylist: boolean
|
||||
}
|
||||
|
||||
export type TabId = 'url' | 'qrcode' | 'embed'
|
||||
@@ -98,7 +98,8 @@ export class VideoDownloadComponent {
|
||||
if (!this.video.isLocal || !this.authService.isLoggedIn()) return of(undefined)
|
||||
|
||||
const user = this.authService.getUser()
|
||||
if (!this.video.isOwnerOrHasSeeAllVideosRight(user)) return of(undefined)
|
||||
// User that can update the video can also get the original video file
|
||||
if (!this.video.isUpdatableBy(user)) return of(undefined)
|
||||
|
||||
return this.videoService.getSource(this.video.id)
|
||||
.pipe(catchError(err => {
|
||||
|
||||
@@ -188,8 +188,8 @@ export class VideoActionsDropdownComponent implements OnChanges {
|
||||
isVideoStatsAvailable () {
|
||||
if (!this.user) return false
|
||||
|
||||
const video = this.video()
|
||||
return video.isLocal && video.isOwnerOrHasSeeAllVideosRight(this.user)
|
||||
// Users that can update the video can also see its stats
|
||||
return this.video().isUpdatableBy(this.user)
|
||||
}
|
||||
|
||||
isVideoRemovable () {
|
||||
@@ -226,6 +226,7 @@ export class VideoActionsDropdownComponent implements OnChanges {
|
||||
|
||||
isVideoDownloadableByAnonymous () {
|
||||
const video = this.video()
|
||||
|
||||
return (
|
||||
video &&
|
||||
video.isLive !== true &&
|
||||
@@ -238,10 +239,11 @@ export class VideoActionsDropdownComponent implements OnChanges {
|
||||
if (!this.user) return false
|
||||
|
||||
const video = this.video()
|
||||
|
||||
return (
|
||||
video &&
|
||||
video.isLive !== true &&
|
||||
video.isOwnerOrHasSeeAllVideosRight(this.user)
|
||||
video.isUpdatableBy(this.user)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,8 +22,7 @@
|
||||
<ng-container i18n>Videos on <strong>{{ instanceName }}</strong></ng-container>
|
||||
} @else {
|
||||
<ng-container i18n>
|
||||
Videos on <strong>{{ instanceName }}</strong> and <strong>{{ totalFollowing }}</strong> {totalFollowing, plural, =1 {other platform} other {other
|
||||
platforms}}
|
||||
Videos on <strong>{{ instanceName }}</strong> and <strong>{{ totalFollowing }}</strong> {totalFollowing, plural, =1 {other platform} other {other platforms}}
|
||||
</ng-container>
|
||||
}
|
||||
</div>
|
||||
@@ -110,8 +109,8 @@
|
||||
|
||||
<div class="with-description">
|
||||
<div i18n>
|
||||
{{ instanceName }} platform subscribes to content from <a routerLink="/about/follows" target="_blank">{{ totalFollowing }} {totalFollowing,
|
||||
plural, =1 {other platform} other {other platforms}}</a>.
|
||||
{{ instanceName }} platform subscribes to content from
|
||||
<a routerLink="/about/follows" target="_blank">{{ totalFollowing }} {totalFollowing, plural, =1 {other platform} other {other platforms}}</a>.
|
||||
</div>
|
||||
<div i18n>Set your display preferences here.</div>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { RouterLink } from '@angular/router'
|
||||
import { AuthService, RedirectService } from '@app/core'
|
||||
import { ServerService } from '@app/core/server/server.service'
|
||||
import { NgbCollapse } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { UserRight, VideoConstant } from '@peertube/peertube-models'
|
||||
import { UserRight, ConstantLabel } from '@peertube/peertube-models'
|
||||
import { AttributesOnly } from '@peertube/peertube-typescript-utils'
|
||||
import debug from 'debug'
|
||||
import { PeertubeCheckboxComponent } from '../shared-forms/peertube-checkbox.component'
|
||||
@@ -70,8 +70,8 @@ export class VideoFiltersHeaderComponent implements OnInit {
|
||||
instanceName: string
|
||||
totalFollowing: number
|
||||
|
||||
private videoCategories: VideoConstant<number>[] = []
|
||||
private videoLanguages: VideoConstant<string>[] = []
|
||||
private videoCategories: ConstantLabel<number>[] = []
|
||||
private videoLanguages: ConstantLabel<string>[] = []
|
||||
|
||||
ngOnInit () {
|
||||
this.instanceName = this.server.getHTMLConfig().instance.name
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getAPIUrl, getOriginUrl } from '@app/helpers'
|
||||
import { getAPIUrl, getEmbedUrl } from '@app/helpers'
|
||||
import { buildPlaylistWatchPath, peertubeTranslate } from '@peertube/peertube-core-utils'
|
||||
import {
|
||||
AccountSummary,
|
||||
ConstantLabel,
|
||||
VideoPlaylist as ServerVideoPlaylist,
|
||||
Thumbnail,
|
||||
VideoChannelSummary,
|
||||
VideoConstant,
|
||||
VideoPlaylistPrivacyType,
|
||||
VideoPlaylistType,
|
||||
VideoPlaylistType_Type
|
||||
@@ -24,11 +24,11 @@ export class VideoPlaylist implements ServerVideoPlaylist {
|
||||
|
||||
displayName: string
|
||||
description: string
|
||||
privacy: VideoConstant<VideoPlaylistPrivacyType>
|
||||
privacy: ConstantLabel<VideoPlaylistPrivacyType>
|
||||
|
||||
videosLength: number
|
||||
|
||||
type: VideoConstant<VideoPlaylistType_Type>
|
||||
type: ConstantLabel<VideoPlaylistType_Type>
|
||||
|
||||
createdAt: Date | string
|
||||
updatedAt: Date | string
|
||||
@@ -68,7 +68,7 @@ export class VideoPlaylist implements ServerVideoPlaylist {
|
||||
this.privacy = hash.privacy
|
||||
|
||||
this.embedPath = hash.embedPath
|
||||
this.embedUrl = hash.embedUrl || (getOriginUrl() + hash.embedPath)
|
||||
this.embedUrl = hash.embedUrl || (getEmbedUrl() + hash.embedPath)
|
||||
|
||||
this.videosLength = hash.videosLength
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { RestExtractor } from '@app/core'
|
||||
import { VideoEmbedPrivacy, VideoEmbedPrivacyUpdate } from '@peertube/peertube-models'
|
||||
import { catchError } from 'rxjs'
|
||||
import { VideoService } from '../shared-main/video/video.service'
|
||||
|
||||
@Injectable()
|
||||
export class VideoEmbedPrivacyService {
|
||||
private authHttp = inject(HttpClient)
|
||||
private restExtractor = inject(RestExtractor)
|
||||
|
||||
getPrivacy (options: {
|
||||
videoId: string
|
||||
}) {
|
||||
const path = `${VideoService.BASE_VIDEO_URL}/${options.videoId}/embed-privacy`
|
||||
|
||||
return this.authHttp.get<VideoEmbedPrivacy>(path)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
|
||||
updatePrivacy (options: {
|
||||
videoId: string
|
||||
settings: VideoEmbedPrivacyUpdate
|
||||
}) {
|
||||
const path = `${VideoService.BASE_VIDEO_URL}/${options.videoId}/embed-privacy`
|
||||
|
||||
return this.authHttp.put(path, options.settings)
|
||||
.pipe(catchError(err => this.restExtractor.handleError(err)))
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,6 @@ export const environment = {
|
||||
production: false,
|
||||
hmr: true,
|
||||
apiUrl: '',
|
||||
originServerUrl: 'http://localhost:9000'
|
||||
originServerUrl: 'http://localhost:9000',
|
||||
embedUrl: 'http://localhost:5173'
|
||||
}
|
||||
|
||||
@@ -2,5 +2,6 @@ export const environment = {
|
||||
production: true,
|
||||
hmr: false,
|
||||
apiUrl: '',
|
||||
originServerUrl: ''
|
||||
originServerUrl: '',
|
||||
embedUrl: ''
|
||||
}
|
||||
|
||||
@@ -12,5 +12,6 @@ export const environment = {
|
||||
production: true,
|
||||
hmr: false,
|
||||
apiUrl: '',
|
||||
originServerUrl: ''
|
||||
originServerUrl: '',
|
||||
embedUrl: ''
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@ export function randomString (length: number) {
|
||||
return result
|
||||
}
|
||||
|
||||
export function splitAndGetNotEmpty (value: string) {
|
||||
export function splitAndGetNotEmpty (value: string): string[] {
|
||||
if (!value) return []
|
||||
|
||||
return value
|
||||
.split('\n')
|
||||
.filter(line => line && line.length !== 0) // Eject empty lines
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
@include peertube-input-text(100%);
|
||||
}
|
||||
|
||||
.pt-textarea {
|
||||
@include peertube-textarea(100%, 150px);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@@ -147,13 +147,14 @@ body {
|
||||
font-family: $main-fonts;
|
||||
|
||||
.error-details {
|
||||
margin-top: 40px;
|
||||
font-size: 80%;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-modal-dialog-content {
|
||||
padding-top: 40px !important;
|
||||
padding-top: 0 !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
// Error display disabled
|
||||
|
||||
@@ -189,7 +189,13 @@ class PeerTubePlugin extends Plugin {
|
||||
this.alterInactivity()
|
||||
}
|
||||
|
||||
displayFatalError () {
|
||||
displayFatalError (options: {
|
||||
log?: boolean // default true
|
||||
error?: Error | MediaError
|
||||
isTechnicalError?: boolean // default true
|
||||
} = {}) {
|
||||
const { log = true, error = this.player.error(), isTechnicalError = true } = options
|
||||
|
||||
// Already displayed an error
|
||||
if (this.errorModal) return
|
||||
|
||||
@@ -197,16 +203,20 @@ class PeerTubePlugin extends Plugin {
|
||||
|
||||
this.player.loadingSpinner.hide()
|
||||
|
||||
const buildModal = (error: MediaError) => {
|
||||
const buildModal = () => {
|
||||
const localize = this.player.localize.bind(this.player)
|
||||
|
||||
const wrapper = document.createElement('div')
|
||||
const header = document.createElement('h1')
|
||||
header.innerText = localize('Failed to play video')
|
||||
wrapper.appendChild(header)
|
||||
|
||||
if (isTechnicalError) {
|
||||
const desc = document.createElement('div')
|
||||
desc.innerText = localize('The video failed to play due to technical issues.')
|
||||
wrapper.appendChild(desc)
|
||||
}
|
||||
|
||||
const details = document.createElement('p')
|
||||
details.classList.add('error-details')
|
||||
details.innerText = error.message
|
||||
@@ -215,7 +225,7 @@ class PeerTubePlugin extends Plugin {
|
||||
return wrapper
|
||||
}
|
||||
|
||||
this.errorModal = this.player.createModal(buildModal(this.player.error()), {
|
||||
this.errorModal = this.player.createModal(buildModal(), {
|
||||
temporary: true,
|
||||
uncloseable: true
|
||||
})
|
||||
@@ -223,11 +233,13 @@ class PeerTubePlugin extends Plugin {
|
||||
|
||||
this.player.addClass('vjs-error-display-enabled')
|
||||
|
||||
if (log) {
|
||||
// Google Bot may throw codecs, but it should not prevent indexing
|
||||
if (/googlebot/i.test(navigator.userAgent)) {
|
||||
console.error(this.player.error())
|
||||
console.error(error)
|
||||
} else {
|
||||
logger.error('Fatal error in player', this.player.error())
|
||||
logger.error('Fatal error in player', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ class PlaylistMenuItem extends Component {
|
||||
|
||||
const thumbnail = super.createEl('img', {
|
||||
src: videoElement.video.thumbnails.length !== 0
|
||||
? window.location.origin + findAppropriateThumbnail(videoElement.video.thumbnails, 80, '16:9').fileUrl // Keep 80 in sync with CSS
|
||||
? findAppropriateThumbnail(videoElement.video.thumbnails, 80, '16:9').fileUrl // Keep 80 in sync with CSS
|
||||
: ''
|
||||
})
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ body {
|
||||
|
||||
#error-content {
|
||||
font-size: 24px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
#error-details {
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ResultList,
|
||||
ServerErrorCode,
|
||||
VideoDetails,
|
||||
VideoEmbedPrivacyPolicy,
|
||||
VideoPlaylist,
|
||||
VideoPlaylistElement,
|
||||
VideoState
|
||||
@@ -224,7 +225,7 @@ export class PeerTubeEmbed {
|
||||
playerSettingsPromise
|
||||
} = await this.videoFetcher.loadVideo({ videoId: uuid, videoPassword: this.videoPassword })
|
||||
|
||||
return this.buildVideoPlayer({
|
||||
return await this.buildVideoPlayer({
|
||||
videoResponse,
|
||||
captionsPromise,
|
||||
chaptersPromise,
|
||||
@@ -233,8 +234,17 @@ export class PeerTubeEmbed {
|
||||
forceAutoplay
|
||||
})
|
||||
} catch (err) {
|
||||
if (await this.handlePasswordError(err)) this.loadVideoAndBuildPlayer({ ...options })
|
||||
else this.playerHTML.displayError(err.message, await this.translationsPromise)
|
||||
if (await this.handlePasswordError(err)) {
|
||||
this.loadVideoAndBuildPlayer({ ...options })
|
||||
return
|
||||
}
|
||||
|
||||
if (this.player?.usingPlugin('peertube')) {
|
||||
this.player.peertube().displayFatalError({ error: err, log: false, isTechnicalError: false })
|
||||
return
|
||||
}
|
||||
|
||||
this.playerHTML.displayError(err.message, await this.translationsPromise)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,11 +270,15 @@ export class PeerTubeEmbed {
|
||||
? await this.videoFetcher.loadVideoToken(videoInfo, this.videoPassword)
|
||||
: undefined
|
||||
|
||||
return { live, video: videoInfo, videoFileToken }
|
||||
const allowed = videoInfo.embedPrivacyPolicy.id !== VideoEmbedPrivacyPolicy.ALL_ALLOWED
|
||||
? await this.videoFetcher.loadEmbedAllowed(videoInfo)
|
||||
: true
|
||||
|
||||
return { live, video: videoInfo, videoFileToken, allowed }
|
||||
})
|
||||
|
||||
const [
|
||||
{ video, live, videoFileToken },
|
||||
{ video, live, videoFileToken, allowed },
|
||||
translations,
|
||||
captionsResponse,
|
||||
chaptersResponse,
|
||||
@@ -280,6 +294,10 @@ export class PeerTubeEmbed {
|
||||
this.buildPlayerIfNeeded()
|
||||
])
|
||||
|
||||
if (!allowed) {
|
||||
throw new Error('This video is not allowed to be embedded on this domain.')
|
||||
}
|
||||
|
||||
const playlist = this.playlistTracker
|
||||
? {
|
||||
onVideoUpdate: (uuid: string) => this.loadVideoAndBuildPlayer({ uuid, forceAutoplay: false }),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HttpStatusCode, LiveVideo, VideoDetails, VideoToken } from '@peertube/peertube-models'
|
||||
import { HttpStatusCode, LiveVideo, VideoDetails, VideoEmbedPrivacyAllowed, VideoToken } from '@peertube/peertube-models'
|
||||
import { logger } from '../../../root-helpers'
|
||||
import { PeerTubeServerError } from '../../../types'
|
||||
import { AuthHTTP } from './auth-http'
|
||||
@@ -57,6 +57,17 @@ export class VideoFetcher {
|
||||
.then(token => token.files.token)
|
||||
}
|
||||
|
||||
loadEmbedAllowed (video: VideoDetails) {
|
||||
if (!document.referrer) return Promise.resolve({ allowed: false })
|
||||
|
||||
const params = new URLSearchParams()
|
||||
params.append('domain', new URL(document.referrer).host)
|
||||
|
||||
return this.http.fetch(this.getVideoUrl(video.uuid) + '/embed-privacy/allowed?' + params.toString(), { optionalAuth: false })
|
||||
.then(res => res.json() as Promise<VideoEmbedPrivacyAllowed>)
|
||||
.then(({ domainAllowed }) => domainAllowed)
|
||||
}
|
||||
|
||||
getVideoViewsUrl (videoUUID: string) {
|
||||
return this.getVideoUrl(videoUUID) + '/views'
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ export interface VideoObject {
|
||||
waitTranscoding: boolean
|
||||
state: VideoStateType
|
||||
|
||||
// If null, the embed has restrictions
|
||||
embedUrl: string | null
|
||||
|
||||
published: string
|
||||
originallyPublishedAt: string
|
||||
updated: string
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
export interface VideoConstant<T> {
|
||||
export interface ConstantLabel<T> {
|
||||
id: T
|
||||
label: string
|
||||
description?: string
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './file-storage.enum.js'
|
||||
export * from './constant-label.model.js'
|
||||
export * from './peertube-error.model.js'
|
||||
export * from './result-list.model.js'
|
||||
export * from './simple-logger.model.js'
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PlayerThemeVideoSetting } from '../../player/player-theme.type.js'
|
||||
import {
|
||||
LiveVideoLatencyModeType,
|
||||
VideoCommentPolicyType,
|
||||
VideoEmbedPrivacyPolicyType,
|
||||
VideoFileMetadata,
|
||||
VideoPrivacyType,
|
||||
VideoStateType,
|
||||
@@ -113,6 +114,11 @@ export interface VideoExportJSON {
|
||||
theme: PlayerThemeVideoSetting
|
||||
}
|
||||
|
||||
videoEmbedPrivacy: {
|
||||
policy: VideoEmbedPrivacyPolicyType
|
||||
domains: string[]
|
||||
}
|
||||
|
||||
archiveFiles: {
|
||||
videoFile: string | null
|
||||
thumbnail: string | null
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Account } from '../../actors/account.model.js'
|
||||
import { VideoChannel } from '../../videos/channel/video-channel.model.js'
|
||||
import { Thumbnail } from '../../videos/index.js'
|
||||
import { VideoConstant } from '../../videos/video-constant.model.js'
|
||||
import { ConstantLabel } from '../../common/constant-label.model.js'
|
||||
import { AbusePredefinedReasonsString } from './abuse-reason.model.js'
|
||||
import { AbuseStateType } from './abuse-state.model.js'
|
||||
|
||||
@@ -55,7 +55,7 @@ export interface AdminAbuse {
|
||||
reporterAccount: Account
|
||||
flaggedAccount: Account
|
||||
|
||||
state: VideoConstant<AbuseStateType>
|
||||
state: ConstantLabel<AbuseStateType>
|
||||
moderationComment?: string
|
||||
|
||||
video?: AdminVideoAbuse
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Video, VideoChannelSummary, VideoConstant } from '../videos/index.js'
|
||||
import { ConstantLabel } from '../common/constant-label.model.js'
|
||||
import { Video, VideoChannelSummary } from '../videos/index.js'
|
||||
|
||||
export interface ChannelOverview {
|
||||
channel: VideoChannelSummary
|
||||
@@ -6,7 +7,7 @@ export interface ChannelOverview {
|
||||
}
|
||||
|
||||
export interface CategoryOverview {
|
||||
category: VideoConstant<number>
|
||||
category: ConstantLabel<number>
|
||||
videos: Video[]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VideoConstant } from '../../videos/index.js'
|
||||
import { ConstantLabel } from '../../common/constant-label.model.js'
|
||||
import { RunnerJobPayload } from './runner-job-payload.model.js'
|
||||
import { RunnerJobPrivatePayload } from './runner-job-private-payload.model.js'
|
||||
import { RunnerJobStateType } from './runner-job-state.model.js'
|
||||
@@ -9,7 +9,7 @@ export interface RunnerJob <T extends RunnerJobPayload = RunnerJobPayload> {
|
||||
|
||||
type: RunnerJobType
|
||||
|
||||
state: VideoConstant<RunnerJobStateType>
|
||||
state: ConstantLabel<RunnerJobStateType>
|
||||
|
||||
payload: T
|
||||
|
||||
@@ -26,7 +26,7 @@ export interface RunnerJob <T extends RunnerJobPayload = RunnerJobPayload> {
|
||||
|
||||
parent?: {
|
||||
type: RunnerJobType
|
||||
state: VideoConstant<RunnerJobStateType>
|
||||
state: ConstantLabel<RunnerJobStateType>
|
||||
uuid: string
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface RunnerJob <T extends RunnerJobPayload = RunnerJobPayload> {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line max-len
|
||||
export interface RunnerJobAdmin <T extends RunnerJobPayload = RunnerJobPayload, U extends RunnerJobPrivatePayload = RunnerJobPrivatePayload> extends RunnerJob<T> {
|
||||
export interface RunnerJobAdmin<T extends RunnerJobPayload = RunnerJobPayload, U extends RunnerJobPrivatePayload = RunnerJobPrivatePayload>
|
||||
extends RunnerJob<T>
|
||||
{
|
||||
privatePayload: U
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { FollowState } from '../actors/index.js'
|
||||
import { AbuseStateType } from '../moderation/index.js'
|
||||
import { PluginType_Type } from '../plugins/index.js'
|
||||
import { VideoChannelCollaboratorStateType } from '../videos/index.js'
|
||||
import { VideoConstant } from '../videos/video-constant.model.js'
|
||||
import { ConstantLabel } from '../common/constant-label.model.js'
|
||||
import { VideoStateType } from '../videos/video-state.enum.js'
|
||||
import { UserNotificationData } from './user-notification-data.model.js'
|
||||
|
||||
@@ -156,14 +156,14 @@ export interface UserNotification {
|
||||
|
||||
videoCaption?: {
|
||||
id: number
|
||||
language: VideoConstant<string>
|
||||
language: ConstantLabel<string>
|
||||
video: VideoInfo
|
||||
}
|
||||
|
||||
videoChannelCollaborator?: {
|
||||
id: number
|
||||
|
||||
state: VideoConstant<VideoChannelCollaboratorStateType>
|
||||
state: ConstantLabel<VideoChannelCollaboratorStateType>
|
||||
|
||||
channel: ActorInfo
|
||||
channelOwner: ActorInfo
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { VideoConstant } from '../video-constant.model.js'
|
||||
import { ConstantLabel } from '../../common/constant-label.model.js'
|
||||
|
||||
export interface VideoCaption {
|
||||
language: VideoConstant<string>
|
||||
language: ConstantLabel<string>
|
||||
|
||||
// TODO: remove, deprecated in 8.0
|
||||
captionPath: string
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { VideoChannelSummary } from '../channel/video-channel.model.js'
|
||||
import { VideoConstant } from '../video-constant.model.js'
|
||||
import { ConstantLabel } from '../../common/constant-label.model.js'
|
||||
import { VideoChannelSyncStateType } from './video-channel-sync-state.enum.js'
|
||||
|
||||
export interface VideoChannelSync {
|
||||
@@ -9,6 +9,6 @@ export interface VideoChannelSync {
|
||||
|
||||
createdAt: string
|
||||
channel: VideoChannelSummary
|
||||
state: VideoConstant<VideoChannelSyncStateType>
|
||||
state: ConstantLabel<VideoChannelSyncStateType>
|
||||
lastSyncAt: string
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ export const VideoChannelActivityAction = {
|
||||
CREATE_CHANNEL_OWNERSHIP: 11,
|
||||
SEND_OWNERSHIP_REQUEST: 12,
|
||||
ACCEPT_OWNERSHIP_REQUEST: 13,
|
||||
REFUSE_OWNERSHIP_REQUEST: 14
|
||||
REFUSE_OWNERSHIP_REQUEST: 14,
|
||||
UPDATE_EMBED_POLICY: 15
|
||||
} as const
|
||||
|
||||
export type VideoChannelActivityActionType = typeof VideoChannelActivityAction[keyof typeof VideoChannelActivityAction]
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './video-embed-privacy-update.model.js'
|
||||
export * from './video-embed-privacy-allowed.model.js'
|
||||
export * from './video-embed-privacy-policy.enum.js'
|
||||
export * from './video-embed-privacy.model.js'
|
||||
@@ -0,0 +1,5 @@
|
||||
export interface VideoEmbedPrivacyAllowed {
|
||||
domainAllowed: boolean
|
||||
|
||||
userBypassAllowed: boolean
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export const VideoEmbedPrivacyPolicy = {
|
||||
ALL_ALLOWED: 1,
|
||||
ALLOWLIST: 2,
|
||||
|
||||
// Federated server imposes restrictions on the embed
|
||||
REMOTE_RESTRICTIONS: 3
|
||||
} as const
|
||||
|
||||
export type VideoEmbedPrivacyPolicyType = typeof VideoEmbedPrivacyPolicy[keyof typeof VideoEmbedPrivacyPolicy]
|
||||
@@ -0,0 +1,7 @@
|
||||
import { VideoEmbedPrivacyPolicyType } from './video-embed-privacy-policy.enum.js'
|
||||
|
||||
export interface VideoEmbedPrivacyUpdate {
|
||||
policy: VideoEmbedPrivacyPolicyType
|
||||
|
||||
domains: string[]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ConstantLabel } from '../../common/constant-label.model.js'
|
||||
import { VideoEmbedPrivacyPolicyType } from './video-embed-privacy-policy.enum.js'
|
||||
|
||||
export interface VideoEmbedPrivacy {
|
||||
policy: ConstantLabel<VideoEmbedPrivacyPolicyType>
|
||||
|
||||
domains: string[]
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { FileStorageType } from '../../common/file-storage.enum.js'
|
||||
import { VideoConstant } from '../video-constant.model.js'
|
||||
import { ConstantLabel } from '../../common/constant-label.model.js'
|
||||
import { VideoFileMetadata } from './video-file-metadata.model.js'
|
||||
|
||||
export interface VideoFile {
|
||||
id: number
|
||||
|
||||
resolution: VideoConstant<number>
|
||||
resolution: ConstantLabel<number>
|
||||
size: number // Bytes
|
||||
|
||||
width?: number
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { VideoConstant } from '../video-constant.model.js'
|
||||
import { ConstantLabel } from '../../common/constant-label.model.js'
|
||||
import { Video } from '../video.model.js'
|
||||
import { VideoImportStateType } from './video-import-state.enum.js'
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface VideoImport {
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
originallyPublishedAt?: string
|
||||
state: VideoConstant<VideoImportStateType>
|
||||
state: ConstantLabel<VideoImportStateType>
|
||||
error?: string
|
||||
|
||||
video?: Video & { tags: string[] }
|
||||
|
||||
@@ -13,6 +13,7 @@ export * from './stats/index.js'
|
||||
export * from './transcoding/index.js'
|
||||
export * from './channel-sync/index.js'
|
||||
export * from './chapter/index.js'
|
||||
export * from './embed-privacy/index.js'
|
||||
|
||||
export * from './nsfw-flag.enum.js'
|
||||
export * from './nsfw-policy.type.js'
|
||||
@@ -20,9 +21,8 @@ export * from './nsfw-policy.type.js'
|
||||
export * from './storyboard.model.js'
|
||||
export * from './thumbnail/index.js'
|
||||
|
||||
export * from './video-constant.model.js'
|
||||
export * from './video-create.model.js'
|
||||
export * from './video-create-update-common.model.js'
|
||||
export * from './video-create.model.js'
|
||||
|
||||
export * from './video-licence.enum.js'
|
||||
export * from './video-privacy.enum.js'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AccountSummary } from '../../actors/index.js'
|
||||
import { VideoChannelSummary } from '../channel/index.js'
|
||||
import { Thumbnail } from '../thumbnail/thumbnail.model.js'
|
||||
import { VideoConstant } from '../video-constant.model.js'
|
||||
import { ConstantLabel } from '../../common/constant-label.model.js'
|
||||
import { VideoPlaylistPrivacyType } from './video-playlist-privacy.model.js'
|
||||
import { VideoPlaylistType_Type } from './video-playlist-type.model.js'
|
||||
|
||||
@@ -16,7 +16,7 @@ export interface VideoPlaylist {
|
||||
|
||||
displayName: string
|
||||
description: string
|
||||
privacy: VideoConstant<VideoPlaylistPrivacyType>
|
||||
privacy: ConstantLabel<VideoPlaylistPrivacyType>
|
||||
|
||||
/**
|
||||
* @deprecated in 8.1, use thumbnails array instead
|
||||
@@ -31,7 +31,7 @@ export interface VideoPlaylist {
|
||||
|
||||
videosLength: number
|
||||
|
||||
type: VideoConstant<VideoPlaylistType_Type>
|
||||
type: ConstantLabel<VideoPlaylistType_Type>
|
||||
|
||||
embedPath: string
|
||||
embedUrl?: string
|
||||
|
||||
@@ -22,9 +22,11 @@ export interface VideoCreateUpdateCommon {
|
||||
|
||||
waitTranscoding?: boolean
|
||||
channelId?: number
|
||||
|
||||
thumbnailfile?: Blob
|
||||
// TODO: remove in v10, deprecated in 8.1
|
||||
previewfile?: Blob
|
||||
|
||||
scheduleUpdate?: VideoScheduleUpdate
|
||||
originallyPublishedAt?: Date | string
|
||||
videoPasswords?: string[]
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { VideoFileMetadata } from './file/index.js'
|
||||
import { VideoConstant } from './video-constant.model.js'
|
||||
import { ConstantLabel } from '../common/constant-label.model.js'
|
||||
|
||||
export interface VideoSource {
|
||||
inputFilename: string
|
||||
|
||||
resolution?: VideoConstant<number>
|
||||
resolution?: ConstantLabel<number>
|
||||
size?: number // Bytes
|
||||
|
||||
width?: number
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Account, AccountSummary } from '../actors/index.js'
|
||||
import { VideoChannel, VideoChannelSummary } from './channel/video-channel.model.js'
|
||||
import { VideoCommentPolicyType } from './comment/video-comment-policy.enum.js'
|
||||
import { VideoEmbedPrivacyPolicyType } from './embed-privacy/video-embed-privacy-policy.enum.js'
|
||||
import { VideoFile } from './file/index.js'
|
||||
import { LiveVideoScheduleEdit } from './live/live-video-schedule.model.js'
|
||||
import { Thumbnail } from './thumbnail/thumbnail.model.js'
|
||||
import { VideoConstant } from './video-constant.model.js'
|
||||
import { ConstantLabel } from '../common/constant-label.model.js'
|
||||
import { VideoPrivacyType } from './video-privacy.enum.js'
|
||||
import { VideoScheduleUpdate } from './video-schedule-update.model.js'
|
||||
import { VideoSource } from './video-source.model.js'
|
||||
@@ -20,10 +21,10 @@ export interface Video extends Partial<VideoAdditionalAttributes> {
|
||||
updatedAt: Date | string
|
||||
publishedAt: Date | string
|
||||
originallyPublishedAt: Date | string
|
||||
category: VideoConstant<number>
|
||||
licence: VideoConstant<number>
|
||||
language: VideoConstant<string>
|
||||
privacy: VideoConstant<VideoPrivacyType>
|
||||
category: ConstantLabel<number>
|
||||
licence: ConstantLabel<number>
|
||||
language: ConstantLabel<string>
|
||||
privacy: ConstantLabel<VideoPrivacyType>
|
||||
|
||||
// Deprecated in 5.0 in favour of truncatedDescription
|
||||
description: string
|
||||
@@ -89,7 +90,7 @@ export interface Video extends Partial<VideoAdditionalAttributes> {
|
||||
// Not included by default, needs query params
|
||||
export interface VideoAdditionalAttributes {
|
||||
waitTranscoding: boolean
|
||||
state: VideoConstant<VideoStateType>
|
||||
state: ConstantLabel<VideoStateType>
|
||||
scheduledUpdate: VideoScheduleUpdate
|
||||
|
||||
blacklisted: boolean
|
||||
@@ -123,7 +124,7 @@ export interface VideoDetails extends Video {
|
||||
|
||||
// Not optional in details (unlike in parent Video)
|
||||
waitTranscoding: boolean
|
||||
state: VideoConstant<VideoStateType>
|
||||
state: ConstantLabel<VideoStateType>
|
||||
|
||||
trackerUrls: string[]
|
||||
|
||||
@@ -131,4 +132,6 @@ export interface VideoDetails extends Video {
|
||||
streamingPlaylists: VideoStreamingPlaylist[]
|
||||
|
||||
inputFileUpdatedAt: string | Date
|
||||
|
||||
embedPrivacyPolicy: ConstantLabel<VideoEmbedPrivacyPolicyType>
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
ServicesCommand,
|
||||
StoryboardCommand,
|
||||
StreamingPlaylistsCommand,
|
||||
VideoEmbedPrivacyCommand,
|
||||
VideoImportsCommand,
|
||||
VideoPasswordsCommand,
|
||||
VideoStatsCommand,
|
||||
@@ -152,6 +153,7 @@ export class PeerTubeServer {
|
||||
videoStudio?: VideoStudioCommand
|
||||
videos?: VideosCommand
|
||||
videoStats?: VideoStatsCommand
|
||||
videoEmbedPrivacy?: VideoEmbedPrivacyCommand
|
||||
views?: ViewsCommand
|
||||
twoFactor?: TwoFactorCommand
|
||||
videoToken?: VideoTokenCommand
|
||||
@@ -462,6 +464,7 @@ export class PeerTubeServer {
|
||||
this.videos = new VideosCommand(this)
|
||||
this.videoStudio = new VideoStudioCommand(this)
|
||||
this.videoStats = new VideoStatsCommand(this)
|
||||
this.videoEmbedPrivacy = new VideoEmbedPrivacyCommand(this)
|
||||
this.views = new ViewsCommand(this)
|
||||
this.twoFactor = new TwoFactorCommand(this)
|
||||
this.videoToken = new VideoTokenCommand(this)
|
||||
|
||||
@@ -16,6 +16,7 @@ export * from './playlists-command.js'
|
||||
export * from './services-command.js'
|
||||
export * from './storyboard-command.js'
|
||||
export * from './streaming-playlists-command.js'
|
||||
export * from './video-embed-privacy-command.js'
|
||||
export * from './comments-command.js'
|
||||
export * from './video-studio-command.js'
|
||||
export * from './video-token-command.js'
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { HttpStatusCode, VideoEmbedPrivacy, VideoEmbedPrivacyAllowed, VideoEmbedPrivacyUpdate } from '@peertube/peertube-models'
|
||||
import { AbstractCommand, OverrideCommandOptions } from '../shared/index.js'
|
||||
|
||||
export class VideoEmbedPrivacyCommand extends AbstractCommand {
|
||||
get (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
}
|
||||
) {
|
||||
const { videoId } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/embed-privacy'
|
||||
|
||||
return this.getRequestBody<VideoEmbedPrivacy>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
isDomainAllowed (
|
||||
options: OverrideCommandOptions & {
|
||||
videoId: number | string
|
||||
domain: string
|
||||
}
|
||||
) {
|
||||
const { videoId } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/embed-privacy/allowed'
|
||||
|
||||
return this.getRequestBody<VideoEmbedPrivacyAllowed>({
|
||||
...options,
|
||||
|
||||
path,
|
||||
query: { domain: options.domain },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.OK_200
|
||||
})
|
||||
}
|
||||
|
||||
update (
|
||||
options: OverrideCommandOptions & VideoEmbedPrivacyUpdate & {
|
||||
videoId: number | string
|
||||
}
|
||||
) {
|
||||
const { videoId, policy, domains } = options
|
||||
const path = '/api/v1/videos/' + videoId + '/embed-privacy'
|
||||
|
||||
return this.putBodyRequest({
|
||||
...options,
|
||||
path,
|
||||
fields: { policy, domains },
|
||||
implicitToken: true,
|
||||
defaultExpectedStatus: HttpStatusCode.NO_CONTENT_204
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ import './channel-syncs.js'
|
||||
import './channels.js'
|
||||
import './video-chapters.js'
|
||||
import './video-comments.js'
|
||||
import './video-embed-privacy.js'
|
||||
import './video-files.js'
|
||||
import './video-imports.js'
|
||||
import './video-playlists.js'
|
||||
|
||||
@@ -0,0 +1,249 @@
|
||||
import { HttpStatusCode, VideoCreateResult, VideoEmbedPrivacyPolicy } from '@peertube/peertube-models'
|
||||
import {
|
||||
PeerTubeServer,
|
||||
cleanupTests,
|
||||
createSingleServer,
|
||||
setAccessTokensToServers,
|
||||
setDefaultVideoChannel
|
||||
} from '@peertube/peertube-server-commands'
|
||||
|
||||
describe('Test video embed privacy validator', function () {
|
||||
let server: PeerTubeServer
|
||||
let video: VideoCreateResult
|
||||
|
||||
let ownerAccessToken: string
|
||||
let userAccessToken: string
|
||||
let editorAccessToken: string
|
||||
let invitedEditorAccessToken: string
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
before(async function () {
|
||||
this.timeout(60000)
|
||||
|
||||
server = await createSingleServer(1)
|
||||
|
||||
await setAccessTokensToServers([ server ])
|
||||
await setDefaultVideoChannel([ server ])
|
||||
|
||||
ownerAccessToken = await server.users.generateUserAndToken('owner')
|
||||
userAccessToken = await server.users.generateUserAndToken('user1')
|
||||
editorAccessToken = await server.channelCollaborators.createEditor('accepted_editor', 'owner_channel')
|
||||
invitedEditorAccessToken = await server.channelCollaborators.createInvited('invited_editor', 'owner_channel')
|
||||
|
||||
video = await server.videos.upload({ token: ownerAccessToken })
|
||||
})
|
||||
|
||||
describe('When getting embed privacy', function () {
|
||||
it('Should fail without a valid uuid', async function () {
|
||||
await server.videoEmbedPrivacy.get({ videoId: '4da6fd', expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
|
||||
})
|
||||
|
||||
it('Should fail with an unknown id', async function () {
|
||||
await server.videoEmbedPrivacy.get({
|
||||
videoId: 'ce0801ef-7124-48df-9b22-b473ace78797',
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without access token', async function () {
|
||||
await server.videoEmbedPrivacy.get({
|
||||
videoId: video.id,
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad access token', async function () {
|
||||
await server.videoEmbedPrivacy.get({
|
||||
videoId: video.id,
|
||||
token: 'toto',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with another user access token', async function () {
|
||||
await server.videoEmbedPrivacy.get({
|
||||
videoId: video.id,
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invited editor access token', async function () {
|
||||
await server.videoEmbedPrivacy.get({
|
||||
videoId: video.id,
|
||||
token: invitedEditorAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with correct params', async function () {
|
||||
for (const token of [ server.accessToken, ownerAccessToken, editorAccessToken ]) {
|
||||
await server.videoEmbedPrivacy.get({ videoId: video.id, token })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('When checking if embed is allowed on a domain', function () {
|
||||
it('Should fail without a valid uuid', async function () {
|
||||
await server.videoEmbedPrivacy.isDomainAllowed({
|
||||
videoId: '4da6fd',
|
||||
domain: 'example.com',
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown id', async function () {
|
||||
await server.videoEmbedPrivacy.isDomainAllowed({
|
||||
videoId: 'ce0801ef-7124-48df-9b22-b473ace78797',
|
||||
domain: 'example.com',
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid domain', async function () {
|
||||
await server.videoEmbedPrivacy.isDomainAllowed({
|
||||
videoId: video.id,
|
||||
domain: '',
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with correct params', async function () {
|
||||
await server.videoEmbedPrivacy.isDomainAllowed({ videoId: video.id, domain: 'example.com' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('When updating embed privacy', function () {
|
||||
it('Should fail without a valid uuid', async function () {
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: '4da6fd',
|
||||
policy: VideoEmbedPrivacyPolicy.ALL_ALLOWED,
|
||||
domains: [],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an unknown id', async function () {
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: 'ce0801ef-7124-48df-9b22-b473ace78797',
|
||||
policy: VideoEmbedPrivacyPolicy.ALL_ALLOWED,
|
||||
domains: [],
|
||||
expectedStatus: HttpStatusCode.NOT_FOUND_404
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail without access token', async function () {
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: video.id,
|
||||
policy: VideoEmbedPrivacyPolicy.ALL_ALLOWED,
|
||||
domains: [],
|
||||
token: null,
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with a bad access token', async function () {
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: video.id,
|
||||
policy: VideoEmbedPrivacyPolicy.ALL_ALLOWED,
|
||||
domains: [],
|
||||
token: 'toto',
|
||||
expectedStatus: HttpStatusCode.UNAUTHORIZED_401
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with another user access token', async function () {
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: video.id,
|
||||
policy: VideoEmbedPrivacyPolicy.ALL_ALLOWED,
|
||||
domains: [],
|
||||
token: userAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invited editor access token', async function () {
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: video.id,
|
||||
policy: VideoEmbedPrivacyPolicy.ALL_ALLOWED,
|
||||
domains: [],
|
||||
token: invitedEditorAccessToken,
|
||||
expectedStatus: HttpStatusCode.FORBIDDEN_403
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with an invalid policy', async function () {
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: video.id,
|
||||
policy: 999 as any,
|
||||
domains: [],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: video.id,
|
||||
policy: VideoEmbedPrivacyPolicy.REMOTE_RESTRICTIONS,
|
||||
domains: [],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with missing policy', async function () {
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: video.id,
|
||||
policy: undefined as any,
|
||||
domains: [],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with invalid domains', async function () {
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: video.id,
|
||||
policy: VideoEmbedPrivacyPolicy.ALL_ALLOWED,
|
||||
domains: 'example.com' as any,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: video.id,
|
||||
policy: VideoEmbedPrivacyPolicy.ALL_ALLOWED,
|
||||
domains: [ 'http://example.com' ],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with missing domains', async function () {
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: video.id,
|
||||
policy: VideoEmbedPrivacyPolicy.ALL_ALLOWED,
|
||||
domains: undefined as any,
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should fail with inconsistent policy', async function () {
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: video.id,
|
||||
policy: VideoEmbedPrivacyPolicy.ALL_ALLOWED,
|
||||
domains: [ 'example.com' ],
|
||||
expectedStatus: HttpStatusCode.BAD_REQUEST_400
|
||||
})
|
||||
})
|
||||
|
||||
it('Should succeed with correct params', async function () {
|
||||
const policy = VideoEmbedPrivacyPolicy.ALLOWLIST
|
||||
const domains = [ 'example.com' ]
|
||||
|
||||
for (const token of [ server.accessToken, ownerAccessToken, editorAccessToken ]) {
|
||||
await server.videoEmbedPrivacy.update({ videoId: video.id, token, policy, domains })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests([ server ])
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
HttpStatusCode,
|
||||
HttpStatusCodeType,
|
||||
LiveVideoError,
|
||||
VideoEmbedPrivacyPolicy,
|
||||
VideoPrivacy,
|
||||
VideoPrivacyType,
|
||||
VideoState,
|
||||
@@ -61,6 +62,7 @@ describe('Save replay setting', function () {
|
||||
thumbnailfile: options.thumbnailfile
|
||||
}
|
||||
})
|
||||
|
||||
return uuid
|
||||
}
|
||||
|
||||
@@ -293,6 +295,13 @@ describe('Save replay setting', function () {
|
||||
|
||||
liveVideoUUID = await createLiveWrapper({ permanent: false, replay: true, replaySettings: { privacy: VideoPrivacy.UNLISTED } })
|
||||
|
||||
await servers[0].playerSettings.updateForVideo({ theme: 'lucide', videoId: liveVideoUUID })
|
||||
await servers[0].videoEmbedPrivacy.update({
|
||||
videoId: liveVideoUUID,
|
||||
domains: [ 'example.com' ],
|
||||
policy: VideoEmbedPrivacyPolicy.ALLOWLIST
|
||||
})
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
|
||||
@@ -335,6 +344,13 @@ describe('Save replay setting', function () {
|
||||
await checkVideoState(liveVideoUUID, VideoState.PUBLISHED)
|
||||
await checkVideoPrivacy(liveVideoUUID, VideoPrivacy.UNLISTED)
|
||||
await checkVideoTags(liveVideoUUID, [ 'tag1', 'tag2' ])
|
||||
|
||||
const playerSettings = await servers[0].playerSettings.getForVideo({ videoId: liveVideoUUID })
|
||||
expect(playerSettings.theme).to.equal('lucide')
|
||||
|
||||
const videoEmbedPrivacy = await servers[0].videoEmbedPrivacy.get({ videoId: liveVideoUUID })
|
||||
expect(videoEmbedPrivacy.policy.id).to.equal(VideoEmbedPrivacyPolicy.ALLOWLIST)
|
||||
expect(videoEmbedPrivacy.domains).to.deep.equal([ 'example.com' ])
|
||||
})
|
||||
|
||||
it('Should find the replay live session', async function () {
|
||||
@@ -436,6 +452,13 @@ describe('Save replay setting', function () {
|
||||
thumbnailfile: 'custom-thumbnail-input.jpg'
|
||||
})
|
||||
|
||||
await servers[0].playerSettings.updateForVideo({ theme: 'lucide', videoId: liveVideoUUID })
|
||||
await servers[0].videoEmbedPrivacy.update({
|
||||
videoId: liveVideoUUID,
|
||||
domains: [ 'example.com' ],
|
||||
policy: VideoEmbedPrivacyPolicy.ALLOWLIST
|
||||
})
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
await checkVideosExist(liveVideoUUID, 0, HttpStatusCode.OK_200)
|
||||
@@ -483,6 +506,13 @@ describe('Save replay setting', function () {
|
||||
await servers[1].videos.get({ id: lastReplayUUID, expectedStatus: HttpStatusCode.OK_200 })
|
||||
|
||||
await checkVideoTags(lastReplayUUID, [ 'tag1', 'tag2' ])
|
||||
|
||||
const playerSettings = await servers[0].playerSettings.getForVideo({ videoId: lastReplayUUID })
|
||||
expect(playerSettings.theme).to.equal('lucide')
|
||||
|
||||
const videoEmbedPrivacy = await servers[0].videoEmbedPrivacy.get({ videoId: lastReplayUUID })
|
||||
expect(videoEmbedPrivacy.policy.id).to.equal(VideoEmbedPrivacyPolicy.ALLOWLIST)
|
||||
expect(videoEmbedPrivacy.domains).to.deep.equal([ 'example.com' ])
|
||||
})
|
||||
|
||||
it('Should have appropriate ended session and replay live session', async function () {
|
||||
|
||||
@@ -155,9 +155,6 @@ function runTest (withObjectStorage: boolean) {
|
||||
await waitJobs([ server ])
|
||||
})
|
||||
|
||||
it('Should not export collaborations', async function () {
|
||||
})
|
||||
|
||||
it('Should have received an email on archive creation', async function () {
|
||||
const email = emails.find(e => {
|
||||
return e['to'][0]['address'] === 'admin' + server.internalServerNumber + '@example.com' &&
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
UserNotificationSettingValue,
|
||||
VideoCommentPolicy,
|
||||
VideoCreateResult,
|
||||
VideoEmbedPrivacyPolicy,
|
||||
VideoPlaylistPrivacy,
|
||||
VideoPlaylistType,
|
||||
VideoPrivacy,
|
||||
@@ -510,6 +511,7 @@ function runTest (withObjectStorage: boolean) {
|
||||
await remoteServer.videos.get({ id: liveVideo.uuid, expectedStatus: HttpStatusCode.UNAUTHORIZED_401 })
|
||||
const video = await remoteServer.videos.getWithPassword({ id: liveVideo.uuid, password: 'password1' })
|
||||
const live = await remoteServer.live.get({ videoId: liveVideo.uuid, token: remoteNoahToken })
|
||||
const liveEmbedPrivacy = await remoteServer.videoEmbedPrivacy.get({ videoId: liveVideo.uuid, token: remoteNoahToken })
|
||||
|
||||
expect(video.isLive).to.be.true
|
||||
expect(live.latencyMode).to.equal(LiveVideoLatencyMode.SMALL_LATENCY)
|
||||
@@ -527,6 +529,9 @@ function runTest (withObjectStorage: boolean) {
|
||||
expect(video.streamingPlaylists).to.have.lengthOf(0)
|
||||
|
||||
expect(video.state.id).to.equal(VideoState.WAITING_FOR_LIVE)
|
||||
|
||||
expect(liveEmbedPrivacy.policy.id).to.equal(VideoEmbedPrivacyPolicy.ALLOWLIST)
|
||||
expect(liveEmbedPrivacy.domains).to.deep.equal([ 'example.com' ])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
VideoChannelActivityAction,
|
||||
VideoChannelActivityTarget,
|
||||
VideoCreateResult,
|
||||
VideoEmbedPrivacyPolicy,
|
||||
VideoImport,
|
||||
VideoPlaylistCreateResult,
|
||||
VideoPlaylistElementCreateResult,
|
||||
@@ -350,6 +351,21 @@ describe('Test channel activities', function () {
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
})
|
||||
|
||||
it('Should update embed privacy', async function () {
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: video.id,
|
||||
policy: VideoEmbedPrivacyPolicy.ALLOWLIST,
|
||||
domains: [ 'example.com' ],
|
||||
token: editorToken
|
||||
})
|
||||
})
|
||||
|
||||
expect(a.account.name).to.equal('editor')
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.UPDATE_EMBED_POLICY)
|
||||
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 })
|
||||
@@ -390,6 +406,88 @@ describe('Test channel activities', function () {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Ownership changes', function () {
|
||||
let video: VideoCreateResult
|
||||
let receiverToken: string
|
||||
|
||||
before(async function () {
|
||||
video = await server.videos.quickUpload({ name: 'video for ownership change', channelId })
|
||||
receiverToken = await server.users.generateUserAndToken('receiver')
|
||||
})
|
||||
|
||||
it('Should send an ownership change request', async function () {
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.changeOwnership.create({ username: 'receiver', videoId: video.id })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.SEND_OWNERSHIP_REQUEST)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
|
||||
expect(a.account.name).to.equal('root')
|
||||
|
||||
expect(a.video.name).to.equal('video for ownership change')
|
||||
|
||||
expect(a.targetAccount.username).to.equal('receiver')
|
||||
expect(a.targetAccount.url).to.equal(server.url + '/accounts/receiver')
|
||||
expect(a.targetAccount.displayName).to.equal('receiver')
|
||||
})
|
||||
|
||||
it('Should refuse an ownership change request', async function () {
|
||||
const { data } = await server.changeOwnership.list({ token: receiverToken })
|
||||
|
||||
const a = await getActivityAfterAction(() => {
|
||||
return server.changeOwnership.refuse({ ownershipId: data[0].id, token: receiverToken })
|
||||
})
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.REFUSE_OWNERSHIP_REQUEST)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
|
||||
expect(a.account.name).to.equal('receiver')
|
||||
|
||||
expect(a.video.name).to.equal('video for ownership change')
|
||||
|
||||
expect(a.targetAccount.username).to.equal('receiver')
|
||||
expect(a.targetAccount.url).to.equal(server.url + '/accounts/receiver')
|
||||
expect(a.targetAccount.displayName).to.equal('receiver')
|
||||
})
|
||||
|
||||
it('Should accept an ownership change request', async function () {
|
||||
await server.changeOwnership.create({ username: 'receiver', videoId: video.id })
|
||||
const { data } = await server.changeOwnership.list({ token: receiverToken })
|
||||
|
||||
const a = await getActivityAfterAction(async () => {
|
||||
return server.changeOwnership.accept({
|
||||
ownershipId: data[0].id,
|
||||
channelId: await server.channels.getDefaultId({ token: receiverToken }),
|
||||
token: receiverToken
|
||||
})
|
||||
})
|
||||
|
||||
{
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.ACCEPT_OWNERSHIP_REQUEST)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
|
||||
expect(a.account.name).to.equal('receiver')
|
||||
|
||||
expect(a.video.name).to.equal('video for ownership change')
|
||||
|
||||
expect(a.targetAccount.username).to.equal('receiver')
|
||||
expect(a.targetAccount.url).to.equal(server.url + '/accounts/receiver')
|
||||
expect(a.targetAccount.displayName).to.equal('receiver')
|
||||
}
|
||||
|
||||
{
|
||||
const { data } = await server.channels.listActivities({ channelName: 'receiver_channel', sort: '-createdAt' })
|
||||
const a = data[0]
|
||||
|
||||
expect(a.action.id).to.equal(VideoChannelActivityAction.ACCEPT_OWNERSHIP_REQUEST)
|
||||
expect(a.targetType.id).to.equal(VideoChannelActivityTarget.VIDEO)
|
||||
|
||||
expect(a.video.name).to.equal('video for ownership change')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('Lives', async function () {
|
||||
let liveVideo: VideoCreateResult
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import './video-channels.js'
|
||||
import './video-chapters.js'
|
||||
import './video-comments.js'
|
||||
import './video-description.js'
|
||||
import './video-embed-privacy.js'
|
||||
import './video-files.js'
|
||||
import './video-imports.js'
|
||||
import './video-nsfw.js'
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
|
||||
|
||||
import { VideoEmbedPrivacy, VideoEmbedPrivacyPolicy } from '@peertube/peertube-models'
|
||||
import {
|
||||
cleanupTests,
|
||||
createMultipleServers,
|
||||
doubleFollow,
|
||||
PeerTubeServer,
|
||||
setAccessTokensToServers,
|
||||
waitJobs
|
||||
} from '@peertube/peertube-server-commands'
|
||||
import { expect } from 'chai'
|
||||
|
||||
describe('Test video embed privacy', function () {
|
||||
let servers: PeerTubeServer[]
|
||||
let videoId: string
|
||||
let userToken: string
|
||||
|
||||
before(async function () {
|
||||
this.timeout(120000)
|
||||
|
||||
servers = await createMultipleServers(2)
|
||||
await setAccessTokensToServers(servers)
|
||||
|
||||
const { uuid } = await servers[0].videos.quickUpload({ name: 'video' })
|
||||
videoId = uuid
|
||||
|
||||
userToken = await servers[0].users.generateUserAndToken('user1')
|
||||
|
||||
await doubleFollow(servers[0], servers[1])
|
||||
})
|
||||
|
||||
it('Should get default video embed privacy', async function () {
|
||||
const policy = { id: VideoEmbedPrivacyPolicy.ALL_ALLOWED, label: 'All allowed' }
|
||||
|
||||
{
|
||||
const body = await servers[0].videoEmbedPrivacy.get({ videoId })
|
||||
expect(body).to.deep.equal({ policy, domains: [] } satisfies VideoEmbedPrivacy)
|
||||
}
|
||||
|
||||
{
|
||||
const video = await servers[0].videos.get({ id: videoId })
|
||||
expect(video.embedPrivacyPolicy).to.deep.equal(policy)
|
||||
}
|
||||
|
||||
{
|
||||
const result = await servers[0].videoEmbedPrivacy.isDomainAllowed({ videoId, domain: 'toto.example.com' })
|
||||
expect(result).to.deep.equal({ domainAllowed: true, userBypassAllowed: null })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should update video embed privacy to allowlist', async function () {
|
||||
await servers[0].videoEmbedPrivacy.update({
|
||||
videoId,
|
||||
policy: VideoEmbedPrivacyPolicy.ALLOWLIST,
|
||||
domains: [ 'example.com' ]
|
||||
})
|
||||
|
||||
const policy = { id: VideoEmbedPrivacyPolicy.ALLOWLIST, label: 'Allowlist' }
|
||||
|
||||
{
|
||||
const body = await servers[0].videoEmbedPrivacy.get({ videoId })
|
||||
expect(body).to.deep.equal({ policy, domains: [ 'example.com' ] } satisfies VideoEmbedPrivacy)
|
||||
}
|
||||
|
||||
{
|
||||
const video = await servers[0].videos.get({ id: videoId })
|
||||
expect(video.embedPrivacyPolicy).to.deep.equal(policy)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should have federated video embed privacy', async function () {
|
||||
await waitJobs(servers)
|
||||
|
||||
const video = await servers[1].videos.get({ id: videoId })
|
||||
expect(video.embedPrivacyPolicy.id).to.equal(VideoEmbedPrivacyPolicy.REMOTE_RESTRICTIONS)
|
||||
})
|
||||
|
||||
it('Should check if embed is allowed on a domain', async function () {
|
||||
for (const server of servers) {
|
||||
{
|
||||
const result = await server.videoEmbedPrivacy.isDomainAllowed({ videoId, domain: 'toto.example.com' })
|
||||
expect(result).to.deep.equal({ domainAllowed: false, userBypassAllowed: true })
|
||||
}
|
||||
|
||||
{
|
||||
const result = await server.videoEmbedPrivacy.isDomainAllowed({ videoId, domain: 'toto.example.com', token: null })
|
||||
expect(result).to.deep.equal({ domainAllowed: false, userBypassAllowed: false })
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const result = await servers[0].videoEmbedPrivacy.isDomainAllowed({ videoId, domain: 'toto.example.com', token: userToken })
|
||||
expect(result).to.deep.equal({ domainAllowed: false, userBypassAllowed: false })
|
||||
}
|
||||
|
||||
{
|
||||
const result = await servers[0].videoEmbedPrivacy.isDomainAllowed({ videoId, domain: 'example.com' })
|
||||
expect(result).to.deep.equal({ domainAllowed: true, userBypassAllowed: null })
|
||||
}
|
||||
|
||||
// Only server 1 knows which domain is allowed
|
||||
{
|
||||
const result = await servers[1].videoEmbedPrivacy.isDomainAllowed({ videoId, domain: 'example.com' })
|
||||
expect(result).to.deep.equal({ domainAllowed: false, userBypassAllowed: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('Should add some domains to video embed privacy', async function () {
|
||||
await servers[0].videoEmbedPrivacy.update({
|
||||
videoId,
|
||||
policy: VideoEmbedPrivacyPolicy.ALLOWLIST,
|
||||
domains: [ 'example.com', 'example2.com' ]
|
||||
})
|
||||
|
||||
const policy = { id: VideoEmbedPrivacyPolicy.ALLOWLIST, label: 'Allowlist' }
|
||||
|
||||
{
|
||||
const body = await servers[0].videoEmbedPrivacy.get({ videoId })
|
||||
expect(body).to.deep.equal({ policy, domains: [ 'example.com', 'example2.com' ] } satisfies VideoEmbedPrivacy)
|
||||
}
|
||||
})
|
||||
|
||||
it('Should remove video embed privacy restriction', async function () {
|
||||
await servers[0].videoEmbedPrivacy.update({
|
||||
videoId,
|
||||
policy: VideoEmbedPrivacyPolicy.ALL_ALLOWED,
|
||||
domains: []
|
||||
})
|
||||
|
||||
const policy = { id: VideoEmbedPrivacyPolicy.ALL_ALLOWED, label: 'All allowed' }
|
||||
|
||||
{
|
||||
const body = await servers[0].videoEmbedPrivacy.get({ videoId })
|
||||
expect(body).to.deep.equal({ policy, domains: [] } satisfies VideoEmbedPrivacy)
|
||||
}
|
||||
|
||||
await waitJobs(servers)
|
||||
|
||||
{
|
||||
const video = await servers[1].videos.get({ id: videoId })
|
||||
expect(video.embedPrivacyPolicy.id).to.equal(VideoEmbedPrivacyPolicy.ALL_ALLOWED)
|
||||
}
|
||||
})
|
||||
|
||||
after(async function () {
|
||||
await cleanupTests(servers)
|
||||
})
|
||||
})
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
UserNotificationSettingValue,
|
||||
VideoCommentObject,
|
||||
VideoCommentPolicy,
|
||||
VideoEmbedPrivacyPolicy,
|
||||
VideoObject,
|
||||
VideoPlaylistPrivacy,
|
||||
VideoPrivacy
|
||||
@@ -348,6 +349,13 @@ export async function prepareImportExportTests (options: {
|
||||
token: noahToken
|
||||
})
|
||||
|
||||
await server.videoEmbedPrivacy.update({
|
||||
videoId: noahLive.uuid,
|
||||
policy: VideoEmbedPrivacyPolicy.ALLOWLIST,
|
||||
domains: [ 'example.com' ],
|
||||
token: noahToken
|
||||
})
|
||||
|
||||
// Views
|
||||
await server.views.view({ id: noahVideo.uuid, token: noahToken, currentTime: 4 })
|
||||
await server.views.view({ id: externalVideo.uuid, token: noahToken, currentTime: 2 })
|
||||
@@ -375,6 +383,8 @@ export async function prepareImportExportTests (options: {
|
||||
|
||||
await waitJobs([ server, remoteServer ])
|
||||
|
||||
await server.channelCollaborators.addEditor({ channel: 'root_channel', editorToken: noahToken, editor: 'noah' })
|
||||
|
||||
const { data: noahVideos } = await server.videos.listMyVideos({ token: noahToken, sort: '-publishedAt' })
|
||||
const noahVODNames = noahVideos.filter(v => !v.isLive).map(v => v.name)
|
||||
|
||||
|
||||
@@ -503,6 +503,7 @@ export async function checkThumbnails (options: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
await testImageGeneratedByFFmpeg({
|
||||
name: thumbnails.find(t => t.width === 280 && t.height === 157).filename,
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
url: server.url + entity.thumbnailPath
|
||||
})
|
||||
|
||||
@@ -512,6 +513,7 @@ export async function checkThumbnails (options: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
await testImageGeneratedByFFmpeg({
|
||||
name: preview.filename,
|
||||
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
||||
url: server.url + video.previewPath
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,19 +10,22 @@ if [ ! -z ${2+x} ] && [ "$2" = "--ar-locale" ]; then
|
||||
clientConfiguration="ar-locale"
|
||||
fi
|
||||
|
||||
embedCommand="cd client/src/standalone/player && npm run dev"
|
||||
playerCommand="cd client/src/standalone/player && npm run dev"
|
||||
embedCommand="cd client && ./node_modules/.bin/vite -c ./src/standalone/videos/vite.config.mjs dev"
|
||||
clientCommand="cd client && NODE_OPTIONS=--max_old_space_size=8192 node_modules/.bin/ng serve --proxy-config proxy.config.json --hmr --configuration $clientConfiguration --host 0.0.0.0 --port 3000"
|
||||
serverCommand="ANGULAR_CLIENT_ENABLED=true NODE_ENV=dev node dist/server"
|
||||
|
||||
if [ ! -z ${1+x} ] && [ "$1" = "--skip-server" ]; then
|
||||
node_modules/.bin/concurrently -k \
|
||||
"$embedCommand" \
|
||||
"$clientCommand"
|
||||
"$playerCommand" \
|
||||
"$clientCommand" \
|
||||
"$embedCommand"
|
||||
else
|
||||
npm run build:server
|
||||
|
||||
node_modules/.bin/concurrently -k \
|
||||
"$embedCommand" \
|
||||
"$playerCommand" \
|
||||
"$clientCommand" \
|
||||
"$embedCommand" \
|
||||
"$serverCommand"
|
||||
fi
|
||||
|
||||
@@ -130,7 +130,8 @@ Object.values(VIDEO_CATEGORIES)
|
||||
'By {1}',
|
||||
'Unavailable video',
|
||||
'Audio only',
|
||||
'Unknown'
|
||||
'Unknown',
|
||||
'This video is not allowed to be embedded on this domain.'
|
||||
])
|
||||
.forEach(v => {
|
||||
serverKeys[v] = v
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
HttpStatusCode,
|
||||
UserRight,
|
||||
VideoChannelActivityAction,
|
||||
VideoEmbedPrivacy,
|
||||
VideoEmbedPrivacyAllowed,
|
||||
VideoEmbedPrivacyPolicy,
|
||||
VideoEmbedPrivacyUpdate
|
||||
} from '@peertube/peertube-models'
|
||||
import { getAuthUser } from '@server/helpers/express-utils.js'
|
||||
import { logger, loggerTagsFactory } from '@server/helpers/logger.js'
|
||||
import { VIDEO_EMBED_PRIVACY_POLICIES } from '@server/initializers/constants.js'
|
||||
import { federateVideoIfNeeded } from '@server/lib/activitypub/videos/index.js'
|
||||
import { checkCanManageVideo } from '@server/middlewares/validators/shared/videos.js'
|
||||
import { VideoChannelActivityModel } from '@server/models/video/video-channel-activity.js'
|
||||
import { VideoEmbedPrivacyDomainModel } from '@server/models/video/video-embed-privacy-domain.js'
|
||||
import { VideoPasswordModel } from '@server/models/video/video-password.js'
|
||||
import { VideoModel } from '@server/models/video/video.js'
|
||||
import express from 'express'
|
||||
import { Transaction } from 'sequelize'
|
||||
import { asyncMiddleware, authenticate, optionalAuthenticate } from '../../../middlewares/index.js'
|
||||
import {
|
||||
getVideoEmbedPrivacyValidator,
|
||||
isVideoEmbedOnDomainAllowedValidator,
|
||||
updateVideoEmbedPrivacyValidator
|
||||
} from '../../../middlewares/validators/index.js'
|
||||
|
||||
const lTags = loggerTagsFactory('api', 'video', 'embed-privacy')
|
||||
const videoEmbedPrivacyRouter = express.Router()
|
||||
|
||||
videoEmbedPrivacyRouter.get(
|
||||
'/:videoId/embed-privacy',
|
||||
authenticate,
|
||||
asyncMiddleware(getVideoEmbedPrivacyValidator),
|
||||
asyncMiddleware(getVideoEmbedPrivacy)
|
||||
)
|
||||
|
||||
videoEmbedPrivacyRouter.put(
|
||||
'/:videoId/embed-privacy',
|
||||
authenticate,
|
||||
asyncMiddleware(updateVideoEmbedPrivacyValidator),
|
||||
asyncMiddleware(updateVideoEmbedPrivacy)
|
||||
)
|
||||
|
||||
videoEmbedPrivacyRouter.get(
|
||||
'/:videoId/embed-privacy/allowed',
|
||||
optionalAuthenticate,
|
||||
asyncMiddleware(isVideoEmbedOnDomainAllowedValidator),
|
||||
asyncMiddleware(isVideoEmbedOnDomainAllowed)
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export {
|
||||
videoEmbedPrivacyRouter
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function getVideoEmbedPrivacy (req: express.Request, res: express.Response) {
|
||||
const domains = await VideoEmbedPrivacyDomainModel.list(res.locals.videoAll.id)
|
||||
const video = res.locals.videoAll
|
||||
|
||||
return res.json(
|
||||
{
|
||||
policy: {
|
||||
id: video.embedPrivacyPolicy,
|
||||
label: VIDEO_EMBED_PRIVACY_POLICIES[video.embedPrivacyPolicy]
|
||||
},
|
||||
domains: domains.map(d => d.domain)
|
||||
} satisfies VideoEmbedPrivacy
|
||||
)
|
||||
}
|
||||
|
||||
async function isVideoEmbedOnDomainAllowed (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.onlyVideo
|
||||
|
||||
const domainAllowed = video.embedPrivacyPolicy === VideoEmbedPrivacyPolicy.ALL_ALLOWED
|
||||
? true
|
||||
: await VideoEmbedPrivacyDomainModel.isDomainAllowed(video.id, req.query.domain)
|
||||
|
||||
const user = getAuthUser(res)
|
||||
|
||||
let userBypassAllowed = domainAllowed === true
|
||||
? null
|
||||
: false
|
||||
|
||||
if (domainAllowed === false && user) {
|
||||
userBypassAllowed = await checkCanManageVideo({
|
||||
user,
|
||||
video: await VideoModel.loadFull(video.id),
|
||||
right: UserRight.UPDATE_ANY_VIDEO,
|
||||
checkIsOwner: false,
|
||||
checkIsLocal: false,
|
||||
req,
|
||||
res: null
|
||||
})
|
||||
}
|
||||
|
||||
return res.json({ domainAllowed, userBypassAllowed } satisfies VideoEmbedPrivacyAllowed)
|
||||
}
|
||||
|
||||
async function updateVideoEmbedPrivacy (req: express.Request, res: express.Response) {
|
||||
const video = res.locals.videoAll
|
||||
const body = req.body as VideoEmbedPrivacyUpdate
|
||||
|
||||
await VideoPasswordModel.sequelize.transaction(async (t: Transaction) => {
|
||||
video.embedPrivacyPolicy = body.policy
|
||||
|
||||
await video.save({ transaction: t })
|
||||
|
||||
await VideoEmbedPrivacyDomainModel.deleteAllDomains(video.id, t)
|
||||
await VideoEmbedPrivacyDomainModel.addDomains(body.domains, video.id, t)
|
||||
|
||||
await VideoChannelActivityModel.addVideoActivity({
|
||||
action: VideoChannelActivityAction.UPDATE_EMBED_POLICY,
|
||||
user: res.locals.oauth.token.User,
|
||||
channel: video.VideoChannel,
|
||||
video,
|
||||
transaction: t
|
||||
})
|
||||
|
||||
await federateVideoIfNeeded(video, false, t)
|
||||
})
|
||||
|
||||
logger.info(`Video embed policy for video with name ${video.name} and uuid ${video.uuid} have been updated`, lTags(video.uuid))
|
||||
|
||||
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user