aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video/video.ts
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video/video.ts')
-rw-r--r--server/models/video/video.ts183
1 files changed, 152 insertions, 31 deletions
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 806b6e046..73626b6a0 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -52,7 +52,7 @@ import {
52 ACTIVITY_PUB, 52 ACTIVITY_PUB,
53 API_VERSION, 53 API_VERSION,
54 CONFIG, 54 CONFIG,
55 CONSTRAINTS_FIELDS, 55 CONSTRAINTS_FIELDS, HLS_PLAYLIST_DIRECTORY, HLS_REDUNDANCY_DIRECTORY,
56 PREVIEWS_SIZE, 56 PREVIEWS_SIZE,
57 REMOTE_SCHEME, 57 REMOTE_SCHEME,
58 STATIC_DOWNLOAD_PATHS, 58 STATIC_DOWNLOAD_PATHS,
@@ -95,6 +95,7 @@ import * as validator from 'validator'
95import { UserVideoHistoryModel } from '../account/user-video-history' 95import { UserVideoHistoryModel } from '../account/user-video-history'
96import { UserModel } from '../account/user' 96import { UserModel } from '../account/user'
97import { VideoImportModel } from './video-import' 97import { VideoImportModel } from './video-import'
98import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
98 99
99// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 100// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
100const indexes: Sequelize.DefineIndexesOptions[] = [ 101const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -160,7 +161,9 @@ export enum ScopeNames {
160 WITH_FILES = 'WITH_FILES', 161 WITH_FILES = 'WITH_FILES',
161 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE', 162 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
162 WITH_BLACKLISTED = 'WITH_BLACKLISTED', 163 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
163 WITH_USER_HISTORY = 'WITH_USER_HISTORY' 164 WITH_USER_HISTORY = 'WITH_USER_HISTORY',
165 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
166 WITH_USER_ID = 'WITH_USER_ID'
164} 167}
165 168
166type ForAPIOptions = { 169type ForAPIOptions = {
@@ -464,6 +467,22 @@ type AvailableForListIDsOptions = {
464 467
465 return query 468 return query
466 }, 469 },
470 [ ScopeNames.WITH_USER_ID ]: {
471 include: [
472 {
473 attributes: [ 'accountId' ],
474 model: () => VideoChannelModel.unscoped(),
475 required: true,
476 include: [
477 {
478 attributes: [ 'userId' ],
479 model: () => AccountModel.unscoped(),
480 required: true
481 }
482 ]
483 }
484 ]
485 },
467 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: { 486 [ ScopeNames.WITH_ACCOUNT_DETAILS ]: {
468 include: [ 487 include: [
469 { 488 {
@@ -528,22 +547,55 @@ type AvailableForListIDsOptions = {
528 } 547 }
529 ] 548 ]
530 }, 549 },
531 [ ScopeNames.WITH_FILES ]: { 550 [ ScopeNames.WITH_FILES ]: (withRedundancies = false) => {
532 include: [ 551 let subInclude: any[] = []
533 { 552
534 model: () => VideoFileModel.unscoped(), 553 if (withRedundancies === true) {
535 // FIXME: typings 554 subInclude = [
536 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join 555 {
537 required: false, 556 attributes: [ 'fileUrl' ],
538 include: [ 557 model: VideoRedundancyModel.unscoped(),
539 { 558 required: false
540 attributes: [ 'fileUrl' ], 559 }
541 model: () => VideoRedundancyModel.unscoped(), 560 ]
542 required: false 561 }
543 } 562
544 ] 563 return {
545 } 564 include: [
546 ] 565 {
566 model: VideoFileModel.unscoped(),
567 // FIXME: typings
568 [ 'separate' as any ]: true, // We may have multiple files, having multiple redundancies so let's separate this join
569 required: false,
570 include: subInclude
571 }
572 ]
573 }
574 },
575 [ ScopeNames.WITH_STREAMING_PLAYLISTS ]: (withRedundancies = false) => {
576 let subInclude: any[] = []
577
578 if (withRedundancies === true) {
579 subInclude = [
580 {
581 attributes: [ 'fileUrl' ],
582 model: VideoRedundancyModel.unscoped(),
583 required: false
584 }
585 ]
586 }
587
588 return {
589 include: [
590 {
591 model: VideoStreamingPlaylistModel.unscoped(),
592 // FIXME: typings
593 [ 'separate' as any ]: true, // We may have multiple streaming playlists, having multiple redundancies so let's separate this join
594 required: false,
595 include: subInclude
596 }
597 ]
598 }
547 }, 599 },
548 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: { 600 [ ScopeNames.WITH_SCHEDULED_UPDATE ]: {
549 include: [ 601 include: [
@@ -666,6 +718,10 @@ export class VideoModel extends Model<VideoModel> {
666 718
667 @AllowNull(false) 719 @AllowNull(false)
668 @Column 720 @Column
721 downloadEnabled: boolean
722
723 @AllowNull(false)
724 @Column
669 waitTranscoding: boolean 725 waitTranscoding: boolean
670 726
671 @AllowNull(false) 727 @AllowNull(false)
@@ -726,6 +782,16 @@ export class VideoModel extends Model<VideoModel> {
726 }) 782 })
727 VideoFiles: VideoFileModel[] 783 VideoFiles: VideoFileModel[]
728 784
785 @HasMany(() => VideoStreamingPlaylistModel, {
786 foreignKey: {
787 name: 'videoId',
788 allowNull: false
789 },
790 hooks: true,
791 onDelete: 'cascade'
792 })
793 VideoStreamingPlaylists: VideoStreamingPlaylistModel[]
794
729 @HasMany(() => VideoShareModel, { 795 @HasMany(() => VideoShareModel, {
730 foreignKey: { 796 foreignKey: {
731 name: 'videoId', 797 name: 'videoId',
@@ -851,6 +917,9 @@ export class VideoModel extends Model<VideoModel> {
851 tasks.push(instance.removeFile(file)) 917 tasks.push(instance.removeFile(file))
852 tasks.push(instance.removeTorrent(file)) 918 tasks.push(instance.removeTorrent(file))
853 }) 919 })
920
921 // Remove playlists file
922 tasks.push(instance.removeStreamingPlaylist())
854 } 923 }
855 924
856 // Do not wait video deletion because we could be in a transaction 925 // Do not wait video deletion because we could be in a transaction
@@ -862,10 +931,6 @@ export class VideoModel extends Model<VideoModel> {
862 return undefined 931 return undefined
863 } 932 }
864 933
865 static list () {
866 return VideoModel.scope(ScopeNames.WITH_FILES).findAll()
867 }
868
869 static listLocal () { 934 static listLocal () {
870 const query = { 935 const query = {
871 where: { 936 where: {
@@ -873,7 +938,7 @@ export class VideoModel extends Model<VideoModel> {
873 } 938 }
874 } 939 }
875 940
876 return VideoModel.scope(ScopeNames.WITH_FILES).findAll(query) 941 return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]).findAll(query)
877 } 942 }
878 943
879 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { 944 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
@@ -1204,6 +1269,16 @@ export class VideoModel extends Model<VideoModel> {
1204 return VideoModel.findOne(options) 1269 return VideoModel.findOne(options)
1205 } 1270 }
1206 1271
1272 static loadWithRights (id: number | string, t?: Sequelize.Transaction) {
1273 const where = VideoModel.buildWhereIdOrUUID(id)
1274 const options = {
1275 where,
1276 transaction: t
1277 }
1278
1279 return VideoModel.scope([ ScopeNames.WITH_BLACKLISTED, ScopeNames.WITH_USER_ID ]).findOne(options)
1280 }
1281
1207 static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { 1282 static loadOnlyId (id: number | string, t?: Sequelize.Transaction) {
1208 const where = VideoModel.buildWhereIdOrUUID(id) 1283 const where = VideoModel.buildWhereIdOrUUID(id)
1209 1284
@@ -1216,8 +1291,8 @@ export class VideoModel extends Model<VideoModel> {
1216 return VideoModel.findOne(options) 1291 return VideoModel.findOne(options)
1217 } 1292 }
1218 1293
1219 static loadWithFile (id: number, t?: Sequelize.Transaction, logging?: boolean) { 1294 static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) {
1220 return VideoModel.scope(ScopeNames.WITH_FILES) 1295 return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ])
1221 .findById(id, { transaction: t, logging }) 1296 .findById(id, { transaction: t, logging })
1222 } 1297 }
1223 1298
@@ -1228,9 +1303,7 @@ export class VideoModel extends Model<VideoModel> {
1228 } 1303 }
1229 } 1304 }
1230 1305
1231 return VideoModel 1306 return VideoModel.findOne(options)
1232 .scope([ ScopeNames.WITH_FILES ])
1233 .findOne(options)
1234 } 1307 }
1235 1308
1236 static loadByUrl (url: string, transaction?: Sequelize.Transaction) { 1309 static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
@@ -1252,7 +1325,11 @@ export class VideoModel extends Model<VideoModel> {
1252 transaction 1325 transaction
1253 } 1326 }
1254 1327
1255 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query) 1328 return VideoModel.scope([
1329 ScopeNames.WITH_ACCOUNT_DETAILS,
1330 ScopeNames.WITH_FILES,
1331 ScopeNames.WITH_STREAMING_PLAYLISTS
1332 ]).findOne(query)
1256 } 1333 }
1257 1334
1258 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) { 1335 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction, userId?: number) {
@@ -1267,9 +1344,37 @@ export class VideoModel extends Model<VideoModel> {
1267 const scopes = [ 1344 const scopes = [
1268 ScopeNames.WITH_TAGS, 1345 ScopeNames.WITH_TAGS,
1269 ScopeNames.WITH_BLACKLISTED, 1346 ScopeNames.WITH_BLACKLISTED,
1347 ScopeNames.WITH_ACCOUNT_DETAILS,
1348 ScopeNames.WITH_SCHEDULED_UPDATE,
1270 ScopeNames.WITH_FILES, 1349 ScopeNames.WITH_FILES,
1350 ScopeNames.WITH_STREAMING_PLAYLISTS
1351 ]
1352
1353 if (userId) {
1354 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] } as any) // FIXME: typings
1355 }
1356
1357 return VideoModel
1358 .scope(scopes)
1359 .findOne(options)
1360 }
1361
1362 static loadForGetAPI (id: number | string, t?: Sequelize.Transaction, userId?: number) {
1363 const where = VideoModel.buildWhereIdOrUUID(id)
1364
1365 const options = {
1366 order: [ [ 'Tags', 'name', 'ASC' ] ],
1367 where,
1368 transaction: t
1369 }
1370
1371 const scopes = [
1372 ScopeNames.WITH_TAGS,
1373 ScopeNames.WITH_BLACKLISTED,
1271 ScopeNames.WITH_ACCOUNT_DETAILS, 1374 ScopeNames.WITH_ACCOUNT_DETAILS,
1272 ScopeNames.WITH_SCHEDULED_UPDATE 1375 ScopeNames.WITH_SCHEDULED_UPDATE,
1376 { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings
1377 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings
1273 ] 1378 ]
1274 1379
1275 if (userId) { 1380 if (userId) {
@@ -1616,6 +1721,14 @@ export class VideoModel extends Model<VideoModel> {
1616 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) 1721 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
1617 } 1722 }
1618 1723
1724 removeStreamingPlaylist (isRedundancy = false) {
1725 const baseDir = isRedundancy ? HLS_REDUNDANCY_DIRECTORY : HLS_PLAYLIST_DIRECTORY
1726
1727 const filePath = join(baseDir, this.uuid)
1728 return remove(filePath)
1729 .catch(err => logger.warn('Cannot delete playlist directory %s.', filePath, { err }))
1730 }
1731
1619 isOutdated () { 1732 isOutdated () {
1620 if (this.isOwned()) return false 1733 if (this.isOwned()) return false
1621 1734
@@ -1650,7 +1763,7 @@ export class VideoModel extends Model<VideoModel> {
1650 1763
1651 generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) { 1764 generateMagnetUri (videoFile: VideoFileModel, baseUrlHttp: string, baseUrlWs: string) {
1652 const xs = this.getTorrentUrl(videoFile, baseUrlHttp) 1765 const xs = this.getTorrentUrl(videoFile, baseUrlHttp)
1653 const announce = [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] 1766 const announce = this.getTrackerUrls(baseUrlHttp, baseUrlWs)
1654 let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ] 1767 let urlList = [ this.getVideoFileUrl(videoFile, baseUrlHttp) ]
1655 1768
1656 const redundancies = videoFile.RedundancyVideos 1769 const redundancies = videoFile.RedundancyVideos
@@ -1667,6 +1780,10 @@ export class VideoModel extends Model<VideoModel> {
1667 return magnetUtil.encode(magnetHash) 1780 return magnetUtil.encode(magnetHash)
1668 } 1781 }
1669 1782
1783 getTrackerUrls (baseUrlHttp: string, baseUrlWs: string) {
1784 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1785 }
1786
1670 getThumbnailUrl (baseUrlHttp: string) { 1787 getThumbnailUrl (baseUrlHttp: string) {
1671 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName() 1788 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName()
1672 } 1789 }
@@ -1690,4 +1807,8 @@ export class VideoModel extends Model<VideoModel> {
1690 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 1807 getVideoFileDownloadUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1691 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile) 1808 return baseUrlHttp + STATIC_DOWNLOAD_PATHS.VIDEOS + this.getVideoFilename(videoFile)
1692 } 1809 }
1810
1811 getBandwidthBits (videoFile: VideoFileModel) {
1812 return Math.ceil((videoFile.size * 8) / this.duration)
1813 }
1693} 1814}