video.ts 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037
  1. import * as Bluebird from 'bluebird'
  2. import { remove } from 'fs-extra'
  3. import { maxBy, minBy, pick } from 'lodash'
  4. import { join } from 'path'
  5. import { FindOptions, Includeable, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize'
  6. import {
  7. AllowNull,
  8. BeforeDestroy,
  9. BelongsTo,
  10. BelongsToMany,
  11. Column,
  12. CreatedAt,
  13. DataType,
  14. Default,
  15. ForeignKey,
  16. HasMany,
  17. HasOne,
  18. Is,
  19. IsInt,
  20. IsUUID,
  21. Min,
  22. Model,
  23. Scopes,
  24. Table,
  25. UpdatedAt
  26. } from 'sequelize-typescript'
  27. import { buildNSFWFilter } from '@server/helpers/express-utils'
  28. import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
  29. import { LiveManager } from '@server/lib/live-manager'
  30. import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths'
  31. import { getServerActor } from '@server/models/application/application'
  32. import { ModelCache } from '@server/models/model-cache'
  33. import { VideoFile } from '@shared/models/videos/video-file.model'
  34. import { ResultList, UserRight, VideoPrivacy, VideoState } from '../../../shared'
  35. import { VideoObject } from '../../../shared/models/activitypub/objects'
  36. import { Video, VideoDetails } from '../../../shared/models/videos'
  37. import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
  38. import { VideoFilter } from '../../../shared/models/videos/video-query.type'
  39. import { VideoStreamingPlaylistType } from '../../../shared/models/videos/video-streaming-playlist.type'
  40. import { peertubeTruncate } from '../../helpers/core-utils'
  41. import { isActivityPubUrlValid } from '../../helpers/custom-validators/activitypub/misc'
  42. import { isBooleanValid } from '../../helpers/custom-validators/misc'
  43. import {
  44. isVideoCategoryValid,
  45. isVideoDescriptionValid,
  46. isVideoDurationValid,
  47. isVideoLanguageValid,
  48. isVideoLicenceValid,
  49. isVideoNameValid,
  50. isVideoPrivacyValid,
  51. isVideoStateValid,
  52. isVideoSupportValid
  53. } from '../../helpers/custom-validators/videos'
  54. import { getVideoFileResolution } from '../../helpers/ffprobe-utils'
  55. import { logger } from '../../helpers/logger'
  56. import { CONFIG } from '../../initializers/config'
  57. import {
  58. ACTIVITY_PUB,
  59. API_VERSION,
  60. CONSTRAINTS_FIELDS,
  61. LAZY_STATIC_PATHS,
  62. REMOTE_SCHEME,
  63. STATIC_DOWNLOAD_PATHS,
  64. STATIC_PATHS,
  65. VIDEO_CATEGORIES,
  66. VIDEO_LANGUAGES,
  67. VIDEO_LICENCES,
  68. VIDEO_PRIVACIES,
  69. VIDEO_STATES,
  70. WEBSERVER
  71. } from '../../initializers/constants'
  72. import { sendDeleteVideo } from '../../lib/activitypub/send'
  73. import {
  74. MChannel,
  75. MChannelAccountDefault,
  76. MChannelId,
  77. MStreamingPlaylist,
  78. MStreamingPlaylistFilesVideo,
  79. MUserAccountId,
  80. MUserId,
  81. MVideoAccountLight,
  82. MVideoAccountLightBlacklistAllFiles,
  83. MVideoAP,
  84. MVideoDetails,
  85. MVideoFileVideo,
  86. MVideoFormattable,
  87. MVideoFormattableDetails,
  88. MVideoForUser,
  89. MVideoFullLight,
  90. MVideoIdThumbnail,
  91. MVideoImmutable,
  92. MVideoThumbnail,
  93. MVideoThumbnailBlacklist,
  94. MVideoWithAllFiles,
  95. MVideoWithFile,
  96. MVideoWithRights
  97. } from '../../types/models'
  98. import { MThumbnail } from '../../types/models/video/thumbnail'
  99. import { MVideoFile, MVideoFileRedundanciesOpt, MVideoFileStreamingPlaylistVideo } from '../../types/models/video/video-file'
  100. import { VideoAbuseModel } from '../abuse/video-abuse'
  101. import { AccountModel } from '../account/account'
  102. import { AccountVideoRateModel } from '../account/account-video-rate'
  103. import { UserVideoHistoryModel } from '../account/user-video-history'
  104. import { ActorModel } from '../activitypub/actor'
  105. import { AvatarModel } from '../avatar/avatar'
  106. import { VideoRedundancyModel } from '../redundancy/video-redundancy'
  107. import { ServerModel } from '../server/server'
  108. import { buildTrigramSearchIndex, buildWhereIdOrUUID, getVideoSort, isOutdated, throwIfNotValid } from '../utils'
  109. import { ScheduleVideoUpdateModel } from './schedule-video-update'
  110. import { TagModel } from './tag'
  111. import { ThumbnailModel } from './thumbnail'
  112. import { VideoBlacklistModel } from './video-blacklist'
  113. import { VideoCaptionModel } from './video-caption'
  114. import { ScopeNames as VideoChannelScopeNames, SummaryOptions, VideoChannelModel } from './video-channel'
  115. import { VideoCommentModel } from './video-comment'
  116. import { VideoFileModel } from './video-file'
  117. import {
  118. videoFilesModelToFormattedJSON,
  119. VideoFormattingJSONOptions,
  120. videoModelToActivityPubObject,
  121. videoModelToFormattedDetailsJSON,
  122. videoModelToFormattedJSON
  123. } from './video-format-utils'
  124. import { VideoImportModel } from './video-import'
  125. import { VideoLiveModel } from './video-live'
  126. import { VideoPlaylistElementModel } from './video-playlist-element'
  127. import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder'
  128. import { VideoShareModel } from './video-share'
  129. import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
  130. import { VideoTagModel } from './video-tag'
  131. import { VideoViewModel } from './video-view'
  132. export enum ScopeNames {
  133. AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS',
  134. FOR_API = 'FOR_API',
  135. WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
  136. WITH_TAGS = 'WITH_TAGS',
  137. WITH_WEBTORRENT_FILES = 'WITH_WEBTORRENT_FILES',
  138. WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
  139. WITH_BLACKLISTED = 'WITH_BLACKLISTED',
  140. WITH_USER_HISTORY = 'WITH_USER_HISTORY',
  141. WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
  142. WITH_USER_ID = 'WITH_USER_ID',
  143. WITH_IMMUTABLE_ATTRIBUTES = 'WITH_IMMUTABLE_ATTRIBUTES',
  144. WITH_THUMBNAILS = 'WITH_THUMBNAILS',
  145. WITH_LIVE = 'WITH_LIVE'
  146. }
  147. export type ForAPIOptions = {
  148. ids?: number[]
  149. videoPlaylistId?: number
  150. withFiles?: boolean
  151. withAccountBlockerIds?: number[]
  152. }
  153. export type AvailableForListIDsOptions = {
  154. serverAccountId: number
  155. followerActorId: number
  156. includeLocalVideos: boolean
  157. attributesType?: 'none' | 'id' | 'all'
  158. filter?: VideoFilter
  159. categoryOneOf?: number[]
  160. nsfw?: boolean
  161. licenceOneOf?: number[]
  162. languageOneOf?: string[]
  163. tagsOneOf?: string[]
  164. tagsAllOf?: string[]
  165. withFiles?: boolean
  166. accountId?: number
  167. videoChannelId?: number
  168. videoPlaylistId?: number
  169. trendingDays?: number
  170. user?: MUserAccountId
  171. historyOfUser?: MUserId
  172. baseWhere?: WhereOptions[]
  173. }
  174. @Scopes(() => ({
  175. [ScopeNames.WITH_IMMUTABLE_ATTRIBUTES]: {
  176. attributes: [ 'id', 'url', 'uuid', 'remote' ]
  177. },
  178. [ScopeNames.FOR_API]: (options: ForAPIOptions) => {
  179. const include: Includeable[] = [
  180. {
  181. model: VideoChannelModel.scope({
  182. method: [
  183. VideoChannelScopeNames.SUMMARY, {
  184. withAccount: true,
  185. withAccountBlockerIds: options.withAccountBlockerIds
  186. } as SummaryOptions
  187. ]
  188. }),
  189. required: true
  190. },
  191. {
  192. attributes: [ 'type', 'filename' ],
  193. model: ThumbnailModel,
  194. required: false
  195. }
  196. ]
  197. const query: FindOptions = {}
  198. if (options.ids) {
  199. query.where = {
  200. id: {
  201. [Op.in]: options.ids
  202. }
  203. }
  204. }
  205. if (options.withFiles === true) {
  206. include.push({
  207. model: VideoFileModel,
  208. required: true
  209. })
  210. }
  211. if (options.videoPlaylistId) {
  212. include.push({
  213. model: VideoPlaylistElementModel.unscoped(),
  214. required: true,
  215. where: {
  216. videoPlaylistId: options.videoPlaylistId
  217. }
  218. })
  219. }
  220. query.include = include
  221. return query
  222. },
  223. [ScopeNames.WITH_THUMBNAILS]: {
  224. include: [
  225. {
  226. model: ThumbnailModel,
  227. required: false
  228. }
  229. ]
  230. },
  231. [ScopeNames.WITH_LIVE]: {
  232. include: [
  233. {
  234. model: VideoLiveModel.unscoped(),
  235. required: false
  236. }
  237. ]
  238. },
  239. [ScopeNames.WITH_USER_ID]: {
  240. include: [
  241. {
  242. attributes: [ 'accountId' ],
  243. model: VideoChannelModel.unscoped(),
  244. required: true,
  245. include: [
  246. {
  247. attributes: [ 'userId' ],
  248. model: AccountModel.unscoped(),
  249. required: true
  250. }
  251. ]
  252. }
  253. ]
  254. },
  255. [ScopeNames.WITH_ACCOUNT_DETAILS]: {
  256. include: [
  257. {
  258. model: VideoChannelModel.unscoped(),
  259. required: true,
  260. include: [
  261. {
  262. attributes: {
  263. exclude: [ 'privateKey', 'publicKey' ]
  264. },
  265. model: ActorModel.unscoped(),
  266. required: true,
  267. include: [
  268. {
  269. attributes: [ 'host' ],
  270. model: ServerModel.unscoped(),
  271. required: false
  272. },
  273. {
  274. model: AvatarModel.unscoped(),
  275. required: false
  276. }
  277. ]
  278. },
  279. {
  280. model: AccountModel.unscoped(),
  281. required: true,
  282. include: [
  283. {
  284. model: ActorModel.unscoped(),
  285. attributes: {
  286. exclude: [ 'privateKey', 'publicKey' ]
  287. },
  288. required: true,
  289. include: [
  290. {
  291. attributes: [ 'host' ],
  292. model: ServerModel.unscoped(),
  293. required: false
  294. },
  295. {
  296. model: AvatarModel.unscoped(),
  297. required: false
  298. }
  299. ]
  300. }
  301. ]
  302. }
  303. ]
  304. }
  305. ]
  306. },
  307. [ScopeNames.WITH_TAGS]: {
  308. include: [ TagModel ]
  309. },
  310. [ScopeNames.WITH_BLACKLISTED]: {
  311. include: [
  312. {
  313. attributes: [ 'id', 'reason', 'unfederated' ],
  314. model: VideoBlacklistModel,
  315. required: false
  316. }
  317. ]
  318. },
  319. [ScopeNames.WITH_WEBTORRENT_FILES]: (withRedundancies = false) => {
  320. let subInclude: any[] = []
  321. if (withRedundancies === true) {
  322. subInclude = [
  323. {
  324. attributes: [ 'fileUrl' ],
  325. model: VideoRedundancyModel.unscoped(),
  326. required: false
  327. }
  328. ]
  329. }
  330. return {
  331. include: [
  332. {
  333. model: VideoFileModel,
  334. separate: true, // We may have multiple files, having multiple redundancies so let's separate this join
  335. required: false,
  336. include: subInclude
  337. }
  338. ]
  339. }
  340. },
  341. [ScopeNames.WITH_STREAMING_PLAYLISTS]: (withRedundancies = false) => {
  342. const subInclude: IncludeOptions[] = [
  343. {
  344. model: VideoFileModel,
  345. required: false
  346. }
  347. ]
  348. if (withRedundancies === true) {
  349. subInclude.push({
  350. attributes: [ 'fileUrl' ],
  351. model: VideoRedundancyModel.unscoped(),
  352. required: false
  353. })
  354. }
  355. return {
  356. include: [
  357. {
  358. model: VideoStreamingPlaylistModel.unscoped(),
  359. separate: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
  360. required: false,
  361. include: subInclude
  362. }
  363. ]
  364. }
  365. },
  366. [ScopeNames.WITH_SCHEDULED_UPDATE]: {
  367. include: [
  368. {
  369. model: ScheduleVideoUpdateModel.unscoped(),
  370. required: false
  371. }
  372. ]
  373. },
  374. [ScopeNames.WITH_USER_HISTORY]: (userId: number) => {
  375. return {
  376. include: [
  377. {
  378. attributes: [ 'currentTime' ],
  379. model: UserVideoHistoryModel.unscoped(),
  380. required: false,
  381. where: {
  382. userId
  383. }
  384. }
  385. ]
  386. }
  387. }
  388. }))
  389. @Table({
  390. tableName: 'video',
  391. indexes: [
  392. buildTrigramSearchIndex('video_name_trigram', 'name'),
  393. { fields: [ 'createdAt' ] },
  394. {
  395. fields: [
  396. { name: 'publishedAt', order: 'DESC' },
  397. { name: 'id', order: 'ASC' }
  398. ]
  399. },
  400. { fields: [ 'duration' ] },
  401. { fields: [ 'views' ] },
  402. { fields: [ 'channelId' ] },
  403. {
  404. fields: [ 'originallyPublishedAt' ],
  405. where: {
  406. originallyPublishedAt: {
  407. [Op.ne]: null
  408. }
  409. }
  410. },
  411. {
  412. fields: [ 'category' ], // We don't care videos with an unknown category
  413. where: {
  414. category: {
  415. [Op.ne]: null
  416. }
  417. }
  418. },
  419. {
  420. fields: [ 'licence' ], // We don't care videos with an unknown licence
  421. where: {
  422. licence: {
  423. [Op.ne]: null
  424. }
  425. }
  426. },
  427. {
  428. fields: [ 'language' ], // We don't care videos with an unknown language
  429. where: {
  430. language: {
  431. [Op.ne]: null
  432. }
  433. }
  434. },
  435. {
  436. fields: [ 'nsfw' ], // Most of the videos are not NSFW
  437. where: {
  438. nsfw: true
  439. }
  440. },
  441. {
  442. fields: [ 'remote' ], // Only index local videos
  443. where: {
  444. remote: false
  445. }
  446. },
  447. {
  448. fields: [ 'uuid' ],
  449. unique: true
  450. },
  451. {
  452. fields: [ 'url' ],
  453. unique: true
  454. }
  455. ]
  456. })
  457. export class VideoModel extends Model {
  458. @AllowNull(false)
  459. @Default(DataType.UUIDV4)
  460. @IsUUID(4)
  461. @Column(DataType.UUID)
  462. uuid: string
  463. @AllowNull(false)
  464. @Is('VideoName', value => throwIfNotValid(value, isVideoNameValid, 'name'))
  465. @Column
  466. name: string
  467. @AllowNull(true)
  468. @Default(null)
  469. @Is('VideoCategory', value => throwIfNotValid(value, isVideoCategoryValid, 'category', true))
  470. @Column
  471. category: number
  472. @AllowNull(true)
  473. @Default(null)
  474. @Is('VideoLicence', value => throwIfNotValid(value, isVideoLicenceValid, 'licence', true))
  475. @Column
  476. licence: number
  477. @AllowNull(true)
  478. @Default(null)
  479. @Is('VideoLanguage', value => throwIfNotValid(value, isVideoLanguageValid, 'language', true))
  480. @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.LANGUAGE.max))
  481. language: string
  482. @AllowNull(false)
  483. @Is('VideoPrivacy', value => throwIfNotValid(value, isVideoPrivacyValid, 'privacy'))
  484. @Column
  485. privacy: VideoPrivacy
  486. @AllowNull(false)
  487. @Is('VideoNSFW', value => throwIfNotValid(value, isBooleanValid, 'NSFW boolean'))
  488. @Column
  489. nsfw: boolean
  490. @AllowNull(true)
  491. @Default(null)
  492. @Is('VideoDescription', value => throwIfNotValid(value, isVideoDescriptionValid, 'description', true))
  493. @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.DESCRIPTION.max))
  494. description: string
  495. @AllowNull(true)
  496. @Default(null)
  497. @Is('VideoSupport', value => throwIfNotValid(value, isVideoSupportValid, 'support', true))
  498. @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.SUPPORT.max))
  499. support: string
  500. @AllowNull(false)
  501. @Is('VideoDuration', value => throwIfNotValid(value, isVideoDurationValid, 'duration'))
  502. @Column
  503. duration: number
  504. @AllowNull(false)
  505. @Default(0)
  506. @IsInt
  507. @Min(0)
  508. @Column
  509. views: number
  510. @AllowNull(false)
  511. @Default(0)
  512. @IsInt
  513. @Min(0)
  514. @Column
  515. likes: number
  516. @AllowNull(false)
  517. @Default(0)
  518. @IsInt
  519. @Min(0)
  520. @Column
  521. dislikes: number
  522. @AllowNull(false)
  523. @Column
  524. remote: boolean
  525. @AllowNull(false)
  526. @Default(false)
  527. @Column
  528. isLive: boolean
  529. @AllowNull(false)
  530. @Is('VideoUrl', value => throwIfNotValid(value, isActivityPubUrlValid, 'url'))
  531. @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEOS.URL.max))
  532. url: string
  533. @AllowNull(false)
  534. @Column
  535. commentsEnabled: boolean
  536. @AllowNull(false)
  537. @Column
  538. downloadEnabled: boolean
  539. @AllowNull(false)
  540. @Column
  541. waitTranscoding: boolean
  542. @AllowNull(false)
  543. @Default(null)
  544. @Is('VideoState', value => throwIfNotValid(value, isVideoStateValid, 'state'))
  545. @Column
  546. state: VideoState
  547. @CreatedAt
  548. createdAt: Date
  549. @UpdatedAt
  550. updatedAt: Date
  551. @AllowNull(false)
  552. @Default(DataType.NOW)
  553. @Column
  554. publishedAt: Date
  555. @AllowNull(true)
  556. @Default(null)
  557. @Column
  558. originallyPublishedAt: Date
  559. @ForeignKey(() => VideoChannelModel)
  560. @Column
  561. channelId: number
  562. @BelongsTo(() => VideoChannelModel, {
  563. foreignKey: {
  564. allowNull: true
  565. },
  566. hooks: true
  567. })
  568. VideoChannel: VideoChannelModel
  569. @BelongsToMany(() => TagModel, {
  570. foreignKey: 'videoId',
  571. through: () => VideoTagModel,
  572. onDelete: 'CASCADE'
  573. })
  574. Tags: TagModel[]
  575. @HasMany(() => ThumbnailModel, {
  576. foreignKey: {
  577. name: 'videoId',
  578. allowNull: true
  579. },
  580. hooks: true,
  581. onDelete: 'cascade'
  582. })
  583. Thumbnails: ThumbnailModel[]
  584. @HasMany(() => VideoPlaylistElementModel, {
  585. foreignKey: {
  586. name: 'videoId',
  587. allowNull: true
  588. },
  589. onDelete: 'set null'
  590. })
  591. VideoPlaylistElements: VideoPlaylistElementModel[]
  592. @HasMany(() => VideoAbuseModel, {
  593. foreignKey: {
  594. name: 'videoId',
  595. allowNull: true
  596. },
  597. onDelete: 'set null'
  598. })
  599. VideoAbuses: VideoAbuseModel[]
  600. @HasMany(() => VideoFileModel, {
  601. foreignKey: {
  602. name: 'videoId',
  603. allowNull: true
  604. },
  605. hooks: true,
  606. onDelete: 'cascade'
  607. })
  608. VideoFiles: VideoFileModel[]
  609. @HasMany(() => VideoStreamingPlaylistModel, {
  610. foreignKey: {
  611. name: 'videoId',
  612. allowNull: false
  613. },
  614. hooks: true,
  615. onDelete: 'cascade'
  616. })
  617. VideoStreamingPlaylists: VideoStreamingPlaylistModel[]
  618. @HasMany(() => VideoShareModel, {
  619. foreignKey: {
  620. name: 'videoId',
  621. allowNull: false
  622. },
  623. onDelete: 'cascade'
  624. })
  625. VideoShares: VideoShareModel[]
  626. @HasMany(() => AccountVideoRateModel, {
  627. foreignKey: {
  628. name: 'videoId',
  629. allowNull: false
  630. },
  631. onDelete: 'cascade'
  632. })
  633. AccountVideoRates: AccountVideoRateModel[]
  634. @HasMany(() => VideoCommentModel, {
  635. foreignKey: {
  636. name: 'videoId',
  637. allowNull: false
  638. },
  639. onDelete: 'cascade',
  640. hooks: true
  641. })
  642. VideoComments: VideoCommentModel[]
  643. @HasMany(() => VideoViewModel, {
  644. foreignKey: {
  645. name: 'videoId',
  646. allowNull: false
  647. },
  648. onDelete: 'cascade'
  649. })
  650. VideoViews: VideoViewModel[]
  651. @HasMany(() => UserVideoHistoryModel, {
  652. foreignKey: {
  653. name: 'videoId',
  654. allowNull: false
  655. },
  656. onDelete: 'cascade'
  657. })
  658. UserVideoHistories: UserVideoHistoryModel[]
  659. @HasOne(() => ScheduleVideoUpdateModel, {
  660. foreignKey: {
  661. name: 'videoId',
  662. allowNull: false
  663. },
  664. onDelete: 'cascade'
  665. })
  666. ScheduleVideoUpdate: ScheduleVideoUpdateModel
  667. @HasOne(() => VideoBlacklistModel, {
  668. foreignKey: {
  669. name: 'videoId',
  670. allowNull: false
  671. },
  672. onDelete: 'cascade'
  673. })
  674. VideoBlacklist: VideoBlacklistModel
  675. @HasOne(() => VideoLiveModel, {
  676. foreignKey: {
  677. name: 'videoId',
  678. allowNull: false
  679. },
  680. onDelete: 'cascade'
  681. })
  682. VideoLive: VideoLiveModel
  683. @HasOne(() => VideoImportModel, {
  684. foreignKey: {
  685. name: 'videoId',
  686. allowNull: true
  687. },
  688. onDelete: 'set null'
  689. })
  690. VideoImport: VideoImportModel
  691. @HasMany(() => VideoCaptionModel, {
  692. foreignKey: {
  693. name: 'videoId',
  694. allowNull: false
  695. },
  696. onDelete: 'cascade',
  697. hooks: true,
  698. ['separate' as any]: true
  699. })
  700. VideoCaptions: VideoCaptionModel[]
  701. @BeforeDestroy
  702. static async sendDelete (instance: MVideoAccountLight, options) {
  703. if (instance.isOwned()) {
  704. if (!instance.VideoChannel) {
  705. instance.VideoChannel = await instance.$get('VideoChannel', {
  706. include: [
  707. ActorModel,
  708. AccountModel
  709. ],
  710. transaction: options.transaction
  711. }) as MChannelAccountDefault
  712. }
  713. return sendDeleteVideo(instance, options.transaction)
  714. }
  715. return undefined
  716. }
  717. @BeforeDestroy
  718. static async removeFiles (instance: VideoModel) {
  719. const tasks: Promise<any>[] = []
  720. logger.info('Removing files of video %s.', instance.url)
  721. if (instance.isOwned()) {
  722. if (!Array.isArray(instance.VideoFiles)) {
  723. instance.VideoFiles = await instance.$get('VideoFiles')
  724. }
  725. // Remove physical files and torrents
  726. instance.VideoFiles.forEach(file => {
  727. tasks.push(instance.removeFile(file))
  728. tasks.push(instance.removeTorrent(file))
  729. })
  730. // Remove playlists file
  731. if (!Array.isArray(instance.VideoStreamingPlaylists)) {
  732. instance.VideoStreamingPlaylists = await instance.$get('VideoStreamingPlaylists')
  733. }
  734. for (const p of instance.VideoStreamingPlaylists) {
  735. tasks.push(instance.removeStreamingPlaylistFiles(p))
  736. }
  737. }
  738. // Do not wait video deletion because we could be in a transaction
  739. Promise.all(tasks)
  740. .catch(err => {
  741. logger.error('Some errors when removing files of video %s in before destroy hook.', instance.uuid, { err })
  742. })
  743. return undefined
  744. }
  745. @BeforeDestroy
  746. static stopLiveIfNeeded (instance: VideoModel) {
  747. if (!instance.isLive) return
  748. logger.info('Stopping live of video %s after video deletion.', instance.uuid)
  749. return LiveManager.Instance.stopSessionOf(instance.id)
  750. }
  751. @BeforeDestroy
  752. static invalidateCache (instance: VideoModel) {
  753. ModelCache.Instance.invalidateCache('video', instance.id)
  754. }
  755. @BeforeDestroy
  756. static async saveEssentialDataToAbuses (instance: VideoModel, options) {
  757. const tasks: Promise<any>[] = []
  758. if (!Array.isArray(instance.VideoAbuses)) {
  759. instance.VideoAbuses = await instance.$get('VideoAbuses')
  760. if (instance.VideoAbuses.length === 0) return undefined
  761. }
  762. logger.info('Saving video abuses details of video %s.', instance.url)
  763. const details = instance.toFormattedDetailsJSON()
  764. for (const abuse of instance.VideoAbuses) {
  765. abuse.deletedVideo = details
  766. tasks.push(abuse.save({ transaction: options.transaction }))
  767. }
  768. Promise.all(tasks)
  769. .catch(err => {
  770. logger.error('Some errors when saving details of video %s in its abuses before destroy hook.', instance.uuid, { err })
  771. })
  772. return undefined
  773. }
  774. static listLocal (): Promise<MVideoWithAllFiles[]> {
  775. const query = {
  776. where: {
  777. remote: false
  778. }
  779. }
  780. return VideoModel.scope([
  781. ScopeNames.WITH_WEBTORRENT_FILES,
  782. ScopeNames.WITH_STREAMING_PLAYLISTS,
  783. ScopeNames.WITH_THUMBNAILS
  784. ]).findAll(query)
  785. }
  786. static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
  787. function getRawQuery (select: string) {
  788. const queryVideo = 'SELECT ' + select + ' FROM "video" AS "Video" ' +
  789. 'INNER JOIN "videoChannel" AS "VideoChannel" ON "VideoChannel"."id" = "Video"."channelId" ' +
  790. 'INNER JOIN "account" AS "Account" ON "Account"."id" = "VideoChannel"."accountId" ' +
  791. 'WHERE "Account"."actorId" = ' + actorId
  792. const queryVideoShare = 'SELECT ' + select + ' FROM "videoShare" AS "VideoShare" ' +
  793. 'INNER JOIN "video" AS "Video" ON "Video"."id" = "VideoShare"."videoId" ' +
  794. 'WHERE "VideoShare"."actorId" = ' + actorId
  795. return `(${queryVideo}) UNION (${queryVideoShare})`
  796. }
  797. const rawQuery = getRawQuery('"Video"."id"')
  798. const rawCountQuery = getRawQuery('COUNT("Video"."id") as "total"')
  799. const query = {
  800. distinct: true,
  801. offset: start,
  802. limit: count,
  803. order: getVideoSort('-createdAt', [ 'Tags', 'name', 'ASC' ] as any), // FIXME: sequelize typings
  804. where: {
  805. id: {
  806. [Op.in]: Sequelize.literal('(' + rawQuery + ')')
  807. },
  808. [Op.or]: getPrivaciesForFederation()
  809. },
  810. include: [
  811. {
  812. attributes: [ 'language', 'fileUrl' ],
  813. model: VideoCaptionModel.unscoped(),
  814. required: false
  815. },
  816. {
  817. attributes: [ 'id', 'url' ],
  818. model: VideoShareModel.unscoped(),
  819. required: false,
  820. // We only want videos shared by this actor
  821. where: {
  822. [Op.and]: [
  823. {
  824. id: {
  825. [Op.not]: null
  826. }
  827. },
  828. {
  829. actorId
  830. }
  831. ]
  832. },
  833. include: [
  834. {
  835. attributes: [ 'id', 'url' ],
  836. model: ActorModel.unscoped()
  837. }
  838. ]
  839. },
  840. {
  841. model: VideoChannelModel.unscoped(),
  842. required: true,
  843. include: [
  844. {
  845. attributes: [ 'name' ],
  846. model: AccountModel.unscoped(),
  847. required: true,
  848. include: [
  849. {
  850. attributes: [ 'id', 'url', 'followersUrl' ],
  851. model: ActorModel.unscoped(),
  852. required: true
  853. }
  854. ]
  855. },
  856. {
  857. attributes: [ 'id', 'url', 'followersUrl' ],
  858. model: ActorModel.unscoped(),
  859. required: true
  860. }
  861. ]
  862. },
  863. {
  864. model: VideoStreamingPlaylistModel.unscoped(),
  865. required: false,
  866. include: [
  867. {
  868. model: VideoFileModel,
  869. required: false
  870. }
  871. ]
  872. },
  873. VideoLiveModel.unscoped(),
  874. VideoFileModel,
  875. TagModel
  876. ]
  877. }
  878. return Bluebird.all([
  879. VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query),
  880. VideoModel.sequelize.query<{ total: string }>(rawCountQuery, { type: QueryTypes.SELECT })
  881. ]).then(([ rows, totals ]) => {
  882. // totals: totalVideos + totalVideoShares
  883. let totalVideos = 0
  884. let totalVideoShares = 0
  885. if (totals[0]) totalVideos = parseInt(totals[0].total, 10)
  886. if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10)
  887. const total = totalVideos + totalVideoShares
  888. return {
  889. data: rows,
  890. total: total
  891. }
  892. })
  893. }
  894. static async listPublishedLiveIds () {
  895. const options = {
  896. attributes: [ 'id' ],
  897. where: {
  898. isLive: true,
  899. state: VideoState.PUBLISHED
  900. }
  901. }
  902. const result = await VideoModel.findAll(options)
  903. return result.map(v => v.id)
  904. }
  905. static listUserVideosForApi (
  906. accountId: number,
  907. start: number,
  908. count: number,
  909. sort: string,
  910. search?: string
  911. ) {
  912. function buildBaseQuery (): FindOptions {
  913. let baseQuery = {
  914. offset: start,
  915. limit: count,
  916. order: getVideoSort(sort),
  917. include: [
  918. {
  919. model: VideoChannelModel,
  920. required: true,
  921. include: [
  922. {
  923. model: AccountModel,
  924. where: {
  925. id: accountId
  926. },
  927. required: true
  928. }
  929. ]
  930. }
  931. ]
  932. }
  933. if (search) {
  934. baseQuery = Object.assign(baseQuery, {
  935. where: {
  936. name: {
  937. [Op.iLike]: '%' + search + '%'
  938. }
  939. }
  940. })
  941. }
  942. return baseQuery
  943. }
  944. const countQuery = buildBaseQuery()
  945. const findQuery = buildBaseQuery()
  946. const findScopes: (string | ScopeOptions)[] = [
  947. ScopeNames.WITH_SCHEDULED_UPDATE,
  948. ScopeNames.WITH_BLACKLISTED,
  949. ScopeNames.WITH_THUMBNAILS
  950. ]
  951. return Promise.all([
  952. VideoModel.count(countQuery),
  953. VideoModel.scope(findScopes).findAll<MVideoForUser>(findQuery)
  954. ]).then(([ count, rows ]) => {
  955. return {
  956. data: rows,
  957. total: count
  958. }
  959. })
  960. }
  961. static async listForApi (options: {
  962. start: number
  963. count: number
  964. sort: string
  965. nsfw: boolean
  966. includeLocalVideos: boolean
  967. withFiles: boolean
  968. categoryOneOf?: number[]
  969. licenceOneOf?: number[]
  970. languageOneOf?: string[]
  971. tagsOneOf?: string[]
  972. tagsAllOf?: string[]
  973. filter?: VideoFilter
  974. accountId?: number
  975. videoChannelId?: number
  976. followerActorId?: number
  977. videoPlaylistId?: number
  978. trendingDays?: number
  979. user?: MUserAccountId
  980. historyOfUser?: MUserId
  981. countVideos?: boolean
  982. search?: string
  983. }) {
  984. if ((options.filter === 'all-local' || options.filter === 'all') && !options.user.hasRight(UserRight.SEE_ALL_VIDEOS)) {
  985. throw new Error('Try to filter all-local but no user has not the see all videos right')
  986. }
  987. const trendingDays = options.sort.endsWith('trending')
  988. ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
  989. : undefined
  990. const serverActor = await getServerActor()
  991. // followerActorId === null has a meaning, so just check undefined
  992. const followerActorId = options.followerActorId !== undefined
  993. ? options.followerActorId
  994. : serverActor.id
  995. const queryOptions = {
  996. start: options.start,
  997. count: options.count,
  998. sort: options.sort,
  999. followerActorId,
  1000. serverAccountId: serverActor.Account.id,
  1001. nsfw: options.nsfw,
  1002. categoryOneOf: options.categoryOneOf,
  1003. licenceOneOf: options.licenceOneOf,
  1004. languageOneOf: options.languageOneOf,
  1005. tagsOneOf: options.tagsOneOf,
  1006. tagsAllOf: options.tagsAllOf,
  1007. filter: options.filter,
  1008. withFiles: options.withFiles,
  1009. accountId: options.accountId,
  1010. videoChannelId: options.videoChannelId,
  1011. videoPlaylistId: options.videoPlaylistId,
  1012. includeLocalVideos: options.includeLocalVideos,
  1013. user: options.user,
  1014. historyOfUser: options.historyOfUser,
  1015. trendingDays,
  1016. search: options.search
  1017. }
  1018. return VideoModel.getAvailableForApi(queryOptions, options.countVideos)
  1019. }
  1020. static async searchAndPopulateAccountAndServer (options: {
  1021. includeLocalVideos: boolean
  1022. search?: string
  1023. start?: number
  1024. count?: number
  1025. sort?: string
  1026. startDate?: string // ISO 8601
  1027. endDate?: string // ISO 8601
  1028. originallyPublishedStartDate?: string
  1029. originallyPublishedEndDate?: string
  1030. nsfw?: boolean
  1031. categoryOneOf?: number[]
  1032. licenceOneOf?: number[]
  1033. languageOneOf?: string[]
  1034. tagsOneOf?: string[]
  1035. tagsAllOf?: string[]
  1036. durationMin?: number // seconds
  1037. durationMax?: number // seconds
  1038. user?: MUserAccountId
  1039. filter?: VideoFilter
  1040. }) {
  1041. const serverActor = await getServerActor()
  1042. const queryOptions = {
  1043. followerActorId: serverActor.id,
  1044. serverAccountId: serverActor.Account.id,
  1045. includeLocalVideos: options.includeLocalVideos,
  1046. nsfw: options.nsfw,
  1047. categoryOneOf: options.categoryOneOf,
  1048. licenceOneOf: options.licenceOneOf,
  1049. languageOneOf: options.languageOneOf,
  1050. tagsOneOf: options.tagsOneOf,
  1051. tagsAllOf: options.tagsAllOf,
  1052. user: options.user,
  1053. filter: options.filter,
  1054. start: options.start,
  1055. count: options.count,
  1056. sort: options.sort,
  1057. startDate: options.startDate,
  1058. endDate: options.endDate,
  1059. originallyPublishedStartDate: options.originallyPublishedStartDate,
  1060. originallyPublishedEndDate: options.originallyPublishedEndDate,
  1061. durationMin: options.durationMin,
  1062. durationMax: options.durationMax,
  1063. search: options.search
  1064. }
  1065. return VideoModel.getAvailableForApi(queryOptions)
  1066. }
  1067. static countLocalLives () {
  1068. const options = {
  1069. where: {
  1070. remote: false,
  1071. isLive: true,
  1072. state: {
  1073. [Op.ne]: VideoState.LIVE_ENDED
  1074. }
  1075. }
  1076. }
  1077. return VideoModel.count(options)
  1078. }
  1079. static countLivesOfAccount (accountId: number) {
  1080. const options = {
  1081. where: {
  1082. remote: false,
  1083. isLive: true,
  1084. state: {
  1085. [Op.ne]: VideoState.LIVE_ENDED
  1086. }
  1087. },
  1088. include: [
  1089. {
  1090. required: true,
  1091. model: VideoChannelModel.unscoped(),
  1092. where: {
  1093. accountId
  1094. }
  1095. }
  1096. ]
  1097. }
  1098. return VideoModel.count(options)
  1099. }
  1100. static load (id: number | string, t?: Transaction): Promise<MVideoThumbnail> {
  1101. const where = buildWhereIdOrUUID(id)
  1102. const options = {
  1103. where,
  1104. transaction: t
  1105. }
  1106. return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
  1107. }
  1108. static loadWithBlacklist (id: number | string, t?: Transaction): Promise<MVideoThumbnailBlacklist> {
  1109. const where = buildWhereIdOrUUID(id)
  1110. const options = {
  1111. where,
  1112. transaction: t
  1113. }
  1114. return VideoModel.scope([
  1115. ScopeNames.WITH_THUMBNAILS,
  1116. ScopeNames.WITH_BLACKLISTED
  1117. ]).findOne(options)
  1118. }
  1119. static loadImmutableAttributes (id: number | string, t?: Transaction): Promise<MVideoImmutable> {
  1120. const fun = () => {
  1121. const query = {
  1122. where: buildWhereIdOrUUID(id),
  1123. transaction: t
  1124. }
  1125. return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
  1126. }
  1127. return ModelCache.Instance.doCache({
  1128. cacheType: 'load-video-immutable-id',
  1129. key: '' + id,
  1130. deleteKey: 'video',
  1131. fun
  1132. })
  1133. }
  1134. static loadWithRights (id: number | string, t?: Transaction): Promise<MVideoWithRights> {
  1135. const where = buildWhereIdOrUUID(id)
  1136. const options = {
  1137. where,
  1138. transaction: t
  1139. }
  1140. return VideoModel.scope([
  1141. ScopeNames.WITH_BLACKLISTED,
  1142. ScopeNames.WITH_USER_ID,
  1143. ScopeNames.WITH_THUMBNAILS
  1144. ]).findOne(options)
  1145. }
  1146. static loadOnlyId (id: number | string, t?: Transaction): Promise<MVideoIdThumbnail> {
  1147. const where = buildWhereIdOrUUID(id)
  1148. const options = {
  1149. attributes: [ 'id' ],
  1150. where,
  1151. transaction: t
  1152. }
  1153. return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
  1154. }
  1155. static loadWithFiles (id: number | string, t?: Transaction, logging?: boolean): Promise<MVideoWithAllFiles> {
  1156. const where = buildWhereIdOrUUID(id)
  1157. const query = {
  1158. where,
  1159. transaction: t,
  1160. logging
  1161. }
  1162. return VideoModel.scope([
  1163. ScopeNames.WITH_WEBTORRENT_FILES,
  1164. ScopeNames.WITH_STREAMING_PLAYLISTS,
  1165. ScopeNames.WITH_THUMBNAILS
  1166. ]).findOne(query)
  1167. }
  1168. static loadByUUID (uuid: string): Promise<MVideoThumbnail> {
  1169. const options = {
  1170. where: {
  1171. uuid
  1172. }
  1173. }
  1174. return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
  1175. }
  1176. static loadByUrl (url: string, transaction?: Transaction): Promise<MVideoThumbnail> {
  1177. const query: FindOptions = {
  1178. where: {
  1179. url
  1180. },
  1181. transaction
  1182. }
  1183. return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
  1184. }
  1185. static loadByUrlImmutableAttributes (url: string, transaction?: Transaction): Promise<MVideoImmutable> {
  1186. const fun = () => {
  1187. const query: FindOptions = {
  1188. where: {
  1189. url
  1190. },
  1191. transaction
  1192. }
  1193. return VideoModel.scope(ScopeNames.WITH_IMMUTABLE_ATTRIBUTES).findOne(query)
  1194. }
  1195. return ModelCache.Instance.doCache({
  1196. cacheType: 'load-video-immutable-url',
  1197. key: url,
  1198. deleteKey: 'video',
  1199. fun
  1200. })
  1201. }
  1202. static loadByUrlAndPopulateAccount (url: string, transaction?: Transaction): Promise<MVideoAccountLightBlacklistAllFiles> {
  1203. const query: FindOptions = {
  1204. where: {
  1205. url
  1206. },
  1207. transaction
  1208. }
  1209. return VideoModel.scope([
  1210. ScopeNames.WITH_ACCOUNT_DETAILS,
  1211. ScopeNames.WITH_WEBTORRENT_FILES,
  1212. ScopeNames.WITH_STREAMING_PLAYLISTS,
  1213. ScopeNames.WITH_THUMBNAILS,
  1214. ScopeNames.WITH_BLACKLISTED
  1215. ]).findOne(query)
  1216. }
  1217. static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Transaction, userId?: number): Promise<MVideoFullLight> {
  1218. const where = buildWhereIdOrUUID(id)
  1219. const options = {
  1220. order: [ [ 'Tags', 'name', 'ASC' ] ] as any,
  1221. where,
  1222. transaction: t
  1223. }
  1224. const scopes: (string | ScopeOptions)[] = [
  1225. ScopeNames.WITH_TAGS,
  1226. ScopeNames.WITH_BLACKLISTED,
  1227. ScopeNames.WITH_ACCOUNT_DETAILS,
  1228. ScopeNames.WITH_SCHEDULED_UPDATE,
  1229. ScopeNames.WITH_WEBTORRENT_FILES,
  1230. ScopeNames.WITH_STREAMING_PLAYLISTS,
  1231. ScopeNames.WITH_THUMBNAILS,
  1232. ScopeNames.WITH_LIVE
  1233. ]
  1234. if (userId) {
  1235. scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
  1236. }
  1237. return VideoModel
  1238. .scope(scopes)
  1239. .findOne(options)
  1240. }
  1241. static loadForGetAPI (parameters: {
  1242. id: number | string
  1243. t?: Transaction
  1244. userId?: number
  1245. }): Promise<MVideoDetails> {
  1246. const { id, t, userId } = parameters
  1247. const where = buildWhereIdOrUUID(id)
  1248. const options = {
  1249. order: [ [ 'Tags', 'name', 'ASC' ] ] as any, // FIXME: sequelize typings
  1250. where,
  1251. transaction: t
  1252. }
  1253. const scopes: (string | ScopeOptions)[] = [
  1254. ScopeNames.WITH_TAGS,
  1255. ScopeNames.WITH_BLACKLISTED,
  1256. ScopeNames.WITH_ACCOUNT_DETAILS,
  1257. ScopeNames.WITH_SCHEDULED_UPDATE,
  1258. ScopeNames.WITH_THUMBNAILS,
  1259. ScopeNames.WITH_LIVE,
  1260. { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
  1261. { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
  1262. ]
  1263. if (userId) {
  1264. scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
  1265. }
  1266. return VideoModel
  1267. .scope(scopes)
  1268. .findOne(options)
  1269. }
  1270. static async getStats () {
  1271. const totalLocalVideos = await VideoModel.count({
  1272. where: {
  1273. remote: false
  1274. }
  1275. })
  1276. let totalLocalVideoViews = await VideoModel.sum('views', {
  1277. where: {
  1278. remote: false
  1279. }
  1280. })
  1281. // Sequelize could return null...
  1282. if (!totalLocalVideoViews) totalLocalVideoViews = 0
  1283. const { total: totalVideos } = await VideoModel.listForApi({
  1284. start: 0,
  1285. count: 0,
  1286. sort: '-publishedAt',
  1287. nsfw: buildNSFWFilter(),
  1288. includeLocalVideos: true,
  1289. withFiles: false
  1290. })
  1291. return {
  1292. totalLocalVideos,
  1293. totalLocalVideoViews,
  1294. totalVideos
  1295. }
  1296. }
  1297. static incrementViews (id: number, views: number) {
  1298. return VideoModel.increment('views', {
  1299. by: views,
  1300. where: {
  1301. id
  1302. }
  1303. })
  1304. }
  1305. static checkVideoHasInstanceFollow (videoId: number, followerActorId: number) {
  1306. // Instances only share videos
  1307. const query = 'SELECT 1 FROM "videoShare" ' +
  1308. 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
  1309. 'WHERE "actorFollow"."actorId" = $followerActorId AND "actorFollow"."state" = \'accepted\' AND "videoShare"."videoId" = $videoId ' +
  1310. 'LIMIT 1'
  1311. const options = {
  1312. type: QueryTypes.SELECT as QueryTypes.SELECT,
  1313. bind: { followerActorId, videoId },
  1314. raw: true
  1315. }
  1316. return VideoModel.sequelize.query(query, options)
  1317. .then(results => results.length === 1)
  1318. }
  1319. static bulkUpdateSupportField (videoChannel: MChannel, t: Transaction) {
  1320. const options = {
  1321. where: {
  1322. channelId: videoChannel.id
  1323. },
  1324. transaction: t
  1325. }
  1326. return VideoModel.update({ support: videoChannel.support }, options)
  1327. }
  1328. static getAllIdsFromChannel (videoChannel: MChannelId): Promise<number[]> {
  1329. const query = {
  1330. attributes: [ 'id' ],
  1331. where: {
  1332. channelId: videoChannel.id
  1333. }
  1334. }
  1335. return VideoModel.findAll(query)
  1336. .then(videos => videos.map(v => v.id))
  1337. }
  1338. // threshold corresponds to how many video the field should have to be returned
  1339. static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
  1340. const serverActor = await getServerActor()
  1341. const followerActorId = serverActor.id
  1342. const queryOptions: BuildVideosQueryOptions = {
  1343. attributes: [ `"${field}"` ],
  1344. group: `GROUP BY "${field}"`,
  1345. having: `HAVING COUNT("${field}") >= ${threshold}`,
  1346. start: 0,
  1347. sort: 'random',
  1348. count,
  1349. serverAccountId: serverActor.Account.id,
  1350. followerActorId,
  1351. includeLocalVideos: true
  1352. }
  1353. const { query, replacements } = buildListQuery(VideoModel, queryOptions)
  1354. return this.sequelize.query<any>(query, { replacements, type: QueryTypes.SELECT })
  1355. .then(rows => rows.map(r => r[field]))
  1356. }
  1357. static buildTrendingQuery (trendingDays: number) {
  1358. return {
  1359. attributes: [],
  1360. subQuery: false,
  1361. model: VideoViewModel,
  1362. required: false,
  1363. where: {
  1364. startDate: {
  1365. [Op.gte]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays)
  1366. }
  1367. }
  1368. }
  1369. }
  1370. private static async getAvailableForApi (
  1371. options: BuildVideosQueryOptions,
  1372. countVideos = true
  1373. ): Promise<ResultList<VideoModel>> {
  1374. function getCount () {
  1375. if (countVideos !== true) return Promise.resolve(undefined)
  1376. const countOptions = Object.assign({}, options, { isCount: true })
  1377. const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, countOptions)
  1378. return VideoModel.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT })
  1379. .then(rows => rows.length !== 0 ? rows[0].total : 0)
  1380. }
  1381. function getModels () {
  1382. if (options.count === 0) return Promise.resolve([])
  1383. const { query, replacements, order } = buildListQuery(VideoModel, options)
  1384. const queryModels = wrapForAPIResults(query, replacements, options, order)
  1385. return VideoModel.sequelize.query<any>(queryModels, { replacements, type: QueryTypes.SELECT, nest: true })
  1386. .then(rows => VideoModel.buildAPIResult(rows))
  1387. }
  1388. const [ count, rows ] = await Promise.all([ getCount(), getModels() ])
  1389. return {
  1390. data: rows,
  1391. total: count
  1392. }
  1393. }
  1394. private static buildAPIResult (rows: any[]) {
  1395. const videosMemo: { [ id: number ]: VideoModel } = {}
  1396. const videoStreamingPlaylistMemo: { [ id: number ]: VideoStreamingPlaylistModel } = {}
  1397. const thumbnailsDone = new Set<number>()
  1398. const historyDone = new Set<number>()
  1399. const videoFilesDone = new Set<number>()
  1400. const videos: VideoModel[] = []
  1401. const avatarKeys = [ 'id', 'filename', 'fileUrl', 'onDisk', 'createdAt', 'updatedAt' ]
  1402. const actorKeys = [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ]
  1403. const serverKeys = [ 'id', 'host' ]
  1404. const videoFileKeys = [ 'id', 'createdAt', 'updatedAt', 'resolution', 'size', 'extname', 'infoHash', 'fps', 'videoId' ]
  1405. const videoStreamingPlaylistKeys = [ 'id' ]
  1406. const videoKeys = [
  1407. 'id',
  1408. 'uuid',
  1409. 'name',
  1410. 'category',
  1411. 'licence',
  1412. 'language',
  1413. 'privacy',
  1414. 'nsfw',
  1415. 'description',
  1416. 'support',
  1417. 'duration',
  1418. 'views',
  1419. 'likes',
  1420. 'dislikes',
  1421. 'remote',
  1422. 'isLive',
  1423. 'url',
  1424. 'commentsEnabled',
  1425. 'downloadEnabled',
  1426. 'waitTranscoding',
  1427. 'state',
  1428. 'publishedAt',
  1429. 'originallyPublishedAt',
  1430. 'channelId',
  1431. 'createdAt',
  1432. 'updatedAt'
  1433. ]
  1434. function buildActor (rowActor: any) {
  1435. const avatarModel = rowActor.Avatar.id !== null
  1436. ? new AvatarModel(pick(rowActor.Avatar, avatarKeys))
  1437. : null
  1438. const serverModel = rowActor.Server.id !== null
  1439. ? new ServerModel(pick(rowActor.Server, serverKeys))
  1440. : null
  1441. const actorModel = new ActorModel(pick(rowActor, actorKeys))
  1442. actorModel.Avatar = avatarModel
  1443. actorModel.Server = serverModel
  1444. return actorModel
  1445. }
  1446. for (const row of rows) {
  1447. if (!videosMemo[row.id]) {
  1448. // Build Channel
  1449. const channel = row.VideoChannel
  1450. const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]))
  1451. channelModel.Actor = buildActor(channel.Actor)
  1452. const account = row.VideoChannel.Account
  1453. const accountModel = new AccountModel(pick(account, [ 'id', 'name' ]))
  1454. accountModel.Actor = buildActor(account.Actor)
  1455. channelModel.Account = accountModel
  1456. const videoModel = new VideoModel(pick(row, videoKeys))
  1457. videoModel.VideoChannel = channelModel
  1458. videoModel.UserVideoHistories = []
  1459. videoModel.Thumbnails = []
  1460. videoModel.VideoFiles = []
  1461. videoModel.VideoStreamingPlaylists = []
  1462. videosMemo[row.id] = videoModel
  1463. // Don't take object value to have a sorted array
  1464. videos.push(videoModel)
  1465. }
  1466. const videoModel = videosMemo[row.id]
  1467. if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) {
  1468. const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]))
  1469. videoModel.UserVideoHistories.push(historyModel)
  1470. historyDone.add(row.userVideoHistory.id)
  1471. }
  1472. if (row.Thumbnails?.id && !thumbnailsDone.has(row.Thumbnails.id)) {
  1473. const thumbnailModel = new ThumbnailModel(pick(row.Thumbnails, [ 'id', 'type', 'filename' ]))
  1474. videoModel.Thumbnails.push(thumbnailModel)
  1475. thumbnailsDone.add(row.Thumbnails.id)
  1476. }
  1477. if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) {
  1478. const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys))
  1479. videoModel.VideoFiles.push(videoFileModel)
  1480. videoFilesDone.add(row.VideoFiles.id)
  1481. }
  1482. if (row.VideoStreamingPlaylists?.id && !videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]) {
  1483. const streamingPlaylist = new VideoStreamingPlaylistModel(pick(row.VideoStreamingPlaylists, videoStreamingPlaylistKeys))
  1484. streamingPlaylist.VideoFiles = []
  1485. videoModel.VideoStreamingPlaylists.push(streamingPlaylist)
  1486. videoStreamingPlaylistMemo[streamingPlaylist.id] = streamingPlaylist
  1487. }
  1488. if (row.VideoStreamingPlaylists?.VideoFiles?.id && !videoFilesDone.has(row.VideoStreamingPlaylists.VideoFiles.id)) {
  1489. const streamingPlaylist = videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]
  1490. const videoFileModel = new VideoFileModel(pick(row.VideoStreamingPlaylists.VideoFiles, videoFileKeys))
  1491. streamingPlaylist.VideoFiles.push(videoFileModel)
  1492. videoFilesDone.add(row.VideoStreamingPlaylists.VideoFiles.id)
  1493. }
  1494. }
  1495. return videos
  1496. }
  1497. static getCategoryLabel (id: number) {
  1498. return VIDEO_CATEGORIES[id] || 'Misc'
  1499. }
  1500. static getLicenceLabel (id: number) {
  1501. return VIDEO_LICENCES[id] || 'Unknown'
  1502. }
  1503. static getLanguageLabel (id: string) {
  1504. return VIDEO_LANGUAGES[id] || 'Unknown'
  1505. }
  1506. static getPrivacyLabel (id: number) {
  1507. return VIDEO_PRIVACIES[id] || 'Unknown'
  1508. }
  1509. static getStateLabel (id: number) {
  1510. return VIDEO_STATES[id] || 'Unknown'
  1511. }
  1512. isBlacklisted () {
  1513. return !!this.VideoBlacklist
  1514. }
  1515. isBlocked () {
  1516. return this.VideoChannel.Account.Actor.Server?.isBlocked() || this.VideoChannel.Account.isBlocked()
  1517. }
  1518. getQualityFileBy<T extends MVideoWithFile> (this: T, fun: (files: MVideoFile[], it: (file: MVideoFile) => number) => MVideoFile) {
  1519. if (Array.isArray(this.VideoFiles) && this.VideoFiles.length !== 0) {
  1520. const file = fun(this.VideoFiles, file => file.resolution)
  1521. return Object.assign(file, { Video: this })
  1522. }
  1523. // No webtorrent files, try with streaming playlist files
  1524. if (Array.isArray(this.VideoStreamingPlaylists) && this.VideoStreamingPlaylists.length !== 0) {
  1525. const streamingPlaylistWithVideo = Object.assign(this.VideoStreamingPlaylists[0], { Video: this })
  1526. const file = fun(streamingPlaylistWithVideo.VideoFiles, file => file.resolution)
  1527. return Object.assign(file, { VideoStreamingPlaylist: streamingPlaylistWithVideo })
  1528. }
  1529. return undefined
  1530. }
  1531. getMaxQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
  1532. return this.getQualityFileBy(maxBy)
  1533. }
  1534. getMinQualityFile<T extends MVideoWithFile> (this: T): MVideoFileVideo | MVideoFileStreamingPlaylistVideo {
  1535. return this.getQualityFileBy(minBy)
  1536. }
  1537. getWebTorrentFile<T extends MVideoWithFile> (this: T, resolution: number): MVideoFileVideo {
  1538. if (Array.isArray(this.VideoFiles) === false) return undefined
  1539. const file = this.VideoFiles.find(f => f.resolution === resolution)
  1540. if (!file) return undefined
  1541. return Object.assign(file, { Video: this })
  1542. }
  1543. async addAndSaveThumbnail (thumbnail: MThumbnail, transaction: Transaction) {
  1544. thumbnail.videoId = this.id
  1545. const savedThumbnail = await thumbnail.save({ transaction })
  1546. if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = []
  1547. // Already have this thumbnail, skip
  1548. if (this.Thumbnails.find(t => t.id === savedThumbnail.id)) return
  1549. this.Thumbnails.push(savedThumbnail)
  1550. }
  1551. generateThumbnailName () {
  1552. return this.uuid + '.jpg'
  1553. }
  1554. getMiniature () {
  1555. if (Array.isArray(this.Thumbnails) === false) return undefined
  1556. return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
  1557. }
  1558. generatePreviewName () {
  1559. return this.uuid + '.jpg'
  1560. }
  1561. hasPreview () {
  1562. return !!this.getPreview()
  1563. }
  1564. getPreview () {
  1565. if (Array.isArray(this.Thumbnails) === false) return undefined
  1566. return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
  1567. }
  1568. isOwned () {
  1569. return this.remote === false
  1570. }
  1571. getWatchStaticPath () {
  1572. return '/videos/watch/' + this.uuid
  1573. }
  1574. getEmbedStaticPath () {
  1575. return '/videos/embed/' + this.uuid
  1576. }
  1577. getMiniatureStaticPath () {
  1578. const thumbnail = this.getMiniature()
  1579. if (!thumbnail) return null
  1580. return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename)
  1581. }
  1582. getPreviewStaticPath () {
  1583. const preview = this.getPreview()
  1584. if (!preview) return null
  1585. // We use a local cache, so specify our cache endpoint instead of potential remote URL
  1586. return join(LAZY_STATIC_PATHS.PREVIEWS, preview.filename)
  1587. }
  1588. toFormattedJSON (this: MVideoFormattable, options?: VideoFormattingJSONOptions): Video {
  1589. return videoModelToFormattedJSON(this, options)
  1590. }
  1591. toFormattedDetailsJSON (this: MVideoFormattableDetails): VideoDetails {
  1592. return videoModelToFormattedDetailsJSON(this)
  1593. }
  1594. getFormattedVideoFilesJSON (): VideoFile[] {
  1595. const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
  1596. let files: MVideoFileRedundanciesOpt[] = []
  1597. if (Array.isArray(this.VideoFiles)) {
  1598. files = files.concat(this.VideoFiles)
  1599. }
  1600. for (const p of (this.VideoStreamingPlaylists || [])) {
  1601. files = files.concat(p.VideoFiles || [])
  1602. }
  1603. return videoFilesModelToFormattedJSON(this, baseUrlHttp, baseUrlWs, files)
  1604. }
  1605. toActivityPubObject (this: MVideoAP): VideoObject {
  1606. return videoModelToActivityPubObject(this)
  1607. }
  1608. getTruncatedDescription () {
  1609. if (!this.description) return null
  1610. const maxLength = CONSTRAINTS_FIELDS.VIDEOS.TRUNCATED_DESCRIPTION.max
  1611. return peertubeTruncate(this.description, { length: maxLength })
  1612. }
  1613. getMaxQualityResolution () {
  1614. const file = this.getMaxQualityFile()
  1615. const videoOrPlaylist = file.getVideoOrStreamingPlaylist()
  1616. const originalFilePath = getVideoFilePath(videoOrPlaylist, file)
  1617. return getVideoFileResolution(originalFilePath)
  1618. }
  1619. getDescriptionAPIPath () {
  1620. return `/api/${API_VERSION}/videos/${this.uuid}/description`
  1621. }
  1622. getHLSPlaylist (): MStreamingPlaylistFilesVideo {
  1623. if (!this.VideoStreamingPlaylists) return undefined
  1624. const playlist = this.VideoStreamingPlaylists.find(p => p.type === VideoStreamingPlaylistType.HLS)
  1625. playlist.Video = this
  1626. return playlist
  1627. }
  1628. setHLSPlaylist (playlist: MStreamingPlaylist) {
  1629. const toAdd = [ playlist ] as [ VideoStreamingPlaylistModel ]
  1630. if (Array.isArray(this.VideoStreamingPlaylists) === false || this.VideoStreamingPlaylists.length === 0) {
  1631. this.VideoStreamingPlaylists = toAdd
  1632. return
  1633. }
  1634. this.VideoStreamingPlaylists = this.VideoStreamingPlaylists
  1635. .filter(s => s.type !== VideoStreamingPlaylistType.HLS)
  1636. .concat(toAdd)
  1637. }
  1638. removeFile (videoFile: MVideoFile, isRedundancy = false) {
  1639. const filePath = getVideoFilePath(this, videoFile, isRedundancy)
  1640. return remove(filePath)
  1641. .catch(err => logger.warn('Cannot delete file %s.', filePath, { err }))
  1642. }
  1643. removeTorrent (videoFile: MVideoFile) {
  1644. const torrentPath = getTorrentFilePath(this, videoFile)
  1645. return remove(torrentPath)
  1646. .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
  1647. }
  1648. async removeStreamingPlaylistFiles (streamingPlaylist: MStreamingPlaylist, isRedundancy = false) {
  1649. const directoryPath = getHLSDirectory(this, isRedundancy)
  1650. await remove(directoryPath)
  1651. if (isRedundancy !== true) {
  1652. const streamingPlaylistWithFiles = streamingPlaylist as MStreamingPlaylistFilesVideo
  1653. streamingPlaylistWithFiles.Video = this
  1654. if (!Array.isArray(streamingPlaylistWithFiles.VideoFiles)) {
  1655. streamingPlaylistWithFiles.VideoFiles = await streamingPlaylistWithFiles.$get('VideoFiles')
  1656. }
  1657. // Remove physical files and torrents
  1658. await Promise.all(
  1659. streamingPlaylistWithFiles.VideoFiles.map(file => streamingPlaylistWithFiles.removeTorrent(file))
  1660. )
  1661. }
  1662. }
  1663. isOutdated () {
  1664. if (this.isOwned()) return false
  1665. return isOutdated(this, ACTIVITY_PUB.VIDEO_REFRESH_INTERVAL)
  1666. }
  1667. hasPrivacyForFederation () {
  1668. return isPrivacyForFederation(this.privacy)
  1669. }
  1670. hasStateForFederation () {
  1671. return isStateForFederation(this.state)
  1672. }
  1673. isNewVideo (newPrivacy: VideoPrivacy) {
  1674. return this.hasPrivacyForFederation() === false && isPrivacyForFederation(newPrivacy) === true
  1675. }
  1676. setAsRefreshed () {
  1677. this.changed('updatedAt', true)
  1678. return this.save()
  1679. }
  1680. requiresAuth () {
  1681. return this.privacy === VideoPrivacy.PRIVATE || this.privacy === VideoPrivacy.INTERNAL || !!this.VideoBlacklist
  1682. }
  1683. setPrivacy (newPrivacy: VideoPrivacy) {
  1684. if (this.privacy === VideoPrivacy.PRIVATE && newPrivacy !== VideoPrivacy.PRIVATE) {
  1685. this.publishedAt = new Date()
  1686. }
  1687. this.privacy = newPrivacy
  1688. }
  1689. isConfidential () {
  1690. return this.privacy === VideoPrivacy.PRIVATE ||
  1691. this.privacy === VideoPrivacy.UNLISTED ||
  1692. this.privacy === VideoPrivacy.INTERNAL
  1693. }
  1694. async publishIfNeededAndSave (t: Transaction) {
  1695. if (this.state !== VideoState.PUBLISHED) {
  1696. this.state = VideoState.PUBLISHED
  1697. this.publishedAt = new Date()
  1698. await this.save({ transaction: t })
  1699. return true
  1700. }
  1701. return false
  1702. }
  1703. getBaseUrls () {
  1704. if (this.isOwned()) {
  1705. return {
  1706. baseUrlHttp: WEBSERVER.URL,
  1707. baseUrlWs: WEBSERVER.WS + '://' + WEBSERVER.HOSTNAME + ':' + WEBSERVER.PORT
  1708. }
  1709. }
  1710. return {
  1711. baseUrlHttp: REMOTE_SCHEME.HTTP + '://' + this.VideoChannel.Account.Actor.Server.host,
  1712. baseUrlWs: REMOTE_SCHEME.WS + '://' + this.VideoChannel.Account.Actor.Server.host
  1713. }
  1714. }
  1715. getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
  1716. return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
  1717. }
  1718. getTorrentUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  1719. return baseUrlHttp + STATIC_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
  1720. }
  1721. getTorrentDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  1722. return baseUrlHttp + STATIC_DOWNLOAD_PATHS.TORRENTS + getTorrentFileName(this, videoFile)
  1723. }
  1724. getVideoFileUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  1725. return baseUrlHttp + STATIC_PATHS.WEBSEED + getVideoFilename(this, videoFile)
  1726. }
  1727. getVideoFileMetadataUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  1728. const path = '/api/v1/videos/'
  1729. return this.isOwned()
  1730. ? baseUrlHttp + path + this.uuid + '/metadata/' + videoFile.id
  1731. : videoFile.metadataUrl
  1732. }
  1733. getVideoRedundancyUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  1734. return baseUrlHttp + STATIC_PATHS.REDUNDANCY + getVideoFilename(this, videoFile)
  1735. }
  1736. getVideoFileDownloadUrl (videoFile: MVideoFile, baseUrlHttp: string) {
  1737. return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + getVideoFilename(this, videoFile)
  1738. }
  1739. getBandwidthBits (videoFile: MVideoFile) {
  1740. return Math.ceil((videoFile.size * 8) / this.duration)
  1741. }
  1742. }