"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-extraneous-class": "off",
+ "@typescript-eslint/no-use-before-define": "off",
// bugged but useful
"@typescript-eslint/restrict-plus-operands": "off"
},
blank_issues_enabled: false
contact_links:
- - name: 💬 IRC
- url: https://kiwiirc.com/client/irc.freenode.net/#peertube
- about: Chat with us via IRC for quick Q/A here
- name: 💬 Matrix
url: https://matrix.to/#/#peertube:matrix.org
about: Chat with us via Matrix for quick Q/A here
+ - name: 💬 IRC
+ url: https://kiwiirc.com/client/irc.freenode.net/#peertube
+ about: Chat with us via IRC for quick Q/A here
- name: 🤷💻🤦 Forum
url: https://framacolibri.org/c/peertube
about: You can ask and answer other questions here
### Features
- * :tada: Most robust uploads using a resumable upload endpoint [#3933](https://github.com/Chocobozzz/PeerTube/pull/3933)
+ * :tada: More robust uploads using a resumable upload endpoint [#3933](https://github.com/Chocobozzz/PeerTube/pull/3933)
* Accessibility/UI:
* :tada: Redesign channel and account page
* :tada: Increase video miniature size
<p align=center>
<strong><a href="https://joinpeertube.org">Website</a></strong>
- | <strong><a href="https://instances.joinpeertube.org">Join an instance</a></strong>
+ | <strong><a href="https://joinpeertube.org/instances">Join an instance</a></strong>
| <strong><a href="#package-create-your-own-instance">Create an instance</a></strong>
| <strong><a href="#contact">Chat with us</a></strong>
| <strong><a href="https://framasoft.org/en/#soutenir">Donate</a></strong>
PeerTube is a free, decentralized and federated video platform developed as an alternative to other platforms that centralize our data and attention, such as YouTube, Dailymotion or Vimeo. :clapper:
-But one organization hosting PeerTube alone may not have enough money to pay for bandwidth and video storage of its servers,
-all servers of PeerTube are interoperable as a federated network, and non-PeerTube servers can be part of the larger Vidiverse
-(federated video network) by talking our implementation of ActivityPub.
-Video load is reduced thanks to P2P in the web browser using <a href="https://github.com/webtorrent/webtorrent">WebTorrent</a> or <a href="https://github.com/novage/p2p-media-loader">p2p-media-loader</a>.
-
-To learn more, see:
+To learn more:
* This [two-minute video](https://framatube.org/videos/watch/217eefeb-883d-45be-b7fc-a788ad8507d3) (hosted on PeerTube) explaining what PeerTube is and how it works
* PeerTube's project homepage, [joinpeertube.org](https://joinpeertube.org)
* Demonstration instances:
- * [peertube.cpy.re](https://peertube.cpy.re)
- * [peertube2.cpy.re](https://peertube2.cpy.re)
- * [peertube3.cpy.re](https://peertube3.cpy.re)
+ * [peertube.cpy.re](https://peertube.cpy.re) (stable)
+ * [peertube2.cpy.re](https://peertube2.cpy.re) (Nightly)
+ * [peertube3.cpy.re](https://peertube3.cpy.re) (RC)
* This [video](https://peertube.cpy.re/videos/watch/da2b08d4-a242-4170-b32a-4ec8cbdca701) demonstrating the communication between PeerTube and [Mastodon](https://github.com/tootsuite/mastodon) (a decentralized Twitter alternative)
:sparkles: Features
----------------------------------------------------------------
+<p align=center>
+ <strong><a href="https://joinpeertube.org/faq#what-are-the-peertube-features-for-viewers">All features for viewers</a></strong>
+ | <strong><a href="https://joinpeertube.org/faq#what-are-the-peertube-features-for-content-creators">All features for content creators</a></strong>
+ | <strong><a href="https://joinpeertube.org/faq#what-are-the-peertube-features-for-administrators">All features for administrators</a></strong>
+</p>
+
<img src="https://lutim.cpy.re/AHbctLjn.png" align="left" height="300px"/>
<h3 align="left">Video streaming, even in live!</h3>
<p align="left">
Content creators can get help from their viewers in the simplest way possible: a support button showing a message linking to their donation accounts or really anything else. No more pay-per-view and advertisements that hurt visitors and <strike>incentivize</strike> alter creativity (more about that in our <a href="https://github.com/Chocobozzz/PeerTube/blob/develop/FAQ.md">FAQ</a>).
</p>
+
+
:raised_hands: Contributing
----------------------------------------------------------------
You can also join the cheerful bunch that makes our community:
* Chat<a name="contact"></a>:
- * IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)**
* Matrix (bridged on IRC and [Discord](https://discord.gg/wj8DDUT)) : **[#peertube:matrix.org](https://matrix.to/#/#peertube:matrix.org)**
+ * IRC : **[#peertube on chat.freenode.net:6697](https://kiwiirc.com/client/irc.freenode.net/#peertube)**
* Forum:
* Framacolibri: [https://framacolibri.org/c/peertube](https://framacolibri.org/c/peertube)
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier } from '@app/core'
import { PluginService } from '@app/core/plugins/plugin.service'
import { compareSemVer } from '@shared/core-utils/miscs/miscs'
-import { PeerTubePlugin } from '@shared/models/plugins/peertube-plugin.model'
-import { PluginType } from '@shared/models/plugins/plugin.type'
+import { PeerTubePlugin, PluginType } from '@shared/models'
@Component({
selector: 'my-plugin-list-installed',
import { ActivatedRoute, Router } from '@angular/router'
import { PluginApiService } from '@app/+admin/plugins/shared/plugin-api.service'
import { ComponentPagination, ConfirmService, hasMoreItems, Notifier, PluginService } from '@app/core'
-import { PeerTubePluginIndex } from '@shared/models/plugins/peertube-plugin-index.model'
-import { PluginType } from '@shared/models/plugins/plugin.type'
+import { PeerTubePluginIndex, PluginType } from '@shared/models'
@Component({
selector: 'my-plugin-search',
import { FormReactiveValidationMessages, FormValidatorService } from '@app/shared/shared-forms'
import { InstanceService } from '@app/shared/shared-instance'
import { VideoCaptionEdit, VideoEdit, VideoService } from '@app/shared/shared-main'
-import { LiveVideo, ServerConfig, VideoConstant, VideoDetails, VideoPrivacy } from '@shared/models'
-import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
+import {
+ LiveVideo,
+ RegisterClientFormFieldOptions,
+ RegisterClientVideoFieldOptions,
+ ServerConfig,
+ VideoConstant,
+ VideoDetails,
+ VideoPrivacy
+} from '@shared/models'
import { I18nPrimengCalendarService } from './i18n-primeng-calendar.service'
import { VideoCaptionAddModalComponent } from './video-caption-add-modal.component'
import { VideoEditType } from './video-edit.type'
import { FormValidatorService } from '@app/shared/shared-forms'
import { VideoCaptionService, VideoEdit, VideoImportService, VideoService } from '@app/shared/shared-main'
import { LoadingBarService } from '@ngx-loading-bar/core'
-import { VideoPrivacy, VideoUpdate } from '@shared/models'
+import { ServerErrorCode, VideoPrivacy, VideoUpdate } from '@shared/models'
import { hydrateFormFromVideo } from '../shared/video-edit-utils'
import { VideoSend } from './video-send'
this.loadingBar.useRef().complete()
this.isImportingVideo = false
this.firstStepError.emit()
- this.notifier.error(err.message)
+
+ let message = err.message
+ if (err.body?.code === ServerErrorCode.INCORRECT_FILES_IN_TORRENT) {
+ message = $localize`Torrents with only 1 file are supported.`
+ }
+
+ this.notifier.error(message)
}
)
}
<ng-container *ngIf="secondStepType === 'upload'" i18n>Upload {{ videoName }}</ng-container>
</div>
- <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [ngClass]="{ 'hide-nav': secondStepType !== undefined }">
- <ng-container ngbNavItem>
+ <div ngbNav #nav="ngbNav" class="nav-tabs video-add-nav" [activeId]="activeNav" (activeIdChange)="onNavChange($event)" [ngClass]="{ 'hide-nav': !!secondStepType }">
+ <ng-container ngbNavItem="upload">
<a ngbNavLink>
<span i18n>Upload a file</span>
</a>
</ng-template>
</ng-container>
- <ng-container ngbNavItem *ngIf="isVideoImportHttpEnabled()">
+ <ng-container ngbNavItem="import-url" *ngIf="isVideoImportHttpEnabled()">
<a ngbNavLink>
<span i18n>Import with URL</span>
</a>
</ng-template>
</ng-container>
- <ng-container ngbNavItem *ngIf="isVideoImportTorrentEnabled()">
+ <ng-container ngbNavItem="import-torrent" *ngIf="isVideoImportTorrentEnabled()">
<a ngbNavLink>
<span i18n>Import with torrent</span>
</a>
</ng-template>
</ng-container>
- <ng-container ngbNavItem *ngIf="isVideoLiveEnabled()">
+ <ng-container ngbNavItem="go-live" *ngIf="isVideoLiveEnabled()">
<a ngbNavLink>
<span i18n>Go live</span>
</a>
import { Component, HostListener, OnInit, ViewChild } from '@angular/core'
+import { ActivatedRoute, Router } from '@angular/router'
import { AuthService, AuthUser, CanComponentDeactivate, ServerService } from '@app/core'
import { ServerConfig } from '@shared/models'
import { VideoEditType } from './shared/video-edit.type'
secondStepType: VideoEditType
videoName: string
- serverConfig: ServerConfig
+
+ activeNav: string
+
+ private serverConfig: ServerConfig
constructor (
private auth: AuthService,
- private serverService: ServerService
+ private serverService: ServerService,
+ private route: ActivatedRoute,
+ private router: Router
) {}
get userInformationLoaded () {
.subscribe(config => this.serverConfig = config)
this.user = this.auth.getUser()
+
+ if (this.route.snapshot.fragment) {
+ this.onNavChange(this.route.snapshot.fragment)
+ }
+ }
+
+ onNavChange (newActiveNav: string) {
+ this.activeNav = newActiveNav
+
+ this.router.navigate([], { fragment: this.activeNav })
}
onFirstStepDone (type: VideoEditType, videoName: string) {
private route: ActivatedRoute,
private router: Router,
private auth: AuthService,
- private serverService: ServerService
+ private serverService: ServerService,
+ private redirectService: RedirectService
) {
super(data)
this.algorithmChangeSub = this.route.queryParams.subscribe(
queryParams => {
- const algorithm = queryParams['alg']
- if (algorithm) {
- this.data.model = algorithm
- } else {
- this.data.model = RedirectService.DEFAULT_TRENDING_ALGORITHM
- }
+ this.data.model = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
}
)
}
}
setSort () {
- const alg = this.data.model !== RedirectService.DEFAULT_TRENDING_ALGORITHM
+ const alg = this.data.model !== this.redirectService.getDefaultTrendingAlgorithm()
? this.data.model
: undefined
protected storageService: LocalStorageService,
protected cfr: ComponentFactoryResolver,
private videoService: VideoService,
+ private redirectService: RedirectService,
private hooks: HooksService
) {
super()
- this.defaultSort = this.parseAlgorithm(RedirectService.DEFAULT_TRENDING_ALGORITHM)
+ this.defaultSort = this.parseAlgorithm(this.redirectService.getDefaultTrendingAlgorithm())
this.headerComponentInjector = this.getInjector()
}
}
protected loadPageRouteParams (queryParams: Params) {
- const algorithm = queryParams['alg'] || RedirectService.DEFAULT_TRENDING_ALGORITHM
+ const algorithm = queryParams['alg'] || this.redirectService.getDefaultTrendingAlgorithm()
this.sort = this.parseAlgorithm(algorithm)
}
switch (algorithm) {
case 'most-viewed':
return '-trending'
+
case 'most-liked':
return '-likes'
+
default:
return '-' + algorithm as VideoSortField
}
}
goToDefaultRoute () {
- return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE)
+ return this.router.navigateByUrl(this.redirectService.getDefaultRoute())
}
ngOnInit () {
export class RedirectService {
// Default route could change according to the instance configuration
static INIT_DEFAULT_ROUTE = '/videos/trending'
- static DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
static INIT_DEFAULT_TRENDING_ALGORITHM = 'most-viewed'
- static DEFAULT_TRENDING_ALGORITHM = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
private previousUrl: string
private currentUrl: string
private redirectingToHomepage = false
+ private defaultTrendingAlgorithm = RedirectService.INIT_DEFAULT_TRENDING_ALGORITHM
+ private defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
constructor (
private router: Router,
// The config is first loaded from the cache so try to get the default route
const tmpConfig = this.serverService.getTmpConfig()
if (tmpConfig?.instance?.defaultClientRoute) {
- RedirectService.DEFAULT_ROUTE = tmpConfig.instance.defaultClientRoute
+ this.defaultRoute = tmpConfig.instance.defaultClientRoute
}
if (tmpConfig?.trending?.videos?.algorithms?.default) {
- RedirectService.DEFAULT_TRENDING_ALGORITHM = tmpConfig.trending.videos.algorithms.default
+ this.defaultTrendingAlgorithm = tmpConfig.trending.videos.algorithms.default
}
// Load default route
const defaultRouteConfig = config.instance.defaultClientRoute
const defaultTrendingConfig = config.trending.videos.algorithms.default
- if (defaultRouteConfig) {
- RedirectService.DEFAULT_ROUTE = defaultRouteConfig
- }
-
- if (defaultTrendingConfig) {
- RedirectService.DEFAULT_TRENDING_ALGORITHM = defaultTrendingConfig
- }
+ if (defaultRouteConfig) this.defaultRoute = defaultRouteConfig
+ if (defaultTrendingConfig) this.defaultTrendingAlgorithm = defaultTrendingConfig
})
// Track previous url
})
}
+ getDefaultRoute () {
+ return this.defaultRoute
+ }
+
+ getDefaultTrendingAlgorithm () {
+ return this.defaultTrendingAlgorithm
+ }
+
redirectToPreviousRoute () {
const exceptions = [
'/verify-account',
this.redirectingToHomepage = true
- console.log('Redirecting to %s...', RedirectService.DEFAULT_ROUTE)
+ console.log('Redirecting to %s...', this.defaultRoute)
- this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange })
+ this.router.navigateByUrl(this.defaultRoute, { skipLocationChange })
.then(() => this.redirectingToHomepage = false)
.catch(() => {
this.redirectingToHomepage = false
console.error(
'Cannot navigate to %s, resetting default route to %s.',
- RedirectService.DEFAULT_ROUTE,
+ this.defaultRoute,
RedirectService.INIT_DEFAULT_ROUTE
)
- RedirectService.DEFAULT_ROUTE = RedirectService.INIT_DEFAULT_ROUTE
- return this.router.navigateByUrl(RedirectService.DEFAULT_ROUTE, { skipLocationChange })
+ this.defaultRoute = RedirectService.INIT_DEFAULT_ROUTE
+ return this.router.navigateByUrl(this.defaultRoute, { skipLocationChange })
})
}
import { HttpClient } from '@angular/common/http'
import { Inject, Injectable, LOCALE_ID } from '@angular/core'
import { getDevLocale, isOnDevLocale, sortBy } from '@app/helpers'
-import { peertubeLocalStorage } from '@root-helpers/peertube-web-storage'
import { getCompleteLocale, isDefaultLocale, peertubeTranslate } from '@shared/core-utils/i18n'
import { SearchTargetType, ServerConfig, ServerStats, VideoConstant } from '@shared/models'
import { environment } from '../../../environments/environment'
private static BASE_LOCALE_URL = environment.apiUrl + '/client/locales/'
private static BASE_STATS_URL = environment.apiUrl + '/api/v1/server/stats'
- private static CONFIG_LOCAL_STORAGE_KEY = 'server-config'
-
configReloaded = new Subject<ServerConfig>()
private localeObservable: Observable<any>
if (!this.configObservable) {
this.configObservable = this.http.get<ServerConfig>(ServerService.BASE_CONFIG_URL)
.pipe(
- tap(config => this.saveConfigLocally(config)),
tap(config => {
this.config = config
this.configLoaded = true
)
}
- private saveConfigLocally (config: ServerConfig) {
- peertubeLocalStorage.setItem(ServerService.CONFIG_LOCAL_STORAGE_KEY, JSON.stringify(config))
- }
-
private loadConfigLocally () {
- const configString = peertubeLocalStorage.getItem(ServerService.CONFIG_LOCAL_STORAGE_KEY)
-
- if (configString) {
- try {
- const parsed = JSON.parse(configString)
- Object.assign(this.config, parsed)
- } catch (err) {
- console.error('Cannot parse config saved in local storage.', err)
- }
+ const configString = window['PeerTubeServerConfig']
+ if (!configString) return
+
+ try {
+ const parsed = JSON.parse(configString)
+ Object.assign(this.config, parsed)
+ } catch (err) {
+ console.error('Cannot parse config saved in from index.html.', err)
}
}
}
: this.userService.getAnonymousUser().theme
if (theme !== 'instance-default') return theme
- return this.serverConfig.theme.default
+
+ const instanceTheme = this.serverConfig.theme.default
+ if (instanceTheme !== 'default') return instanceTheme
+
+ // Default to dark theme if available and wanted by the user
+ if (
+ this.themes.find(t => t.name === 'dark') &&
+ window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
+ ) {
+ return 'dark'
+ }
+
+ return instanceTheme
}
private loadTheme (name: string) {
<!-- description tag -->
<!-- custom css tag -->
<!-- meta tags -->
+ <!-- server config -->
<!-- /!\ Do not remove it /!\ -->
</head>
import { RegisterClientHelpers } from 'src/types/register-client-option.model'
import { getHookType, internalRunHook } from '@shared/core-utils/plugins/hooks'
-import { RegisterClientFormFieldOptions, RegisterClientVideoFieldOptions } from '@shared/models/plugins/register-client-form-field.model'
import {
ClientHookName,
clientHookObject,
ClientScript,
PluginType,
+ RegisterClientFormFieldOptions,
RegisterClientHookOptions,
- ServerConfigPlugin,
- RegisterClientSettingsScript
+ RegisterClientSettingsScript,
+ RegisterClientVideoFieldOptions,
+ ServerConfigPlugin
} from '../../../shared/models'
import { ClientScript as ClientScriptModule } from '../types/client-script.model'
import { importModule } from './utils'
<!DOCTYPE html>
<html>
<head>
- <!-- title tag -->
-
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex">
<meta property="og:platform" content="PeerTube" />
+
+ <!-- /!\ The following comment is used by the server to prerender some tags /!\ -->
+
+ <!-- title tag -->
+ <!-- description tag -->
<!-- custom css tag -->
+ <!-- meta tags -->
+ <!-- server config -->
+
+ <!-- /!\ Do not remove it /!\ -->
+
<link rel="icon" type="image/png" href="/client/assets/images/favicon.png" />
</head>
import './embed.scss'
import videojs from 'video.js'
import { peertubeTranslate } from '../../../../shared/core-utils/i18n'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import {
+ ClientHookName,
+ HTMLServerConfig,
+ PluginType,
ResultList,
- ServerConfig,
UserRefreshToken,
VideoCaption,
VideoDetails,
VideoPlaylist,
VideoPlaylistElement,
- VideoStreamingPlaylistType,
- PluginType,
- ClientHookName
+ VideoStreamingPlaylistType
} from '../../../../shared/models'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import { P2PMediaLoaderOptions, PeertubePlayerManagerOptions, PlayerMode } from '../../assets/player/peertube-player-manager'
import { VideoJSCaption } from '../../assets/player/peertube-videojs-typings'
import { TranslationsManager } from '../../assets/player/translations-manager'
+import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage'
import { Hooks, loadPlugin, runHook } from '../../root-helpers/plugins'
import { Tokens } from '../../root-helpers/users'
-import { peertubeLocalStorage } from '../../root-helpers/peertube-web-storage'
import { objectToUrlEncoded } from '../../root-helpers/utils'
-import { PeerTubeEmbedApi } from './embed-api'
import { RegisterClientHelpers } from '../../types/register-client-option.model'
+import { PeerTubeEmbedApi } from './embed-api'
type Translations = { [ id: string ]: string }
CLIENT_SECRET: 'client_secret'
}
+ config: HTMLServerConfig
+
private translationsPromise: Promise<{ [id: string]: string }>
- private configPromise: Promise<ServerConfig>
private PeertubePlayerManagerModulePromise: Promise<any>
private playlist: VideoPlaylist
constructor (private videoWrapperId: string) {
this.wrapperElement = document.getElementById(this.videoWrapperId)
+
+ try {
+ this.config = JSON.parse(window['PeerTubeServerConfig'])
+ } catch (err) {
+ console.error('Cannot parse HTML config.', err)
+ }
}
getVideoUrl (id: string) {
return this.refreshFetch(url.toString(), { headers: this.headers })
}
- loadConfig (): Promise<ServerConfig> {
- return this.refreshFetch('/api/v1/config')
- .then(res => res.json())
- }
-
removeElement (element: HTMLElement) {
element.parentElement.removeChild(element)
}
this.playerElement.setAttribute('playsinline', 'true')
this.wrapperElement.appendChild(this.playerElement)
+ // Issue when we parsed config from HTML, fallback to API
+ if (!this.config) {
+ this.config = await this.refreshFetch('/api/v1/config')
+ .then(res => res.json())
+ }
+
const videoInfoPromise = videoResponse.json()
.then((videoInfo: VideoDetails) => {
if (!alreadyHadPlayer) this.loadPlaceholder(videoInfo)
return videoInfo
})
- const [ videoInfoTmp, serverTranslations, captionsResponse, config, PeertubePlayerManagerModule ] = await Promise.all([
+ const [ videoInfoTmp, serverTranslations, captionsResponse, PeertubePlayerManagerModule ] = await Promise.all([
videoInfoPromise,
this.translationsPromise,
captionsPromise,
- this.configPromise,
this.PeertubePlayerManagerModulePromise
])
- await this.ensurePluginsAreLoaded(config, serverTranslations)
+ await this.ensurePluginsAreLoaded(serverTranslations)
const videoInfo: VideoDetails = videoInfoTmp
this.buildCSS()
- await this.buildDock(videoInfo, config)
+ await this.buildDock(videoInfo)
this.initializeApi()
private async initCore () {
if (this.userTokens) this.setHeadersFromTokens()
- this.configPromise = this.loadConfig()
this.translationsPromise = TranslationsManager.getServerTranslations(window.location.origin, navigator.language)
this.PeertubePlayerManagerModulePromise = import('../../assets/player/peertube-player-manager')
}
}
- private async buildDock (videoInfo: VideoDetails, config: ServerConfig) {
+ private async buildDock (videoInfo: VideoDetails) {
if (!this.controls) return
// On webtorrent fallback, player may have been disposed
const title = this.title ? videoInfo.name : undefined
- const description = config.tracker.enabled && this.warningTitle
+ const description = this.config.tracker.enabled && this.warningTitle
? '<span class="text">' + peertubeTranslate('Watching this video may reveal your IP address to others.') + '</span>'
: undefined
return window.location.pathname.split('/')[1] === 'video-playlists'
}
- private async ensurePluginsAreLoaded (config: ServerConfig, translations?: { [ id: string ]: string }) {
- if (config.plugin.registered.length === 0) return
+ private async ensurePluginsAreLoaded (translations?: { [ id: string ]: string }) {
+ if (this.config.plugin.registered.length === 0) return
- for (const plugin of config.plugin.registered) {
+ for (const plugin of this.config.plugin.registered) {
for (const key of Object.keys(plugin.clientScripts)) {
const clientScript = plugin.clientScripts[key]
import { getDurationFromVideoFile, getVideoFileBitrate, getVideoFileFPS, getVideoFileResolution } from '../server/helpers/ffprobe-utils'
import { getMaxBitrate } from '../shared/models/videos'
import { VideoModel } from '../server/models/video/video'
-import { optimizeOriginalVideofile } from '../server/lib/video-transcoding'
+import { optimizeOriginalVideofile } from '../server/lib/transcoding/video-transcoding'
import { initDatabaseModels } from '../server/initializers/database'
import { basename, dirname } from 'path'
import { copy, move, remove } from 'fs-extra'
import * as ffmpeg from 'fluent-ffmpeg'
import { buildx264VODCommand, runCommand, TranscodeOptions } from '@server/helpers/ffmpeg-utils'
import { exit } from 'process'
-import { VideoTranscodingProfilesManager } from '@server/lib/video-transcoding-profiles'
+import { VideoTranscodingProfilesManager } from '@server/lib/transcoding/video-transcoding-profiles'
program
.arguments('<path>')
import * as Bluebird from 'bluebird'
import { getUUIDFromFilename } from '../server/helpers/utils'
import { ThumbnailModel } from '../server/models/video/thumbnail'
-import { ActorImageModel } from '../server/models/account/actor-image'
+import { ActorImageModel } from '../server/models/actor/actor-image'
import { uniq, values } from 'lodash'
import { ThumbnailType } from '@shared/models'
import * as program from 'commander'
import { initDatabaseModels } from '../server/initializers/database'
-import { UserModel } from '../server/models/account/user'
+import { UserModel } from '../server/models/user/user'
import { isUserPasswordValid } from '../server/helpers/custom-validators/users'
program
registerTSPaths()
import { WEBSERVER } from '../server/initializers/constants'
-import { ActorFollowModel } from '../server/models/activitypub/actor-follow'
+import { ActorFollowModel } from '../server/models/actor/actor-follow'
import { VideoModel } from '../server/models/video/video'
-import { ActorModel } from '../server/models/activitypub/actor'
+import { ActorModel } from '../server/models/actor/actor'
import {
getLocalAccountActivityPubUrl,
getLocalVideoActivityPubUrl,
import { videoPlaylistElementAPGetValidator, videoPlaylistsGetValidator } from '../../middlewares/validators/videos/video-playlists'
import { AccountModel } from '../../models/account/account'
import { AccountVideoRateModel } from '../../models/account/account-video-rate'
-import { ActorFollowModel } from '../../models/activitypub/actor-follow'
+import { ActorFollowModel } from '../../models/actor/actor-follow'
import { VideoModel } from '../../models/video/video'
import { VideoCaptionModel } from '../../models/video/video-caption'
import { VideoCommentModel } from '../../models/video/video-comment'
function activityPubResponse (data: any, res: express.Response) {
return res.type('application/activity+json; charset=utf-8')
.json(data)
- .end()
}
export {
const auditLogger = auditLoggerFactory('config')
configRouter.get('/about', getAbout)
+
configRouter.get('/',
asyncMiddleware(getConfig)
)
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
getCustomConfig
)
+
configRouter.put('/custom',
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
customConfigUpdateValidator,
asyncMiddleware(updateCustomConfig)
)
+
configRouter.delete('/custom',
authenticate,
ensureUserHasRight(UserRight.MANAGE_CONFIGURATION),
}
}
- return res.json(about).end()
+ return res.json(about)
}
function getCustomConfig (req: express.Request, res: express.Response) {
const data = customConfig()
- return res.json(data).end()
+ return res.json(data)
}
async function deleteCustomConfig (req: express.Request, res: express.Response) {
import * as express from 'express'
-import { getFormattedObjects } from '../../helpers/utils'
+import { logger } from '@server/helpers/logger'
+import { getFormattedObjects } from '@server/helpers/utils'
+import { listAvailablePluginsFromIndex } from '@server/lib/plugins/plugin-index'
+import { PluginManager } from '@server/lib/plugins/plugin-manager'
import {
asyncMiddleware,
authenticate,
+ availablePluginsSortValidator,
ensureUserHasRight,
paginationValidator,
+ pluginsSortValidator,
setDefaultPagination,
setDefaultSort
-} from '../../middlewares'
-import { availablePluginsSortValidator, pluginsSortValidator } from '../../middlewares/validators'
-import { PluginModel } from '../../models/server/plugin'
-import { UserRight } from '../../../shared/models/users'
+} from '@server/middlewares'
import {
existingPluginValidator,
installOrUpdatePluginValidator,
listPluginsValidator,
uninstallPluginValidator,
updatePluginSettingsValidator
-} from '../../middlewares/validators/plugins'
-import { PluginManager } from '../../lib/plugins/plugin-manager'
-import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model'
-import { ManagePlugin } from '../../../shared/models/plugins/manage-plugin.model'
-import { logger } from '../../helpers/logger'
-import { listAvailablePluginsFromIndex } from '../../lib/plugins/plugin-index'
-import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
-import { RegisteredServerSettings } from '../../../shared/models/plugins/register-server-setting.model'
-import { PublicServerSetting } from '../../../shared/models/plugins/public-server.setting'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+} from '@server/middlewares/validators/plugins'
+import { PluginModel } from '@server/models/server/plugin'
+import { HttpStatusCode } from '@shared/core-utils'
+import {
+ InstallOrUpdatePlugin,
+ ManagePlugin,
+ PeertubePluginIndexList,
+ PublicServerSetting,
+ RegisteredServerSettings,
+ UserRight
+} from '@shared/models'
const pluginRouter = express.Router()
import * as express from 'express'
+import { getServerActor } from '@server/models/application/application'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import { UserRight } from '../../../../shared/models/users'
import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils'
import { SERVER_ACTOR_NAME } from '../../../initializers/constants'
+import { sequelizeTypescript } from '../../../initializers/database'
+import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow'
import { sendAccept, sendReject, sendUndoFollow } from '../../../lib/activitypub/send'
+import { JobQueue } from '../../../lib/job-queue'
+import { removeRedundanciesOfServer } from '../../../lib/redundancy'
import {
asyncMiddleware,
authenticate,
followingSortValidator,
followValidator,
getFollowerValidator,
- removeFollowingValidator,
- listFollowsValidator
+ listFollowsValidator,
+ removeFollowingValidator
} from '../../../middlewares/validators'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
-import { JobQueue } from '../../../lib/job-queue'
-import { removeRedundanciesOfServer } from '../../../lib/redundancy'
-import { sequelizeTypescript } from '../../../initializers/database'
-import { autoFollowBackIfNeeded } from '../../../lib/activitypub/follow'
-import { getServerActor } from '@server/models/application/application'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
const serverFollowsRouter = express.Router()
serverFollowsRouter.get('/following',
import 'multer'
import * as express from 'express'
import { logger } from '@server/helpers/logger'
-import { UserNotificationModel } from '@server/models/account/user-notification'
+import { UserNotificationModel } from '@server/models/user/user-notification'
import { getServerActor } from '@server/models/application/application'
import { UserRight } from '../../../../shared/models/users'
import { getFormattedObjects } from '../../../helpers/utils'
usersResetPasswordValidator,
usersVerifyEmailValidator
} from '../../../middlewares/validators'
-import { UserModel } from '../../../models/account/user'
+import { UserModel } from '../../../models/user/user'
import { meRouter } from './me'
import { myAbusesRouter } from './my-abuses'
import { myBlocklistRouter } from './my-blocklist'
const oldUserAuditView = new UserAuditView(userToUpdate.toFormattedJSON())
const roleChanged = body.role !== undefined && body.role !== userToUpdate.role
- if (body.password !== undefined) userToUpdate.password = body.password
- if (body.email !== undefined) userToUpdate.email = body.email
- if (body.emailVerified !== undefined) userToUpdate.emailVerified = body.emailVerified
- if (body.videoQuota !== undefined) userToUpdate.videoQuota = body.videoQuota
- if (body.videoQuotaDaily !== undefined) userToUpdate.videoQuotaDaily = body.videoQuotaDaily
- if (body.role !== undefined) userToUpdate.role = body.role
- if (body.adminFlags !== undefined) userToUpdate.adminFlags = body.adminFlags
- if (body.pluginAuth !== undefined) userToUpdate.pluginAuth = body.pluginAuth
+ const keysToUpdate: (keyof UserUpdate)[] = [
+ 'password',
+ 'email',
+ 'emailVerified',
+ 'videoQuota',
+ 'videoQuotaDaily',
+ 'role',
+ 'adminFlags',
+ 'pluginAuth'
+ ]
+
+ for (const key of keysToUpdate) {
+ if (body[key] !== undefined) userToUpdate.set(key, body[key])
+ }
const user = await userToUpdate.save()
import { updateAvatarValidator } from '../../../middlewares/validators/actor-image'
import { AccountModel } from '../../../models/account/account'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
-import { UserModel } from '../../../models/account/user'
+import { UserModel } from '../../../models/user/user'
import { VideoModel } from '../../../models/video/video'
import { VideoImportModel } from '../../../models/video/video-import'
+import { AttributesOnly } from '@shared/core-utils'
const auditLogger = auditLoggerFactory('users')
const user = res.locals.oauth.token.user
- if (body.password !== undefined) user.password = body.password
- if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
- if (body.webTorrentEnabled !== undefined) user.webTorrentEnabled = body.webTorrentEnabled
- if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
- if (body.autoPlayNextVideo !== undefined) user.autoPlayNextVideo = body.autoPlayNextVideo
- if (body.autoPlayNextVideoPlaylist !== undefined) user.autoPlayNextVideoPlaylist = body.autoPlayNextVideoPlaylist
- if (body.videosHistoryEnabled !== undefined) user.videosHistoryEnabled = body.videosHistoryEnabled
- if (body.videoLanguages !== undefined) user.videoLanguages = body.videoLanguages
- if (body.theme !== undefined) user.theme = body.theme
- if (body.noInstanceConfigWarningModal !== undefined) user.noInstanceConfigWarningModal = body.noInstanceConfigWarningModal
- if (body.noWelcomeModal !== undefined) user.noWelcomeModal = body.noWelcomeModal
+ const keysToUpdate: (keyof UserUpdateMe & keyof AttributesOnly<UserModel>)[] = [
+ 'password',
+ 'nsfwPolicy',
+ 'webTorrentEnabled',
+ 'autoPlayVideo',
+ 'autoPlayNextVideo',
+ 'autoPlayNextVideoPlaylist',
+ 'videosHistoryEnabled',
+ 'videoLanguages',
+ 'theme',
+ 'noInstanceConfigWarningModal',
+ 'noWelcomeModal'
+ ]
+
+ for (const key of keysToUpdate) {
+ if (body[key] !== undefined) user.set(key, body[key])
+ }
if (body.email !== undefined) {
if (CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
await sequelizeTypescript.transaction(async t => {
await user.save({ transaction: t })
- if (body.displayName !== undefined || body.description !== undefined) {
- const userAccount = await AccountModel.load(user.Account.id, t)
+ if (body.displayName === undefined && body.description === undefined) return
- if (body.displayName !== undefined) userAccount.name = body.displayName
- if (body.description !== undefined) userAccount.description = body.description
- await userAccount.save({ transaction: t })
+ const userAccount = await AccountModel.load(user.Account.id, t)
- await sendUpdateActor(userAccount, t)
- }
+ if (body.displayName !== undefined) userAccount.name = body.displayName
+ if (body.description !== undefined) userAccount.description = body.description
+ await userAccount.save({ transaction: t })
+
+ await sendUpdateActor(userAccount, t)
})
if (sendVerificationEmail === true) {
import { AccountBlocklistModel } from '../../../models/account/account-blocklist'
import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../../../lib/blocklist'
import { ServerBlocklistModel } from '../../../models/server/server-blocklist'
-import { UserNotificationModel } from '@server/models/account/user-notification'
+import { UserNotificationModel } from '@server/models/user/user-notification'
import { logger } from '@server/helpers/logger'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
userHistoryRemoveValidator
} from '../../../middlewares'
import { getFormattedObjects } from '../../../helpers/utils'
-import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
+import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
import { sequelizeTypescript } from '../../../initializers/database'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-import * as express from 'express'
import 'multer'
+import * as express from 'express'
+import { UserNotificationModel } from '@server/models/user/user-notification'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { UserNotificationSetting } from '../../../../shared/models/users'
+import { getFormattedObjects } from '../../../helpers/utils'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
setDefaultSort,
userNotificationsSortValidator
} from '../../../middlewares'
-import { getFormattedObjects } from '../../../helpers/utils'
-import { UserNotificationModel } from '../../../models/account/user-notification'
-import { meRouter } from './me'
import {
listUserNotificationsValidator,
markAsReadUserNotificationsValidator,
updateNotificationSettingsValidator
} from '../../../middlewares/validators/user-notifications'
-import { UserNotificationSetting } from '../../../../shared/models/users'
-import { UserNotificationSettingModel } from '../../../models/account/user-notification-setting'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { UserNotificationSettingModel } from '../../../models/user/user-notification-setting'
+import { meRouter } from './me'
const myNotificationsRouter = express.Router()
userSubscriptionsSortValidator,
videosSortValidator
} from '../../../middlewares/validators'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
import { VideoModel } from '../../../models/video/video'
const mySubscriptionsRouter = express.Router()
return res.json({ banner: banner.toFormattedJSON() })
}
+
async function updateVideoChannelAvatar (req: express.Request, res: express.Response) {
const avatarPhysicalFile = req.files['avatarfile'][0]
const videoChannel = res.locals.videoChannel
try {
await sequelizeTypescript.transaction(async t => {
- const sequelizeOptions = {
- transaction: t
- }
-
if (videoChannelInfoToUpdate.displayName !== undefined) videoChannelInstance.name = videoChannelInfoToUpdate.displayName
if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.description = videoChannelInfoToUpdate.description
}
}
- const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) as MChannelBannerAccountDefault
+ const videoChannelInstanceUpdated = await videoChannelInstance.save({ transaction: t }) as MChannelBannerAccountDefault
await sendUpdateActor(videoChannelInstanceUpdated, t)
auditLogger.update(
id: videoPlaylistCreated.id,
uuid: videoPlaylistCreated.uuid
}
- }).end()
+ })
}
async function updateVideoPlaylist (req: express.Request, res: express.Response) {
import * as express from 'express'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import { ResultList, ThreadsResultList, UserRight } from '../../../../shared/models'
-import { VideoCommentCreate } from '../../../../shared/models/videos/video-comment.model'
+import { VideoCommentCreate } from '../../../../shared/models/videos/comment/video-comment.model'
import { auditLoggerFactory, CommentAuditView, getAuditIdFromRes } from '../../../helpers/audit-logger'
import { getFormattedObjects } from '../../../helpers/utils'
import { sequelizeTypescript } from '../../../initializers/database'
import * as magnetUtil from 'magnet-uri'
import * as parseTorrent from 'parse-torrent'
import { join } from 'path'
+import { getEnabledResolutions } from '@server/lib/config'
import { setVideoTags } from '@server/lib/video'
+import { FilteredModelAttributes } from '@server/types'
import {
MChannelAccountDefault,
MThumbnail,
MVideoThumbnail,
MVideoWithBlacklistLight
} from '@server/types/models'
-import { MVideoImport, MVideoImportFormattable } from '@server/types/models/video/video-import'
-import { VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared'
+import { MVideoImportFormattable } from '@server/types/models/video/video-import'
+import { ServerErrorCode, VideoImportCreate, VideoImportState, VideoPrivacy, VideoState } from '../../../../shared'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import { ThumbnailType } from '../../../../shared/models/videos/thumbnail.type'
import { auditLoggerFactory, getAuditIdFromRes, VideoImportAuditView } from '../../../helpers/audit-logger'
import { moveAndProcessCaptionFile } from '../../../helpers/captions-utils'
import { isArray } from '../../../helpers/custom-validators/misc'
-import { createReqFiles } from '../../../helpers/express-utils'
+import { cleanUpReqFiles, createReqFiles } from '../../../helpers/express-utils'
import { logger } from '../../../helpers/logger'
import { getSecureTorrentName } from '../../../helpers/utils'
-import { getYoutubeDLInfo, getYoutubeDLSubs, YoutubeDLInfo } from '../../../helpers/youtube-dl'
+import { YoutubeDL, YoutubeDLInfo } from '../../../helpers/youtube-dl'
import { CONFIG } from '../../../initializers/config'
import { MIMETYPES } from '../../../initializers/constants'
import { sequelizeTypescript } from '../../../initializers/database'
let magnetUri: string
if (torrentfile) {
- torrentName = torrentfile.originalname
+ const result = await processTorrentOrAbortRequest(req, res, torrentfile)
+ if (!result) return
- // Rename the torrent to a secured name
- const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName))
- await move(torrentfile.path, newTorrentPath)
- torrentfile.path = newTorrentPath
-
- const buf = await readFile(torrentfile.path)
- const parsedTorrent = parseTorrent(buf)
-
- videoName = isArray(parsedTorrent.name) ? parsedTorrent.name[0] : parsedTorrent.name as string
+ videoName = result.name
+ torrentName = result.torrentName
} else {
- magnetUri = body.magnetUri
-
- const parsed = magnetUtil.decode(magnetUri)
- videoName = isArray(parsed.name) ? parsed.name[0] : parsed.name as string
+ const result = processMagnetURI(body)
+ magnetUri = result.magnetUri
+ videoName = result.name
}
const video = buildVideo(res.locals.videoChannel.id, body, { name: videoName })
const thumbnailModel = await processThumbnail(req, video)
const previewModel = await processPreview(req, video)
- const tags = body.tags || undefined
- const videoImportAttributes = {
- magnetUri,
- torrentName,
- state: VideoImportState.PENDING,
- userId: user.id
- }
const videoImport = await insertIntoDB({
video,
thumbnailModel,
previewModel,
videoChannel: res.locals.videoChannel,
- tags,
- videoImportAttributes,
- user
+ tags: body.tags || undefined,
+ user,
+ videoImportAttributes: {
+ magnetUri,
+ torrentName,
+ state: VideoImportState.PENDING,
+ userId: user.id
+ }
})
// Create job to import the video
const payload = {
- type: torrentfile ? 'torrent-file' as 'torrent-file' : 'magnet-uri' as 'magnet-uri',
+ type: torrentfile
+ ? 'torrent-file' as 'torrent-file'
+ : 'magnet-uri' as 'magnet-uri',
videoImportId: videoImport.id,
magnetUri
}
const targetUrl = body.targetUrl
const user = res.locals.oauth.token.User
+ const youtubeDL = new YoutubeDL(targetUrl, getEnabledResolutions('vod'))
+
// Get video infos
let youtubeDLInfo: YoutubeDLInfo
try {
- youtubeDLInfo = await getYoutubeDLInfo(targetUrl)
+ youtubeDLInfo = await youtubeDL.getYoutubeDLInfo()
} catch (err) {
logger.info('Cannot fetch information from import for URL %s.', targetUrl, { err })
previewModel = await processPreviewFromUrl(youtubeDLInfo.thumbnailUrl, video)
}
- const tags = body.tags || youtubeDLInfo.tags
- const videoImportAttributes = {
- targetUrl,
- state: VideoImportState.PENDING,
- userId: user.id
- }
const videoImport = await insertIntoDB({
video,
thumbnailModel,
previewModel,
videoChannel: res.locals.videoChannel,
- tags,
- videoImportAttributes,
- user
+ tags: body.tags || youtubeDLInfo.tags,
+ user,
+ videoImportAttributes: {
+ targetUrl,
+ state: VideoImportState.PENDING,
+ userId: user.id
+ }
})
// Get video subtitles
- try {
- const subtitles = await getYoutubeDLSubs(targetUrl)
-
- logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl)
-
- for (const subtitle of subtitles) {
- const videoCaption = new VideoCaptionModel({
- videoId: video.id,
- language: subtitle.language,
- filename: VideoCaptionModel.generateCaptionName(subtitle.language)
- }) as MVideoCaption
-
- // Move physical file
- await moveAndProcessCaptionFile(subtitle, videoCaption)
-
- await sequelizeTypescript.transaction(async t => {
- await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t)
- })
- }
- } catch (err) {
- logger.warn('Cannot get video subtitles.', { err })
- }
+ await processYoutubeSubtitles(youtubeDL, targetUrl, video.id)
// Create job to import the video
const payload = {
privacy: body.privacy || VideoPrivacy.PRIVATE,
duration: 0, // duration will be set by the import job
channelId: channelId,
- originallyPublishedAt: body.originallyPublishedAt || importData.originallyPublishedAt
+ originallyPublishedAt: body.originallyPublishedAt
+ ? new Date(body.originallyPublishedAt)
+ : importData.originallyPublishedAt
}
const video = new VideoModel(videoData)
video.url = getLocalVideoActivityPubUrl(video)
previewModel: MThumbnail
videoChannel: MChannelAccountDefault
tags: string[]
- videoImportAttributes: Partial<MVideoImport>
+ videoImportAttributes: FilteredModelAttributes<VideoImportModel>
user: MUser
}): Promise<MVideoImportFormattable> {
const { video, thumbnailModel, previewModel, videoChannel, tags, videoImportAttributes, user } = parameters
return videoImport
}
+
+async function processTorrentOrAbortRequest (req: express.Request, res: express.Response, torrentfile: Express.Multer.File) {
+ const torrentName = torrentfile.originalname
+
+ // Rename the torrent to a secured name
+ const newTorrentPath = join(CONFIG.STORAGE.TORRENTS_DIR, getSecureTorrentName(torrentName))
+ await move(torrentfile.path, newTorrentPath, { overwrite: true })
+ torrentfile.path = newTorrentPath
+
+ const buf = await readFile(torrentfile.path)
+ const parsedTorrent = parseTorrent(buf) as parseTorrent.Instance
+
+ if (parsedTorrent.files.length !== 1) {
+ cleanUpReqFiles(req)
+
+ res.status(HttpStatusCode.BAD_REQUEST_400)
+ .json({
+ code: ServerErrorCode.INCORRECT_FILES_IN_TORRENT,
+ error: 'Torrents with only 1 file are supported.'
+ })
+
+ return undefined
+ }
+
+ return {
+ name: extractNameFromArray(parsedTorrent.name),
+ torrentName
+ }
+}
+
+function processMagnetURI (body: VideoImportCreate) {
+ const magnetUri = body.magnetUri
+ const parsed = magnetUtil.decode(magnetUri)
+
+ return {
+ name: extractNameFromArray(parsed.name),
+ magnetUri
+ }
+}
+
+function extractNameFromArray (name: string | string[]) {
+ return isArray(name) ? name[0] : name
+}
+
+async function processYoutubeSubtitles (youtubeDL: YoutubeDL, targetUrl: string, videoId: number) {
+ try {
+ const subtitles = await youtubeDL.getYoutubeDLSubs()
+
+ logger.info('Will create %s subtitles from youtube import %s.', subtitles.length, targetUrl)
+
+ for (const subtitle of subtitles) {
+ const videoCaption = new VideoCaptionModel({
+ videoId,
+ language: subtitle.language,
+ filename: VideoCaptionModel.generateCaptionName(subtitle.language)
+ }) as MVideoCaption
+
+ // Move physical file
+ await moveAndProcessCaptionFile(subtitle, videoCaption)
+
+ await sequelizeTypescript.transaction(async t => {
+ await VideoCaptionModel.insertOrReplaceLanguage(videoCaption, t)
+ })
+ }
+ } catch (err) {
+ logger.warn('Cannot get video subtitles.', { err })
+ }
+}
import * as express from 'express'
-import { move } from 'fs-extra'
-import { extname } from 'path'
import toInt from 'validator/lib/toInt'
-import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload'
-import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
-import { changeVideoChannelShare } from '@server/lib/activitypub/share'
-import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
import { LiveManager } from '@server/lib/live-manager'
-import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
-import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
import { getServerActor } from '@server/models/application/application'
-import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
-import { uploadx } from '@uploadx/core'
-import { VideoCreate, VideosCommonQuery, VideoState, VideoUpdate } from '../../../../shared'
+import { VideosCommonQuery } from '../../../../shared'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
-import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
-import { buildNSFWFilter, createReqFiles, getCountVideos } from '../../../helpers/express-utils'
-import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
-import { logger, loggerTagsFactory } from '../../../helpers/logger'
+import { buildNSFWFilter, getCountVideos } from '../../../helpers/express-utils'
+import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils'
-import { CONFIG } from '../../../initializers/config'
-import {
- DEFAULT_AUDIO_RESOLUTION,
- MIMETYPES,
- VIDEO_CATEGORIES,
- VIDEO_LANGUAGES,
- VIDEO_LICENCES,
- VIDEO_PRIVACIES
-} from '../../../initializers/constants'
+import { VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES, VIDEO_PRIVACIES } from '../../../initializers/constants'
import { sequelizeTypescript } from '../../../initializers/database'
import { sendView } from '../../../lib/activitypub/send/send-view'
-import { federateVideoIfNeeded, fetchRemoteVideoDescription } from '../../../lib/activitypub/videos'
+import { fetchRemoteVideoDescription } from '../../../lib/activitypub/videos'
import { JobQueue } from '../../../lib/job-queue'
-import { Notifier } from '../../../lib/notifier'
import { Hooks } from '../../../lib/plugins/hooks'
import { Redis } from '../../../lib/redis'
-import { generateVideoMiniature } from '../../../lib/thumbnail'
-import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
import {
asyncMiddleware,
asyncRetryTransactionMiddleware,
setDefaultPagination,
setDefaultVideosSort,
videoFileMetadataGetValidator,
- videosAddLegacyValidator,
- videosAddResumableInitValidator,
- videosAddResumableValidator,
videosCustomGetValidator,
videosGetValidator,
videosRemoveValidator,
- videosSortValidator,
- videosUpdateValidator
+ videosSortValidator
} from '../../../middlewares'
-import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
import { VideoModel } from '../../../models/video/video'
import { VideoFileModel } from '../../../models/video/video-file'
import { blacklistRouter } from './blacklist'
import { liveRouter } from './live'
import { ownershipVideoRouter } from './ownership'
import { rateVideoRouter } from './rate'
+import { updateRouter } from './update'
+import { uploadRouter } from './upload'
import { watchingRouter } from './watching'
-const lTags = loggerTagsFactory('api', 'video')
const auditLogger = auditLoggerFactory('videos')
const videosRouter = express.Router()
-const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() })
-
-const reqVideoFileAdd = createReqFiles(
- [ 'videofile', 'thumbnailfile', 'previewfile' ],
- Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
- {
- videofile: CONFIG.STORAGE.TMP_DIR,
- thumbnailfile: CONFIG.STORAGE.TMP_DIR,
- previewfile: CONFIG.STORAGE.TMP_DIR
- }
-)
-
-const reqVideoFileAddResumable = createReqFiles(
- [ 'thumbnailfile', 'previewfile' ],
- MIMETYPES.IMAGE.MIMETYPE_EXT,
- {
- thumbnailfile: getResumableUploadPath(),
- previewfile: getResumableUploadPath()
- }
-)
-
-const reqVideoFileUpdate = createReqFiles(
- [ 'thumbnailfile', 'previewfile' ],
- MIMETYPES.IMAGE.MIMETYPE_EXT,
- {
- thumbnailfile: CONFIG.STORAGE.TMP_DIR,
- previewfile: CONFIG.STORAGE.TMP_DIR
- }
-)
videosRouter.use('/', blacklistRouter)
videosRouter.use('/', rateVideoRouter)
videosRouter.use('/', ownershipVideoRouter)
videosRouter.use('/', watchingRouter)
videosRouter.use('/', liveRouter)
+videosRouter.use('/', uploadRouter)
+videosRouter.use('/', updateRouter)
videosRouter.get('/categories', listVideoCategories)
videosRouter.get('/licences', listVideoLicences)
asyncMiddleware(listVideos)
)
-videosRouter.post('/upload',
- authenticate,
- reqVideoFileAdd,
- asyncMiddleware(videosAddLegacyValidator),
- asyncRetryTransactionMiddleware(addVideoLegacy)
-)
-
-videosRouter.post('/upload-resumable',
- authenticate,
- reqVideoFileAddResumable,
- asyncMiddleware(videosAddResumableInitValidator),
- uploadxMiddleware
-)
-
-videosRouter.delete('/upload-resumable',
- authenticate,
- uploadxMiddleware
-)
-
-videosRouter.put('/upload-resumable',
- authenticate,
- uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes
- asyncMiddleware(videosAddResumableValidator),
- asyncMiddleware(addVideoResumable)
-)
-
-videosRouter.put('/:id',
- authenticate,
- reqVideoFileUpdate,
- asyncMiddleware(videosUpdateValidator),
- asyncRetryTransactionMiddleware(updateVideo)
-)
-
videosRouter.get('/:id/description',
asyncMiddleware(videosGetValidator),
asyncMiddleware(getVideoDescription)
res.json(VIDEO_PRIVACIES)
}
-async function addVideoLegacy (req: express.Request, res: express.Response) {
- // Uploading the video could be long
- // Set timeout to 10 minutes, as Express's default is 2 minutes
- req.setTimeout(1000 * 60 * 10, () => {
- logger.error('Upload video has timed out.')
- return res.sendStatus(HttpStatusCode.REQUEST_TIMEOUT_408)
- })
-
- const videoPhysicalFile = req.files['videofile'][0]
- const videoInfo: VideoCreate = req.body
- const files = req.files
-
- return addVideo({ res, videoPhysicalFile, videoInfo, files })
-}
-
-async function addVideoResumable (_req: express.Request, res: express.Response) {
- const videoPhysicalFile = res.locals.videoFileResumable
- const videoInfo = videoPhysicalFile.metadata
- const files = { previewfile: videoInfo.previewfile }
-
- // Don't need the meta file anymore
- await deleteResumableUploadMetaFile(videoPhysicalFile.path)
-
- return addVideo({ res, videoPhysicalFile, videoInfo, files })
-}
-
-async function addVideo (options: {
- res: express.Response
- videoPhysicalFile: express.VideoUploadFile
- videoInfo: VideoCreate
- files: express.UploadFiles
-}) {
- const { res, videoPhysicalFile, videoInfo, files } = options
- const videoChannel = res.locals.videoChannel
- const user = res.locals.oauth.token.User
-
- const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id)
-
- videoData.state = CONFIG.TRANSCODING.ENABLED
- ? VideoState.TO_TRANSCODE
- : VideoState.PUBLISHED
-
- videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware
-
- const video = new VideoModel(videoData) as MVideoFullLight
- video.VideoChannel = videoChannel
- video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
-
- const videoFile = new VideoFileModel({
- extname: extname(videoPhysicalFile.filename),
- size: videoPhysicalFile.size,
- videoStreamingPlaylistId: null,
- metadata: await getMetadataFromFile(videoPhysicalFile.path)
- })
-
- if (videoFile.isAudio()) {
- videoFile.resolution = DEFAULT_AUDIO_RESOLUTION
- } else {
- videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path)
- videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution
- }
-
- videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname)
-
- // Move physical file
- const destination = getVideoFilePath(video, videoFile)
- await move(videoPhysicalFile.path, destination)
- // This is important in case if there is another attempt in the retry process
- videoPhysicalFile.filename = getVideoFilePath(video, videoFile)
- videoPhysicalFile.path = destination
-
- const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
- video,
- files,
- fallback: type => generateVideoMiniature({ video, videoFile, type })
- })
-
- const { videoCreated } = await sequelizeTypescript.transaction(async t => {
- const sequelizeOptions = { transaction: t }
-
- const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
-
- await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
- await videoCreated.addAndSaveThumbnail(previewModel, t)
-
- // Do not forget to add video channel information to the created video
- videoCreated.VideoChannel = res.locals.videoChannel
-
- videoFile.videoId = video.id
- await videoFile.save(sequelizeOptions)
-
- video.VideoFiles = [ videoFile ]
-
- await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
-
- // Schedule an update in the future?
- if (videoInfo.scheduleUpdate) {
- await ScheduleVideoUpdateModel.create({
- videoId: video.id,
- updateAt: videoInfo.scheduleUpdate.updateAt,
- privacy: videoInfo.scheduleUpdate.privacy || null
- }, { transaction: t })
- }
-
- // Channel has a new content, set as updated
- await videoCreated.VideoChannel.setAsUpdated(t)
-
- await autoBlacklistVideoIfNeeded({
- video,
- user,
- isRemote: false,
- isNew: true,
- transaction: t
- })
-
- auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
- logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))
-
- return { videoCreated }
- })
-
- // Create the torrent file in async way because it could be long
- createTorrentAndSetInfoHashAsync(video, videoFile)
- .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) }))
- .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id))
- .then(refreshedVideo => {
- if (!refreshedVideo) return
-
- // Only federate and notify after the torrent creation
- Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo)
-
- return retryTransactionWrapper(() => {
- return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
- })
- })
- .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
-
- if (video.state === VideoState.TO_TRANSCODE) {
- await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user)
- }
-
- Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
-
- return res.json({
- video: {
- id: videoCreated.id,
- uuid: videoCreated.uuid
- }
- })
-}
-
-async function updateVideo (req: express.Request, res: express.Response) {
- const videoInstance = res.locals.videoAll
- const videoFieldsSave = videoInstance.toJSON()
- const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
- const videoInfoToUpdate: VideoUpdate = req.body
-
- const wasConfidentialVideo = videoInstance.isConfidential()
- const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation()
-
- const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
- video: videoInstance,
- files: req.files,
- fallback: () => Promise.resolve(undefined),
- automaticallyGenerated: false
- })
-
- try {
- const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => {
- const sequelizeOptions = { transaction: t }
- const oldVideoChannel = videoInstance.VideoChannel
-
- if (videoInfoToUpdate.name !== undefined) videoInstance.name = videoInfoToUpdate.name
- if (videoInfoToUpdate.category !== undefined) videoInstance.category = videoInfoToUpdate.category
- if (videoInfoToUpdate.licence !== undefined) videoInstance.licence = videoInfoToUpdate.licence
- if (videoInfoToUpdate.language !== undefined) videoInstance.language = videoInfoToUpdate.language
- if (videoInfoToUpdate.nsfw !== undefined) videoInstance.nsfw = videoInfoToUpdate.nsfw
- if (videoInfoToUpdate.waitTranscoding !== undefined) videoInstance.waitTranscoding = videoInfoToUpdate.waitTranscoding
- if (videoInfoToUpdate.support !== undefined) videoInstance.support = videoInfoToUpdate.support
- if (videoInfoToUpdate.description !== undefined) videoInstance.description = videoInfoToUpdate.description
- if (videoInfoToUpdate.commentsEnabled !== undefined) videoInstance.commentsEnabled = videoInfoToUpdate.commentsEnabled
- if (videoInfoToUpdate.downloadEnabled !== undefined) videoInstance.downloadEnabled = videoInfoToUpdate.downloadEnabled
-
- if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) {
- videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt)
- }
-
- let isNewVideo = false
- if (videoInfoToUpdate.privacy !== undefined) {
- isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy)
-
- const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10)
- videoInstance.setPrivacy(newPrivacy)
-
- // Unfederate the video if the new privacy is not compatible with federation
- if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) {
- await VideoModel.sendDelete(videoInstance, { transaction: t })
- }
- }
-
- const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) as MVideoFullLight
-
- if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t)
- if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t)
-
- // Video tags update?
- if (videoInfoToUpdate.tags !== undefined) {
- await setVideoTags({
- video: videoInstanceUpdated,
- tags: videoInfoToUpdate.tags,
- transaction: t
- })
- }
-
- // Video channel update?
- if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
- await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
- videoInstanceUpdated.VideoChannel = res.locals.videoChannel
-
- if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
- }
-
- // Schedule an update in the future?
- if (videoInfoToUpdate.scheduleUpdate) {
- await ScheduleVideoUpdateModel.upsert({
- videoId: videoInstanceUpdated.id,
- updateAt: videoInfoToUpdate.scheduleUpdate.updateAt,
- privacy: videoInfoToUpdate.scheduleUpdate.privacy || null
- }, { transaction: t })
- } else if (videoInfoToUpdate.scheduleUpdate === null) {
- await ScheduleVideoUpdateModel.deleteByVideoId(videoInstanceUpdated.id, t)
- }
-
- await autoBlacklistVideoIfNeeded({
- video: videoInstanceUpdated,
- user: res.locals.oauth.token.User,
- isRemote: false,
- isNew: false,
- transaction: t
- })
-
- await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t)
-
- auditLogger.update(
- getAuditIdFromRes(res),
- new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()),
- oldVideoAuditView
- )
- logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid))
-
- return videoInstanceUpdated
- })
-
- if (wasConfidentialVideo) {
- Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated)
- }
-
- Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body })
- } catch (err) {
- // Force fields we want to update
- // If the transaction is retried, sequelize will think the object has not changed
- // So it will skip the SQL request, even if the last one was ROLLBACKed!
- resetSequelizeInstance(videoInstance, videoFieldsSave)
-
- throw err
- }
-
- return res.type('json')
- .status(HttpStatusCode.NO_CONTENT_204)
- .end()
-}
-
-async function getVideo (req: express.Request, res: express.Response) {
+async function getVideo (_req: express.Request, res: express.Response) {
// We need more attributes
const userId: number = res.locals.oauth ? res.locals.oauth.token.User.id : null
async function getVideoDescription (req: express.Request, res: express.Response) {
const videoInstance = res.locals.videoAll
- let description = ''
- if (videoInstance.isOwned()) {
- description = videoInstance.description
- } else {
- description = await fetchRemoteVideoDescription(videoInstance)
- }
+ const description = videoInstance.isOwned()
+ ? videoInstance.description
+ : await fetchRemoteVideoDescription(videoInstance)
return res.json({ description })
}
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
-async function removeVideo (req: express.Request, res: express.Response) {
+async function removeVideo (_req: express.Request, res: express.Response) {
const videoInstance = res.locals.videoAll
await sequelizeTypescript.transaction(async t => {
.status(HttpStatusCode.NO_CONTENT_204)
.end()
}
-
-async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) {
- await createTorrentAndSetInfoHash(video, fileArg)
-
- // Refresh videoFile because the createTorrentAndSetInfoHash could be long
- const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id)
- // File does not exist anymore, remove the generated torrent
- if (!refreshedFile) return fileArg.removeTorrent()
-
- refreshedFile.infoHash = fileArg.infoHash
- refreshedFile.torrentFilename = fileArg.torrentFilename
-
- return refreshedFile.save()
-}
return res.json(getFormattedObjects(resultList.data, resultList.total))
}
-async function acceptOwnership (req: express.Request, res: express.Response) {
+function acceptOwnership (req: express.Request, res: express.Response) {
return sequelizeTypescript.transaction(async t => {
const videoChangeOwnership = res.locals.videoChangeOwnership
const channel = res.locals.videoChannel
})
}
-async function refuseOwnership (req: express.Request, res: express.Response) {
+function refuseOwnership (req: express.Request, res: express.Response) {
return sequelizeTypescript.transaction(async t => {
const videoChangeOwnership = res.locals.videoChangeOwnership
--- /dev/null
+import * as express from 'express'
+import { Transaction } from 'sequelize/types'
+import { changeVideoChannelShare } from '@server/lib/activitypub/share'
+import { buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
+import { FilteredModelAttributes } from '@server/types'
+import { MVideoFullLight } from '@server/types/models'
+import { VideoUpdate } from '../../../../shared'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
+import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
+import { resetSequelizeInstance } from '../../../helpers/database-utils'
+import { createReqFiles } from '../../../helpers/express-utils'
+import { logger, loggerTagsFactory } from '../../../helpers/logger'
+import { CONFIG } from '../../../initializers/config'
+import { MIMETYPES } from '../../../initializers/constants'
+import { sequelizeTypescript } from '../../../initializers/database'
+import { federateVideoIfNeeded } from '../../../lib/activitypub/videos'
+import { Notifier } from '../../../lib/notifier'
+import { Hooks } from '../../../lib/plugins/hooks'
+import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
+import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videosUpdateValidator } from '../../../middlewares'
+import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
+import { VideoModel } from '../../../models/video/video'
+
+const lTags = loggerTagsFactory('api', 'video')
+const auditLogger = auditLoggerFactory('videos')
+const updateRouter = express.Router()
+
+const reqVideoFileUpdate = createReqFiles(
+ [ 'thumbnailfile', 'previewfile' ],
+ MIMETYPES.IMAGE.MIMETYPE_EXT,
+ {
+ thumbnailfile: CONFIG.STORAGE.TMP_DIR,
+ previewfile: CONFIG.STORAGE.TMP_DIR
+ }
+)
+
+updateRouter.put('/:id',
+ authenticate,
+ reqVideoFileUpdate,
+ asyncMiddleware(videosUpdateValidator),
+ asyncRetryTransactionMiddleware(updateVideo)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+ updateRouter
+}
+
+// ---------------------------------------------------------------------------
+
+export async function updateVideo (req: express.Request, res: express.Response) {
+ const videoInstance = res.locals.videoAll
+ const videoFieldsSave = videoInstance.toJSON()
+ const oldVideoAuditView = new VideoAuditView(videoInstance.toFormattedDetailsJSON())
+ const videoInfoToUpdate: VideoUpdate = req.body
+
+ const wasConfidentialVideo = videoInstance.isConfidential()
+ const hadPrivacyForFederation = videoInstance.hasPrivacyForFederation()
+
+ const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
+ video: videoInstance,
+ files: req.files,
+ fallback: () => Promise.resolve(undefined),
+ automaticallyGenerated: false
+ })
+
+ try {
+ const videoInstanceUpdated = await sequelizeTypescript.transaction(async t => {
+ const sequelizeOptions = { transaction: t }
+ const oldVideoChannel = videoInstance.VideoChannel
+
+ const keysToUpdate: (keyof VideoUpdate & FilteredModelAttributes<VideoModel>)[] = [
+ 'name',
+ 'category',
+ 'licence',
+ 'language',
+ 'nsfw',
+ 'waitTranscoding',
+ 'support',
+ 'description',
+ 'commentsEnabled',
+ 'downloadEnabled'
+ ]
+
+ for (const key of keysToUpdate) {
+ if (videoInfoToUpdate[key] !== undefined) videoInstance.set(key, videoInfoToUpdate[key])
+ }
+
+ if (videoInfoToUpdate.originallyPublishedAt !== undefined && videoInfoToUpdate.originallyPublishedAt !== null) {
+ videoInstance.originallyPublishedAt = new Date(videoInfoToUpdate.originallyPublishedAt)
+ }
+
+ // Privacy update?
+ let isNewVideo = false
+ if (videoInfoToUpdate.privacy !== undefined) {
+ isNewVideo = await updateVideoPrivacy({ videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction: t })
+ }
+
+ const videoInstanceUpdated = await videoInstance.save(sequelizeOptions) as MVideoFullLight
+
+ // Thumbnail & preview updates?
+ if (thumbnailModel) await videoInstanceUpdated.addAndSaveThumbnail(thumbnailModel, t)
+ if (previewModel) await videoInstanceUpdated.addAndSaveThumbnail(previewModel, t)
+
+ // Video tags update?
+ if (videoInfoToUpdate.tags !== undefined) {
+ await setVideoTags({ video: videoInstanceUpdated, tags: videoInfoToUpdate.tags, transaction: t })
+ }
+
+ // Video channel update?
+ if (res.locals.videoChannel && videoInstanceUpdated.channelId !== res.locals.videoChannel.id) {
+ await videoInstanceUpdated.$set('VideoChannel', res.locals.videoChannel, { transaction: t })
+ videoInstanceUpdated.VideoChannel = res.locals.videoChannel
+
+ if (hadPrivacyForFederation === true) await changeVideoChannelShare(videoInstanceUpdated, oldVideoChannel, t)
+ }
+
+ // Schedule an update in the future?
+ await updateSchedule(videoInstanceUpdated, videoInfoToUpdate, t)
+
+ await autoBlacklistVideoIfNeeded({
+ video: videoInstanceUpdated,
+ user: res.locals.oauth.token.User,
+ isRemote: false,
+ isNew: false,
+ transaction: t
+ })
+
+ await federateVideoIfNeeded(videoInstanceUpdated, isNewVideo, t)
+
+ auditLogger.update(
+ getAuditIdFromRes(res),
+ new VideoAuditView(videoInstanceUpdated.toFormattedDetailsJSON()),
+ oldVideoAuditView
+ )
+ logger.info('Video with name %s and uuid %s updated.', videoInstance.name, videoInstance.uuid, lTags(videoInstance.uuid))
+
+ return videoInstanceUpdated
+ })
+
+ if (wasConfidentialVideo) {
+ Notifier.Instance.notifyOnNewVideoIfNeeded(videoInstanceUpdated)
+ }
+
+ Hooks.runAction('action:api.video.updated', { video: videoInstanceUpdated, body: req.body })
+ } catch (err) {
+ // Force fields we want to update
+ // If the transaction is retried, sequelize will think the object has not changed
+ // So it will skip the SQL request, even if the last one was ROLLBACKed!
+ resetSequelizeInstance(videoInstance, videoFieldsSave)
+
+ throw err
+ }
+
+ return res.type('json')
+ .status(HttpStatusCode.NO_CONTENT_204)
+ .end()
+}
+
+async function updateVideoPrivacy (options: {
+ videoInstance: MVideoFullLight
+ videoInfoToUpdate: VideoUpdate
+ hadPrivacyForFederation: boolean
+ transaction: Transaction
+}) {
+ const { videoInstance, videoInfoToUpdate, hadPrivacyForFederation, transaction } = options
+ const isNewVideo = videoInstance.isNewVideo(videoInfoToUpdate.privacy)
+
+ const newPrivacy = parseInt(videoInfoToUpdate.privacy.toString(), 10)
+ videoInstance.setPrivacy(newPrivacy)
+
+ // Unfederate the video if the new privacy is not compatible with federation
+ if (hadPrivacyForFederation && !videoInstance.hasPrivacyForFederation()) {
+ await VideoModel.sendDelete(videoInstance, { transaction })
+ }
+
+ return isNewVideo
+}
+
+function updateSchedule (videoInstance: MVideoFullLight, videoInfoToUpdate: VideoUpdate, transaction: Transaction) {
+ if (videoInfoToUpdate.scheduleUpdate) {
+ return ScheduleVideoUpdateModel.upsert({
+ videoId: videoInstance.id,
+ updateAt: new Date(videoInfoToUpdate.scheduleUpdate.updateAt),
+ privacy: videoInfoToUpdate.scheduleUpdate.privacy || null
+ }, { transaction })
+ } else if (videoInfoToUpdate.scheduleUpdate === null) {
+ return ScheduleVideoUpdateModel.deleteByVideoId(videoInstance.id, transaction)
+ }
+}
--- /dev/null
+import * as express from 'express'
+import { move } from 'fs-extra'
+import { extname } from 'path'
+import { deleteResumableUploadMetaFile, getResumableUploadPath } from '@server/helpers/upload'
+import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
+import { getLocalVideoActivityPubUrl } from '@server/lib/activitypub/url'
+import { addOptimizeOrMergeAudioJob, buildLocalVideoFromReq, buildVideoThumbnailsFromReq, setVideoTags } from '@server/lib/video'
+import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
+import { MVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
+import { uploadx } from '@uploadx/core'
+import { VideoCreate, VideoState } from '../../../../shared'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs'
+import { auditLoggerFactory, getAuditIdFromRes, VideoAuditView } from '../../../helpers/audit-logger'
+import { retryTransactionWrapper } from '../../../helpers/database-utils'
+import { createReqFiles } from '../../../helpers/express-utils'
+import { getMetadataFromFile, getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
+import { logger, loggerTagsFactory } from '../../../helpers/logger'
+import { CONFIG } from '../../../initializers/config'
+import { DEFAULT_AUDIO_RESOLUTION, MIMETYPES } from '../../../initializers/constants'
+import { sequelizeTypescript } from '../../../initializers/database'
+import { federateVideoIfNeeded } from '../../../lib/activitypub/videos'
+import { Notifier } from '../../../lib/notifier'
+import { Hooks } from '../../../lib/plugins/hooks'
+import { generateVideoMiniature } from '../../../lib/thumbnail'
+import { autoBlacklistVideoIfNeeded } from '../../../lib/video-blacklist'
+import {
+ asyncMiddleware,
+ asyncRetryTransactionMiddleware,
+ authenticate,
+ videosAddLegacyValidator,
+ videosAddResumableInitValidator,
+ videosAddResumableValidator
+} from '../../../middlewares'
+import { ScheduleVideoUpdateModel } from '../../../models/video/schedule-video-update'
+import { VideoModel } from '../../../models/video/video'
+import { VideoFileModel } from '../../../models/video/video-file'
+
+const lTags = loggerTagsFactory('api', 'video')
+const auditLogger = auditLoggerFactory('videos')
+const uploadRouter = express.Router()
+const uploadxMiddleware = uploadx.upload({ directory: getResumableUploadPath() })
+
+const reqVideoFileAdd = createReqFiles(
+ [ 'videofile', 'thumbnailfile', 'previewfile' ],
+ Object.assign({}, MIMETYPES.VIDEO.MIMETYPE_EXT, MIMETYPES.IMAGE.MIMETYPE_EXT),
+ {
+ videofile: CONFIG.STORAGE.TMP_DIR,
+ thumbnailfile: CONFIG.STORAGE.TMP_DIR,
+ previewfile: CONFIG.STORAGE.TMP_DIR
+ }
+)
+
+const reqVideoFileAddResumable = createReqFiles(
+ [ 'thumbnailfile', 'previewfile' ],
+ MIMETYPES.IMAGE.MIMETYPE_EXT,
+ {
+ thumbnailfile: getResumableUploadPath(),
+ previewfile: getResumableUploadPath()
+ }
+)
+
+uploadRouter.post('/upload',
+ authenticate,
+ reqVideoFileAdd,
+ asyncMiddleware(videosAddLegacyValidator),
+ asyncRetryTransactionMiddleware(addVideoLegacy)
+)
+
+uploadRouter.post('/upload-resumable',
+ authenticate,
+ reqVideoFileAddResumable,
+ asyncMiddleware(videosAddResumableInitValidator),
+ uploadxMiddleware
+)
+
+uploadRouter.delete('/upload-resumable',
+ authenticate,
+ uploadxMiddleware
+)
+
+uploadRouter.put('/upload-resumable',
+ authenticate,
+ uploadxMiddleware, // uploadx doesn't use call next() before the file upload completes
+ asyncMiddleware(videosAddResumableValidator),
+ asyncMiddleware(addVideoResumable)
+)
+
+// ---------------------------------------------------------------------------
+
+export {
+ uploadRouter
+}
+
+// ---------------------------------------------------------------------------
+
+export async function addVideoLegacy (req: express.Request, res: express.Response) {
+ // Uploading the video could be long
+ // Set timeout to 10 minutes, as Express's default is 2 minutes
+ req.setTimeout(1000 * 60 * 10, () => {
+ logger.error('Upload video has timed out.')
+ return res.sendStatus(HttpStatusCode.REQUEST_TIMEOUT_408)
+ })
+
+ const videoPhysicalFile = req.files['videofile'][0]
+ const videoInfo: VideoCreate = req.body
+ const files = req.files
+
+ return addVideo({ res, videoPhysicalFile, videoInfo, files })
+}
+
+export async function addVideoResumable (_req: express.Request, res: express.Response) {
+ const videoPhysicalFile = res.locals.videoFileResumable
+ const videoInfo = videoPhysicalFile.metadata
+ const files = { previewfile: videoInfo.previewfile }
+
+ // Don't need the meta file anymore
+ await deleteResumableUploadMetaFile(videoPhysicalFile.path)
+
+ return addVideo({ res, videoPhysicalFile, videoInfo, files })
+}
+
+async function addVideo (options: {
+ res: express.Response
+ videoPhysicalFile: express.VideoUploadFile
+ videoInfo: VideoCreate
+ files: express.UploadFiles
+}) {
+ const { res, videoPhysicalFile, videoInfo, files } = options
+ const videoChannel = res.locals.videoChannel
+ const user = res.locals.oauth.token.User
+
+ const videoData = buildLocalVideoFromReq(videoInfo, videoChannel.id)
+
+ videoData.state = CONFIG.TRANSCODING.ENABLED
+ ? VideoState.TO_TRANSCODE
+ : VideoState.PUBLISHED
+
+ videoData.duration = videoPhysicalFile.duration // duration was added by a previous middleware
+
+ const video = new VideoModel(videoData) as MVideoFullLight
+ video.VideoChannel = videoChannel
+ video.url = getLocalVideoActivityPubUrl(video) // We use the UUID, so set the URL after building the object
+
+ const videoFile = await buildNewFile(video, videoPhysicalFile)
+
+ // Move physical file
+ const destination = getVideoFilePath(video, videoFile)
+ await move(videoPhysicalFile.path, destination)
+ // This is important in case if there is another attempt in the retry process
+ videoPhysicalFile.filename = getVideoFilePath(video, videoFile)
+ videoPhysicalFile.path = destination
+
+ const [ thumbnailModel, previewModel ] = await buildVideoThumbnailsFromReq({
+ video,
+ files,
+ fallback: type => generateVideoMiniature({ video, videoFile, type })
+ })
+
+ const { videoCreated } = await sequelizeTypescript.transaction(async t => {
+ const sequelizeOptions = { transaction: t }
+
+ const videoCreated = await video.save(sequelizeOptions) as MVideoFullLight
+
+ await videoCreated.addAndSaveThumbnail(thumbnailModel, t)
+ await videoCreated.addAndSaveThumbnail(previewModel, t)
+
+ // Do not forget to add video channel information to the created video
+ videoCreated.VideoChannel = res.locals.videoChannel
+
+ videoFile.videoId = video.id
+ await videoFile.save(sequelizeOptions)
+
+ video.VideoFiles = [ videoFile ]
+
+ await setVideoTags({ video, tags: videoInfo.tags, transaction: t })
+
+ // Schedule an update in the future?
+ if (videoInfo.scheduleUpdate) {
+ await ScheduleVideoUpdateModel.create({
+ videoId: video.id,
+ updateAt: new Date(videoInfo.scheduleUpdate.updateAt),
+ privacy: videoInfo.scheduleUpdate.privacy || null
+ }, sequelizeOptions)
+ }
+
+ // Channel has a new content, set as updated
+ await videoCreated.VideoChannel.setAsUpdated(t)
+
+ await autoBlacklistVideoIfNeeded({
+ video,
+ user,
+ isRemote: false,
+ isNew: true,
+ transaction: t
+ })
+
+ auditLogger.create(getAuditIdFromRes(res), new VideoAuditView(videoCreated.toFormattedDetailsJSON()))
+ logger.info('Video with name %s and uuid %s created.', videoInfo.name, videoCreated.uuid, lTags(videoCreated.uuid))
+
+ return { videoCreated }
+ })
+
+ createTorrentFederate(video, videoFile)
+
+ if (video.state === VideoState.TO_TRANSCODE) {
+ await addOptimizeOrMergeAudioJob(videoCreated, videoFile, user)
+ }
+
+ Hooks.runAction('action:api.video.uploaded', { video: videoCreated })
+
+ return res.json({
+ video: {
+ id: videoCreated.id,
+ uuid: videoCreated.uuid
+ }
+ })
+}
+
+async function buildNewFile (video: MVideo, videoPhysicalFile: express.VideoUploadFile) {
+ const videoFile = new VideoFileModel({
+ extname: extname(videoPhysicalFile.filename),
+ size: videoPhysicalFile.size,
+ videoStreamingPlaylistId: null,
+ metadata: await getMetadataFromFile(videoPhysicalFile.path)
+ })
+
+ if (videoFile.isAudio()) {
+ videoFile.resolution = DEFAULT_AUDIO_RESOLUTION
+ } else {
+ videoFile.fps = await getVideoFileFPS(videoPhysicalFile.path)
+ videoFile.resolution = (await getVideoFileResolution(videoPhysicalFile.path)).videoFileResolution
+ }
+
+ videoFile.filename = generateVideoFilename(video, false, videoFile.resolution, videoFile.extname)
+
+ return videoFile
+}
+
+async function createTorrentAndSetInfoHashAsync (video: MVideo, fileArg: MVideoFile) {
+ await createTorrentAndSetInfoHash(video, fileArg)
+
+ // Refresh videoFile because the createTorrentAndSetInfoHash could be long
+ const refreshedFile = await VideoFileModel.loadWithVideo(fileArg.id)
+ // File does not exist anymore, remove the generated torrent
+ if (!refreshedFile) return fileArg.removeTorrent()
+
+ refreshedFile.infoHash = fileArg.infoHash
+ refreshedFile.torrentFilename = fileArg.torrentFilename
+
+ return refreshedFile.save()
+}
+
+function createTorrentFederate (video: MVideoFullLight, videoFile: MVideoFile): void {
+ // Create the torrent file in async way because it could be long
+ createTorrentAndSetInfoHashAsync(video, videoFile)
+ .catch(err => logger.error('Cannot create torrent file for video %s', video.url, { err, ...lTags(video.uuid) }))
+ .then(() => VideoModel.loadAndPopulateAccountAndServerAndTags(video.id))
+ .then(refreshedVideo => {
+ if (!refreshedVideo) return
+
+ // Only federate and notify after the torrent creation
+ Notifier.Instance.notifyOnNewVideoIfNeeded(refreshedVideo)
+
+ return retryTransactionWrapper(() => {
+ return sequelizeTypescript.transaction(t => federateVideoIfNeeded(refreshedVideo, true, t))
+ })
+ })
+ .catch(err => logger.error('Cannot federate or notify video creation %s', video.url, { err, ...lTags(video.uuid) }))
+}
import * as express from 'express'
import { UserWatchingVideo } from '../../../../shared'
import { asyncMiddleware, asyncRetryTransactionMiddleware, authenticate, videoWatchingValidator } from '../../../middlewares'
-import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
+import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
const watchingRouter = express.Router()
import { actorImagePathUnsafeCache, pushActorImageProcessInQueue } from '../lib/actor-image'
import { VideosCaptionCache, VideosPreviewCache } from '../lib/files-cache'
import { asyncMiddleware } from '../middlewares'
-import { ActorImageModel } from '../models/account/actor-image'
+import { ActorImageModel } from '../models/actor/actor-image'
const lazyStaticRouter = express.Router()
const maxWidth = parseInt(req.query.maxwidth, 10)
const embedUrl = webserverUrl + embedPath
- let embedWidth = EMBED_SIZE.width
- let embedHeight = EMBED_SIZE.height
const embedTitle = escapeHTML(title)
let thumbnailUrl = previewPath
? webserverUrl + previewPath
: undefined
- if (maxHeight < embedHeight) embedHeight = maxHeight
+ let embedWidth = EMBED_SIZE.width
if (maxWidth < embedWidth) embedWidth = maxWidth
+ let embedHeight = EMBED_SIZE.height
+ if (maxHeight < embedHeight) embedHeight = maxHeight
+
// Our thumbnail is too big for the consumer
if (
(maxHeight !== undefined && maxHeight < previewSize.height) ||
import * as express from 'express'
import { join } from 'path'
import { serveIndexHTML } from '@server/lib/client-html'
-import { getRegisteredPlugins, getRegisteredThemes } from '@server/lib/config'
+import { getEnabledResolutions, getRegisteredPlugins, getRegisteredThemes } from '@server/lib/config'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
-import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo'
+import { HttpNodeinfoDiasporaSoftwareNsSchema20 } from '../../shared/models/nodeinfo/nodeinfo.model'
import { root } from '../helpers/core-utils'
import { CONFIG, isEmailEnabled } from '../initializers/config'
import {
WEBSERVER
} from '../initializers/constants'
import { getThemeOrDefault } from '../lib/plugins/theme-utils'
-import { getEnabledResolutions } from '../lib/video-transcoding'
import { asyncMiddleware } from '../middlewares'
import { cacheRoute } from '../middlewares/cache'
-import { UserModel } from '../models/account/user'
+import { UserModel } from '../models/user/user'
import { VideoModel } from '../models/video/video'
import { VideoCommentModel } from '../models/video/video-comment'
-import { ActorModel } from '../models/activitypub/actor'
+import { ActorModel } from '../models/actor/actor'
import { MActorAccountChannelId, MActorFull } from '../types/models'
type ActorFetchByUrlType = 'all' | 'association-ids'
import { AUDIT_LOG_FILENAME } from '@server/initializers/constants'
import { AdminAbuse, User, VideoChannel, VideoDetails, VideoImport } from '../../shared'
import { CustomConfig } from '../../shared/models/server/custom-config.model'
-import { VideoComment } from '../../shared/models/videos/video-comment.model'
+import { VideoComment } from '../../shared/models/videos/comment/video-comment.model'
import { CONFIG } from '../initializers/config'
import { jsonLoggerFormat, labelFormatter } from './logger'
})
}
-function isArray (value: any) {
+function isArray (value: any): value is any[] {
return Array.isArray(value)
}
})
}
-function updateInstanceWithAnother <T extends Model<T>> (instanceToUpdate: Model<T>, baseInstance: Model<T>) {
+function updateInstanceWithAnother <M, T extends U, U extends Model<M>> (instanceToUpdate: T, baseInstance: U) {
const obj = baseInstance.toJSON()
for (const key of Object.keys(obj)) {
return fn()
}
-function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean } & Model<T>> (
+function deleteNonExistingModels <T extends { hasSameUniqueKeysThan (other: T): boolean } & Pick<Model, 'destroy'>> (
fromDatabase: T[],
newModels: T[],
t: Transaction
import * as express from 'express'
import * as multer from 'multer'
+import { extname } from 'path'
+import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
+import { CONFIG } from '../initializers/config'
import { REMOTE_SCHEME } from '../initializers/constants'
+import { isArray } from './custom-validators/misc'
import { logger } from './logger'
import { deleteFileAndCatch, generateRandomString } from './utils'
-import { extname } from 'path'
-import { isArray } from './custom-validators/misc'
-import { CONFIG } from '../initializers/config'
import { getExtFromMimetype } from './video'
-import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
function buildNSFWFilter (res?: express.Response, paramNSFW?: string) {
if (paramNSFW === 'true') return true
return null
}
-function cleanUpReqFiles (req: { files: { [fieldname: string]: Express.Multer.File[] } | Express.Multer.File[] }) {
- const files = req.files
-
- if (!files) return
+function cleanUpReqFiles (
+ req: { files: { [fieldname: string]: Express.Multer.File[] } | Express.Multer.File[] }
+) {
+ const filesObject = req.files
+ if (!filesObject) return
- if (isArray(files)) {
- (files as Express.Multer.File[]).forEach(f => deleteFileAndCatch(f.path))
+ if (isArray(filesObject)) {
+ filesObject.forEach(f => deleteFileAndCatch(f.path))
return
}
- for (const key of Object.keys(files)) {
- const file = files[key]
+ for (const key of Object.keys(filesObject)) {
+ const files = filesObject[key]
- if (isArray(file)) file.forEach(f => deleteFileAndCatch(f.path))
- else deleteFileAndCatch(file.path)
+ files.forEach(f => deleteFileAndCatch(f.path))
}
}
import * as ffmpeg from 'fluent-ffmpeg'
-import { VideoFileMetadata } from '@shared/models/videos/video-file-metadata'
-import { getMaxBitrate, VideoResolution } from '../../shared/models/videos'
+import { getMaxBitrate, VideoFileMetadata, VideoResolution } from '../../shared/models/videos'
import { CONFIG } from '../initializers/config'
import { VIDEO_TRANSCODING_FPS } from '../initializers/constants'
import { logger } from './logger'
import { Response } from 'express'
-import { UserModel } from '@server/models/account/user'
+import { UserModel } from '@server/models/user/user'
import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
import { AccountModel } from '../../models/account/account'
import { MAccountDefault } from '../../types/models'
-import { UserModel } from '../models/account/user'
+import { UserModel } from '../models/user/user'
import * as ipaddr from 'ipaddr.js'
import { CONFIG } from '../initializers/config'
import * as WebFinger from 'webfinger.js'
import { WebFingerData } from '../../shared'
-import { ActorModel } from '../models/activitypub/actor'
-import { isTestInstance } from './core-utils'
-import { isActivityPubUrlValid } from './custom-validators/activitypub/misc'
import { WEBSERVER } from '../initializers/constants'
+import { ActorModel } from '../models/actor/actor'
import { MActorFull } from '../types/models'
+import { isTestInstance } from './core-utils'
+import { isActivityPubUrlValid } from './custom-validators/activitypub/misc'
const webfinger = new WebFinger({
webfist_fallback: false,
import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
import { VideoResolution } from '../../shared/models/videos'
import { CONSTRAINTS_FIELDS, VIDEO_CATEGORIES, VIDEO_LANGUAGES, VIDEO_LICENCES } from '../initializers/constants'
-import { getEnabledResolutions } from '../lib/video-transcoding'
import { peertubeTruncate, pipelinePromise, root } from './core-utils'
import { isVideoFileExtnameValid } from './custom-validators/videos'
import { logger } from './logger'
maxBuffer: 1024 * 1024 * 10 // 10MB
}
-function getYoutubeDLInfo (url: string, opts?: string[]): Promise<YoutubeDLInfo> {
- return new Promise<YoutubeDLInfo>((res, rej) => {
- let args = opts || [ '-j', '--flat-playlist' ]
+class YoutubeDL {
- if (CONFIG.IMPORT.VIDEOS.HTTP.FORCE_IPV4) {
- args.push('--force-ipv4')
- }
+ constructor (private readonly url: string = '', private readonly enabledResolutions: number[] = []) {
- args = wrapWithProxyOptions(args)
- args = [ '-f', getYoutubeDLVideoFormat() ].concat(args)
+ }
- safeGetYoutubeDL()
- .then(youtubeDL => {
- youtubeDL.getInfo(url, args, processOptions, (err, info) => {
- if (err) return rej(err)
- if (info.is_live === true) return rej(new Error('Cannot download a live streaming.'))
+ getYoutubeDLInfo (opts?: string[]): Promise<YoutubeDLInfo> {
+ return new Promise<YoutubeDLInfo>((res, rej) => {
+ let args = opts || [ '-j', '--flat-playlist' ]
- const obj = buildVideoInfo(normalizeObject(info))
- if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video'
+ if (CONFIG.IMPORT.VIDEOS.HTTP.FORCE_IPV4) {
+ args.push('--force-ipv4')
+ }
- return res(obj)
- })
- })
- .catch(err => rej(err))
- })
-}
+ args = this.wrapWithProxyOptions(args)
+ args = [ '-f', this.getYoutubeDLVideoFormat() ].concat(args)
-function getYoutubeDLSubs (url: string, opts?: object): Promise<YoutubeDLSubs> {
- return new Promise<YoutubeDLSubs>((res, rej) => {
- const cwd = CONFIG.STORAGE.TMP_DIR
- const options = opts || { all: true, format: 'vtt', cwd }
-
- safeGetYoutubeDL()
- .then(youtubeDL => {
- youtubeDL.getSubs(url, options, (err, files) => {
- if (err) return rej(err)
- if (!files) return []
-
- logger.debug('Get subtitles from youtube dl.', { url, files })
-
- const subtitles = files.reduce((acc, filename) => {
- const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i)
- if (!matched || !matched[1]) return acc
-
- return [
- ...acc,
- {
- language: matched[1],
- path: join(cwd, filename),
- filename
- }
- ]
- }, [])
+ YoutubeDL.safeGetYoutubeDL()
+ .then(youtubeDL => {
+ youtubeDL.getInfo(this.url, args, processOptions, (err, info) => {
+ if (err) return rej(err)
+ if (info.is_live === true) return rej(new Error('Cannot download a live streaming.'))
- return res(subtitles)
+ const obj = this.buildVideoInfo(this.normalizeObject(info))
+ if (obj.name && obj.name.length < CONSTRAINTS_FIELDS.VIDEOS.NAME.min) obj.name += ' video'
+
+ return res(obj)
+ })
})
- })
- .catch(err => rej(err))
- })
-}
+ .catch(err => rej(err))
+ })
+ }
-function getYoutubeDLVideoFormat () {
- /**
- * list of format selectors in order or preference
- * see https://github.com/ytdl-org/youtube-dl#format-selection
- *
- * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope
- * of being able to do a "quick-transcode"
- * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9)
- * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback
- *
- * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499
- **/
- const enabledResolutions = getEnabledResolutions('vod')
- const resolution = enabledResolutions.length === 0
- ? VideoResolution.H_720P
- : Math.max(...enabledResolutions)
-
- return [
- `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1
- `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2
- `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3
- `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`,
- 'best[vcodec!*=av01][vcodec!*=vp9.2]', // case fallback for known formats
- 'best' // Ultimate fallback
- ].join('/')
-}
+ getYoutubeDLSubs (opts?: object): Promise<YoutubeDLSubs> {
+ return new Promise<YoutubeDLSubs>((res, rej) => {
+ const cwd = CONFIG.STORAGE.TMP_DIR
+ const options = opts || { all: true, format: 'vtt', cwd }
+
+ YoutubeDL.safeGetYoutubeDL()
+ .then(youtubeDL => {
+ youtubeDL.getSubs(this.url, options, (err, files) => {
+ if (err) return rej(err)
+ if (!files) return []
+
+ logger.debug('Get subtitles from youtube dl.', { url: this.url, files })
+
+ const subtitles = files.reduce((acc, filename) => {
+ const matched = filename.match(/\.([a-z]{2})(-[a-z]+)?\.(vtt|ttml)/i)
+ if (!matched || !matched[1]) return acc
+
+ return [
+ ...acc,
+ {
+ language: matched[1],
+ path: join(cwd, filename),
+ filename
+ }
+ ]
+ }, [])
+
+ return res(subtitles)
+ })
+ })
+ .catch(err => rej(err))
+ })
+ }
-function downloadYoutubeDLVideo (url: string, fileExt: string, timeout: number) {
- // Leave empty the extension, youtube-dl will add it
- const pathWithoutExtension = generateVideoImportTmpPath(url, '')
+ getYoutubeDLVideoFormat () {
+ /**
+ * list of format selectors in order or preference
+ * see https://github.com/ytdl-org/youtube-dl#format-selection
+ *
+ * case #1 asks for a mp4 using h264 (avc1) and the exact resolution in the hope
+ * of being able to do a "quick-transcode"
+ * case #2 is the first fallback. No "quick-transcode" means we can get anything else (like vp9)
+ * case #3 is the resolution-degraded equivalent of #1, and already a pretty safe fallback
+ *
+ * in any case we avoid AV1, see https://github.com/Chocobozzz/PeerTube/issues/3499
+ **/
+ const resolution = this.enabledResolutions.length === 0
+ ? VideoResolution.H_720P
+ : Math.max(...this.enabledResolutions)
+
+ return [
+ `bestvideo[vcodec^=avc1][height=${resolution}]+bestaudio[ext=m4a]`, // case #1
+ `bestvideo[vcodec!*=av01][vcodec!*=vp9.2][height=${resolution}]+bestaudio`, // case #2
+ `bestvideo[vcodec^=avc1][height<=${resolution}]+bestaudio[ext=m4a]`, // case #3
+ `bestvideo[vcodec!*=av01][vcodec!*=vp9.2]+bestaudio`,
+ 'best[vcodec!*=av01][vcodec!*=vp9.2]', // case fallback for known formats
+ 'best' // Ultimate fallback
+ ].join('/')
+ }
- let timer
+ downloadYoutubeDLVideo (fileExt: string, timeout: number) {
+ // Leave empty the extension, youtube-dl will add it
+ const pathWithoutExtension = generateVideoImportTmpPath(this.url, '')
- logger.info('Importing youtubeDL video %s to %s', url, pathWithoutExtension)
+ let timer
- let options = [ '-f', getYoutubeDLVideoFormat(), '-o', pathWithoutExtension ]
- options = wrapWithProxyOptions(options)
+ logger.info('Importing youtubeDL video %s to %s', this.url, pathWithoutExtension)
- if (process.env.FFMPEG_PATH) {
- options = options.concat([ '--ffmpeg-location', process.env.FFMPEG_PATH ])
- }
+ let options = [ '-f', this.getYoutubeDLVideoFormat(), '-o', pathWithoutExtension ]
+ options = this.wrapWithProxyOptions(options)
- logger.debug('YoutubeDL options for %s.', url, { options })
+ if (process.env.FFMPEG_PATH) {
+ options = options.concat([ '--ffmpeg-location', process.env.FFMPEG_PATH ])
+ }
- return new Promise<string>((res, rej) => {
- safeGetYoutubeDL()
- .then(youtubeDL => {
- youtubeDL.exec(url, options, processOptions, async err => {
- clearTimeout(timer)
+ logger.debug('YoutubeDL options for %s.', this.url, { options })
- try {
- // If youtube-dl did not guess an extension for our file, just use .mp4 as default
- if (await pathExists(pathWithoutExtension)) {
- await move(pathWithoutExtension, pathWithoutExtension + '.mp4')
- }
+ return new Promise<string>((res, rej) => {
+ YoutubeDL.safeGetYoutubeDL()
+ .then(youtubeDL => {
+ youtubeDL.exec(this.url, options, processOptions, async err => {
+ clearTimeout(timer)
+
+ try {
+ // If youtube-dl did not guess an extension for our file, just use .mp4 as default
+ if (await pathExists(pathWithoutExtension)) {
+ await move(pathWithoutExtension, pathWithoutExtension + '.mp4')
+ }
- const path = await guessVideoPathWithExtension(pathWithoutExtension, fileExt)
+ const path = await this.guessVideoPathWithExtension(pathWithoutExtension, fileExt)
- if (err) {
- remove(path)
- .catch(err => logger.error('Cannot delete path on YoutubeDL error.', { err }))
+ if (err) {
+ remove(path)
+ .catch(err => logger.error('Cannot delete path on YoutubeDL error.', { err }))
+ return rej(err)
+ }
+
+ return res(path)
+ } catch (err) {
return rej(err)
}
-
- return res(path)
- } catch (err) {
- return rej(err)
- }
+ })
+
+ timer = setTimeout(() => {
+ const err = new Error('YoutubeDL download timeout.')
+
+ this.guessVideoPathWithExtension(pathWithoutExtension, fileExt)
+ .then(path => remove(path))
+ .finally(() => rej(err))
+ .catch(err => {
+ logger.error('Cannot remove file in youtubeDL timeout.', { err })
+ return rej(err)
+ })
+ }, timeout)
})
+ .catch(err => rej(err))
+ })
+ }
- timer = setTimeout(() => {
- const err = new Error('YoutubeDL download timeout.')
+ buildOriginallyPublishedAt (obj: any) {
+ let originallyPublishedAt: Date = null
- guessVideoPathWithExtension(pathWithoutExtension, fileExt)
- .then(path => remove(path))
- .finally(() => rej(err))
- .catch(err => {
- logger.error('Cannot remove file in youtubeDL timeout.', { err })
- return rej(err)
- })
- }, timeout)
- })
- .catch(err => rej(err))
- })
-}
-
-// Thanks: https://github.com/przemyslawpluta/node-youtube-dl/blob/master/lib/downloader.js
-// We rewrote it to avoid sync calls
-async function updateYoutubeDLBinary () {
- logger.info('Updating youtubeDL binary.')
+ const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date)
+ if (uploadDateMatcher) {
+ originallyPublishedAt = new Date()
+ originallyPublishedAt.setHours(0, 0, 0, 0)
- const binDirectory = join(root(), 'node_modules', 'youtube-dl', 'bin')
- const bin = join(binDirectory, 'youtube-dl')
- const detailsPath = join(binDirectory, 'details')
- const url = process.env.YOUTUBE_DL_DOWNLOAD_HOST || 'https://yt-dl.org/downloads/latest/youtube-dl'
+ const year = parseInt(uploadDateMatcher[1], 10)
+ // Month starts from 0
+ const month = parseInt(uploadDateMatcher[2], 10) - 1
+ const day = parseInt(uploadDateMatcher[3], 10)
- await ensureDir(binDirectory)
+ originallyPublishedAt.setFullYear(year, month, day)
+ }
- try {
- const result = await got(url, { followRedirect: false })
+ return originallyPublishedAt
+ }
- if (result.statusCode !== HttpStatusCode.FOUND_302) {
- logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
- return
+ private async guessVideoPathWithExtension (tmpPath: string, sourceExt: string) {
+ if (!isVideoFileExtnameValid(sourceExt)) {
+ throw new Error('Invalid video extension ' + sourceExt)
}
- const newUrl = result.headers.location
- const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1]
+ const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ]
- const downloadFileStream = got.stream(newUrl)
- const writeStream = createWriteStream(bin, { mode: 493 })
+ for (const extension of extensions) {
+ const path = tmpPath + extension
- await pipelinePromise(
- downloadFileStream,
- writeStream
- )
-
- const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' })
- await writeFile(detailsPath, details, { encoding: 'utf8' })
+ if (await pathExists(path)) return path
+ }
- logger.info('youtube-dl updated to version %s.', newVersion)
- } catch (err) {
- logger.error('Cannot update youtube-dl.', { err })
+ throw new Error('Cannot guess path of ' + tmpPath)
}
-}
-async function safeGetYoutubeDL () {
- let youtubeDL
+ private normalizeObject (obj: any) {
+ const newObj: any = {}
- try {
- youtubeDL = require('youtube-dl')
- } catch (e) {
- // Download binary
- await updateYoutubeDLBinary()
- youtubeDL = require('youtube-dl')
- }
+ for (const key of Object.keys(obj)) {
+ // Deprecated key
+ if (key === 'resolution') continue
- return youtubeDL
-}
+ const value = obj[key]
-function buildOriginallyPublishedAt (obj: any) {
- let originallyPublishedAt: Date = null
+ if (typeof value === 'string') {
+ newObj[key] = value.normalize()
+ } else {
+ newObj[key] = value
+ }
+ }
- const uploadDateMatcher = /^(\d{4})(\d{2})(\d{2})$/.exec(obj.upload_date)
- if (uploadDateMatcher) {
- originallyPublishedAt = new Date()
- originallyPublishedAt.setHours(0, 0, 0, 0)
+ return newObj
+ }
- const year = parseInt(uploadDateMatcher[1], 10)
- // Month starts from 0
- const month = parseInt(uploadDateMatcher[2], 10) - 1
- const day = parseInt(uploadDateMatcher[3], 10)
+ private buildVideoInfo (obj: any): YoutubeDLInfo {
+ return {
+ name: this.titleTruncation(obj.title),
+ description: this.descriptionTruncation(obj.description),
+ category: this.getCategory(obj.categories),
+ licence: this.getLicence(obj.license),
+ language: this.getLanguage(obj.language),
+ nsfw: this.isNSFW(obj),
+ tags: this.getTags(obj.tags),
+ thumbnailUrl: obj.thumbnail || undefined,
+ originallyPublishedAt: this.buildOriginallyPublishedAt(obj),
+ ext: obj.ext
+ }
+ }
- originallyPublishedAt.setFullYear(year, month, day)
+ private titleTruncation (title: string) {
+ return peertubeTruncate(title, {
+ length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
+ separator: /,? +/,
+ omission: ' […]'
+ })
}
- return originallyPublishedAt
-}
+ private descriptionTruncation (description: string) {
+ if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined
-// ---------------------------------------------------------------------------
+ return peertubeTruncate(description, {
+ length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max,
+ separator: /,? +/,
+ omission: ' […]'
+ })
+ }
-export {
- updateYoutubeDLBinary,
- getYoutubeDLVideoFormat,
- downloadYoutubeDLVideo,
- getYoutubeDLSubs,
- getYoutubeDLInfo,
- safeGetYoutubeDL,
- buildOriginallyPublishedAt
-}
+ private isNSFW (info: any) {
+ return info.age_limit && info.age_limit >= 16
+ }
-// ---------------------------------------------------------------------------
+ private getTags (tags: any) {
+ if (Array.isArray(tags) === false) return []
-async function guessVideoPathWithExtension (tmpPath: string, sourceExt: string) {
- if (!isVideoFileExtnameValid(sourceExt)) {
- throw new Error('Invalid video extension ' + sourceExt)
+ return tags
+ .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min)
+ .map(t => t.normalize())
+ .slice(0, 5)
}
- const extensions = [ sourceExt, '.mp4', '.mkv', '.webm' ]
+ private getLicence (licence: string) {
+ if (!licence) return undefined
- for (const extension of extensions) {
- const path = tmpPath + extension
+ if (licence.includes('Creative Commons Attribution')) return 1
- if (await pathExists(path)) return path
- }
+ for (const key of Object.keys(VIDEO_LICENCES)) {
+ const peertubeLicence = VIDEO_LICENCES[key]
+ if (peertubeLicence.toLowerCase() === licence.toLowerCase()) return parseInt(key, 10)
+ }
- throw new Error('Cannot guess path of ' + tmpPath)
-}
+ return undefined
+ }
-function normalizeObject (obj: any) {
- const newObj: any = {}
+ private getCategory (categories: string[]) {
+ if (!categories) return undefined
- for (const key of Object.keys(obj)) {
- // Deprecated key
- if (key === 'resolution') continue
+ const categoryString = categories[0]
+ if (!categoryString || typeof categoryString !== 'string') return undefined
- const value = obj[key]
+ if (categoryString === 'News & Politics') return 11
- if (typeof value === 'string') {
- newObj[key] = value.normalize()
- } else {
- newObj[key] = value
+ for (const key of Object.keys(VIDEO_CATEGORIES)) {
+ const category = VIDEO_CATEGORIES[key]
+ if (categoryString.toLowerCase() === category.toLowerCase()) return parseInt(key, 10)
}
- }
- return newObj
-}
-
-function buildVideoInfo (obj: any): YoutubeDLInfo {
- return {
- name: titleTruncation(obj.title),
- description: descriptionTruncation(obj.description),
- category: getCategory(obj.categories),
- licence: getLicence(obj.license),
- language: getLanguage(obj.language),
- nsfw: isNSFW(obj),
- tags: getTags(obj.tags),
- thumbnailUrl: obj.thumbnail || undefined,
- originallyPublishedAt: buildOriginallyPublishedAt(obj),
- ext: obj.ext
+ return undefined
}
-}
-function titleTruncation (title: string) {
- return peertubeTruncate(title, {
- length: CONSTRAINTS_FIELDS.VIDEOS.NAME.max,
- separator: /,? +/,
- omission: ' […]'
- })
-}
+ private getLanguage (language: string) {
+ return VIDEO_LANGUAGES[language] ? language : undefined
+ }
-function descriptionTruncation (description: string) {
- if (!description || description.length < CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.min) return undefined
+ private wrapWithProxyOptions (options: string[]) {
+ if (CONFIG.IMPORT.VIDEOS.HTTP.PROXY.ENABLED) {
+ logger.debug('Using proxy for YoutubeDL')
- return peertubeTruncate(description, {
- length: CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max,
- separator: /,? +/,
- omission: ' […]'
- })
-}
+ return [ '--proxy', CONFIG.IMPORT.VIDEOS.HTTP.PROXY.URL ].concat(options)
+ }
-function isNSFW (info: any) {
- return info.age_limit && info.age_limit >= 16
-}
+ return options
+ }
-function getTags (tags: any) {
- if (Array.isArray(tags) === false) return []
+ // Thanks: https://github.com/przemyslawpluta/node-youtube-dl/blob/master/lib/downloader.js
+ // We rewrote it to avoid sync calls
+ static async updateYoutubeDLBinary () {
+ logger.info('Updating youtubeDL binary.')
- return tags
- .filter(t => t.length < CONSTRAINTS_FIELDS.VIDEOS.TAG.max && t.length > CONSTRAINTS_FIELDS.VIDEOS.TAG.min)
- .map(t => t.normalize())
- .slice(0, 5)
-}
+ const binDirectory = join(root(), 'node_modules', 'youtube-dl', 'bin')
+ const bin = join(binDirectory, 'youtube-dl')
+ const detailsPath = join(binDirectory, 'details')
+ const url = process.env.YOUTUBE_DL_DOWNLOAD_HOST || 'https://yt-dl.org/downloads/latest/youtube-dl'
-function getLicence (licence: string) {
- if (!licence) return undefined
+ await ensureDir(binDirectory)
- if (licence.includes('Creative Commons Attribution')) return 1
+ try {
+ const result = await got(url, { followRedirect: false })
- for (const key of Object.keys(VIDEO_LICENCES)) {
- const peertubeLicence = VIDEO_LICENCES[key]
- if (peertubeLicence.toLowerCase() === licence.toLowerCase()) return parseInt(key, 10)
- }
+ if (result.statusCode !== HttpStatusCode.FOUND_302) {
+ logger.error('youtube-dl update error: did not get redirect for the latest version link. Status %d', result.statusCode)
+ return
+ }
- return undefined
-}
+ const newUrl = result.headers.location
+ const newVersion = /yt-dl\.org\/downloads\/(\d{4}\.\d\d\.\d\d(\.\d)?)\/youtube-dl/.exec(newUrl)[1]
-function getCategory (categories: string[]) {
- if (!categories) return undefined
+ const downloadFileStream = got.stream(newUrl)
+ const writeStream = createWriteStream(bin, { mode: 493 })
- const categoryString = categories[0]
- if (!categoryString || typeof categoryString !== 'string') return undefined
+ await pipelinePromise(
+ downloadFileStream,
+ writeStream
+ )
- if (categoryString === 'News & Politics') return 11
+ const details = JSON.stringify({ version: newVersion, path: bin, exec: 'youtube-dl' })
+ await writeFile(detailsPath, details, { encoding: 'utf8' })
- for (const key of Object.keys(VIDEO_CATEGORIES)) {
- const category = VIDEO_CATEGORIES[key]
- if (categoryString.toLowerCase() === category.toLowerCase()) return parseInt(key, 10)
+ logger.info('youtube-dl updated to version %s.', newVersion)
+ } catch (err) {
+ logger.error('Cannot update youtube-dl.', { err })
+ }
}
- return undefined
-}
-
-function getLanguage (language: string) {
- return VIDEO_LANGUAGES[language] ? language : undefined
-}
+ static async safeGetYoutubeDL () {
+ let youtubeDL
-function wrapWithProxyOptions (options: string[]) {
- if (CONFIG.IMPORT.VIDEOS.HTTP.PROXY.ENABLED) {
- logger.debug('Using proxy for YoutubeDL')
+ try {
+ youtubeDL = require('youtube-dl')
+ } catch (e) {
+ // Download binary
+ await this.updateYoutubeDLBinary()
+ youtubeDL = require('youtube-dl')
+ }
- return [ '--proxy', CONFIG.IMPORT.VIDEOS.HTTP.PROXY.URL ].concat(options)
+ return youtubeDL
}
+}
+
+// ---------------------------------------------------------------------------
- return options
+export {
+ YoutubeDL
}
import { isProdInstance, isTestInstance, parseSemVersion } from '../helpers/core-utils'
import { isArray } from '../helpers/custom-validators/misc'
import { logger } from '../helpers/logger'
-import { UserModel } from '../models/account/user'
+import { UserModel } from '../models/user/user'
import { ApplicationModel, getServerActor } from '../models/application/application'
import { OAuthClientModel } from '../models/oauth/oauth-client'
import { CONFIG, isEmailEnabled } from './config'
TITLE: '<!-- title tag -->',
DESCRIPTION: '<!-- description tag -->',
CUSTOM_CSS: '<!-- custom css tag -->',
- META_TAGS: '<!-- meta tags -->'
+ META_TAGS: '<!-- meta tags -->',
+ SERVER_CONFIG: '<!-- server config -->'
}
// ---------------------------------------------------------------------------
import { Sequelize as SequelizeTypescript } from 'sequelize-typescript'
import { TrackerModel } from '@server/models/server/tracker'
import { VideoTrackerModel } from '@server/models/server/video-tracker'
+import { UserModel } from '@server/models/user/user'
+import { UserNotificationModel } from '@server/models/user/user-notification'
+import { UserVideoHistoryModel } from '@server/models/user/user-video-history'
import { isTestInstance } from '../helpers/core-utils'
import { logger } from '../helpers/logger'
import { AbuseModel } from '../models/abuse/abuse'
import { AccountModel } from '../models/account/account'
import { AccountBlocklistModel } from '../models/account/account-blocklist'
import { AccountVideoRateModel } from '../models/account/account-video-rate'
-import { ActorImageModel } from '../models/account/actor-image'
-import { UserModel } from '../models/account/user'
-import { UserNotificationModel } from '../models/account/user-notification'
-import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
-import { UserVideoHistoryModel } from '../models/account/user-video-history'
-import { ActorModel } from '../models/activitypub/actor'
-import { ActorFollowModel } from '../models/activitypub/actor-follow'
+import { ActorModel } from '../models/actor/actor'
+import { ActorFollowModel } from '../models/actor/actor-follow'
+import { ActorImageModel } from '../models/actor/actor-image'
import { ApplicationModel } from '../models/application/application'
import { OAuthClientModel } from '../models/oauth/oauth-client'
import { OAuthTokenModel } from '../models/oauth/oauth-token'
import { PluginModel } from '../models/server/plugin'
import { ServerModel } from '../models/server/server'
import { ServerBlocklistModel } from '../models/server/server-blocklist'
+import { UserNotificationSettingModel } from '../models/user/user-notification-setting'
import { ScheduleVideoUpdateModel } from '../models/video/schedule-video-update'
import { TagModel } from '../models/video/tag'
import { ThumbnailModel } from '../models/video/thumbnail'
import { UserRole } from '../../shared'
import { logger } from '../helpers/logger'
import { createApplicationActor, createUserAccountAndChannelAndPlaylist } from '../lib/user'
-import { UserModel } from '../models/account/user'
+import { UserModel } from '../models/user/user'
import { ApplicationModel } from '../models/application/application'
import { OAuthClientModel } from '../models/oauth/oauth-client'
import { applicationExist, clientsExist, usersExist } from './checker-after-init'
import { MIMETYPES, WEBSERVER } from '../../initializers/constants'
import { sequelizeTypescript } from '../../initializers/database'
import { AccountModel } from '../../models/account/account'
-import { ActorImageModel } from '../../models/account/actor-image'
-import { ActorModel } from '../../models/activitypub/actor'
+import { ActorModel } from '../../models/actor/actor'
+import { ActorImageModel } from '../../models/actor/actor-image'
import { ServerModel } from '../../models/server/server'
import { VideoChannelModel } from '../../models/video/video-channel'
import {
return actorRefreshed
}
-function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string, uuid?: string) {
+function buildActorInstance (type: ActivityPubActorType, url: string, preferredUsername: string) {
return new ActorModel({
type,
url,
preferredUsername,
- uuid,
publicKey: null,
privateKey: null,
followersCount: 0,
import { Transaction } from 'sequelize'
import { ActivityAudience } from '../../../shared/models/activitypub'
import { ACTIVITY_PUB } from '../../initializers/constants'
-import { ActorModel } from '../../models/activitypub/actor'
+import { ActorModel } from '../../models/actor/actor'
import { VideoModel } from '../../models/video/video'
import { VideoShareModel } from '../../models/video/video-share'
import { MActorFollowersUrl, MActorLight, MActorUrl, MCommentOwner, MCommentOwnerVideo, MVideoId } from '../../types/models'
import { ActivityAccept } from '../../../../shared/models/activitypub'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
-import { addFetchOutboxJob } from '../actor'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorDefault, MActorSignature } from '../../../types/models'
+import { addFetchOutboxJob } from '../actor'
async function processAcceptActivity (options: APProcessorOptions<ActivityAccept>) {
const { byActor: targetActor, inboxActor } = options
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { logger } from '../../../helpers/logger'
import { sequelizeTypescript } from '../../../initializers/database'
-import { ActorModel } from '../../../models/activitypub/actor'
+import { ActorModel } from '../../../models/actor/actor'
import { VideoModel } from '../../../models/video/video'
import { VideoCommentModel } from '../../../models/video/video-comment'
import { VideoPlaylistModel } from '../../../models/video/video-playlist'
+import { getServerActor } from '@server/models/application/application'
import { ActivityFollow } from '../../../../shared/models/activitypub'
+import { getAPId } from '../../../helpers/activitypub'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
import { logger } from '../../../helpers/logger'
-import { sequelizeTypescript } from '../../../initializers/database'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
-import { sendAccept, sendReject } from '../send'
-import { Notifier } from '../../notifier'
-import { getAPId } from '../../../helpers/activitypub'
import { CONFIG } from '../../../initializers/config'
+import { sequelizeTypescript } from '../../../initializers/database'
+import { ActorModel } from '../../../models/actor/actor'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActorFollowActors, MActorSignature } from '../../../types/models'
+import { Notifier } from '../../notifier'
import { autoFollowBackIfNeeded } from '../follow'
-import { getServerActor } from '@server/models/application/application'
+import { sendAccept, sendReject } from '../send'
async function processFollowActivity (options: APProcessorOptions<ActivityFollow>) {
const { activity, byActor } = options
import { ActivityReject } from '../../../../shared/models/activitypub/activity'
import { sequelizeTypescript } from '../../../initializers/database'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
import { MActor } from '../../../types/models'
import { logger } from '../../../helpers/logger'
import { sequelizeTypescript } from '../../../initializers/database'
import { AccountVideoRateModel } from '../../../models/account/account-video-rate'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { ActorModel } from '../../../models/actor/actor'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
import { VideoRedundancyModel } from '../../../models/redundancy/video-redundancy'
import { VideoShareModel } from '../../../models/video/video-share'
import { APProcessorOptions } from '../../../types/activitypub-processor.model'
+import { isRedundancyAccepted } from '@server/lib/redundancy'
+import { ActorImageType } from '@shared/models'
import { ActivityUpdate, CacheFileObject, VideoObject } from '../../../../shared/models/activitypub'
import { ActivityPubActor } from '../../../../shared/models/activitypub/activitypub-actor'
+import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
+import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
+import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
import { resetSequelizeInstance, retryTransactionWrapper } from '../../../helpers/database-utils'
import { logger } from '../../../helpers/logger'
import { sequelizeTypescript } from '../../../initializers/database'
import { AccountModel } from '../../../models/account/account'
-import { ActorModel } from '../../../models/activitypub/actor'
+import { ActorModel } from '../../../models/actor/actor'
import { VideoChannelModel } from '../../../models/video/video-channel'
+import { APProcessorOptions } from '../../../types/activitypub-processor.model'
+import { MAccountIdActor, MActorSignature } from '../../../types/models'
import { getImageInfoIfExists, updateActorImageInstance, updateActorInstance } from '../actor'
-import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos'
-import { sanitizeAndCheckVideoTorrentObject } from '../../../helpers/custom-validators/activitypub/videos'
-import { isCacheFileObjectValid } from '../../../helpers/custom-validators/activitypub/cache-file'
import { createOrUpdateCacheFile } from '../cache-file'
-import { forwardVideoRelatedActivity } from '../send/utils'
-import { PlaylistObject } from '../../../../shared/models/activitypub/objects/playlist-object'
import { createOrUpdateVideoPlaylist } from '../playlist'
-import { APProcessorOptions } from '../../../types/activitypub-processor.model'
-import { MActorSignature, MAccountIdActor } from '../../../types/models'
-import { isRedundancyAccepted } from '@server/lib/redundancy'
-import { ActorImageType } from '@shared/models'
+import { forwardVideoRelatedActivity } from '../send/utils'
+import { getOrCreateVideoAndAccountAndChannel, getOrCreateVideoChannelFromVideoObject, updateVideoFromAP } from '../videos'
async function processUpdateActivity (options: APProcessorOptions<ActivityUpdate>) {
const { activity, byActor } = options
import { getServerActor } from '@server/models/application/application'
import { ActivityAudience, ActivityDelete } from '../../../../shared/models/activitypub'
import { logger } from '../../../helpers/logger'
-import { ActorModel } from '../../../models/activitypub/actor'
+import { ActorModel } from '../../../models/actor/actor'
import { VideoCommentModel } from '../../../models/video/video-comment'
import { VideoShareModel } from '../../../models/video/video-share'
import { MActorUrl } from '../../../types/models'
import { MActorAudience, MVideoImmutable, MVideoUrl } from '@server/types/models'
import { ActivityAudience, ActivityView } from '../../../../shared/models/activitypub'
import { logger } from '../../../helpers/logger'
-import { ActorModel } from '../../../models/activitypub/actor'
+import { ActorModel } from '../../../models/actor/actor'
import { audiencify, getAudience } from '../audience'
import { getLocalVideoViewActivityPubUrl } from '../url'
import { sendVideoRelatedActivity } from './utils'
import { Transaction } from 'sequelize'
+import { getServerActor } from '@server/models/application/application'
+import { ContextType } from '@shared/models/activitypub/context'
import { Activity, ActivityAudience } from '../../../../shared/models/activitypub'
+import { afterCommitIfTransaction } from '../../../helpers/database-utils'
import { logger } from '../../../helpers/logger'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { ActorModel } from '../../../models/actor/actor'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
+import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../types/models'
import { JobQueue } from '../../job-queue'
import { getActorsInvolvedInVideo, getAudienceFromFollowersOf, getRemoteVideoAudience } from '../audience'
-import { afterCommitIfTransaction } from '../../../helpers/database-utils'
-import { MActor, MActorId, MActorLight, MActorWithInboxes, MVideoAccountLight, MVideoId, MVideoImmutable } from '../../../types/models'
-import { getServerActor } from '@server/models/application/application'
-import { ContextType } from '@shared/models/activitypub/context'
async function sendVideoRelatedActivity (activityBuilder: (audience: ActivityAudience) => Activity, options: {
byActor: MActorLight
import * as express from 'express'
import { AccessDeniedError } from 'oauth2-server'
import { PluginManager } from '@server/lib/plugins/plugin-manager'
-import { ActorModel } from '@server/models/activitypub/actor'
+import { ActorModel } from '@server/models/actor/actor'
import { MOAuthClient } from '@server/types/models'
import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
import { MUser } from '@server/types/models/user/user'
import { UserRole } from '@shared/models/users/user-role'
import { logger } from '../../helpers/logger'
import { CONFIG } from '../../initializers/config'
-import { UserModel } from '../../models/account/user'
+import { UserModel } from '../../models/user/user'
import { OAuthClientModel } from '../../models/oauth/oauth-client'
import { OAuthTokenModel } from '../../models/oauth/oauth-token'
import { createUserAccountAndChannelAndPlaylist } from '../user'
import { readFile } from 'fs-extra'
import { join } from 'path'
import validator from 'validator'
+import { escapeHTML } from '@shared/core-utils/renderer'
+import { HTMLServerConfig } from '@shared/models'
import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/core-utils/i18n/i18n'
import { HttpStatusCode } from '../../shared/core-utils/miscs/http-error-codes'
import { VideoPlaylistPrivacy, VideoPrivacy } from '../../shared/models/videos'
import { isTestInstance, sha256 } from '../helpers/core-utils'
-import { escapeHTML } from '@shared/core-utils/renderer'
import { logger } from '../helpers/logger'
+import { mdToPlainText } from '../helpers/markdown'
import { CONFIG } from '../initializers/config'
import {
ACCEPT_HEADERS,
import { getActivityStreamDuration } from '../models/video/video-format-utils'
import { VideoPlaylistModel } from '../models/video/video-playlist'
import { MAccountActor, MChannelActor } from '../types/models'
-import { mdToPlainText } from '../helpers/markdown'
+import { getHTMLServerConfig } from './config'
type Tags = {
ogType: string
if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
const buffer = await readFile(path)
+ const serverConfig = await getHTMLServerConfig()
let html = buffer.toString()
html = await ClientHtml.addAsyncPluginCSS(html)
html = ClientHtml.addCustomCSS(html)
html = ClientHtml.addTitleTag(html)
+ html = ClientHtml.addDescriptionTag(html)
+ html = ClientHtml.addServerConfig(html, serverConfig)
ClientHtml.htmlCache[path] = html
if (!isTestInstance() && ClientHtml.htmlCache[path]) return ClientHtml.htmlCache[path]
const buffer = await readFile(path)
+ const serverConfig = await getHTMLServerConfig()
let html = buffer.toString()
html = ClientHtml.addFaviconContentHash(html)
html = ClientHtml.addLogoContentHash(html)
html = ClientHtml.addCustomCSS(html)
+ html = ClientHtml.addServerConfig(html, serverConfig)
html = await ClientHtml.addAsyncPluginCSS(html)
ClientHtml.htmlCache[path] = html
return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.CUSTOM_CSS, styleTag)
}
+ private static addServerConfig (htmlStringPage: string, serverConfig: HTMLServerConfig) {
+ const serverConfigString = JSON.stringify(serverConfig)
+ const configScriptTag = `<script type="application/javascript">window.PeerTubeServerConfig = '${serverConfigString}'</script>`
+
+ return htmlStringPage.replace(CUSTOM_HTML_TAG_COMMENTS.SERVER_CONFIG, configScriptTag)
+ }
+
private static async addAsyncPluginCSS (htmlStringPage: string) {
const globalCSSContent = await readFile(PLUGIN_GLOBAL_CSS_PATH)
if (globalCSSContent.byteLength === 0) return htmlStringPage
import { getServerCommit } from '@server/helpers/utils'
import { CONFIG, isEmailEnabled } from '@server/initializers/config'
import { CONSTRAINTS_FIELDS, DEFAULT_THEME_NAME, PEERTUBE_VERSION } from '@server/initializers/constants'
-import { RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models'
+import { HTMLServerConfig, RegisteredExternalAuthConfig, RegisteredIdAndPassAuthConfig, ServerConfig } from '@shared/models'
import { Hooks } from './plugins/hooks'
import { PluginManager } from './plugins/plugin-manager'
import { getThemeOrDefault } from './plugins/theme-utils'
-import { getEnabledResolutions } from './video-transcoding'
-import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
-
-let serverCommit: string
+import { VideoTranscodingProfilesManager } from './transcoding/video-transcoding-profiles'
async function getServerConfig (ip?: string): Promise<ServerConfig> {
- if (serverCommit === undefined) serverCommit = await getServerCommit()
-
const { allowed } = await Hooks.wrapPromiseFun(
isSignupAllowed,
{
)
const allowedForCurrentIP = isSignupAllowedForCurrentIP(ip)
+
+ const signup = {
+ allowed,
+ allowedForCurrentIP,
+ requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
+ }
+
+ const htmlConfig = await getHTMLServerConfig()
+
+ return { ...htmlConfig, signup }
+}
+
+// Config injected in HTML
+let serverCommit: string
+async function getHTMLServerConfig (): Promise<HTMLServerConfig> {
+ if (serverCommit === undefined) serverCommit = await getServerCommit()
+
const defaultTheme = getThemeOrDefault(CONFIG.THEME.DEFAULT, DEFAULT_THEME_NAME)
return {
},
serverVersion: PEERTUBE_VERSION,
serverCommit,
- signup: {
- allowed,
- allowedForCurrentIP,
- requiresEmailVerification: CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION
- },
transcoding: {
hls: {
enabled: CONFIG.TRANSCODING.HLS.ENABLED
}))
}
+function getEnabledResolutions (type: 'vod' | 'live') {
+ const transcoding = type === 'vod'
+ ? CONFIG.TRANSCODING
+ : CONFIG.LIVE.TRANSCODING
+
+ return Object.keys(transcoding.RESOLUTIONS)
+ .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true)
+ .map(r => parseInt(r, 10))
+}
+
// ---------------------------------------------------------------------------
export {
getServerConfig,
getRegisteredThemes,
- getRegisteredPlugins
+ getEnabledResolutions,
+ getRegisteredPlugins,
+ getHTMLServerConfig
}
// ---------------------------------------------------------------------------
import * as Bull from 'bull'
-import { logger } from '../../../helpers/logger'
-import { REMOTE_SCHEME, WEBSERVER } from '../../../initializers/constants'
-import { sendFollow } from '../../activitypub/send'
+import { getLocalActorFollowActivityPubUrl } from '@server/lib/activitypub/url'
+import { ActivitypubFollowPayload } from '@shared/models'
import { sanitizeHost } from '../../../helpers/core-utils'
-import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger'
-import { getOrCreateActorAndServerAndModel } from '../../activitypub/actor'
import { retryTransactionWrapper } from '../../../helpers/database-utils'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { Notifier } from '../../notifier'
+import { logger } from '../../../helpers/logger'
+import { loadActorUrlOrGetFromWebfinger } from '../../../helpers/webfinger'
+import { REMOTE_SCHEME, WEBSERVER } from '../../../initializers/constants'
import { sequelizeTypescript } from '../../../initializers/database'
+import { ActorModel } from '../../../models/actor/actor'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
import { MActor, MActorFollowActors, MActorFull } from '../../../types/models'
-import { ActivitypubFollowPayload } from '@shared/models'
-import { getLocalActorFollowActivityPubUrl } from '@server/lib/activitypub/url'
+import { getOrCreateActorAndServerAndModel } from '../../activitypub/actor'
+import { sendFollow } from '../../activitypub/send'
+import { Notifier } from '../../notifier'
async function processActivityPubFollow (job: Bull.Job) {
const payload = job.data as ActivitypubFollowPayload
import * as Bull from 'bull'
+import { refreshVideoPlaylistIfNeeded } from '@server/lib/activitypub/playlist'
+import { RefreshPayload } from '@shared/models'
import { logger } from '../../../helpers/logger'
import { fetchVideoByUrl } from '../../../helpers/video'
+import { ActorModel } from '../../../models/actor/actor'
+import { VideoPlaylistModel } from '../../../models/video/video-playlist'
import { refreshActorIfNeeded } from '../../activitypub/actor'
import { refreshVideoIfNeeded } from '../../activitypub/videos'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { VideoPlaylistModel } from '../../../models/video/video-playlist'
-import { RefreshPayload } from '@shared/models'
-import { refreshVideoPlaylistIfNeeded } from '@server/lib/activitypub/playlist'
async function refreshAPObject (job: Bull.Job) {
const payload = job.data as RefreshPayload
import * as Bull from 'bull'
import { generateAndSaveActorKeys } from '@server/lib/activitypub/actor'
-import { ActorModel } from '@server/models/activitypub/actor'
+import { ActorModel } from '@server/models/actor/actor'
import { ActorKeysPayload } from '@shared/models'
import { logger } from '../../../helpers/logger'
+import { buildDigest } from '@server/helpers/peertube-crypto'
+import { getServerActor } from '@server/models/application/application'
+import { ContextType } from '@shared/models/activitypub/context'
import { buildSignedActivity } from '../../../../helpers/activitypub'
-import { ActorModel } from '../../../../models/activitypub/actor'
import { ACTIVITY_PUB, HTTP_SIGNATURE } from '../../../../initializers/constants'
+import { ActorModel } from '../../../../models/actor/actor'
import { MActor } from '../../../../types/models'
-import { getServerActor } from '@server/models/application/application'
-import { buildDigest } from '@server/helpers/peertube-crypto'
-import { ContextType } from '@shared/models/activitypub/context'
type Payload <T> = { body: T, contextType?: ContextType, signatureActorId?: number }
import { extname } from 'path'
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
import { generateVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
-import { UserModel } from '@server/models/account/user'
+import { UserModel } from '@server/models/user/user'
import { MVideoFullLight } from '@server/types/models'
import { VideoFileImportPayload } from '@shared/models'
import { getVideoFileFPS, getVideoFileResolution } from '../../../helpers/ffprobe-utils'
import { logger } from '../../../helpers/logger'
import { getSecureTorrentName } from '../../../helpers/utils'
import { createTorrentAndSetInfoHash, downloadWebTorrentVideo } from '../../../helpers/webtorrent'
-import { downloadYoutubeDLVideo } from '../../../helpers/youtube-dl'
import { CONFIG } from '../../../initializers/config'
import { VIDEO_IMPORT_TIMEOUT } from '../../../initializers/constants'
import { sequelizeTypescript } from '../../../initializers/database'
import { federateVideoIfNeeded } from '../../activitypub/videos'
import { Notifier } from '../../notifier'
import { generateVideoMiniature } from '../../thumbnail'
+import { YoutubeDL } from '@server/helpers/youtube-dl'
+import { getEnabledResolutions } from '@server/lib/config'
async function processVideoImport (job: Bull.Job) {
const payload = job.data as VideoImportPayload
videoImportId: videoImport.id
}
+ const youtubeDL = new YoutubeDL(videoImport.targetUrl, getEnabledResolutions('vod'))
+
return processFile(
- () => downloadYoutubeDLVideo(videoImport.targetUrl, payload.fileExt, VIDEO_IMPORT_TIMEOUT),
+ () => youtubeDL.downloadYoutubeDLVideo(payload.fileExt, VIDEO_IMPORT_TIMEOUT),
videoImport,
options
)
import { VIDEO_LIVE } from '@server/initializers/constants'
import { LiveManager } from '@server/lib/live-manager'
import { generateVideoMiniature } from '@server/lib/thumbnail'
+import { generateHlsPlaylistResolutionFromTS } from '@server/lib/transcoding/video-transcoding'
import { publishAndFederateIfNeeded } from '@server/lib/video'
import { getHLSDirectory } from '@server/lib/video-paths'
-import { generateHlsPlaylistResolutionFromTS } from '@server/lib/video-transcoding'
import { VideoModel } from '@server/models/video/video'
import { VideoFileModel } from '@server/models/video/video-file'
import { VideoLiveModel } from '@server/models/video/video-live'
import { TranscodeOptionsType } from '@server/helpers/ffmpeg-utils'
import { getTranscodingJobPriority, publishAndFederateIfNeeded } from '@server/lib/video'
import { getVideoFilePath } from '@server/lib/video-paths'
-import { UserModel } from '@server/models/account/user'
+import { UserModel } from '@server/models/user/user'
import { MUser, MUserId, MVideoFullLight, MVideoUUID, MVideoWithFile } from '@server/types/models'
import {
HLSTranscodingPayload,
mergeAudioVideofile,
optimizeOriginalVideofile,
transcodeNewWebTorrentResolution
-} from '../../video-transcoding'
+} from '../../transcoding/video-transcoding'
import { JobQueue } from '../job-queue'
type HandlerFunction = (job: Bull.Job, payload: VideoTranscodingPayload, video: MVideoFullLight, user: MUser) => Promise<any>
}
await VideoViewModel.create({
- startDate,
- endDate,
+ startDate: new Date(startDate),
+ endDate: new Date(endDate),
views,
videoId
})
import { logger } from '@server/helpers/logger'
import { CONFIG, registerConfigChangedHandler } from '@server/initializers/config'
import { MEMOIZE_TTL, P2P_MEDIA_LOADER_PEER_VERSION, VIDEO_LIVE, VIEW_LIFETIME, WEBSERVER } from '@server/initializers/constants'
-import { UserModel } from '@server/models/account/user'
+import { UserModel } from '@server/models/user/user'
import { VideoModel } from '@server/models/video/video'
import { VideoFileModel } from '@server/models/video/video-file'
import { VideoLiveModel } from '@server/models/video/video-live'
import { JobQueue } from './job-queue'
import { cleanupLive } from './job-queue/handlers/video-live-ending'
import { PeerTubeSocket } from './peertube-socket'
+import { VideoTranscodingProfilesManager } from './transcoding/video-transcoding-profiles'
import { isAbleToUploadVideo } from './user'
import { getHLSDirectory } from './video-paths'
-import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
import memoizee = require('memoizee')
const NodeRtmpSession = require('node-media-server/node_rtmp_session')
import { VideoObject } from '../../shared/models/activitypub/objects'
import { VideoCommentObject } from '../../shared/models/activitypub/objects/video-comment-object'
import { LiveVideoCreate, VideoCreate, VideoImportCreate } from '../../shared/models/videos'
-import { VideoCommentCreate } from '../../shared/models/videos/video-comment.model'
-import { UserModel } from '../models/account/user'
-import { ActorModel } from '../models/activitypub/actor'
+import { VideoCommentCreate } from '../../shared/models/videos/comment/video-comment.model'
+import { ActorModel } from '../models/actor/actor'
+import { UserModel } from '../models/user/user'
import { VideoModel } from '../models/video/video'
import { VideoCommentModel } from '../models/video/video-comment'
import { sendAbuse } from './activitypub/send/send-flag'
import { logger } from '../helpers/logger'
import { CONFIG } from '../initializers/config'
import { AccountBlocklistModel } from '../models/account/account-blocklist'
-import { UserModel } from '../models/account/user'
-import { UserNotificationModel } from '../models/account/user-notification'
+import { UserModel } from '../models/user/user'
+import { UserNotificationModel } from '../models/user/user-notification'
import { MAbuseFull, MAbuseMessage, MAccountServer, MActorFollowFull, MApplication, MPlugin } from '../types/models'
import { MCommentOwnerVideo, MVideoAccountLight, MVideoFullLight } from '../types/models/video'
import { isBlockedByServerOrAccount } from './blocklist'
-import { ServerActionHookName, ServerFilterHookName } from '../../../shared/models/plugins/server-hook.model'
-import { PluginManager } from './plugin-manager'
-import { logger } from '../../helpers/logger'
import * as Bluebird from 'bluebird'
+import { ServerActionHookName, ServerFilterHookName } from '../../../shared/models'
+import { logger } from '../../helpers/logger'
+import { PluginManager } from './plugin-manager'
type PromiseFunction <U, T> = (params: U) => Promise<T> | Bluebird<T>
type RawFunction <U, T> = (params: U) => T
import { addAccountInBlocklist, addServerInBlocklist, removeAccountFromBlocklist, removeServerFromBlocklist } from '../blocklist'
import { getServerConfig } from '../config'
import { blacklistVideo, unblacklistVideo } from '../video-blacklist'
-import { UserModel } from '@server/models/account/user'
+import { UserModel } from '@server/models/user/user'
function buildPluginHelpers (pluginModel: MPlugin, npmName: string): PeerTubeHelpers {
const logger = buildPluginLogger(npmName)
import { sanitizeUrl } from '@server/helpers/core-utils'
-import { ResultList } from '../../../shared/models'
-import { PeertubePluginIndexList } from '../../../shared/models/plugins/peertube-plugin-index-list.model'
-import { PeerTubePluginIndex } from '../../../shared/models/plugins/peertube-plugin-index.model'
+import { logger } from '@server/helpers/logger'
+import { doJSONRequest } from '@server/helpers/requests'
+import { CONFIG } from '@server/initializers/config'
+import { PEERTUBE_VERSION } from '@server/initializers/constants'
+import { PluginModel } from '@server/models/server/plugin'
import {
+ PeerTubePluginIndex,
+ PeertubePluginIndexList,
PeertubePluginLatestVersionRequest,
- PeertubePluginLatestVersionResponse
-} from '../../../shared/models/plugins/peertube-plugin-latest-version.model'
-import { logger } from '../../helpers/logger'
-import { doJSONRequest } from '../../helpers/requests'
-import { CONFIG } from '../../initializers/config'
-import { PEERTUBE_VERSION } from '../../initializers/constants'
-import { PluginModel } from '../../models/server/plugin'
+ PeertubePluginLatestVersionResponse,
+ ResultList
+} from '@shared/models'
import { PluginManager } from './plugin-manager'
async function listAvailablePluginsFromIndex (options: PeertubePluginIndexList) {
import { ensureDir, outputFile, readJSON } from 'fs-extra'
import { basename, join } from 'path'
import { MOAuthTokenUser, MUser } from '@server/types/models'
-import { RegisterServerHookOptions } from '@shared/models/plugins/register-server-hook.model'
+import { getCompleteLocale } from '@shared/core-utils'
+import { ClientScript, PluginPackageJson, PluginTranslation, PluginTranslationPaths, RegisterServerHookOptions } from '@shared/models'
import { getHookType, internalRunHook } from '../../../shared/core-utils/plugins/hooks'
-import {
- ClientScript,
- PluginPackageJson,
- PluginTranslationPaths as PackagePluginTranslations
-} from '../../../shared/models/plugins/plugin-package-json.model'
-import { PluginTranslation } from '../../../shared/models/plugins/plugin-translation.model'
import { PluginType } from '../../../shared/models/plugins/plugin.type'
-import { ServerHook, ServerHookName } from '../../../shared/models/plugins/server-hook.model'
+import { ServerHook, ServerHookName } from '../../../shared/models/plugins/server/server-hook.model'
import { isLibraryCodeValid, isPackageJSONValid } from '../../helpers/custom-validators/plugins'
import { logger } from '../../helpers/logger'
import { CONFIG } from '../../initializers/config'
import { ClientHtml } from '../client-html'
import { RegisterHelpers } from './register-helpers'
import { installNpmPlugin, installNpmPluginFromDisk, removeNpmPlugin } from './yarn'
-import { getCompleteLocale } from '@shared/core-utils'
export interface RegisteredPlugin {
npmName: string
// ###################### Translations ######################
- private async addTranslations (plugin: PluginModel, npmName: string, translationPaths: PackagePluginTranslations) {
+ private async addTranslations (plugin: PluginModel, npmName: string, translationPaths: PluginTranslationPaths) {
for (const locale of Object.keys(translationPaths)) {
const path = translationPaths[locale]
const json = await readJSON(join(this.getPluginPath(plugin.name, plugin.type), path))
PluginVideoLicenceManager,
PluginVideoPrivacyManager,
RegisterServerHookOptions,
- RegisterServerSettingOptions
+ RegisterServerSettingOptions,
+ serverHookObject
} from '@shared/models'
-import { serverHookObject } from '@shared/models/plugins/server-hook.model'
-import { VideoTranscodingProfilesManager } from '../video-transcoding-profiles'
+import { VideoTranscodingProfilesManager } from '../transcoding/video-transcoding-profiles'
import { buildPluginHelpers } from './plugin-helpers-builder'
type AlterableVideoConstant = 'language' | 'licence' | 'category' | 'privacy' | 'playlistPrivacy'
-import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
-import { sendUndoCacheFile } from './activitypub/send'
import { Transaction } from 'sequelize'
-import { MActorSignature, MVideoRedundancyVideo } from '@server/types/models'
-import { CONFIG } from '@server/initializers/config'
import { logger } from '@server/helpers/logger'
-import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
-import { Activity } from '@shared/models'
+import { CONFIG } from '@server/initializers/config'
+import { ActorFollowModel } from '@server/models/actor/actor-follow'
import { getServerActor } from '@server/models/application/application'
+import { MActorSignature, MVideoRedundancyVideo } from '@server/types/models'
+import { Activity } from '@shared/models'
+import { VideoRedundancyModel } from '../models/redundancy/video-redundancy'
+import { sendUndoCacheFile } from './activitypub/send'
async function removeVideoRedundancy (videoRedundancy: MVideoRedundancyVideo, t?: Transaction) {
const serverActor = await getServerActor()
import { isTestInstance } from '../../helpers/core-utils'
import { logger } from '../../helpers/logger'
-import { ActorFollowModel } from '../../models/activitypub/actor-follow'
-import { AbstractScheduler } from './abstract-scheduler'
import { ACTOR_FOLLOW_SCORE, SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
+import { ActorFollowModel } from '../../models/actor/actor-follow'
import { ActorFollowScoreCache } from '../files-cache'
+import { AbstractScheduler } from './abstract-scheduler'
export class ActorFollowScheduler extends AbstractScheduler {
import { chunk } from 'lodash'
import { doJSONRequest } from '@server/helpers/requests'
import { JobQueue } from '@server/lib/job-queue'
-import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
+import { ActorFollowModel } from '@server/models/actor/actor-follow'
import { getServerActor } from '@server/models/application/application'
import { logger } from '../../helpers/logger'
import { CONFIG } from '../../initializers/config'
import { logger } from '../../helpers/logger'
import { AbstractScheduler } from './abstract-scheduler'
import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
-import { UserVideoHistoryModel } from '../../models/account/user-video-history'
+import { UserVideoHistoryModel } from '../../models/user/user-video-history'
import { CONFIG } from '../../initializers/config'
export class RemoveOldHistoryScheduler extends AbstractScheduler {
-import { AbstractScheduler } from './abstract-scheduler'
+import { YoutubeDL } from '@server/helpers/youtube-dl'
import { SCHEDULER_INTERVALS_MS } from '../../initializers/constants'
-import { updateYoutubeDLBinary } from '../../helpers/youtube-dl'
+import { AbstractScheduler } from './abstract-scheduler'
export class YoutubeDlUpdateScheduler extends AbstractScheduler {
}
protected internalExecute () {
- return updateYoutubeDLBinary()
+ return YoutubeDL.updateYoutubeDLBinary()
}
static get Instance () {
import { CONFIG } from '@server/initializers/config'
-import { UserModel } from '@server/models/account/user'
-import { ActorFollowModel } from '@server/models/activitypub/actor-follow'
+import { UserModel } from '@server/models/user/user'
+import { ActorFollowModel } from '@server/models/actor/actor-follow'
import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy'
import { VideoModel } from '@server/models/video/video'
import { VideoChannelModel } from '@server/models/video/video-channel'
import { logger } from '@server/helpers/logger'
-import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../shared/models/videos'
-import { buildStreamSuffix, resetSupportedEncoders } from '../helpers/ffmpeg-utils'
+import { AvailableEncoders, EncoderOptionsBuilder, getTargetBitrate, VideoResolution } from '../../../shared/models/videos'
+import { buildStreamSuffix, resetSupportedEncoders } from '../../helpers/ffmpeg-utils'
import {
canDoQuickAudioTranscode,
ffprobePromise,
getMaxAudioBitrate,
getVideoFileBitrate,
getVideoStreamFromFile
-} from '../helpers/ffprobe-utils'
-import { VIDEO_TRANSCODING_FPS } from '../initializers/constants'
+} from '../../helpers/ffprobe-utils'
+import { VIDEO_TRANSCODING_FPS } from '../../initializers/constants'
/**
*
import { basename, extname as extnameUtil, join } from 'path'
import { createTorrentAndSetInfoHash } from '@server/helpers/webtorrent'
import { MStreamingPlaylistFilesVideo, MVideoFile, MVideoFullLight } from '@server/types/models'
-import { VideoResolution } from '../../shared/models/videos'
-import { VideoStreamingPlaylistType } from '../../shared/models/videos/video-streaming-playlist.type'
-import { transcode, TranscodeOptions, TranscodeOptionsType } from '../helpers/ffmpeg-utils'
-import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../helpers/ffprobe-utils'
-import { logger } from '../helpers/logger'
-import { CONFIG } from '../initializers/config'
-import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../initializers/constants'
-import { VideoFileModel } from '../models/video/video-file'
-import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
-import { updateMasterHLSPlaylist, updateSha256VODSegments } from './hls'
-import { generateVideoFilename, generateVideoStreamingPlaylistName, getVideoFilePath } from './video-paths'
+import { VideoResolution } from '../../../shared/models/videos'
+import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
+import { transcode, TranscodeOptions, TranscodeOptionsType } from '../../helpers/ffmpeg-utils'
+import { canDoQuickTranscode, getDurationFromVideoFile, getMetadataFromFile, getVideoFileFPS } from '../../helpers/ffprobe-utils'
+import { logger } from '../../helpers/logger'
+import { CONFIG } from '../../initializers/config'
+import { HLS_STREAMING_PLAYLIST_DIRECTORY, P2P_MEDIA_LOADER_PEER_VERSION, WEBSERVER } from '../../initializers/constants'
+import { VideoFileModel } from '../../models/video/video-file'
+import { VideoStreamingPlaylistModel } from '../../models/video/video-streaming-playlist'
+import { updateMasterHLSPlaylist, updateSha256VODSegments } from '../hls'
+import { generateVideoFilename, generateVideoStreamingPlaylistName, getVideoFilePath } from '../video-paths'
import { VideoTranscodingProfilesManager } from './video-transcoding-profiles'
/**
})
}
-function getEnabledResolutions (type: 'vod' | 'live') {
- const transcoding = type === 'vod'
- ? CONFIG.TRANSCODING
- : CONFIG.LIVE.TRANSCODING
-
- return Object.keys(transcoding.RESOLUTIONS)
- .filter(key => transcoding.ENABLED && transcoding.RESOLUTIONS[key] === true)
- .map(r => parseInt(r, 10))
-}
-
// ---------------------------------------------------------------------------
export {
generateHlsPlaylistResolutionFromTS,
optimizeOriginalVideofile,
transcodeNewWebTorrentResolution,
- mergeAudioVideofile,
- getEnabledResolutions
+ mergeAudioVideofile
}
// ---------------------------------------------------------------------------
import { Transaction } from 'sequelize/types'
import { v4 as uuidv4 } from 'uuid'
-import { UserModel } from '@server/models/account/user'
+import { UserModel } from '@server/models/user/user'
+import { MActorDefault } from '@server/types/models/actor'
import { ActivityPubActorType } from '../../shared/models/activitypub'
import { UserNotificationSetting, UserNotificationSettingValue } from '../../shared/models/users'
import { SERVER_ACTOR_NAME, WEBSERVER } from '../initializers/constants'
import { sequelizeTypescript } from '../initializers/database'
import { AccountModel } from '../models/account/account'
-import { UserNotificationSettingModel } from '../models/account/user-notification-setting'
-import { ActorModel } from '../models/activitypub/actor'
-import { MAccountDefault, MActorDefault, MChannelActor } from '../types/models'
+import { ActorModel } from '../models/actor/actor'
+import { UserNotificationSettingModel } from '../models/user/user-notification-setting'
+import { MAccountDefault, MChannelActor } from '../types/models'
import { MUser, MUserDefault, MUserId } from '../types/models/user'
import { buildActorInstance, generateAndSaveActorKeys } from './activitypub/actor'
import { getLocalAccountActivityPubUrl } from './activitypub/url'
import * as Sequelize from 'sequelize'
-import { v4 as uuidv4 } from 'uuid'
import { VideoChannelCreate } from '../../shared/models'
import { VideoModel } from '../models/video/video'
import { VideoChannelModel } from '../models/video/video-channel'
import { federateVideoIfNeeded } from './activitypub/videos'
async function createLocalVideoChannel (videoChannelInfo: VideoChannelCreate, account: MAccountId, t: Sequelize.Transaction) {
- const uuid = uuidv4()
const url = getLocalVideoChannelActivityPubUrl(videoChannelInfo.name)
- const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name, uuid)
+ const actorInstance = buildActorInstance('Group', url, videoChannelInfo.name)
const actorInstanceCreated = await actorInstance.save({ transaction: t })
import { logger } from '@server/helpers/logger'
import { sequelizeTypescript } from '@server/initializers/database'
import { ResultList } from '../../shared/models'
-import { VideoCommentThreadTree } from '../../shared/models/videos/video-comment.model'
+import { VideoCommentThreadTree } from '../../shared/models/videos/comment/video-comment.model'
import { VideoCommentModel } from '../models/video/video-comment'
import { MAccountDefault, MComment, MCommentOwnerVideo, MCommentOwnerVideoReply, MVideoFullLight } from '../types/models'
import { sendCreateVideoComment, sendDeleteVideoComment } from './activitypub/send'
privacy: videoInfo.privacy || VideoPrivacy.PRIVATE,
channelId: channelId,
originallyPublishedAt: videoInfo.originallyPublishedAt
+ ? new Date(videoInfo.originallyPublishedAt)
+ : null
}
}
import * as express from 'express'
import { body, param, query } from 'express-validator'
+import { isFollowStateValid } from '@server/helpers/custom-validators/follows'
+import { getServerActor } from '@server/models/application/application'
+import { MActorFollowActorsDefault } from '@server/types/models'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
import { isTestInstance } from '../../helpers/core-utils'
+import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
import { isEachUniqueHostValid, isHostValid } from '../../helpers/custom-validators/servers'
import { logger } from '../../helpers/logger'
+import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
import { SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
-import { ActorFollowModel } from '../../models/activitypub/actor-follow'
+import { ActorModel } from '../../models/actor/actor'
+import { ActorFollowModel } from '../../models/actor/actor-follow'
import { areValidationErrors } from './utils'
-import { ActorModel } from '../../models/activitypub/actor'
-import { loadActorUrlOrGetFromWebfinger } from '../../helpers/webfinger'
-import { isActorTypeValid, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
-import { MActorFollowActorsDefault } from '@server/types/models'
-import { isFollowStateValid } from '@server/helpers/custom-validators/follows'
-import { getServerActor } from '@server/models/application/application'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
const listFollowsValidator = [
query('state')
import * as express from 'express'
import { body, param, query, ValidationChain } from 'express-validator'
-import { logger } from '../../helpers/logger'
-import { areValidationErrors } from './utils'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { PluginType } from '../../../shared/models/plugins/plugin.type'
+import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/server/api/install-plugin.model'
+import { exists, isBooleanValid, isSafePath, toBooleanOrNull, toIntOrNull } from '../../helpers/custom-validators/misc'
import { isNpmPluginNameValid, isPluginNameValid, isPluginTypeValid, isPluginVersionValid } from '../../helpers/custom-validators/plugins'
+import { logger } from '../../helpers/logger'
+import { CONFIG } from '../../initializers/config'
import { PluginManager } from '../../lib/plugins/plugin-manager'
-import { isBooleanValid, isSafePath, toBooleanOrNull, exists, toIntOrNull } from '../../helpers/custom-validators/misc'
import { PluginModel } from '../../models/server/plugin'
-import { InstallOrUpdatePlugin } from '../../../shared/models/plugins/install-plugin.model'
-import { PluginType } from '../../../shared/models/plugins/plugin.type'
-import { CONFIG } from '../../initializers/config'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { areValidationErrors } from './utils'
const getPluginValidator = (pluginType: PluginType, withVersion = true) => {
const validators: (ValidationChain | express.Handler)[] = [
import * as express from 'express'
import { body, param, query } from 'express-validator'
-import { logger } from '../../helpers/logger'
-import { areValidationErrors } from './utils'
-import { ActorFollowModel } from '../../models/activitypub/actor-follow'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
import { areValidActorHandles, isValidActorHandle } from '../../helpers/custom-validators/activitypub/actor'
import { toArray } from '../../helpers/custom-validators/misc'
+import { logger } from '../../helpers/logger'
import { WEBSERVER } from '../../initializers/constants'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { ActorFollowModel } from '../../models/actor/actor-follow'
+import { areValidationErrors } from './utils'
const userSubscriptionListValidator = [
query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
import { isSignupAllowed, isSignupAllowedForCurrentIP } from '../../helpers/signup'
import { isThemeRegistered } from '../../lib/plugins/theme-utils'
import { Redis } from '../../lib/redis'
-import { UserModel } from '../../models/account/user'
-import { ActorModel } from '../../models/activitypub/actor'
+import { UserModel } from '../../models/user/user'
+import { ActorModel } from '../../models/actor/actor'
import { areValidationErrors } from './utils'
const usersListValidator = [
import { VIDEO_CHANNELS } from '@server/initializers/constants'
import { MChannelAccountDefault, MUser } from '@server/types/models'
import { UserRight } from '../../../../shared'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import { isActorPreferredUsernameValid } from '../../../helpers/custom-validators/activitypub/actor'
import { isBooleanValid, toBooleanOrNull } from '../../../helpers/custom-validators/misc'
import {
} from '../../../helpers/custom-validators/video-channels'
import { logger } from '../../../helpers/logger'
import { doesLocalVideoChannelNameExist, doesVideoChannelNameWithHostExist } from '../../../helpers/middlewares'
-import { ActorModel } from '../../../models/activitypub/actor'
+import { ActorModel } from '../../../models/actor/actor'
import { VideoChannelModel } from '../../../models/video/video-channel'
import { areValidationErrors } from '../utils'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
const videoChannelsAddValidator = [
body('name').custom(isActorPreferredUsernameValid).withMessage('Should have a valid channel name'),
cleanUpReqFiles(req)
return res.status(HttpStatusCode.CONFLICT_409)
.json({ error: 'HTTP import is not enabled on this instance.' })
- .end()
}
if (CONFIG.IMPORT.VIDEOS.TORRENT.ENABLED !== true && (req.body.magnetUri || torrentFile)) {
cleanUpReqFiles(req)
return res.status(HttpStatusCode.CONFLICT_409)
.json({ error: 'Torrent/magnet URI import is not enabled on this instance.' })
- .end()
}
if (!await doesVideoChannelOfAccountExist(req.body.channelId, user, res)) return cleanUpReqFiles(req)
return res.status(HttpStatusCode.BAD_REQUEST_400)
.json({ error: 'Should have a magnetUri or a targetUrl or a torrent file.' })
- .end()
}
if (!await isImportAccepted(req, res)) return cleanUpReqFiles(req)
import { MUserAccountId, MVideoWithRights } from '@server/types/models'
import { ServerErrorCode, UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
-import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/video-change-ownership-accept.model'
+import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/change-ownership/video-change-ownership-accept.model'
import {
exists,
isBooleanValid,
import * as express from 'express'
import { query } from 'express-validator'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
import { isWebfingerLocalResourceValid } from '../../helpers/custom-validators/webfinger'
+import { getHostWithPort } from '../../helpers/express-utils'
import { logger } from '../../helpers/logger'
-import { ActorModel } from '../../models/activitypub/actor'
+import { ActorModel } from '../../models/actor/actor'
import { areValidationErrors } from './utils'
-import { getHostWithPort } from '../../helpers/express-utils'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
const webfingerValidator = [
query('resource').custom(isWebfingerLocalResourceValid).withMessage('Should have a valid webfinger resource'),
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses'
import { MAbuseMessage, MAbuseMessageFormattable } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { AbuseMessage } from '@shared/models'
import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account'
import { getSort, throwIfNotValid } from '../utils'
}
]
})
-export class AbuseMessageModel extends Model {
+export class AbuseMessageModel extends Model<Partial<AttributesOnly<AbuseMessageModel>>> {
@AllowNull(false)
@Is('AbuseMessage', value => throwIfNotValid(value, isAbuseMessageValid, 'message'))
UpdatedAt
} from 'sequelize-typescript'
import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses'
-import { abusePredefinedReasonsMap } from '@shared/core-utils/abuse'
+import { abusePredefinedReasonsMap, AttributesOnly } from '@shared/core-utils'
import {
AbuseFilter,
AbuseObject,
}
]
})
-export class AbuseModel extends Model {
+export class AbuseModel extends Model<Partial<AttributesOnly<AbuseModel>>> {
@AllowNull(false)
@Default(null)
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoDetails } from '@shared/models'
import { VideoModel } from '../video/video'
import { AbuseModel } from './abuse'
}
]
})
-export class VideoAbuseModel extends Model {
+export class VideoAbuseModel extends Model<Partial<AttributesOnly<VideoAbuseModel>>> {
@CreatedAt
createdAt: Date
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoCommentModel } from '../video/video-comment'
import { AbuseModel } from './abuse'
}
]
})
-export class VideoCommentAbuseModel extends Model {
+export class VideoCommentAbuseModel extends Model<Partial<AttributesOnly<VideoCommentAbuseModel>>> {
@CreatedAt
createdAt: Date
import { Op } from 'sequelize'
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
import { MAccountBlocklist, MAccountBlocklistAccounts, MAccountBlocklistFormattable } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { AccountBlock } from '../../../shared/models'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
import { ServerModel } from '../server/server'
import { getSort, searchAttribute } from '../utils'
import { AccountModel } from './account'
}
]
})
-export class AccountBlocklistModel extends Model {
+export class AccountBlocklistModel extends Model<Partial<AttributesOnly<AccountBlocklistModel>>> {
@CreatedAt
createdAt: Date
MAccountVideoRateAccountVideo,
MAccountVideoRateFormattable
} from '@server/types/models/video/video-rate'
+import { AttributesOnly } from '@shared/core-utils'
import { AccountVideoRate } from '../../../shared'
import { VideoRateType } from '../../../shared/models/videos'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { CONSTRAINTS_FIELDS, VIDEO_RATE_TYPES } from '../../initializers/constants'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
import { buildLocalAccountIdsIn, getSort, throwIfNotValid } from '../utils'
import { VideoModel } from '../video/video'
import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from '../video/video-channel'
}
]
})
-export class AccountVideoRateModel extends Model {
+export class AccountVideoRateModel extends Model<Partial<AttributesOnly<AccountVideoRateModel>>> {
@AllowNull(false)
@Column(DataType.ENUM(...values(VIDEO_RATE_TYPES)))
UpdatedAt
} from 'sequelize-typescript'
import { ModelCache } from '@server/models/model-cache'
+import { AttributesOnly } from '@shared/core-utils'
import { Account, AccountSummary } from '../../../shared/models/actors'
import { isAccountDescriptionValid } from '../../helpers/custom-validators/accounts'
import { CONSTRAINTS_FIELDS, SERVER_ACTOR_NAME, WEBSERVER } from '../../initializers/constants'
-import { sendDeleteActor } from '../../lib/activitypub/send'
+import { sendDeleteActor } from '../../lib/activitypub/send/send-delete'
import {
MAccount,
MAccountActor,
MAccountSummaryFormattable,
MChannelActor
} from '../../types/models'
-import { ActorModel } from '../activitypub/actor'
-import { ActorFollowModel } from '../activitypub/actor-follow'
+import { ActorModel } from '../actor/actor'
+import { ActorFollowModel } from '../actor/actor-follow'
+import { ActorImageModel } from '../actor/actor-image'
import { ApplicationModel } from '../application/application'
-import { ActorImageModel } from './actor-image'
import { ServerModel } from '../server/server'
import { ServerBlocklistModel } from '../server/server-blocklist'
+import { UserModel } from '../user/user'
import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from '../video/video'
import { VideoChannelModel } from '../video/video-channel'
import { VideoCommentModel } from '../video/video-comment'
import { VideoPlaylistModel } from '../video/video-playlist'
import { AccountBlocklistModel } from './account-blocklist'
-import { UserModel } from './user'
export enum ScopeNames {
SUMMARY = 'SUMMARY'
}
]
})
-export class AccountModel extends Model {
+export class AccountModel extends Model<Partial<AttributesOnly<AccountModel>>> {
@AllowNull(false)
@Column
MActorFollowFormattable,
MActorFollowSubscriptions
} from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { ActivityPubActorType } from '@shared/models'
import { FollowState } from '../../../shared/models/actors'
import { ActorFollow } from '../../../shared/models/actors/follow.model'
}
]
})
-export class ActorFollowModel extends Model {
+export class ActorFollowModel extends Model<Partial<AttributesOnly<ActorFollowModel>>> {
@AllowNull(false)
@Column(DataType.ENUM(...values(FOLLOW_STATES)))
if (serverIds.length === 0) return
const me = await getServerActor()
- const serverIdsString = createSafeIn(ActorFollowModel, serverIds)
+ const serverIdsString = createSafeIn(ActorFollowModel.sequelize, serverIds)
const query = `UPDATE "actorFollow" SET "score" = LEAST("score" + ${value}, ${ACTOR_FOLLOW_SCORE.MAX}) ` +
'WHERE id IN (' +
import { join } from 'path'
import { AfterDestroy, AllowNull, Column, CreatedAt, Default, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { MActorImageFormattable } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { ActorImageType } from '@shared/models'
import { ActorImage } from '../../../shared/models/actors/actor-image.model'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
}
]
})
-export class ActorImageModel extends Model {
+export class ActorImageModel extends Model<Partial<AttributesOnly<ActorImageModel>>> {
@AllowNull(false)
@Column
UpdatedAt
} from 'sequelize-typescript'
import { ModelCache } from '@server/models/model-cache'
+import { AttributesOnly } from '@shared/core-utils'
import { ActivityIconObject, ActivityPubActorType } from '../../../shared/models/activitypub'
import { ActorImage } from '../../../shared/models/actors/actor-image.model'
import { activityPubContextify } from '../../helpers/activitypub'
MActorWithInboxes
} from '../../types/models'
import { AccountModel } from '../account/account'
-import { ActorImageModel } from '../account/actor-image'
import { ServerModel } from '../server/server'
import { isOutdated, throwIfNotValid } from '../utils'
import { VideoModel } from '../video/video'
import { VideoChannelModel } from '../video/video-channel'
import { ActorFollowModel } from './actor-follow'
+import { ActorImageModel } from './actor-image'
enum ScopeNames {
FULL = 'FULL'
}
]
})
-export class ActorModel extends Model {
+export class ActorModel extends Model<Partial<AttributesOnly<ActorModel>>> {
@AllowNull(false)
@Column(DataType.ENUM(...values(ACTIVITY_PUB_ACTOR_TYPES)))
+import * as memoizee from 'memoizee'
import { AllowNull, Column, Default, DefaultScope, HasOne, IsInt, Model, Table } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
import { AccountModel } from '../account/account'
-import * as memoizee from 'memoizee'
export const getServerActor = memoizee(async function () {
const application = await ApplicationModel.load()
tableName: 'application',
timestamps: false
})
-export class ApplicationModel extends Model {
+export class ApplicationModel extends Model<Partial<AttributesOnly<ApplicationModel>>> {
@AllowNull(false)
@Default(0)
import { AllowNull, Column, CreatedAt, DataType, HasMany, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
import { OAuthTokenModel } from './oauth-token'
@Table({
}
]
})
-export class OAuthClientModel extends Model {
+export class OAuthClientModel extends Model<Partial<AttributesOnly<OAuthClientModel>>> {
@AllowNull(false)
@Column
import { TokensCache } from '@server/lib/auth/tokens-cache'
import { MUserAccountId } from '@server/types/models'
import { MOAuthTokenUser } from '@server/types/models/oauth/oauth-token'
+import { AttributesOnly } from '@shared/core-utils'
import { logger } from '../../helpers/logger'
import { AccountModel } from '../account/account'
-import { UserModel } from '../account/user'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
+import { UserModel } from '../user/user'
import { OAuthClientModel } from './oauth-client'
export type OAuthTokenInfo = {
}
]
})
-export class OAuthTokenModel extends Model {
+export class OAuthTokenModel extends Model<Partial<AttributesOnly<OAuthTokenModel>>> {
@AllowNull(false)
@Column
} from 'sequelize-typescript'
import { getServerActor } from '@server/models/application/application'
import { MActor, MVideoForRedundancyAPI, MVideoRedundancy, MVideoRedundancyAP, MVideoRedundancyVideo } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoRedundanciesTarget } from '@shared/models/redundancy/video-redundancies-filters.model'
import {
FileRedundancyInformation,
import { logger } from '../../helpers/logger'
import { CONFIG } from '../../initializers/config'
import { CONSTRAINTS_FIELDS, MIMETYPES } from '../../initializers/constants'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
import { ServerModel } from '../server/server'
import { getSort, getVideoSort, parseAggregateResult, throwIfNotValid } from '../utils'
import { ScheduleVideoUpdateModel } from '../video/schedule-video-update'
}
]
})
-export class VideoRedundancyModel extends Model {
+export class VideoRedundancyModel extends Model<Partial<AttributesOnly<VideoRedundancyModel>>> {
@CreatedAt
createdAt: Date
import { FindAndCountOptions, json, QueryTypes } from 'sequelize'
import { AllowNull, Column, CreatedAt, DataType, DefaultScope, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { MPlugin, MPluginFormattable } from '@server/types/models'
-import { PeerTubePlugin } from '../../../shared/models/plugins/peertube-plugin.model'
-import { PluginType } from '../../../shared/models/plugins/plugin.type'
-import { RegisterServerSettingOptions } from '../../../shared/models/plugins/register-server-setting.model'
+import { AttributesOnly } from '@shared/core-utils'
+import { PeerTubePlugin, PluginType, RegisterServerSettingOptions } from '../../../shared/models'
import {
isPluginDescriptionValid,
isPluginHomepage,
}
]
})
-export class PluginModel extends Model {
+export class PluginModel extends Model<Partial<AttributesOnly<PluginModel>>> {
@AllowNull(false)
@Is('PluginName', value => throwIfNotValid(value, isPluginNameValid, 'name'))
import { Op } from 'sequelize'
import { BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
import { MServerBlocklist, MServerBlocklistAccountServer, MServerBlocklistFormattable } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { ServerBlock } from '@shared/models'
import { AccountModel } from '../account/account'
import { getSort, searchAttribute } from '../utils'
}
]
})
-export class ServerBlocklistModel extends Model {
+export class ServerBlocklistModel extends Model<Partial<AttributesOnly<ServerBlocklistModel>>> {
@CreatedAt
createdAt: Date
import { AllowNull, Column, CreatedAt, Default, HasMany, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { MServer, MServerFormattable } from '@server/types/models/server'
+import { AttributesOnly } from '@shared/core-utils'
import { isHostValid } from '../../helpers/custom-validators/servers'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
import { throwIfNotValid } from '../utils'
import { ServerBlocklistModel } from './server-blocklist'
}
]
})
-export class ServerModel extends Model {
+export class ServerModel extends Model<Partial<AttributesOnly<ServerModel>>> {
@AllowNull(false)
@Is('Host', value => throwIfNotValid(value, isHostValid, 'valid host'))
import { AllowNull, BelongsToMany, Column, CreatedAt, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { Transaction } from 'sequelize/types'
import { MTracker } from '@server/types/models/server/tracker'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoModel } from '../video/video'
import { VideoTrackerModel } from './video-tracker'
}
]
})
-export class TrackerModel extends Model {
+export class TrackerModel extends Model<Partial<AttributesOnly<TrackerModel>>> {
@AllowNull(false)
@Column
import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoModel } from '../video/video'
import { TrackerModel } from './tracker'
}
]
})
-export class VideoTrackerModel extends Model {
+export class VideoTrackerModel extends Model<Partial<AttributesOnly<VideoTrackerModel>>> {
@CreatedAt
createdAt: Date
} from 'sequelize-typescript'
import { TokensCache } from '@server/lib/auth/tokens-cache'
import { MNotificationSettingFormattable } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { UserNotificationSetting, UserNotificationSettingValue } from '../../../shared/models/users/user-notification-setting.model'
import { isUserNotificationSettingValid } from '../../helpers/custom-validators/user-notifications'
import { throwIfNotValid } from '../utils'
}
]
})
-export class UserNotificationSettingModel extends Model {
+export class UserNotificationSettingModel extends Model<Partial<AttributesOnly<UserNotificationSettingModel>>> {
@AllowNull(false)
@Default(null)
import { FindOptions, ModelIndexesOptions, Op, WhereOptions } from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
import { UserNotificationIncludes, UserNotificationModelForApi } from '@server/types/models/user'
+import { AttributesOnly } from '@shared/core-utils'
import { UserNotification, UserNotificationType } from '../../../shared'
import { isBooleanValid } from '../../helpers/custom-validators/misc'
import { isUserNotificationTypeValid } from '../../helpers/custom-validators/user-notifications'
import { AbuseModel } from '../abuse/abuse'
import { VideoAbuseModel } from '../abuse/video-abuse'
import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
-import { ActorModel } from '../activitypub/actor'
-import { ActorFollowModel } from '../activitypub/actor-follow'
+import { AccountModel } from '../account/account'
+import { ActorModel } from '../actor/actor'
+import { ActorFollowModel } from '../actor/actor-follow'
+import { ActorImageModel } from '../actor/actor-image'
import { ApplicationModel } from '../application/application'
import { PluginModel } from '../server/plugin'
import { ServerModel } from '../server/server'
import { VideoChannelModel } from '../video/video-channel'
import { VideoCommentModel } from '../video/video-comment'
import { VideoImportModel } from '../video/video-import'
-import { AccountModel } from './account'
-import { ActorImageModel } from './actor-image'
import { UserModel } from './user'
enum ScopeNames {
}
] as (ModelIndexesOptions & { where?: WhereOptions })[]
})
-export class UserNotificationModel extends Model {
+export class UserNotificationModel extends Model<Partial<AttributesOnly<UserNotificationModel>>> {
@AllowNull(false)
@Default(null)
+import { DestroyOptions, Op, Transaction } from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, IsInt, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { MUserAccountId, MUserId } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoModel } from '../video/video'
import { UserModel } from './user'
-import { DestroyOptions, Op, Transaction } from 'sequelize'
-import { MUserAccountId, MUserId } from '@server/types/models'
@Table({
tableName: 'userVideoHistory',
}
]
})
-export class UserVideoHistoryModel extends Model {
+export class UserVideoHistoryModel extends Model<Partial<AttributesOnly<UserVideoHistoryModel>>> {
@CreatedAt
createdAt: Date
MUserWithNotificationSetting,
MVideoWithRights
} from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { hasUserRight, USER_ROLE_LABELS } from '../../../shared/core-utils/users'
import { AbuseState, MyUser, UserRight, VideoPlaylistType, VideoPrivacy } from '../../../shared/models'
import { User, UserRole } from '../../../shared/models/users'
import { comparePassword, cryptPassword } from '../../helpers/peertube-crypto'
import { DEFAULT_USER_THEME_NAME, NSFW_POLICY_TYPES } from '../../initializers/constants'
import { getThemeOrDefault } from '../../lib/plugins/theme-utils'
-import { ActorModel } from '../activitypub/actor'
-import { ActorFollowModel } from '../activitypub/actor-follow'
+import { AccountModel } from '../account/account'
+import { ActorModel } from '../actor/actor'
+import { ActorFollowModel } from '../actor/actor-follow'
+import { ActorImageModel } from '../actor/actor-image'
import { OAuthTokenModel } from '../oauth/oauth-token'
import { getSort, throwIfNotValid } from '../utils'
import { VideoModel } from '../video/video'
import { VideoImportModel } from '../video/video-import'
import { VideoLiveModel } from '../video/video-live'
import { VideoPlaylistModel } from '../video/video-playlist'
-import { AccountModel } from './account'
import { UserNotificationSettingModel } from './user-notification-setting'
-import { ActorImageModel } from './actor-image'
enum ScopeNames {
FOR_ME_API = 'FOR_ME_API',
}
]
})
-export class UserModel extends Model {
+export class UserModel extends Model<Partial<AttributesOnly<UserModel>>> {
@AllowNull(true)
@Is('UserPassword', value => throwIfNotValid(value, isUserPasswordValid, 'user password', true))
-import { literal, Op, OrderItem } from 'sequelize'
-import { Model, Sequelize } from 'sequelize-typescript'
+import { literal, Op, OrderItem, Sequelize } from 'sequelize'
import { Col } from 'sequelize/types/lib/utils'
import validator from 'validator'
return total
}
-const createSafeIn = (model: typeof Model, stringArr: (string | number)[]) => {
+function createSafeIn (sequelize: Sequelize, stringArr: (string | number)[]) {
return stringArr.map(t => {
return t === null
? null
- : model.sequelize.escape('' + t)
+ : sequelize.escape('' + t)
}).join(', ')
}
-import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
-import { ScopeNames as VideoScopeNames, VideoModel } from './video'
-import { VideoPrivacy } from '../../../shared/models/videos'
import { Op, Transaction } from 'sequelize'
+import { AllowNull, BelongsTo, Column, CreatedAt, Default, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { MScheduleVideoUpdateFormattable, MScheduleVideoUpdateVideoAll } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
+import { VideoPrivacy } from '../../../shared/models/videos'
+import { ScopeNames as VideoScopeNames, VideoModel } from './video'
@Table({
tableName: 'scheduleVideoUpdate',
}
]
})
-export class ScheduleVideoUpdateModel extends Model {
+export class ScheduleVideoUpdateModel extends Model<Partial<AttributesOnly<ScheduleVideoUpdateModel>>> {
@AllowNull(false)
@Default(null)
import { col, fn, QueryTypes, Transaction } from 'sequelize'
import { AllowNull, BelongsToMany, Column, CreatedAt, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { MTag } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoPrivacy, VideoState } from '../../../shared/models/videos'
import { isVideoTagValid } from '../../helpers/custom-validators/videos'
import { throwIfNotValid } from '../utils'
}
]
})
-export class TagModel extends Model {
+export class TagModel extends Model<Partial<AttributesOnly<TagModel>>> {
@AllowNull(false)
@Is('VideoTag', value => throwIfNotValid(value, isVideoTagValid, 'tag'))
} from 'sequelize-typescript'
import { afterCommitIfTransaction } from '@server/helpers/database-utils'
import { MThumbnail, MThumbnailVideo, MVideo } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
import { logger } from '../../helpers/logger'
import { CONFIG } from '../../initializers/config'
}
]
})
-export class ThumbnailModel extends Model {
+export class ThumbnailModel extends Model<Partial<AttributesOnly<ThumbnailModel>>> {
@AllowNull(false)
@Column
import { FindOptions } from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, Default, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { MVideoBlacklist, MVideoBlacklistFormattable } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoBlacklist, VideoBlacklistType } from '../../../shared/models/videos'
import { isVideoBlacklistReasonValid, isVideoBlacklistTypeValid } from '../../helpers/custom-validators/video-blacklist'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
}
]
})
-export class VideoBlacklistModel extends Model {
+export class VideoBlacklistModel extends Model<Partial<AttributesOnly<VideoBlacklistModel>>> {
@AllowNull(true)
@Is('VideoBlacklistReason', value => throwIfNotValid(value, isVideoBlacklistReasonValid, 'reason', true))
} from 'sequelize-typescript'
import { v4 as uuidv4 } from 'uuid'
import { MVideo, MVideoCaption, MVideoCaptionFormattable, MVideoCaptionVideo } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoCaption } from '../../../shared/models/videos/caption/video-caption.model'
import { isVideoCaptionLanguageValid } from '../../helpers/custom-validators/video-captions'
import { logger } from '../../helpers/logger'
}
]
})
-export class VideoCaptionModel extends Model {
+export class VideoCaptionModel extends Model<Partial<AttributesOnly<VideoCaptionModel>>> {
@CreatedAt
createdAt: Date
import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
import { MVideoChangeOwnershipFormattable, MVideoChangeOwnershipFull } from '@server/types/models/video/video-change-ownership'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoChangeOwnership, VideoChangeOwnershipStatus } from '../../../shared/models/videos'
import { AccountModel } from '../account/account'
import { getSort } from '../utils'
]
}
}))
-export class VideoChangeOwnershipModel extends Model {
+export class VideoChangeOwnershipModel extends Model<Partial<AttributesOnly<VideoChangeOwnershipModel>>> {
@CreatedAt
createdAt: Date
} from 'sequelize-typescript'
import { setAsUpdated } from '@server/helpers/database-utils'
import { MAccountActor } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { ActivityPubActor } from '../../../shared/models/activitypub'
import { VideoChannel, VideoChannelSummary } from '../../../shared/models/videos'
import {
MChannelSummaryFormattable
} from '../../types/models/video'
import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
-import { ActorImageModel } from '../account/actor-image'
-import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
-import { ActorFollowModel } from '../activitypub/actor-follow'
+import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor'
+import { ActorFollowModel } from '../actor/actor-follow'
+import { ActorImageModel } from '../actor/actor-image'
import { ServerModel } from '../server/server'
import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video'
}
]
})
-export class VideoChannelModel extends Model {
+export class VideoChannelModel extends Model<Partial<AttributesOnly<VideoChannelModel>>> {
@AllowNull(false)
@Is('VideoChannelName', value => throwIfNotValid(value, isVideoChannelNameValid, 'name'))
} from 'sequelize-typescript'
import { getServerActor } from '@server/models/application/application'
import { MAccount, MAccountId, MUserAccountId } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoPrivacy } from '@shared/models'
import { ActivityTagObject, ActivityTombstoneObject } from '../../../shared/models/activitypub/objects/common-objects'
import { VideoCommentObject } from '../../../shared/models/activitypub/objects/video-comment-object'
-import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/video-comment.model'
+import { VideoComment, VideoCommentAdmin } from '../../../shared/models/videos/comment/video-comment.model'
import { actorNameAlphabet } from '../../helpers/custom-validators/activitypub/actor'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { regexpCapture } from '../../helpers/regexp'
} from '../../types/models/video'
import { VideoCommentAbuseModel } from '../abuse/video-comment-abuse'
import { AccountModel } from '../account/account'
-import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
+import { ActorModel, unusedActorAttributesForAPI } from '../actor/actor'
import {
buildBlockedAccountSQL,
buildBlockedAccountSQLOptimized,
}
]
})
-export class VideoCommentModel extends Model {
+export class VideoCommentModel extends Model<Partial<AttributesOnly<VideoCommentModel>>> {
@CreatedAt
createdAt: Date
import { extractVideo } from '@server/helpers/video'
import { getTorrentFilePath } from '@server/lib/video-paths'
import { MStreamingPlaylistVideo, MVideo, MVideoWithHost } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import {
isVideoFileExtnameValid,
isVideoFileInfoHashValid,
}
]
})
-export class VideoFileModel extends Model {
+export class VideoFileModel extends Model<Partial<AttributesOnly<VideoFileModel>>> {
@CreatedAt
createdAt: Date
Table,
UpdatedAt
} from 'sequelize-typescript'
+import { afterCommitIfTransaction } from '@server/helpers/database-utils'
import { MVideoImportDefault, MVideoImportFormattable } from '@server/types/models/video/video-import'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoImport, VideoImportState } from '../../../shared'
import { isVideoImportStateValid, isVideoImportTargetUrlValid } from '../../helpers/custom-validators/video-imports'
import { isVideoMagnetUriValid } from '../../helpers/custom-validators/videos'
import { CONSTRAINTS_FIELDS, VIDEO_IMPORT_STATES } from '../../initializers/constants'
-import { UserModel } from '../account/user'
+import { UserModel } from '../user/user'
import { getSort, throwIfNotValid } from '../utils'
import { ScopeNames as VideoModelScopeNames, VideoModel } from './video'
-import { afterCommitIfTransaction } from '@server/helpers/database-utils'
@DefaultScope(() => ({
include: [
}
]
})
-export class VideoImportModel extends Model {
+export class VideoImportModel extends Model<Partial<AttributesOnly<VideoImportModel>>> {
@CreatedAt
createdAt: Date
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, DefaultScope, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
import { WEBSERVER } from '@server/initializers/constants'
import { MVideoLive, MVideoLiveVideo } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { LiveVideo, VideoState } from '@shared/models'
import { VideoModel } from './video'
import { VideoBlacklistModel } from './video-blacklist'
}
]
})
-export class VideoLiveModel extends Model {
+export class VideoLiveModel extends Model<Partial<AttributesOnly<VideoLiveModel>>> {
@AllowNull(true)
@Column(DataType.STRING)
import { getSort, throwIfNotValid } from '../utils'
import { ForAPIOptions, ScopeNames as VideoScopeNames, VideoModel } from './video'
import { VideoPlaylistModel } from './video-playlist'
+import { AttributesOnly } from '@shared/core-utils'
@Table({
tableName: 'videoPlaylistElement',
}
]
})
-export class VideoPlaylistElementModel extends Model {
+export class VideoPlaylistElementModel extends Model<Partial<AttributesOnly<VideoPlaylistElementModel>>> {
@CreatedAt
createdAt: Date
validate: false // We use a literal to update the position
}
- return VideoPlaylistElementModel.update({ position: Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`) }, query)
+ const positionQuery = Sequelize.literal(`${newPosition} + "position" - ${firstPosition}`)
+ return VideoPlaylistElementModel.update({ position: positionQuery as any }, query)
}
static increasePositionOf (
} from 'sequelize-typescript'
import { v4 as uuidv4 } from 'uuid'
import { MAccountId, MChannelId } from '@server/types/models'
+import { AttributesOnly } from '@shared/core-utils'
import { ActivityIconObject } from '../../../shared/models/activitypub/objects'
import { PlaylistObject } from '../../../shared/models/activitypub/objects/playlist-object'
import { VideoPlaylistPrivacy } from '../../../shared/models/videos/playlist/video-playlist-privacy.model'
MVideoPlaylistIdWithElements
} from '../../types/models/video/video-playlist'
import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions } from '../account/account'
+import { ActorModel } from '../actor/actor'
import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdated, throwIfNotValid } from '../utils'
import { ThumbnailModel } from './thumbnail'
import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
import { VideoPlaylistElementModel } from './video-playlist-element'
-import { ActorModel } from '../activitypub/actor'
enum ScopeNames {
AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
}
]
})
-export class VideoPlaylistModel extends Model {
+export class VideoPlaylistModel extends Model<Partial<AttributesOnly<VideoPlaylistModel>>> {
@CreatedAt
createdAt: Date
-import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models'
-import { buildDirectionAndField, createSafeIn } from '@server/models/utils'
-import { Model } from 'sequelize-typescript'
-import { MUserAccountId, MUserId } from '@server/types/models'
+import { Sequelize } from 'sequelize/types'
import validator from 'validator'
import { exists } from '@server/helpers/custom-validators/misc'
+import { buildDirectionAndField, createSafeIn } from '@server/models/utils'
+import { MUserAccountId, MUserId } from '@server/types/models'
+import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models'
export type BuildVideosQueryOptions = {
attributes?: string[]
having?: string
}
-function buildListQuery (model: typeof Model, options: BuildVideosQueryOptions) {
+function buildListQuery (sequelize: Sequelize, options: BuildVideosQueryOptions) {
const and: string[] = []
const joins: string[] = []
const replacements: any = {}
const blockerIds = [ options.serverAccountId ]
if (options.user) blockerIds.push(options.user.Account.id)
- const inClause = createSafeIn(model, blockerIds)
+ const inClause = createSafeIn(sequelize, blockerIds)
and.push(
'NOT EXISTS (' +
'EXISTS (' +
' SELECT 1 FROM "videoTag" ' +
' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
- ' WHERE lower("tag"."name") IN (' + createSafeIn(model, tagsOneOfLower) + ') ' +
+ ' WHERE lower("tag"."name") IN (' + createSafeIn(sequelize, tagsOneOfLower) + ') ' +
' AND "video"."id" = "videoTag"."videoId"' +
')'
)
'EXISTS (' +
' SELECT 1 FROM "videoTag" ' +
' INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
- ' WHERE lower("tag"."name") IN (' + createSafeIn(model, tagsAllOfLower) + ') ' +
+ ' WHERE lower("tag"."name") IN (' + createSafeIn(sequelize, tagsAllOfLower) + ') ' +
' AND "video"."id" = "videoTag"."videoId" ' +
' GROUP BY "videoTag"."videoId" HAVING COUNT(*) = ' + tagsAllOfLower.length +
')'
languagesQueryParts.push(
'EXISTS (' +
' SELECT 1 FROM "videoCaption" WHERE "videoCaption"."language" ' +
- ' IN (' + createSafeIn(model, languages) + ') AND ' +
+ ' IN (' + createSafeIn(sequelize, languages) + ') AND ' +
' "videoCaption"."videoId" = "video"."id"' +
')'
)
}
if (options.search) {
- const escapedSearch = model.sequelize.escape(options.search)
- const escapedLikeSearch = model.sequelize.escape('%' + options.search + '%')
+ const escapedSearch = sequelize.escape(options.search)
+ const escapedLikeSearch = sequelize.escape('%' + options.search + '%')
cte.push(
'"trigramSearch" AS (' +
import { literal, Op, QueryTypes, Transaction } from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Scopes, Table, UpdatedAt } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
import { CONSTRAINTS_FIELDS } from '../../initializers/constants'
import { MActorDefault } from '../../types/models'
import { MVideoShareActor, MVideoShareFull } from '../../types/models/video'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
import { buildLocalActorIdsIn, throwIfNotValid } from '../utils'
import { VideoModel } from './video'
}
]
})
-export class VideoShareModel extends Model {
+export class VideoShareModel extends Model<Partial<AttributesOnly<VideoShareModel>>> {
@AllowNull(false)
@Is('VideoShareUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import { throwIfNotValid } from '../utils'
import { VideoModel } from './video'
+import { AttributesOnly } from '@shared/core-utils'
@Table({
tableName: 'videoStreamingPlaylist',
}
]
})
-export class VideoStreamingPlaylistModel extends Model {
+export class VideoStreamingPlaylistModel extends Model<Partial<AttributesOnly<VideoStreamingPlaylistModel>>> {
@CreatedAt
createdAt: Date
import { Column, CreatedAt, ForeignKey, Model, Table, UpdatedAt } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
import { TagModel } from './tag'
import { VideoModel } from './video'
}
]
})
-export class VideoTagModel extends Model {
+export class VideoTagModel extends Model<Partial<AttributesOnly<VideoTagModel>>> {
@CreatedAt
createdAt: Date
+import * as Sequelize from 'sequelize'
import { AllowNull, BelongsTo, Column, CreatedAt, ForeignKey, Model, Table } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoModel } from './video'
-import * as Sequelize from 'sequelize'
@Table({
tableName: 'videoView',
}
]
})
-export class VideoViewModel extends Model {
+export class VideoViewModel extends Model<Partial<AttributesOnly<VideoViewModel>>> {
@CreatedAt
createdAt: Date
import { getHLSDirectory, getVideoFilePath } from '@server/lib/video-paths'
import { getServerActor } from '@server/models/application/application'
import { ModelCache } from '@server/models/model-cache'
+import { AttributesOnly } from '@shared/core-utils'
import { VideoFile } from '@shared/models/videos/video-file.model'
import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
import { VideoObject } from '../../../shared/models/activitypub/objects'
import { VideoAbuseModel } from '../abuse/video-abuse'
import { AccountModel } from '../account/account'
import { AccountVideoRateModel } from '../account/account-video-rate'
-import { ActorImageModel } from '../account/actor-image'
-import { UserModel } from '../account/user'
-import { UserVideoHistoryModel } from '../account/user-video-history'
-import { ActorModel } from '../activitypub/actor'
+import { ActorModel } from '../actor/actor'
+import { ActorImageModel } from '../actor/actor-image'
import { VideoRedundancyModel } from '../redundancy/video-redundancy'
import { ServerModel } from '../server/server'
import { TrackerModel } from '../server/tracker'
import { VideoTrackerModel } from '../server/video-tracker'
+import { UserModel } from '../user/user'
+import { UserVideoHistoryModel } from '../user/user-video-history'
import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
import { ScheduleVideoUpdateModel } from './schedule-video-update'
import { TagModel } from './tag'
}
]
})
-export class VideoModel extends Model {
+export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
@AllowNull(false)
@Default(DataType.UUIDV4)
includeLocalVideos: true
}
- const { query, replacements } = buildListQuery(VideoModel, queryOptions)
+ const { query, replacements } = buildListQuery(VideoModel.sequelize, queryOptions)
return this.sequelize.query<any>(query, { replacements, type: QueryTypes.SELECT })
.then(rows => rows.map(r => r[field]))
if (countVideos !== true) return Promise.resolve(undefined)
const countOptions = Object.assign({}, options, { isCount: true })
- const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, countOptions)
+ const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel.sequelize, countOptions)
return VideoModel.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT })
.then(rows => rows.length !== 0 ? rows[0].total : 0)
function getModels () {
if (options.count === 0) return Promise.resolve([])
- const { query, replacements, order } = buildListQuery(VideoModel, options)
+ const { query, replacements, order } = buildListQuery(VideoModel.sequelize, options)
const queryModels = wrapForAPIResults(query, replacements, options, order)
return VideoModel.sequelize.query<any>(queryModels, { replacements, type: QueryTypes.SELECT, nest: true })
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
import 'mocha'
-
+import { HttpStatusCode } from '@shared/core-utils'
import {
checkBadCountPagination,
checkBadSortPagination,
flushAndRunServer,
immutableAssign,
installPlugin,
- makeGetRequest, makePostBodyRequest, makePutBodyRequest,
+ makeGetRequest,
+ makePostBodyRequest,
+ makePutBodyRequest,
ServerInfo,
setAccessTokensToServers,
userLogin
-} from '../../../../shared/extra-utils'
-import { PluginType } from '../../../../shared/models/plugins/plugin.type'
-import { PeerTubePlugin } from '../../../../shared/models/plugins/peertube-plugin.model'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+} from '@shared/extra-utils'
+import { PeerTubePlugin, PluginType } from '@shared/models'
describe('Test server plugins API validators', function () {
let server: ServerInfo
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-import * as chai from 'chai'
import 'mocha'
-import { AccountBlock, ServerBlock, Video, UserNotification, UserNotificationType } from '../../../../shared/index'
+import * as chai from 'chai'
import {
+ addAccountToAccountBlocklist,
+ addAccountToServerBlocklist,
+ addServerToAccountBlocklist,
+ addServerToServerBlocklist,
+ addVideoCommentReply,
+ addVideoCommentThread,
cleanupTests,
createUser,
deleteVideoComment,
doubleFollow,
+ findCommentId,
flushAndRunMultipleServers,
- ServerInfo,
- uploadVideo,
- userLogin,
follow,
- unfollow
-} from '../../../../shared/extra-utils/index'
-import { setAccessTokensToServers } from '../../../../shared/extra-utils/users/login'
-import { getVideosList, getVideosListWithToken } from '../../../../shared/extra-utils/videos/videos'
-import {
- addVideoCommentReply,
- addVideoCommentThread,
- getVideoCommentThreads,
- getVideoThreadComments,
- findCommentId
-} from '../../../../shared/extra-utils/videos/video-comments'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
-import {
- addAccountToAccountBlocklist,
- addAccountToServerBlocklist,
- addServerToAccountBlocklist,
- addServerToServerBlocklist,
getAccountBlocklistByAccount,
getAccountBlocklistByServer,
getServerBlocklistByAccount,
getServerBlocklistByServer,
+ getUserNotifications,
+ getVideoCommentThreads,
+ getVideosList,
+ getVideosListWithToken,
+ getVideoThreadComments,
removeAccountFromAccountBlocklist,
removeAccountFromServerBlocklist,
removeServerFromAccountBlocklist,
- removeServerFromServerBlocklist
-} from '../../../../shared/extra-utils/users/blocklist'
-import { getUserNotifications } from '../../../../shared/extra-utils/users/user-notifications'
+ removeServerFromServerBlocklist,
+ ServerInfo,
+ setAccessTokensToServers,
+ unfollow,
+ uploadVideo,
+ userLogin,
+ waitJobs
+} from '@shared/extra-utils'
+import {
+ AccountBlock,
+ ServerBlock,
+ UserNotification,
+ UserNotificationType,
+ Video,
+ VideoComment,
+ VideoCommentThreadTree
+} from '@shared/models'
const expect = chai.expect
import 'mocha'
import * as chai from 'chai'
-import { cleanupTests, getVideoCommentThreads, getVideoThreadComments, updateMyUser } from '../../../../shared/extra-utils'
-import { ServerInfo, uploadVideo } from '../../../../shared/extra-utils/index'
-import { MockSmtpServer } from '../../../../shared/extra-utils/miscs/email'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { addAccountToAccountBlocklist, removeAccountFromAccountBlocklist } from '../../../../shared/extra-utils/users/blocklist'
import {
+ addAccountToAccountBlocklist,
+ addVideoCommentReply,
+ addVideoCommentThread,
checkCommentMention,
CheckerBaseParams,
checkNewCommentOnMyVideo,
- prepareNotificationsTest
-} from '../../../../shared/extra-utils/users/user-notifications'
-import { addVideoCommentReply, addVideoCommentThread } from '../../../../shared/extra-utils/videos/video-comments'
-import { UserNotification } from '../../../../shared/models/users'
-import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
+ cleanupTests,
+ getVideoCommentThreads,
+ getVideoThreadComments,
+ MockSmtpServer,
+ prepareNotificationsTest,
+ removeAccountFromAccountBlocklist,
+ ServerInfo,
+ updateMyUser,
+ uploadVideo,
+ waitJobs
+} from '@shared/extra-utils'
+import { UserNotification, VideoCommentThreadTree } from '@shared/models'
const expect = chai.expect
import 'mocha'
import * as chai from 'chai'
-import { VideoComment } from '@shared/models/videos/video-comment.model'
+import { Video, VideoComment } from '@shared/models'
import {
+ addVideoCommentReply,
addVideoCommentThread,
bulkRemoveCommentsOf,
cleanupTests,
createUser,
+ doubleFollow,
flushAndRunMultipleServers,
getVideoCommentThreads,
getVideosList,
setAccessTokensToServers,
uploadVideo,
userLogin,
- waitJobs,
- addVideoCommentReply
+ waitJobs
} from '../../../../shared/extra-utils/index'
-import { doubleFollow } from '../../../../shared/extra-utils/server/follows'
-import { Video } from '@shared/models'
const expect = chai.expect
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-import * as chai from 'chai'
import 'mocha'
-import { Video, VideoPrivacy } from '../../../../shared/models/videos'
-import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
-import { cleanupTests, completeVideoCheck, deleteVideoComment } from '../../../../shared/extra-utils'
+import * as chai from 'chai'
import {
+ addVideoCommentReply,
+ addVideoCommentThread,
+ cleanupTests,
+ completeVideoCheck,
+ createUser,
+ createVideoCaption,
+ dateIsValid,
+ deleteVideoComment,
+ expectAccountFollows,
flushAndRunMultipleServers,
- getVideosList,
- ServerInfo,
- setAccessTokensToServers,
- uploadVideo
-} from '../../../../shared/extra-utils/index'
-import { dateIsValid } from '../../../../shared/extra-utils/miscs/miscs'
-import {
follow,
getFollowersListPaginationAndSort,
getFollowingListPaginationAndSort,
- unfollow
-} from '../../../../shared/extra-utils/server/follows'
-import { expectAccountFollows } from '../../../../shared/extra-utils/users/accounts'
-import { userLogin } from '../../../../shared/extra-utils/users/login'
-import { createUser } from '../../../../shared/extra-utils/users/users'
-import {
- addVideoCommentReply,
- addVideoCommentThread,
getVideoCommentThreads,
- getVideoThreadComments
-} from '../../../../shared/extra-utils/videos/video-comments'
-import { rateVideo } from '../../../../shared/extra-utils/videos/videos'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { createVideoCaption, listVideoCaptions, testCaptionFile } from '../../../../shared/extra-utils/videos/video-captions'
-import { VideoCaption } from '../../../../shared/models/videos/caption/video-caption.model'
+ getVideosList,
+ getVideoThreadComments,
+ listVideoCaptions,
+ rateVideo,
+ ServerInfo,
+ setAccessTokensToServers,
+ testCaptionFile,
+ unfollow,
+ uploadVideo,
+ userLogin,
+ waitJobs
+} from '@shared/extra-utils'
+import { Video, VideoCaption, VideoComment, VideoCommentThreadTree, VideoPrivacy } from '@shared/models'
const expect = chai.expect
import 'mocha'
import { JobState, Video } from '../../../../shared/models'
import { VideoPrivacy } from '../../../../shared/models/videos'
-import { VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
+import { VideoCommentThreadTree } from '../../../../shared/models/videos/comment/video-comment.model'
import {
cleanupTests,
// Wait video expiration
await wait(11000)
- for (let i = 0; i < 3; i++) {
- await getVideo(servers[1].url, videoIdsServer1[i])
- await waitJobs([ servers[1] ])
- await wait(1500)
+ for (let i = 0; i < 5; i++) {
+ try {
+ await getVideo(servers[1].url, videoIdsServer1[i])
+ await waitJobs([ servers[1] ])
+ await wait(1500)
+ } catch {}
}
for (const id of videoIdsServer1) {
updatePluginSettings,
wait,
waitUntilLog
-} from '../../../../shared/extra-utils'
-import { PeerTubePluginIndex } from '../../../../shared/models/plugins/peertube-plugin-index.model'
-import { PeerTubePlugin } from '../../../../shared/models/plugins/peertube-plugin.model'
-import { PluginPackageJson } from '../../../../shared/models/plugins/plugin-package-json.model'
-import { PluginType } from '../../../../shared/models/plugins/plugin.type'
-import { PublicServerSetting } from '../../../../shared/models/plugins/public-server.setting'
-import { ServerConfig } from '../../../../shared/models/server'
-import { User } from '../../../../shared/models/users'
+} from '@shared/extra-utils'
+import { PeerTubePlugin, PeerTubePluginIndex, PluginPackageJson, PluginType, PublicServerSetting, ServerConfig, User } from '@shared/models'
const expect = chai.expect
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
-import * as chai from 'chai'
import 'mocha'
+import * as chai from 'chai'
import { join } from 'path'
import * as request from 'supertest'
-import { VideoPrivacy } from '../../../../shared/models/videos'
-import { VideoComment, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
+import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
import {
addVideoChannel,
checkTmpIsEmpty,
wait,
webtorrentAdd
} from '../../../../shared/extra-utils'
+import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
import {
addVideoCommentReply,
addVideoCommentThread,
deleteVideoComment,
+ findCommentId,
getVideoCommentThreads,
- getVideoThreadComments,
- findCommentId
+ getVideoThreadComments
} from '../../../../shared/extra-utils/videos/video-comments'
-import { waitJobs } from '../../../../shared/extra-utils/server/jobs'
-import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
+import { VideoComment, VideoCommentThreadTree, VideoPrivacy } from '../../../../shared/models/videos'
const expect = chai.expect
import 'mocha'
import * as chai from 'chai'
-
+import { VideoComment, VideoCommentAdmin, VideoCommentThreadTree } from '@shared/models'
import { cleanupTests, testImage } from '../../../../shared/extra-utils'
import {
createUser,
getVideoCommentThreads,
getVideoThreadComments
} from '../../../../shared/extra-utils/videos/video-comments'
-import { VideoComment, VideoCommentAdmin, VideoCommentThreadTree } from '../../../../shared/models/videos/video-comment.model'
const expect = chai.expect
import 'mocha'
import * as chai from 'chai'
import * as request from 'supertest'
-import { Account, VideoPlaylistPrivacy } from '@shared/models'
+import { Account, HTMLServerConfig, ServerConfig, VideoPlaylistPrivacy } from '@shared/models'
import {
addVideoInPlaylist,
cleanupTests,
doubleFollow,
flushAndRunMultipleServers,
getAccount,
+ getConfig,
getCustomConfig,
getVideosList,
makeHTMLRequest,
waitJobs
} from '../../shared/extra-utils'
import { HttpStatusCode } from '@shared/core-utils/miscs/http-error-codes'
+import { omit } from 'lodash'
const expect = chai.expect
-function checkIndexTags (html: string, title: string, description: string, css: string) {
+function checkIndexTags (html: string, title: string, description: string, css: string, config: ServerConfig) {
expect(html).to.contain('<title>' + title + '</title>')
expect(html).to.contain('<meta name="description" content="' + description + '" />')
expect(html).to.contain('<style class="custom-css-style">' + css + '</style>')
+
+ const htmlConfig: HTMLServerConfig = omit(config, 'signup')
+ expect(html).to.contain(`<script type="application/javascript">window.PeerTubeServerConfig = '${JSON.stringify(htmlConfig)}'</script>`)
}
describe('Test a client controllers', function () {
describe('Index HTML', function () {
it('Should have valid index html tags (title, description...)', async function () {
+ const resConfig = await getConfig(servers[0].url)
const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
const description = 'PeerTube, an ActivityPub-federated video streaming platform using P2P directly in your web browser.'
- checkIndexTags(res.text, 'PeerTube', description, '')
+ checkIndexTags(res.text, 'PeerTube', description, '', resConfig.body)
})
it('Should update the customized configuration and have the correct index html tags', async function () {
}
})
+ const resConfig = await getConfig(servers[0].url)
const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
- checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }')
+ checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body)
})
it('Should have valid index html updated tags (title, description...)', async function () {
+ const resConfig = await getConfig(servers[0].url)
const res = await makeHTMLRequest(servers[0].url, '/videos/trending')
- checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }')
+ checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body)
})
it('Should use the original video URL for the canonical tag', async function () {
})
})
+ describe('Embed HTML', function () {
+
+ it('Should have the correct embed html tags', async function () {
+ const resConfig = await getConfig(servers[0].url)
+ const res = await makeHTMLRequest(servers[0].url, servers[0].video.embedPath)
+
+ checkIndexTags(res.text, 'PeerTube updated', 'my short description', 'body { background-color: red; }', resConfig.body)
+ })
+ })
+
after(async function () {
await cleanupTests(servers)
})
import { cleanupTests, flushAndRunMultipleServers, ServerInfo, waitUntilLog } from '../../../shared/extra-utils/server/servers'
import { getGoodVideoUrl, getMyVideoImports, importVideo } from '../../../shared/extra-utils/videos/video-imports'
import {
+ VideoCommentThreadTree,
VideoDetails,
VideoImport,
VideoImportState,
VideoPlaylistPrivacy,
VideoPrivacy
} from '../../../shared/models/videos'
-import { VideoCommentThreadTree } from '../../../shared/models/videos/video-comment.model'
const expect = chai.expect
import { advancedVideosSearch, getClient, getVideoCategories, login, uploadVideo } from '../../shared/extra-utils/index'
import { sha256 } from '../helpers/core-utils'
import { doRequestAndSaveToFile } from '../helpers/requests'
-import { buildOriginallyPublishedAt, getYoutubeDLVideoFormat, safeGetYoutubeDL } from '../helpers/youtube-dl'
import { CONSTRAINTS_FIELDS } from '../initializers/constants'
import { buildCommonVideoOptions, buildVideoAttributesFromCommander, getLogger, getServerCredentials } from './cli'
+import { YoutubeDL } from '@server/helpers/youtube-dl'
type UserInfo = {
username: string
user.password = await promptPassword()
}
- const youtubeDL = await safeGetYoutubeDL()
+ const youtubeDLBinary = await YoutubeDL.safeGetYoutubeDL()
- let info = await getYoutubeDLInfo(youtubeDL, options.targetUrl, command.args)
+ let info = await getYoutubeDLInfo(youtubeDLBinary, options.targetUrl, command.args)
if (!Array.isArray(info)) info = [ info ]
if (uploadsObject) {
console.log('Fixing URL to %s.', uploadsObject.url)
- info = await getYoutubeDLInfo(youtubeDL, uploadsObject.url, command.args)
+ info = await getYoutubeDLInfo(youtubeDLBinary, uploadsObject.url, command.args)
}
let infoArray: any[]
youtubeInfo: any
}) {
const { youtubeInfo, cwd, url, user } = parameters
+ const youtubeDL = new YoutubeDL('', [])
log.debug('Fetching object.', youtubeInfo)
const videoInfo = await fetchObject(youtubeInfo)
log.debug('Fetched object.', videoInfo)
- const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo)
+ const originallyPublishedAt = youtubeDL.buildOriginallyPublishedAt(videoInfo)
if (options.since && originallyPublishedAt && originallyPublishedAt.getTime() < options.since.getTime()) {
log.info('Video "%s" has been published before "%s", don\'t upload it.\n',
videoInfo.title, formatDate(options.since))
log.info('Downloading video "%s"...', videoInfo.title)
- const youtubeDLOptions = [ '-f', getYoutubeDLVideoFormat(), ...command.args, '-o', path ]
+ const youtubeDLOptions = [ '-f', youtubeDL.getYoutubeDLVideoFormat(), ...command.args, '-o', path ]
try {
- const youtubeDL = await safeGetYoutubeDL()
- const youtubeDLExec = promisify(youtubeDL.exec).bind(youtubeDL)
+ const youtubeDLBinary = await YoutubeDL.safeGetYoutubeDL()
+ const youtubeDLExec = promisify(youtubeDLBinary.exec).bind(youtubeDLBinary)
const output = await youtubeDLExec(videoInfo.url, youtubeDLOptions, processOptions)
log.info(output.join('\n'))
await uploadVideoOnPeerTube({
+ youtubeDL,
cwd,
url,
user,
}
async function uploadVideoOnPeerTube (parameters: {
+ youtubeDL: YoutubeDL
videoInfo: any
videoPath: string
cwd: string
url: string
user: { username: string, password: string }
}) {
- const { videoInfo, videoPath, cwd, url, user } = parameters
+ const { youtubeDL, videoInfo, videoPath, cwd, url, user } = parameters
const category = await getCategory(videoInfo.categories, url)
const licence = getLicence(videoInfo.license)
await doRequestAndSaveToFile(videoInfo.thumbnail, thumbnailfile)
}
- const originallyPublishedAt = buildOriginallyPublishedAt(videoInfo)
+ const originallyPublishedAt = youtubeDL.buildOriginallyPublishedAt(videoInfo)
const defaultAttributes = {
name: truncate(videoInfo.title, {
const url = buildUrl(info)
return new Promise<any>(async (res, rej) => {
- const youtubeDL = await safeGetYoutubeDL()
+ const youtubeDL = await YoutubeDL.safeGetYoutubeDL()
youtubeDL.getInfo(url, undefined, processOptions, (err, videoInfo) => {
if (err) return rej(err)
registerTSPaths()
import * as program from 'commander'
-import { PluginType } from '../../shared/models/plugins/plugin.type'
import { installPlugin, listPlugins, uninstallPlugin, updatePlugin } from '../../shared/extra-utils/server/plugins'
import { getAdminTokenOrDie, getServerCredentials } from './cli'
-import { PeerTubePlugin } from '../../shared/models/plugins/peertube-plugin.model'
+import { PeerTubePlugin, PluginType } from '../../shared/models'
import { isAbsolute } from 'path'
import * as CliTable3 from 'cli-table3'
import commander = require('commander')
import { FunctionProperties, PickWith } from '@shared/core-utils'
import { AccountModel } from '../../../models/account/account'
-import { MChannelDefault } from '../video/video-channels'
-import { MAccountBlocklistId } from './account-blocklist'
import {
MActor,
MActorAPAccount,
MActorSummary,
MActorSummaryFormattable,
MActorUrl
-} from './actor'
+} from '../actor'
+import { MChannelDefault } from '../video/video-channels'
+import { MAccountBlocklistId } from './account-blocklist'
type Use<K extends keyof AccountModel, M> = PickWith<AccountModel, K, M>
export * from './account'
export * from './account-blocklist'
-export * from './actor-follow'
-export * from './actor-image'
-export * from './actor'
import { PickWith } from '@shared/core-utils'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
import {
MActor,
MActorChannelAccountActor,
-import { ActorImageModel } from '../../../models/account/actor-image'
import { FunctionProperties } from '@shared/core-utils'
+import { ActorImageModel } from '../../../models/actor/actor-image'
export type MActorImage = ActorImageModel
-
import { FunctionProperties, PickWith, PickWithOpt } from '@shared/core-utils'
-import { ActorModel } from '../../../models/activitypub/actor'
+import { ActorModel } from '../../../models/actor/actor'
+import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from '../account'
import { MServer, MServerHost, MServerHostBlocks, MServerRedundancyAllowed } from '../server'
import { MChannel, MChannelAccountActor, MChannelAccountDefault, MChannelId, MChannelIdActor } from '../video'
-import { MAccount, MAccountDefault, MAccountId, MAccountIdActor } from './account'
import { MActorImage, MActorImageFormattable } from './actor-image'
type Use<K extends keyof ActorModel, M> = PickWith<ActorModel, K, M>
--- /dev/null
+export * from './actor-follow'
+export * from './actor-image'
+export * from './actor'
+export * from './abuse'
export * from './account'
+export * from './actor'
export * from './application'
-export * from './moderation'
export * from './oauth'
export * from './server'
export * from './user'
-import { UserNotificationSettingModel } from '@server/models/account/user-notification-setting'
+import { UserNotificationSettingModel } from '@server/models/user/user-notification-setting'
export type MNotificationSetting = Omit<UserNotificationSettingModel, 'User'>
import { VideoCommentAbuseModel } from '@server/models/abuse/video-comment-abuse'
import { ApplicationModel } from '@server/models/application/application'
import { PluginModel } from '@server/models/server/plugin'
+import { UserNotificationModel } from '@server/models/user/user-notification'
import { PickWith, PickWithOpt } from '@shared/core-utils'
import { AbuseModel } from '../../../models/abuse/abuse'
import { AccountModel } from '../../../models/account/account'
-import { ActorImageModel } from '../../../models/account/actor-image'
-import { UserNotificationModel } from '../../../models/account/user-notification'
-import { ActorModel } from '../../../models/activitypub/actor'
-import { ActorFollowModel } from '../../../models/activitypub/actor-follow'
+import { ActorModel } from '../../../models/actor/actor'
+import { ActorFollowModel } from '../../../models/actor/actor-follow'
+import { ActorImageModel } from '../../../models/actor/actor-image'
import { ServerModel } from '../../../models/server/server'
import { VideoModel } from '../../../models/video/video'
import { VideoBlacklistModel } from '../../../models/video/video-blacklist'
-import { UserVideoHistoryModel } from '../../../models/account/user-video-history'
+import { UserVideoHistoryModel } from '../../../models/user/user-video-history'
export type MUserVideoHistory = Omit<UserVideoHistoryModel, 'Video' | 'User'>
import { AccountModel } from '@server/models/account/account'
+import { UserModel } from '@server/models/user/user'
import { MVideoPlaylist } from '@server/types/models'
import { PickWith, PickWithOpt } from '@shared/core-utils'
-import { UserModel } from '../../../models/account/user'
import {
MAccount,
MAccountDefault,
MAccountSummaryBlocks,
MAccountSummaryFormattable,
MAccountUrl,
- MAccountUserId,
+ MAccountUserId
+} from '../account'
+import {
MActor,
MActorAccountChannelId,
MActorAPChannel,
MActorSummary,
MActorSummaryFormattable,
MActorUrl
-} from '../account'
+} from '../actor'
import { MVideo } from './video'
type Use<K extends keyof VideoChannelModel, M> = PickWith<VideoChannelModel, K, M>
-import { VideoShareModel } from '../../../models/video/video-share'
import { PickWith } from '@shared/core-utils'
-import { MActorDefault } from '../account'
+import { VideoShareModel } from '../../../models/video/video-share'
+import { MActorDefault } from '../actor'
import { MVideo } from './video'
type Use<K extends keyof VideoShareModel, M> = PickWith<VideoShareModel, K, M>
-import { Router, Response } from 'express'
+import { Response, Router } from 'express'
import { Logger } from 'winston'
-import { ActorModel } from '@server/models/activitypub/actor'
+import { ActorModel } from '@server/models/actor/actor'
import {
PluginPlaylistPrivacyManager,
PluginSettingsManager,
-import { Model } from 'sequelize-typescript'
+import { AttributesOnly } from '@shared/core-utils'
+import { Model } from 'sequelize'
// Thanks to sequelize-typescript: https://github.com/RobinBuschmann/sequelize-typescript
export type RecursivePartial<T> = { [P in keyof T]?: RecursivePartial<T[P]> }
-export type FilteredModelAttributes<T extends Model<T>> = RecursivePartial<Omit<T, keyof Model<any>>> & {
+export type FilteredModelAttributes<T extends Model<any>> = Partial<AttributesOnly<T>> & {
id?: number | any
createdAt?: Date | any
updatedAt?: Date | any
export type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>
+export type AttributesOnly<T> = {
+ [K in keyof T]: T[K] extends Function ? never : T[K]
+}
+
export type PickWith<T, KT extends keyof T, V> = {
[P in KT]: T[P] extends V ? V : never
}
export * from './bulk/bulk'
+
export * from './cli/cli'
+
export * from './feeds/feeds'
+
export * from './mock-servers/mock-instances-index'
-export * from './miscs/miscs'
+
+export * from './miscs/email'
export * from './miscs/sql'
+export * from './miscs/miscs'
export * from './miscs/stubs'
+
export * from './moderation/abuses'
export * from './plugins/mock-blocklist'
+
export * from './requests/check-api-params'
export * from './requests/requests'
+
export * from './search/videos'
+
export * from './server/activitypub'
export * from './server/clients'
export * from './server/config'
export * from './server/jobs'
export * from './server/plugins'
export * from './server/servers'
+
export * from './users/accounts'
+export * from './users/blocklist'
export * from './users/login'
+export * from './users/user-notifications'
+export * from './users/user-subscriptions'
export * from './users/users'
+
export * from './videos/live'
export * from './videos/services'
export * from './videos/video-blacklist'
import { readJSON, writeJSON } from 'fs-extra'
import { join } from 'path'
import { RegisteredServerSettings } from '@shared/models'
-import { PeertubePluginIndexList } from '../../models/plugins/peertube-plugin-index-list.model'
+import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
+import { PeertubePluginIndexList } from '../../models/plugins/plugin-index/peertube-plugin-index-list.model'
import { PluginType } from '../../models/plugins/plugin.type'
import { buildServerDirectory, root } from '../miscs/miscs'
import { makeGetRequest, makePostBodyRequest, makePutBodyRequest } from '../requests/requests'
import { ServerInfo } from './servers'
-import { HttpStatusCode } from '../../../shared/core-utils/miscs/http-error-codes'
function listPlugins (parameters: {
url: string
uuid: string
name?: string
url?: string
+
account?: {
name: string
}
+
+ embedPath?: string
}
remoteVideo?: {
--- /dev/null
+export * from './nodeinfo.model'
-export * from './videos-overview'
+export * from './videos-overview.model'
--- /dev/null
+export * from './client-hook.model'
+export * from './plugin-client-scope.type'
+export * from './plugin-element-placeholder.type'
+export * from './register-client-form-field.model'
+export * from './register-client-hook.model'
+export * from './register-client-settings-script.model'
-import { RegisterServerSettingOptions } from "./register-server-setting.model"
+import { RegisterServerSettingOptions } from '../server'
export interface RegisterClientSettingsScript {
isSettingHidden (options: {
-export * from './client-hook.model'
+export * from './client'
+export * from './plugin-index'
+export * from './server'
export * from './hook-type.enum'
-export * from './install-plugin.model'
-export * from './manage-plugin.model'
-export * from './peertube-plugin-index-list.model'
-export * from './peertube-plugin-index.model'
-export * from './peertube-plugin-latest-version.model'
-export * from './peertube-plugin.model'
-export * from './plugin-client-scope.type'
-export * from './plugin-element-placeholder.type'
export * from './plugin-package-json.model'
-export * from './plugin-playlist-privacy-manager.model'
-export * from './plugin-settings-manager.model'
-export * from './plugin-storage-manager.model'
-export * from './plugin-transcoding-manager.model'
-export * from './plugin-translation.model'
-export * from './plugin-video-category-manager.model'
-export * from './plugin-video-language-manager.model'
-export * from './plugin-video-licence-manager.model'
-export * from './plugin-video-privacy-manager.model'
export * from './plugin.type'
-export * from './public-server.setting'
-export * from './register-client-hook.model'
-export * from './register-client-settings-script.model'
-export * from './register-client-form-field.model'
-export * from './register-server-hook.model'
-export * from './register-server-setting.model'
-export * from './server-hook.model'
--- /dev/null
+export * from './peertube-plugin-index-list.model'
+export * from './peertube-plugin-index.model'
+export * from './peertube-plugin-latest-version.model'
-import { PluginType } from './plugin.type'
+import { PluginType } from '../plugin.type'
export interface PeertubePluginIndexList {
start: number
-import { PluginClientScope } from './plugin-client-scope.type'
+import { PluginClientScope } from './client/plugin-client-scope.type'
export type PluginTranslationPaths = {
[ locale: string ]: string
--- /dev/null
+export * from './install-plugin.model'
+export * from './manage-plugin.model'
+export * from './peertube-plugin.model'
-import { PluginType } from './plugin.type'
+import { PluginType } from '../../plugin.type'
export interface PeerTubePlugin {
name: string
--- /dev/null
+export * from './api'
+export * from './managers'
+export * from './settings'
+export * from './plugin-translation.model'
+export * from './register-server-hook.model'
+export * from './server-hook.model'
--- /dev/null
+
+export * from './plugin-playlist-privacy-manager.model'
+export * from './plugin-settings-manager.model'
+export * from './plugin-storage-manager.model'
+export * from './plugin-transcoding-manager.model'
+export * from './plugin-video-category-manager.model'
+export * from './plugin-video-language-manager.model'
+export * from './plugin-video-licence-manager.model'
+export * from './plugin-video-privacy-manager.model'
-import { VideoPlaylistPrivacy } from '../videos/playlist/video-playlist-privacy.model'
+import { VideoPlaylistPrivacy } from '../../../videos/playlist/video-playlist-privacy.model'
export interface PluginPlaylistPrivacyManager {
// PUBLIC = 1,
-import { EncoderOptionsBuilder } from '../videos/video-transcoding.model'
+import { EncoderOptionsBuilder } from '../../../videos/video-transcoding.model'
export interface PluginTranscodingManager {
addLiveProfile (encoder: string, profile: string, builder: EncoderOptionsBuilder): boolean
-import { VideoPrivacy } from '../videos/video-privacy.enum'
+import { VideoPrivacy } from '../../../videos/video-privacy.enum'
export interface PluginVideoPrivacyManager {
// PUBLIC = 1
--- /dev/null
+export * from './public-server.setting'
+export * from './register-server-setting.model'
-import { RegisterClientFormFieldOptions } from './register-client-form-field.model'
+import { RegisterClientFormFieldOptions } from '../../client'
export type RegisterServerSettingOptions = RegisterClientFormFieldOptions & {
// If the setting is not private, anyone can view its value (client code included)
-export * from './videos-redundancy-strategy.model'
export * from './video-redundancies-filters.model'
+export * from './video-redundancy-config-filter.type'
export * from './video-redundancy.model'
+export * from './videos-redundancy-strategy.model'
dismissable: boolean
}
}
+
+export type HTMLServerConfig = Omit<ServerConfig, 'signup'>
DOES_NOT_RESPECT_FOLLOW_CONSTRAINTS = 1,
MAX_INSTANCE_LIVES_LIMIT_REACHED = 2,
MAX_USER_LIVES_LIMIT_REACHED = 3,
+ INCORRECT_FILES_IN_TORRENT = 4
}
--- /dev/null
+export * from './video-change-ownership-accept.model'
+export * from './video-change-ownership-create.model'
+export * from './video-change-ownership.model'
-import { Account } from '../actors'
-import { Video } from './video.model'
+import { Account } from '../../actors'
+import { Video } from '../video.model'
export interface VideoChangeOwnership {
id: number
--- /dev/null
+export * from './video-comment.model'
-import { Account } from '../actors'
+import { Account } from '../../actors'
export interface VideoComment {
id: number
export * from './blacklist'
export * from './caption'
+export * from './change-ownership'
export * from './channel'
+export * from './comment'
export * from './live'
export * from './import'
export * from './playlist'
export * from './thumbnail.type'
-export * from './video-change-ownership-accept.model'
-export * from './video-change-ownership-create.model'
-export * from './video-change-ownership.model'
-
-export * from './video-comment.model'
export * from './video-constant.model'
export * from './video-create.model'
-export * from './video-file-metadata'
-export * from './video-file.model'
-export * from './live/live-video.model'
+export * from './video-file-metadata.model'
+export * from './video-file.model'
export * from './video-privacy.enum'
export * from './video-query.type'
import { VideoConstant } from './video-constant.model'
-import { VideoFileMetadata } from './video-file-metadata'
+import { VideoFileMetadata } from './video-file-metadata.model'
import { VideoResolution } from './video-resolution.enum'
export interface VideoFile {
version: 3.2.0-rc.1
contact:
name: PeerTube Community
- url: 'https://joinpeertube.org'
+ url: https://joinpeertube.org
license:
name: AGPLv3.0
- url: 'https://github.com/Chocobozzz/PeerTube/blob/master/LICENSE'
+ url: https://github.com/Chocobozzz/PeerTube/blob/master/LICENSE
x-logo:
- url: 'https://joinpeertube.org/img/brand.png'
+ url: https://joinpeertube.org/img/brand.png
altText: PeerTube Project Homepage
description: |
The PeerTube API is built on HTTP(S) and is RESTful. You can use your favorite
- [Kotlin](https://framagit.org/framasoft/peertube/clients/kotlin)
See the [REST API quick start](https://docs.joinpeertube.org/api-rest-getting-started) for a few
- examples of using with the PeerTube API.
+ examples of using the PeerTube API.
# Authentication
When you sign up for an account on a PeerTube instance, you are given the possibility
- to generate sessions on it, and authenticate there using a session token. Only __one
- session token can currently be used at a time__.
+ to generate sessions on it, and authenticate there using an access token. Only __one
+ access token can currently be used at a time__.
## Roles
# Errors
The API uses standard HTTP status codes to indicate the success or failure
- of the API call. The body of the response will be JSON in the following
- formats.
+ of the API call.
```
+ HTTP 1.1 404 Not Found
+ Content-Type: application/json
+
{
- "error": "Account not found" // error debug message
+ "errorCode": 1
+ "error": "Account not found"
}
```
- Some errors benefit from a more detailed message:
+ We provide error codes for [a growing number of cases](https://github.com/Chocobozzz/PeerTube/blob/develop/shared/models/server/server-error-code.enum.ts),
+ but it is still optional.
+
+ ### Validation errors
+
+ Each parameter is evaluated on its own against a set of rules before the route validator
+ proceeds with potential testing involving parameter combinations. Errors coming from Validation
+ errors appear earlier and benefit from a more detailed error type:
+
```
+ HTTP 1.1 400 Bad Request
+ Content-Type: application/json
+
{
"errors": {
- "id": { // where 'id' is the name of the parameter concerned by the error.
- "value": "a117eb-c6a9-4756-bb09-2a956239f", // value that triggered the error.
- "msg": "Should have an valid id", // error debug message
+ "id": {
+ "value": "a117eb-c6a9-4756-bb09-2a956239f",
+ "msg": "Should have a valid id",
"param": "id",
- "location": "params" // 'params', 'body', 'header', 'query' or 'cookies'
+ "location": "params"
}
}
}
```
+ Where `id` is the name of the field concerned by the error, within the route definition.
+ `errors.<field>.location` can be either 'params', 'body', 'header', 'query' or 'cookies', and
+ `errors.<field>.value` reports the value that didn't pass validation whose `errors.<field>.msg`
+ is about.
+
# Rate limits
We are rate-limiting all endpoints of PeerTube's API. Custom values can be set by administrators:
- | Endpoint | Calls | Time frame |
- |-------------------------|------------------|---------------------------|
- | `/*` | 50 | 10 seconds |
- | `POST /users/token` | 15 | 5 minutes |
- | `POST /users/register` | 2¹ | 5 minutes |
- | `POST /users/ask-send-verify-email` | 3 | 5 minutes |
+ | Endpoint (prefix: `/api/v1`) | Calls | Time frame |
+ |------------------------------|---------------|--------------|
+ | `/*` | 50 | 10 seconds |
+ | `POST /users/token` | 15 | 5 minutes |
+ | `POST /users/register` | 2<sup>*</sup> | 5 minutes |
+ | `POST /users/ask-send-verify-email` | 3 | 5 minutes |
- Depending on the endpoint, ¹failed requests are not taken into account. A service
+ Depending on the endpoint, <sup>*</sup>failed requests are not taken into account. A service
limit is announced by a `429 Too Many Requests` status code.
You can get details about the current state of your rate limit by reading the
| Header | Description |
|-------------------------|------------------------------------------------------------|
- | X-RateLimit-Limit | Number of max requests allowed in the current time period |
- | X-RateLimit-Remaining | Number of remaining requests in the current time period |
- | X-RateLimit-Reset | Timestamp of end of current time period as UNIX timestamp |
- | Retry-After | Seconds to delay after the first `429` is received |
+ | `X-RateLimit-Limit` | Number of max requests allowed in the current time period |
+ | `X-RateLimit-Remaining` | Number of remaining requests in the current time period |
+ | `X-RateLimit-Reset` | Timestamp of end of current time period as UNIX timestamp |
+ | `Retry-After` | Seconds to delay after the first `429` is received |
+
+ # CORS
+
+ This API features [Cross-Origin Resource Sharing (CORS)](https://fetch.spec.whatwg.org/),
+ allowing cross-domain communication from the browser for some routes:
+
+ | Endpoint |
+ |------------------------- ---|
+ | `/api/*` |
+ | `/download/*` |
+ | `/lazy-static/*` |
+ | `/live/segments-sha256/*` |
+ | `/.well-known/webfinger` |
+
+ In addition, all routes serving ActivityPub are CORS-enabled for all origins.
externalDocs:
url: https://docs.joinpeertube.org/api-rest-reference.html
tags:
+ - name: Register
+ description: |
+ As a visitor, you can use this API to open an account (if registrations are open on
+ that PeerTube instance). As an admin, you should use the dedicated [User creation
+ API](#operation/addUser) instead.
+ - name: Session
+ x-displayName: Login/Logout
+ description: |
+ Sessions deal with access tokens over time. Only __one session token can currently be used at a time__.
- name: Accounts
description: >
Accounts encompass remote accounts discovered across the federation,
For importing videos as your own, refer to [video imports](#operation/importVideo).
x-tagGroups:
+ - name: Auth
+ tags:
+ - Register
+ - Session
- name: Accounts
tags:
- Accounts
tags:
- Accounts
summary: Get an account
+ operationId: getAccount
parameters:
- $ref: '#/components/parameters/name'
responses:
$ref: '#/components/schemas/Account'
'404':
description: account not found
+
'/accounts/{name}/videos':
get:
tags:
- Accounts
- Video
summary: 'List videos of an account'
+ operationId: getAccountVideos
parameters:
- $ref: '#/components/parameters/name'
- $ref: '#/components/parameters/categoryOneOf'
json = r.json()
print(json)
+
/accounts:
get:
tags:
- Accounts
summary: List accounts
+ operationId: getAccounts
parameters:
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
type: array
items:
$ref: '#/components/schemas/Account'
+
/config:
get:
tags:
- Config
summary: Get instance public configuration
+ operationId: getConfig
responses:
'200':
description: successful operation
examples:
nightly:
externalValue: https://peertube2.cpy.re/api/v1/config
+
/config/about:
get:
summary: Get instance "About" information
+ operationId: getAbout
tags:
- Config
responses:
examples:
nightly:
externalValue: https://peertube2.cpy.re/api/v1/config/about
+
/config/custom:
get:
summary: Get instance runtime configuration
+ operationId: getCustomConfig
tags:
- Config
security:
$ref: '#/components/schemas/ServerConfigCustom'
put:
summary: Set instance runtime configuration
+ operationId: putCustomConfig
tags:
- Config
security:
- webtorrent and hls are disabled with transcoding enabled - you need at least one enabled
delete:
summary: Delete instance runtime configuration
+ operationId: delCustomConfig
tags:
- Config
security:
responses:
'200':
description: successful operation
+
/jobs/{state}:
get:
summary: List instance jobs
+ operationId: getJobs
security:
- OAuth2:
- admin
maxItems: 100
items:
$ref: '#/components/schemas/Job'
- '/server/following/{host}':
+
+ /server/followers:
+ get:
+ tags:
+ - Instance Follows
+ summary: List instances following the server
+ parameters:
+ - $ref: '#/components/parameters/followState'
+ - $ref: '#/components/parameters/actorType'
+ - $ref: '#/components/parameters/start'
+ - $ref: '#/components/parameters/count'
+ - $ref: '#/components/parameters/sort'
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ total:
+ type: integer
+ example: 1
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/Follow'
+
+ '/server/followers/{nameWithHost}':
delete:
+ summary: Remove or reject a follower to your server
security:
- OAuth2:
- admin
tags:
- Instance Follows
- summary: Unfollow a server
parameters:
- - name: host
+ - name: nameWithHost
in: path
required: true
- description: 'The host to unfollow '
+ description: The remote actor handle to remove from your followers
schema:
type: string
- format: hostname
+ format: email
responses:
- '201':
+ '204':
description: successful operation
- /server/followers:
- get:
+ '404':
+ description: follower not found
+
+ '/server/followers/{nameWithHost}/reject':
+ post:
+ summary: Reject a pending follower to your server
+ security:
+ - OAuth2:
+ - admin
tags:
- Instance Follows
- summary: List instance followers
parameters:
- - $ref: '#/components/parameters/start'
- - $ref: '#/components/parameters/count'
- - $ref: '#/components/parameters/sort'
+ - name: nameWithHost
+ in: path
+ required: true
+ description: The remote actor handle to remove from your followers
+ schema:
+ type: string
+ format: email
responses:
- '200':
+ '204':
description: successful operation
- content:
- application/json:
- schema:
- type: array
- items:
- $ref: '#/components/schemas/Follow'
+ '404':
+ description: follower not found
+
+ '/server/followers/{nameWithHost}/accept':
+ post:
+ summary: Accept a pending follower to your server
+ security:
+ - OAuth2:
+ - admin
+ tags:
+ - Instance Follows
+ parameters:
+ - name: nameWithHost
+ in: path
+ required: true
+ description: The remote actor handle to remove from your followers
+ schema:
+ type: string
+ format: email
+ responses:
+ '204':
+ description: successful operation
+ '404':
+ description: follower not found
+
/server/following:
get:
tags:
- Instance Follows
summary: List instances followed by the server
parameters:
- - name: state
- in: query
- schema:
- type: string
- enum:
- - pending
- - accepted
- - name: actorType
- in: query
- schema:
- type: string
- enum:
- - Person
- - Application
- - Group
- - Service
- - Organization
+ - $ref: '#/components/parameters/followState'
+ - $ref: '#/components/parameters/actorType'
- $ref: '#/components/parameters/start'
- $ref: '#/components/parameters/count'
- $ref: '#/components/parameters/sort'
content:
application/json:
schema:
- type: array
- items:
- $ref: '#/components/schemas/Follow'
+ type: object
+ properties:
+ total:
+ type: integer
+ example: 1
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/Follow'
post:
security:
- OAuth2:
- admin
tags:
- Instance Follows
- summary: Follow a server
+ summary: Follow a list of servers
responses:
'204':
description: successful operation
type: string
format: hostname
uniqueItems: true
+
+ '/server/following/{host}':
+ delete:
+ summary: Unfollow a server
+ security:
+ - OAuth2:
+ - admin
+ tags:
+ - Instance Follows
+ parameters:
+ - name: host
+ in: path
+ required: true
+ description: The host to unfollow
+ schema:
+ type: string
+ format: hostname
+ responses:
+ '204':
+ description: successful operation
+ '404':
+ description: host not found
+
/users:
post:
summary: Create a user
+ operationId: addUser
security:
- OAuth2:
- admin
$ref: '#/components/schemas/AddUserResponse'
links:
# GET /users/{id}
- GetUserId:
- operationId: getUserId
+ GetUser:
+ operationId: getUser
parameters:
id: '$response.body#/user/id'
# PUT /users/{id}
- PutUserId:
- operationId: putUserId
+ PutUser:
+ operationId: putUser
parameters:
id: '$response.body#/user/id'
# DELETE /users/{id}
- DelUserId:
- operationId: delUserId
+ DelUser:
+ operationId: delUser
parameters:
id: '$response.body#/user/id'
'403':
required: true
get:
summary: List users
+ operationId: getUsers
security:
- OAuth2:
- admin
type: array
items:
$ref: '#/components/schemas/User'
+
'/users/{id}':
parameters:
- $ref: '#/components/parameters/id'
- admin
tags:
- Users
- operationId: delUserId
+ operationId: delUser
responses:
'204':
description: successful operation
- OAuth2: []
tags:
- Users
- operationId: getUserId
+ operationId: getUser
parameters:
- name: withStats
in: query
- OAuth2: []
tags:
- Users
- operationId: putUserId
+ operationId: putUser
responses:
'204':
description: successful operation
schema:
$ref: '#/components/schemas/UpdateUser'
required: true
+
+ /oauth-clients/local:
+ get:
+ summary: Login prerequisite
+ description: You need to retrieve a client id and secret before [logging in](#operation/getOAuthToken).
+ operationId: getOAuthClient
+ tags:
+ - Session
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/OAuthClient'
+ links:
+ UseOAuthClientToLogin:
+ operationId: getOAuthToken
+ parameters:
+ client_id: '$response.body#/client_id'
+ client_secret: '$response.body#/client_secret'
+ x-codeSamples:
+ - lang: Shell
+ source: |
+ API="https://peertube2.cpy.re/api/v1"
+
+ ## AUTH
+ curl -s "$API/oauth-clients/local"
+
+ /users/token:
+ post:
+ summary: Login
+ operationId: getOAuthToken
+ description: With your [client id and secret](#operation/getOAuthClient), you can retrieve an access and refresh tokens.
+ tags:
+ - Session
+ requestBody:
+ content:
+ application/x-www-form-urlencoded:
+ schema:
+ oneOf:
+ - $ref: '#/components/schemas/OAuthToken-password'
+ - $ref: '#/components/schemas/OAuthToken-refresh_token'
+ discriminator:
+ propertyName: grant_type
+ mapping:
+ password: '#/components/schemas/OAuthToken-password'
+ refresh_token: '#/components/schemas/OAuthToken-refresh_token'
+ responses:
+ '200':
+ description: successful operation
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ token_type:
+ type: string
+ example: Bearer
+ access_token:
+ type: string
+ example: 90286a0bdf0f7315d9d3fe8dabf9e1d2be9c97d0
+ description: valid for 1 day
+ refresh_token:
+ type: string
+ example: 2e0d675df9fc96d2e4ec8a3ebbbf45eca9137bb7
+ description: valid for 2 weeks
+ expires_in:
+ type: integer
+ minimum: 0
+ example: 14399
+ refresh_token_expires_in:
+ type: integer
+ minimum: 0
+ example: 1209600
+ x-codeSamples:
+ - lang: Shell
+ source: |
+ ## DEPENDENCIES: jq
+ API="https://peertube2.cpy.re/api/v1"
+ USERNAME="<your_username>"
+ PASSWORD="<your_password>"
+
+ ## AUTH
+ client_id=$(curl -s "$API/oauth-clients/local" | jq -r ".client_id")
+ client_secret=$(curl -s "$API/oauth-clients/local" | jq -r ".client_secret")
+ curl -s "$API/users/token" \
+ --data client_id="$client_id" \
+ --data client_secret="$client_secret" \
+ --data grant_type=password \
+ --data username="$USERNAME" \
+ --data password="$PASSWORD" \
+ | jq -r ".access_token"
+
+ /users/revoke-token:
+ post:
+ summary: Logout
+ description: Revokes your access token and its associated refresh token, destroying your current session.
+ operationId: revokeOAuthToken
+ tags:
+ - Session
+ security:
+ - OAuth2: []
+ responses:
+ '200':
+ description: successful operation
+
/users/register:
post:
summary: Register a user
+ operationId: registerUser
tags:
- Users
+ - Register
responses:
'204':
description: successful operation
schema:
$ref: '#/components/schemas/RegisterUser'
required: true
+
+ /users/{id}/verify-email:
+ post:
+ summary: Verify a user
+ operationId: verifyUser
+ description: |
+ Following a user registration, the new user will receive an email asking to click a link
+ containing a secret.
+ tags:
+ - Users
+ - Register
+ parameters:
+ - $ref: '#/components/parameters/id'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ verificationString:
+ type: string
+ format: url
+ isPendingEmail:
+ type: boolean
+ required:
+ - verificationString
+ responses:
+ '204':
+ description: successful operation
+ '403':
+ description: invalid verification string
+ '404':
+ description: user not found
+
+ /users/ask-send-verify-email:
+ post:
+ summary: Resend user verification link
+ operationId: resendEmailToVerifyUser
+ tags:
+ - Users
+ - Register
+ responses:
+ '204':
+ description: successful operation
+
/users/me:
get:
summary: Get my user information
+ operationId: getUserInfo
security:
- OAuth2:
- user
$ref: '#/components/schemas/User'
put:
summary: Update my user information
+ operationId: putUserInfo
security:
- OAuth2:
- user
schema:
$ref: '#/components/schemas/UpdateMe'
required: true
+
/users/me/videos/imports:
get:
summary: Get video imports of my user
application/json:
schema:
$ref: '#/components/schemas/VideoImportsList'
+
/users/me/video-quota-used:
get:
summary: Get my user used quota
type: number
description: The user video quota used today in bytes
example: 1681014151
+
'/users/me/videos/{videoId}/rating':
get:
summary: Get rate of my user for a video
application/json:
schema:
$ref: '#/components/schemas/GetMeVideoRating'
+
/users/me/videos:
get:
summary: Get videos of my user
application/json:
schema:
$ref: '#/components/schemas/VideoListResponse'
+
/users/me/subscriptions:
get:
summary: Get my user subscriptions
responses:
'200':
description: successful operation
+
/users/me/subscriptions/exist:
get:
summary: Get if subscriptions exist for my user
application/json:
schema:
type: object
+
/users/me/subscriptions/videos:
get:
summary: List videos of subscriptions of my user
application/json:
schema:
$ref: '#/components/schemas/VideoListResponse'
+
'/users/me/subscriptions/{subscriptionHandle}':
get:
summary: Get subscription of my user
responses:
'200':
description: successful operation
+
/users/me/notifications:
get:
summary: List my notifications
application/json:
schema:
$ref: '#/components/schemas/NotificationListResponse'
+
/users/me/notifications/read:
post:
summary: Mark notifications as read by their id
responses:
'204':
description: successful operation
+
/users/me/notifications/read-all:
post:
summary: Mark all my notification as read
responses:
'204':
description: successful operation
+
/users/me/notification-settings:
put:
summary: Update my notification settings
responses:
'204':
description: successful operation
+
/users/me/history/videos:
get:
summary: List watched videos history
application/json:
schema:
$ref: '#/components/schemas/VideoListResponse'
+
/users/me/history/videos/remove:
post:
summary: Clear video history
responses:
'204':
description: successful operation
+
/users/me/avatar/pick:
post:
summary: Update my user avatar
encoding:
avatarfile:
contentType: image/png, image/jpeg
+
/users/me/avatar:
delete:
summary: Delete my avatar
responses:
'200':
description: successful operation
+
'/videos/ownership/{id}/accept':
post:
summary: Accept ownership change request
description: cannot terminate an ownership change of another user
'404':
description: video owneship change not found
+
'/videos/ownership/{id}/refuse':
post:
summary: Refuse ownership change request
description: cannot terminate an ownership change of another user
'404':
description: video owneship change not found
+
'/videos/{id}/give-ownership':
post:
summary: Request ownership change
description: changing video ownership to a remote account is not supported yet
'404':
description: video not found
+
/videos:
get:
summary: List videos
+ operationId: getVideos
tags:
- Video
parameters:
application/json:
schema:
$ref: '#/components/schemas/VideoListResponse'
+
/videos/categories:
get:
summary: List available video categories
examples:
nightly:
externalValue: https://peertube2.cpy.re/api/v1/videos/categories
+
/videos/licences:
get:
summary: List available video licences
examples:
nightly:
externalValue: https://peertube2.cpy.re/api/v1/videos/licences
+
/videos/languages:
get:
summary: List available video languages
examples:
nightly:
externalValue: https://peertube2.cpy.re/api/v1/videos/languages
+
/videos/privacies:
get:
summary: List available video privacy policies
examples:
nightly:
externalValue: https://peertube2.cpy.re/api/v1/videos/privacies
+
'/videos/{id}':
put:
summary: Update a video
+ operationId: putVideo
security:
- OAuth2: []
tags:
type: string
support:
description: A text tell the audience how to support the video creator
- example: Please support my work on <insert crowdfunding plateform>! <3
+ example: Please support our work on https://soutenir.framasoft.org/en/ <3
type: string
nsfw:
description: Whether or not this video contains sensitive content
contentType: image/jpeg
get:
summary: Get a video
+ operationId: getVideo
tags:
- Video
parameters:
$ref: '#/components/schemas/VideoDetails'
delete:
summary: Delete a video
+ operationId: delVideo
security:
- OAuth2: []
tags:
responses:
'204':
description: successful operation
+
'/videos/{id}/description':
get:
summary: Get complete video description
+ operationId: getVideoDesc
tags:
- Video
parameters:
maxLength: 10000
example: |
**[Want to help to translate this video?](https://weblate.framasoft.org/projects/what-is-peertube-video/)**\r\n\r\n**Take back the control of your videos! [#JoinPeertube](https://joinpeertube.org)**
+
'/videos/{id}/views':
post:
summary: Add a view to a video
responses:
'204':
description: successful operation
+
'/videos/{id}/watching':
put:
summary: Set watching progress of a video
responses:
'204':
description: successful operation
+
/videos/upload:
post:
summary: Upload a video
FILE_PATH="<your_file_path>"
CHANNEL_ID="<your_channel_id>"
NAME="<video_name>"
+ API="https://peertube2.cpy.re/api/v1"
- API_PATH="https://peertube2.cpy.re/api/v1"
## AUTH
- client_id=$(curl -s "$API_PATH/oauth-clients/local" | jq -r ".client_id")
- client_secret=$(curl -s "$API_PATH/oauth-clients/local" | jq -r ".client_secret")
- token=$(curl -s "$API_PATH/users/token" \
+ client_id=$(curl -s "$API/oauth-clients/local" | jq -r ".client_id")
+ client_secret=$(curl -s "$API/oauth-clients/local" | jq -r ".client_secret")
+ token=$(curl -s "$API/users/token" \
--data client_id="$client_id" \
--data client_secret="$client_secret" \
--data grant_type=password \
- --data response_type=code \
--data username="$USERNAME" \
--data password="$PASSWORD" \
| jq -r ".access_token")
+
## VIDEO UPLOAD
- curl -s "$API_PATH/videos/upload" \
+ curl -s "$API/videos/upload" \
-H "Authorization: Bearer $token" \
--max-time 600 \
--form videofile=@"$FILE_PATH" \
--form channelId=$CHANNEL_ID \
--form name="$NAME"
+
/videos/upload-resumable:
post:
summary: Initialize the resumable upload of a video
schema:
type: number
example: 0
+
/videos/imports:
post:
summary: Import a video
content:
multipart/form-data:
schema:
- type: object
- properties:
- torrentfile:
- description: Torrent File
- type: string
- format: binary
- targetUrl:
- $ref: '#/components/schemas/VideoImport/properties/targetUrl'
- magnetUri:
- $ref: '#/components/schemas/VideoImport/properties/magnetUri'
- channelId:
- description: Channel id that will contain this video
- allOf:
- - $ref: '#/components/schemas/VideoChannel/properties/id'
- thumbnailfile:
- description: Video thumbnail file
- type: string
- format: binary
- previewfile:
- description: Video preview file
- type: string
- format: binary
- privacy:
- $ref: '#/components/schemas/VideoPrivacySet'
- category:
- $ref: '#/components/schemas/VideoCategorySet'
- licence:
- $ref: '#/components/schemas/VideoLicenceSet'
- language:
- $ref: '#/components/schemas/VideoLanguageSet'
- description:
- description: Video description
- type: string
- waitTranscoding:
- description: Whether or not we wait transcoding before publish the video
- type: boolean
- support:
- description: A text tell the audience how to support the video creator
- example: Please support my work on <insert crowdfunding plateform>! <3
- type: string
- nsfw:
- description: Whether or not this video contains sensitive content
- type: boolean
- name:
- description: Video name
- type: string
- minLength: 3
- maxLength: 120
- tags:
- description: Video tags (maximum 5 tags each between 2 and 30 characters)
- type: array
- minItems: 1
- maxItems: 5
- items:
- type: string
- minLength: 2
- maxLength: 30
- commentsEnabled:
- description: Enable or disable comments for this video
- type: boolean
- downloadEnabled:
- description: Enable or disable downloading for this video
- type: boolean
- scheduleUpdate:
- $ref: '#/components/schemas/VideoScheduledUpdate'
- required:
- - channelId
- - name
+ $ref: '#/components/schemas/VideoCreateImport'
encoding:
torrentfile:
contentType: application/x-bittorrent
/videos/live:
post:
summary: Create a live
- operationId: createLive
+ operationId: addLive
security:
- OAuth2: []
tags:
type: string
support:
description: A text tell the audience how to support the creator
- example: Please support my work on <insert crowdfunding plateform>! <3
+ example: Please support our work on https://soutenir.framasoft.org/en/ <3
type: string
nsfw:
description: Whether or not this live video/replay contains sensitive content
type: array
items:
$ref: '#/components/schemas/Abuse'
-
post:
summary: Report an abuse
security:
- $ref: '#/components/schemas/Video/properties/id'
startAt:
type: integer
+ format: seconds
description: Timestamp in the video that marks the beginning of the report
minimum: 0
endAt:
type: integer
+ format: seconds
description: Timestamp in the video that marks the ending of the report
minimum: 0
comment:
required:
- reason
responses:
- '204':
+ '200':
description: successful operation
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ abuse:
+ type: object
+ properties:
+ id:
+ $ref: '#/components/schemas/id'
'400':
description: incorrect request parameters
+
'/abuses/{abuseId}':
put:
summary: Update an abuse
description: successful operation
'404':
description: block not found
+
'/abuses/{abuseId}/messages':
get:
summary: List messages of an abuse
content:
application/json:
schema:
- type: array
- items:
- $ref: '#/components/schemas/AbuseMessage'
-
+ type: object
+ properties:
+ total:
+ type: integer
+ example: 1
+ data:
+ type: array
+ items:
+ $ref: '#/components/schemas/AbuseMessage'
post:
summary: Add message to an abuse
security:
description: successful operation
'400':
description: incorrect request parameters
+
'/abuses/{abuseId}/messages/{abuseMessageId}':
delete:
summary: Delete an abuse message
'/videos/{id}/blacklist':
post:
summary: Block a video
+ operationId: addVideoBlock
security:
- OAuth2:
- admin
description: successful operation
delete:
summary: Unblock a video by its id
+ operationId: delVideoBlock
security:
- OAuth2:
- admin
description: successful operation
'404':
description: block not found
+
/videos/blacklist:
get:
tags:
- Video Blocks
summary: List video blocks
+ operationId: getVideoBlocks
security:
- OAuth2:
- admin
type: array
items:
$ref: '#/components/schemas/VideoBlacklist'
+
/videos/{id}/captions:
get:
summary: List captions of a video
+ operationId: getVideoCaptions
tags:
- Video Captions
parameters:
type: array
items:
$ref: '#/components/schemas/VideoCaption'
+
/videos/{id}/captions/{captionLanguage}:
put:
summary: Add or replace a video caption
+ operationId: addVideoCaption
security:
- OAuth2:
- user
description: video or language not found
delete:
summary: Delete a video caption
+ operationId: delVideoCaption
security:
- OAuth2:
- user
description: successful operation
'404':
description: video or language or caption for that language not found
+
/video-channels:
get:
summary: List video channels
+ operationId: getVideoChannels
tags:
- Video Channels
parameters:
$ref: '#/components/schemas/VideoChannelList'
post:
summary: Create a video channel
+ operationId: addVideoChannel
security:
- OAuth2: []
tags:
responses:
'204':
description: successful operation
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ videoChannel:
+ type: object
+ properties:
+ id:
+ $ref: '#/components/schemas/VideoChannel/properties/id'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/VideoChannelCreate'
+
'/video-channels/{channelHandle}':
get:
summary: Get a video channel
+ operationId: getVideoChannel
tags:
- Video Channels
parameters:
$ref: '#/components/schemas/VideoChannel'
put:
summary: Update a video channel
+ operationId: putVideoChannel
security:
- OAuth2: []
tags:
$ref: '#/components/schemas/VideoChannelUpdate'
delete:
summary: Delete a video channel
+ operationId: delVideoChannel
security:
- OAuth2: []
tags:
responses:
'204':
description: successful operation
+
'/video-channels/{channelHandle}/videos':
get:
summary: List videos of a video channel
+ operationId: getVideoChannelVideos
tags:
- Video
- Video Channels
application/json:
schema:
$ref: '#/components/schemas/VideoListResponse'
+
'/video-channels/{channelHandle}/avatar/pick':
post:
summary: Update channel avatar
encoding:
avatarfile:
contentType: image/png, image/jpeg
+
'/video-channels/{channelHandle}/avatar':
delete:
summary: Delete channel avatar
'204':
description: successful operation
-
'/video-channels/{channelHandle}/banner/pick':
post:
summary: Update channel banner
encoding:
bannerfile:
contentType: image/png, image/jpeg
+
'/video-channels/{channelHandle}/banner':
delete:
summary: Delete channel banner
post:
summary: Create a video playlist
description: If the video playlist is set as public, `videoChannelId` is mandatory.
- operationId: createPlaylist
+ operationId: addPlaylist
security:
- OAuth2: []
tags:
thumbnailfile:
contentType: image/jpeg
- /video-playlists/{id}:
+ /video-playlists/{playlistId}:
get:
summary: Get a video playlist
tags:
- Video Playlists
parameters:
- - $ref: '#/components/parameters/idOrUUID'
+ - $ref: '#/components/parameters/playlistId'
responses:
'200':
description: successful operation
'204':
description: successful operation
parameters:
- - $ref: '#/components/parameters/idOrUUID'
+ - $ref: '#/components/parameters/playlistId'
requestBody:
content:
multipart/form-data:
tags:
- Video Playlists
parameters:
- - $ref: '#/components/parameters/idOrUUID'
+ - $ref: '#/components/parameters/playlistId'
responses:
'204':
description: successful operation
- /video-playlists/{id}/videos:
+ /video-playlists/{playlistId}/videos:
get:
summary: 'List videos of a playlist'
+ operationId: getVideoPlaylistVideos
tags:
- Videos
- Video Playlists
parameters:
- - $ref: '#/components/parameters/idOrUUID'
+ - $ref: '#/components/parameters/playlistId'
responses:
'200':
description: successful operation
schema:
$ref: '#/components/schemas/VideoListResponse'
post:
- summary: 'Add a video in a playlist'
+ summary: Add a video in a playlist
+ operationId: addVideoPlaylistVideo
security:
- OAuth2: []
tags:
- Videos
- Video Playlists
parameters:
- - $ref: '#/components/parameters/idOrUUID'
+ - $ref: '#/components/parameters/playlistId'
responses:
'200':
description: successful operation
properties:
id:
type: integer
+ example: 2
requestBody:
content:
application/json:
type: object
properties:
videoId:
- allOf:
+ oneOf:
+ - $ref: '#/components/schemas/Video/properties/uuid'
- $ref: '#/components/schemas/Video/properties/id'
description: Video to add in the playlist
startTimestamp:
type: integer
- description: Start the video at this specific timestamp (in seconds)
+ format: seconds
+ description: Start the video at this specific timestamp
stopTimestamp:
type: integer
- description: Stop the video at this specific timestamp (in seconds)
+ format: seconds
+ description: Stop the video at this specific timestamp
required:
- videoId
- /video-playlists/{id}/videos/reorder:
+ /video-playlists/{playlistId}/videos/reorder:
post:
summary: 'Reorder a playlist'
+ operationId: reorderVideoPlaylist
security:
- OAuth2: []
tags:
- Video Playlists
parameters:
- - $ref: '#/components/parameters/idOrUUID'
+ - $ref: '#/components/parameters/playlistId'
responses:
'204':
description: successful operation
- startPosition
- insertAfterPosition
- /video-playlists/{id}/videos/{playlistElementId}:
+ /video-playlists/{playlistId}/videos/{playlistElementId}:
put:
- summary: 'Update a playlist element'
+ summary: Update a playlist element
+ operationId: putVideoPlaylistVideo
security:
- OAuth2: []
tags:
- Video Playlists
parameters:
- - $ref: '#/components/parameters/idOrUUID'
+ - $ref: '#/components/parameters/playlistId'
- $ref: '#/components/parameters/playlistElementId'
responses:
'204':
properties:
startTimestamp:
type: integer
- description: 'Start the video at this specific timestamp (in seconds)'
+ format: seconds
+ description: Start the video at this specific timestamp
stopTimestamp:
type: integer
- description: 'Stop the video at this specific timestamp (in seconds)'
+ format: seconds
+ description: Stop the video at this specific timestamp
delete:
- summary: 'Delete an element from a playlist'
+ summary: Delete an element from a playlist
+ operationId: delVideoPlaylistVideo
security:
- OAuth2: []
tags:
- Video Playlists
parameters:
- - $ref: '#/components/parameters/idOrUUID'
+ - $ref: '#/components/parameters/playlistId'
- $ref: '#/components/parameters/playlistElementId'
responses:
'204':
'/users/me/video-playlists/videos-exist':
get:
- summary: 'Check video exists in my playlists'
+ summary: Check video exists in my playlists
security:
- OAuth2: []
tags:
type: integer
startTimestamp:
type: integer
+ format: seconds
stopTimestamp:
type: integer
+ format: seconds
'/accounts/{name}/video-channels':
get:
application/json:
schema:
$ref: '#/components/schemas/VideoChannelList'
+
'/accounts/{name}/ratings':
get:
summary: List ratings of an account
type: array
items:
$ref: '#/components/schemas/VideoRating'
+
'/videos/{id}/comment-threads':
get:
summary: List threads of a video
type: object
properties:
text:
- type: string
- description: 'Text comment'
+ allOf:
+ - $ref: '#/components/schemas/VideoComment/properties/text'
+ format: markdown
+ maxLength: 10000
required:
- text
application/json:
schema:
$ref: '#/components/schemas/VideoCommentThreadTree'
+
'/videos/{id}/comments/{commentId}':
post:
summary: Reply to a thread of a video
type: object
properties:
text:
- $ref: '#/components/schemas/VideoComment/properties/text'
+ allOf:
+ - $ref: '#/components/schemas/VideoComment/properties/text'
+ format: markdown
+ maxLength: 10000
required:
- text
-
delete:
summary: Delete a comment or a reply
security:
description: comment or video does not exist
'409':
description: comment is already deleted
+
'/videos/{id}/rate':
put:
summary: Like/dislike a video
- Video Rates
parameters:
- $ref: '#/components/parameters/idOrUUID'
+ requestBody:
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ rating:
+ type: string
+ enum:
+ - like
+ - dislike
+ required:
+ - rating
responses:
'204':
description: successful operation
'404':
description: video does not exist
+
/search/videos:
get:
tags:
- Search
summary: Search videos
+ operationId: searchVideos
parameters:
- name: search
in: query
$ref: '#/components/schemas/VideoListResponse'
'500':
description: search index unavailable
+
/search/video-channels:
get:
tags:
- Search
summary: Search channels
+ operationId: searchChannels
parameters:
- name: search
in: query
$ref: '#/components/schemas/VideoChannelList'
'500':
description: search index unavailable
- /blocklist/accounts:
+
+ /server/blocklist/accounts:
get:
tags:
- Account Blocks
description: successful operation
'409':
description: self-blocking forbidden
- '/blocklist/accounts/{accountName}':
+
+ '/server/blocklist/accounts/{accountName}':
delete:
tags:
- Account Blocks
description: successful operation
'404':
description: account or account block does not exist
- /blocklist/servers:
+
+ /server/blocklist/servers:
get:
tags:
- Server Blocks
required:
- host
responses:
- '200':
+ '204':
description: successful operation
'409':
description: self-blocking forbidden
- '/blocklist/servers/{host}':
+
+ '/server/blocklist/servers/{host}':
delete:
tags:
- Server Blocks
type: string
format: hostname
responses:
- '201':
+ '204':
description: successful operation
'404':
description: account block does not exist
- /redundancy/{host}:
+
+ /server/redundancy/{host}:
put:
tags:
- Instance Redundancy
description: successful operation
'404':
description: server is not already known
- /redundancy/videos:
+
+ /server/redundancy/videos:
get:
tags:
- Video Mirroring
summary: List videos being mirrored
+ operationId: getMirroredVideos
security:
- OAuth2:
- admin
tags:
- Video Mirroring
summary: Mirror a video
+ operationId: putMirroredVideo
security:
- OAuth2:
- admin
description: video does not exist
'409':
description: video is already mirrored
- /redundancy/videos/{redundancyId}:
+
+ /server/redundancy/videos/{redundancyId}:
delete:
tags:
- Video Mirroring
summary: Delete a mirror done on a video
+ operationId: delMirroredVideo
security:
- OAuth2:
- admin
description: successful operation
'404':
description: video redundancy not found
+
'/feeds/video-comments.{format}':
get:
tags:
- Feeds
summary: List comments on videos
+ operationId: getSyndicatedComments
parameters:
- name: format
in: path
description: video, video channel or account not found
'406':
description: accept header unsupported
+
'/feeds/videos.{format}':
get:
tags:
- Feeds
summary: List videos
+ operationId: getSyndicatedVideos
parameters:
- name: format
in: path
description: video channel or account not found
'406':
description: accept header unsupported
+
'/feeds/subscriptions.{format}':
get:
tags:
- Feeds
- Account
summary: List videos of subscriptions tied to a token
+ operationId: getSyndicatedSubscriptionVideos
parameters:
- name: format
in: path
type: object
'406':
description: accept header unsupported
+
/plugins:
get:
tags:
- Plugins
summary: List plugins
+ operationId: getPlugins
security:
- OAuth2:
- admin
application/json:
schema:
$ref: '#/components/schemas/PluginResponse'
+
/plugins/available:
get:
tags:
- Plugins
summary: List available plugins
+ operationId: getAvailablePlugins
security:
- OAuth2:
- admin
$ref: '#/components/schemas/PluginResponse'
'503':
description: plugin index unavailable
+
/plugins/install:
post:
tags:
- Plugins
summary: Install a plugin
+ operationId: addPlugin
security:
- OAuth2:
- admin
description: successful operation
'400':
description: should have either `npmName` or `path` set
+
/plugins/update:
post:
tags:
- Plugins
summary: Update a plugin
+ operationId: updatePlugin
security:
- OAuth2:
- admin
description: should have either `npmName` or `path` set
'404':
description: existing plugin not found
+
/plugins/uninstall:
post:
tags:
- Plugins
summary: Uninstall a plugin
+ operationId: uninstallPlugin
security:
- OAuth2:
- admin
description: successful operation
'404':
description: existing plugin not found
+
/plugins/{npmName}:
get:
tags:
- Plugins
summary: Get a plugin
+ operationId: getPlugin
security:
- OAuth2:
- admin
$ref: '#/components/schemas/Plugin'
'404':
description: plugin not found
+
/plugins/{npmName}/settings:
put:
tags:
description: successful operation
'404':
description: plugin not found
+
/plugins/{npmName}/public-settings:
get:
tags:
additionalProperties: true
'404':
description: plugin not found
+
/plugins/{npmName}/registered-settings:
get:
tags:
additionalProperties: true
'404':
description: plugin not found
+
servers:
- url: 'https://peertube2.cpy.re/api/v1'
description: Live Test Server (live data - latest nightly version)
oneOf:
- $ref: '#/components/schemas/id'
- $ref: '#/components/schemas/UUIDv4'
+ playlistId:
+ name: playlistId
+ in: path
+ required: true
+ description: Playlist id
+ schema:
+ $ref: '#/components/schemas/VideoPlaylist/properties/id'
playlistElementId:
name: playlistElementId
in: path
- activitypub-refresher
- video-redundancy
- video-live-ending
+ followState:
+ name: state
+ in: query
+ schema:
+ type: string
+ enum:
+ - pending
+ - accepted
+ actorType:
+ name: actorType
+ in: query
+ schema:
+ type: string
+ enum:
+ - Person
+ - Application
+ - Group
+ - Service
+ - Organization
securitySchemes:
OAuth2:
description: |
Authenticating via OAuth requires the following steps:
- Have an activated account
- - [Generate](https://docs.joinpeertube.org/api-rest-getting-started) a
- Bearer Token for that account at `/api/v1/users/token`
- - Make authenticated requests, putting *Authorization: Bearer <token\>*
+ - [Generate] an access token for that account at `/api/v1/users/token`.
+ - Make requests with the *Authorization: Bearer <token\>* header
- Profit, depending on the role assigned to the account
- Note that the __access token is valid for 1 day__ and, and is given
+ Note that the __access token is valid for 1 day__ and is given
along with a __refresh token valid for 2 weeks__.
+
+ [Generate]: https://docs.joinpeertube.org/api-rest-getting-started
type: oauth2
flows:
password:
- tokenUrl: 'https://peertube.example.com/api/v1/users/token'
+ tokenUrl: /api/v1/users/token
scopes:
admin: Admin scope
moderator: Moderator scope
maxLength: 36
username:
type: string
- description: The username of the user
+ description: immutable name of the user, used to find or mention its actor
example: chocobozzz
- pattern: '/^[a-z0-9._]{1,50}$/'
+ pattern: '/^[a-z0-9._]+$/'
minLength: 1
maxLength: 50
usernameChannel:
type: string
- description: The username for the default channel
- example: The Capybara Channel
- pattern: '/^[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789\\-_.:]+$/'
+ description: immutable name of the channel, used to interact with its actor
+ example: framasoft_videos
+ pattern: '/^[a-zA-Z0-9\\-_.:]+$/'
+ minLength: 1
+ maxLength: 50
password:
type: string
format: password
- description: The password of the user
minLength: 6
maxLength: 255
type: integer
startTimestamp:
type: integer
+ format: seconds
stopTimestamp:
type: integer
+ format: seconds
video:
nullable: true
allOf:
duration:
type: integer
example: 1419
+ format: seconds
description: duration of the video in seconds
isLocal:
type: boolean
support:
type: string
description: A text tell the audience how to support the video creator
- example: Please support my work on <insert crowdfunding plateform>! <3
+ example: Please support our work on https://soutenir.framasoft.org/en/ <3
minLength: 3
maxLength: 1000
channel:
label:
type: string
example: Pending
+ VideoCreateImport:
+ allOf:
+ - type: object
+ additionalProperties: false
+ oneOf:
+ - properties:
+ targetUrl:
+ $ref: '#/components/schemas/VideoImport/properties/targetUrl'
+ required: [targetUrl]
+ - properties:
+ magnetUri:
+ $ref: '#/components/schemas/VideoImport/properties/magnetUri'
+ required: [magnetUri]
+ - properties:
+ torrentfile:
+ $ref: '#/components/schemas/VideoImport/properties/torrentfile'
+ required: [torrentfile]
+ - $ref: '#/components/schemas/VideoUploadRequestCommon'
+ required:
+ - channelId
+ - name
VideoImport:
properties:
id:
- $ref: '#/components/schemas/id'
+ readOnly: true
+ allOf:
+ - $ref: '#/components/schemas/id'
targetUrl:
type: string
format: url
description: magnet URI allowing to resolve the import's source video
example: magnet:?xs=https%3A%2F%2Fframatube.org%2Fstatic%2Ftorrents%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.torrent&xt=urn:btih:38b4747ff788b30bf61f59d1965cd38f9e48e01f&dn=What+is+PeerTube%3F&tr=wss%3A%2F%2Fframatube.org%2Ftracker%2Fsocket&tr=https%3A%2F%2Fframatube.org%2Ftracker%2Fannounce&ws=https%3A%2F%2Fframatube.org%2Fstatic%2Fwebseed%2F9c9de5e8-0a1e-484a-b099-e80766180a6d-240.mp4
pattern: /magnet:\?xt=urn:[a-z0-9]+:[a-z0-9]{32}/i
+ torrentfile:
+ writeOnly: true
+ type: string
+ format: binary
+ description: Torrent file containing only the video file
torrentName:
+ readOnly: true
type: string
state:
- $ref: '#/components/schemas/VideoImportStateConstant'
+ readOnly: true
+ allOf:
+ - $ref: '#/components/schemas/VideoImportStateConstant'
error:
+ readOnly: true
type: string
createdAt:
+ readOnly: true
type: string
format: date-time
updatedAt:
+ readOnly: true
type: string
format: date-time
video:
+ readOnly: true
nullable: true
allOf:
- $ref: '#/components/schemas/Video'
format: url
text:
type: string
- description: Text of the comment in Markdown
+ format: html
+ description: Text of the comment
minLength: 1
- maxLength: 10000
+ example: This video is wonderful!
threadId:
- type: integer
- inReplyToCommentId:
$ref: '#/components/schemas/id'
+ inReplyToCommentId:
+ nullable: true
+ allOf:
+ - $ref: '#/components/schemas/id'
videoId:
$ref: '#/components/schemas/Video/properties/id'
createdAt:
updatedAt:
type: string
format: date-time
+ deletedAt:
+ nullable: true
+ type: string
+ format: date-time
+ default: null
+ isDeleted:
+ type: boolean
+ default: false
totalRepliesFromVideoAuthor:
type: integer
minimum: 0
type: string
format: url
name:
- description: immutable name of the actor
+ description: immutable name of the actor, used to find or mention it
allOf:
- $ref: '#/components/schemas/username'
host:
- $ref: '#/components/schemas/User/properties/id'
displayName:
type: string
- description: name displayed on the account's profile
+ description: editable name of the account, displayed in its representations
+ minLength: 3
+ maxLength: 120
description:
type: string
description: text or bio displayed on the account's profile
properties:
currentTime:
type: integer
+ format: seconds
description: timestamp within the video, in seconds
example: 5
ServerConfig:
type: boolean
support:
description: A text tell the audience how to support the video creator
- example: Please support my work on <insert crowdfunding plateform>! <3
+ example: Please support our work on https://soutenir.framasoft.org/en/ <3
type: string
nsfw:
description: Whether or not this video contains sensitive content
UpdateUser:
properties:
email:
- type: string
- format: email
description: The updated email of the user
+ allOf:
+ - $ref: '#/components/schemas/User/properties/email'
emailVerified:
type: boolean
description: Set the email as verified
adminFlags:
$ref: '#/components/schemas/UserAdminFlags'
UpdateMe:
+ # see shared/models/users/user-update-me.model.ts:
properties:
password:
$ref: '#/components/schemas/password'
+ currentPassword:
+ $ref: '#/components/schemas/password'
email:
+ description: new email used for login and service communications
+ allOf:
+ - $ref: '#/components/schemas/User/properties/email'
+ displayName:
type: string
- format: email
- description: Your new email
+ description: new name of the user in its representations
+ minLength: 3
+ maxLength: 120
displayNSFW:
type: string
- description: Your new displayNSFW
+ description: new NSFW display policy
enum:
- 'true'
- 'false'
- both
+ webTorrentEnabled:
+ type: boolean
+ description: whether to enable P2P in the player or not
autoPlayVideo:
type: boolean
- description: Your new autoPlayVideo
- required:
- - password
- - email
- - displayNSFW
- - autoPlayVideo
+ description: new preference regarding playing videos automatically
+ autoPlayNextVideo:
+ type: boolean
+ description: new preference regarding playing following videos automatically
+ autoPlayNextVideoPlaylist:
+ type: boolean
+ description: new preference regarding playing following playlist videos automatically
+ videosHistoryEnabled:
+ type: boolean
+ description: whether to keep track of watched history or not
+ videoLanguages:
+ type: array
+ items:
+ type: string
+ description: list of languages to filter videos down to
+ theme:
+ type: string
+ noInstanceConfigWarningModal:
+ type: boolean
+ noWelcomeModal:
+ type: boolean
GetMeVideoRating:
properties:
id:
RegisterUser:
properties:
username:
- $ref: '#/components/schemas/username'
+ description: immutable name of the user, used to find or mention its actor
+ allOf:
+ - $ref: '#/components/schemas/username'
password:
$ref: '#/components/schemas/password'
email:
type: string
format: email
- description: The email of the user
+ description: email of the user, used for login or service communications
displayName:
type: string
- description: The user display name
+ description: editable name of the user, displayed in its representations
minLength: 1
maxLength: 120
channel:
type: object
+ description: channel base information used to create the first channel of the user
properties:
name:
$ref: '#/components/schemas/usernameChannel'
displayName:
- type: string
- description: The display name for the default channel
- minLength: 1
- maxLength: 120
+ $ref: '#/components/schemas/VideoChannel/properties/displayName'
required:
- username
- password
- email
+ OAuthClient:
+ properties:
+ client_id:
+ type: string
+ pattern: /^[a-z0-9]$/
+ maxLength: 32
+ minLength: 32
+ example: v1ikx5hnfop4mdpnci8nsqh93c45rldf
+ client_secret:
+ type: string
+ pattern: /^[a-zA-Z0-9]$/
+ maxLength: 32
+ minLength: 32
+ example: AjWiOapPltI6EnsWQwlFarRtLh4u8tDt
+ OAuthToken-password:
+ allOf:
+ - $ref: '#/components/schemas/OAuthClient'
+ - type: object
+ properties:
+ grant_type:
+ type: string
+ enum:
+ - password
+ - refresh_token
+ default: password
+ username:
+ $ref: '#/components/schemas/User/properties/username'
+ password:
+ $ref: '#/components/schemas/password'
+ required:
+ - client_id
+ - client_secret
+ - grant_type
+ - username
+ - password
+ OAuthToken-refresh_token:
+ allOf:
+ - $ref: '#/components/schemas/OAuthClient'
+ - type: object
+ properties:
+ grant_type:
+ type: string
+ enum:
+ - password
+ - refresh_token
+ default: password
+ refresh_token:
+ type: string
+ example: 2e0d675df9fc96d2e4ec8a3ebbbf45eca9137bb7
+ required:
+ - client_id
+ - client_secret
+ - grant_type
+ - refresh_token
+
VideoChannel:
properties:
# GET/POST/PUT properties
displayName:
type: string
+ description: editable name of the channel, displayed in its representations
example: Videos of Framasoft
minLength: 1
maxLength: 120
support:
type: string
description: text shown by default on all videos of this channel, to tell the audience how to support it
- example: Please support my work on <insert crowdfunding plateform>! <3
+ example: Please support our work on https://soutenir.framasoft.org/en/ <3
minLength: 3
maxLength: 1000
# GET-only properties