video.ts 53 KB

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