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.ts155
1 files changed, 90 insertions, 65 deletions
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 38447797e..9840d17fd 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -107,6 +107,8 @@ import { VideoImportModel } from './video-import'
107import { VideoStreamingPlaylistModel } from './video-streaming-playlist' 107import { VideoStreamingPlaylistModel } from './video-streaming-playlist'
108import { VideoPlaylistElementModel } from './video-playlist-element' 108import { VideoPlaylistElementModel } from './video-playlist-element'
109import { CONFIG } from '../../initializers/config' 109import { CONFIG } from '../../initializers/config'
110import { ThumbnailModel } from './thumbnail'
111import { ThumbnailType } from '../../../shared/models/videos/thumbnail.type'
110 112
111// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 113// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
112const indexes: Sequelize.DefineIndexesOptions[] = [ 114const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -181,7 +183,8 @@ export enum ScopeNames {
181 WITH_BLACKLISTED = 'WITH_BLACKLISTED', 183 WITH_BLACKLISTED = 'WITH_BLACKLISTED',
182 WITH_USER_HISTORY = 'WITH_USER_HISTORY', 184 WITH_USER_HISTORY = 'WITH_USER_HISTORY',
183 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS', 185 WITH_STREAMING_PLAYLISTS = 'WITH_STREAMING_PLAYLISTS',
184 WITH_USER_ID = 'WITH_USER_ID' 186 WITH_USER_ID = 'WITH_USER_ID',
187 WITH_THUMBNAILS = 'WITH_THUMBNAILS'
185} 188}
186 189
187type ForAPIOptions = { 190type ForAPIOptions = {
@@ -473,6 +476,14 @@ type AvailableForListIDsOptions = {
473 476
474 return query 477 return query
475 }, 478 },
479 [ ScopeNames.WITH_THUMBNAILS ]: {
480 include: [
481 {
482 model: () => ThumbnailModel,
483 required: false
484 }
485 ]
486 },
476 [ ScopeNames.WITH_USER_ID ]: { 487 [ ScopeNames.WITH_USER_ID ]: {
477 include: [ 488 include: [
478 { 489 {
@@ -771,6 +782,16 @@ export class VideoModel extends Model<VideoModel> {
771 }) 782 })
772 Tags: TagModel[] 783 Tags: TagModel[]
773 784
785 @HasMany(() => ThumbnailModel, {
786 foreignKey: {
787 name: 'videoId',
788 allowNull: true
789 },
790 hooks: true,
791 onDelete: 'cascade'
792 })
793 Thumbnails: ThumbnailModel[]
794
774 @HasMany(() => VideoPlaylistElementModel, { 795 @HasMany(() => VideoPlaylistElementModel, {
775 foreignKey: { 796 foreignKey: {
776 name: 'videoId', 797 name: 'videoId',
@@ -920,15 +941,11 @@ export class VideoModel extends Model<VideoModel> {
920 941
921 logger.info('Removing files of video %s.', instance.url) 942 logger.info('Removing files of video %s.', instance.url)
922 943
923 tasks.push(instance.removeThumbnail())
924
925 if (instance.isOwned()) { 944 if (instance.isOwned()) {
926 if (!Array.isArray(instance.VideoFiles)) { 945 if (!Array.isArray(instance.VideoFiles)) {
927 instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[] 946 instance.VideoFiles = await instance.$get('VideoFiles') as VideoFileModel[]
928 } 947 }
929 948
930 tasks.push(instance.removePreview())
931
932 // Remove physical files and torrents 949 // Remove physical files and torrents
933 instance.VideoFiles.forEach(file => { 950 instance.VideoFiles.forEach(file => {
934 tasks.push(instance.removeFile(file)) 951 tasks.push(instance.removeFile(file))
@@ -955,7 +972,11 @@ export class VideoModel extends Model<VideoModel> {
955 } 972 }
956 } 973 }
957 974
958 return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]).findAll(query) 975 return VideoModel.scope([
976 ScopeNames.WITH_FILES,
977 ScopeNames.WITH_STREAMING_PLAYLISTS,
978 ScopeNames.WITH_THUMBNAILS
979 ]).findAll(query)
959 } 980 }
960 981
961 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) { 982 static listAllAndSharedByActorForOutbox (actorId: number, start: number, count: number) {
@@ -1048,7 +1069,7 @@ export class VideoModel extends Model<VideoModel> {
1048 1069
1049 return Bluebird.all([ 1070 return Bluebird.all([
1050 // FIXME: typing issue 1071 // FIXME: typing issue
1051 VideoModel.findAll(query as any), 1072 VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findAll(query as any),
1052 VideoModel.sequelize.query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT }) 1073 VideoModel.sequelize.query(rawCountQuery, { type: Sequelize.QueryTypes.SELECT })
1053 ]).then(([ rows, totals ]) => { 1074 ]).then(([ rows, totals ]) => {
1054 // totals: totalVideos + totalVideoShares 1075 // totals: totalVideos + totalVideoShares
@@ -1102,12 +1123,14 @@ export class VideoModel extends Model<VideoModel> {
1102 }) 1123 })
1103 } 1124 }
1104 1125
1105 return VideoModel.findAndCountAll(query).then(({ rows, count }) => { 1126 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS)
1106 return { 1127 .findAndCountAll(query)
1107 data: rows, 1128 .then(({ rows, count }) => {
1108 total: count 1129 return {
1109 } 1130 data: rows,
1110 }) 1131 total: count
1132 }
1133 })
1111 } 1134 }
1112 1135
1113 static async listForApi (options: { 1136 static async listForApi (options: {
@@ -1296,7 +1319,7 @@ export class VideoModel extends Model<VideoModel> {
1296 transaction: t 1319 transaction: t
1297 } 1320 }
1298 1321
1299 return VideoModel.findOne(options) 1322 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
1300 } 1323 }
1301 1324
1302 static loadWithRights (id: number | string, t?: Sequelize.Transaction) { 1325 static loadWithRights (id: number | string, t?: Sequelize.Transaction) {
@@ -1306,7 +1329,11 @@ export class VideoModel extends Model<VideoModel> {
1306 transaction: t 1329 transaction: t
1307 } 1330 }
1308 1331
1309 return VideoModel.scope([ ScopeNames.WITH_BLACKLISTED, ScopeNames.WITH_USER_ID ]).findOne(options) 1332 return VideoModel.scope([
1333 ScopeNames.WITH_BLACKLISTED,
1334 ScopeNames.WITH_USER_ID,
1335 ScopeNames.WITH_THUMBNAILS
1336 ]).findOne(options)
1310 } 1337 }
1311 1338
1312 static loadOnlyId (id: number | string, t?: Sequelize.Transaction) { 1339 static loadOnlyId (id: number | string, t?: Sequelize.Transaction) {
@@ -1318,12 +1345,15 @@ export class VideoModel extends Model<VideoModel> {
1318 transaction: t 1345 transaction: t
1319 } 1346 }
1320 1347
1321 return VideoModel.findOne(options) 1348 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
1322 } 1349 }
1323 1350
1324 static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) { 1351 static loadWithFiles (id: number, t?: Sequelize.Transaction, logging?: boolean) {
1325 return VideoModel.scope([ ScopeNames.WITH_FILES, ScopeNames.WITH_STREAMING_PLAYLISTS ]) 1352 return VideoModel.scope([
1326 .findByPk(id, { transaction: t, logging }) 1353 ScopeNames.WITH_FILES,
1354 ScopeNames.WITH_STREAMING_PLAYLISTS,
1355 ScopeNames.WITH_THUMBNAILS
1356 ]).findByPk(id, { transaction: t, logging })
1327 } 1357 }
1328 1358
1329 static loadByUUIDWithFile (uuid: string) { 1359 static loadByUUIDWithFile (uuid: string) {
@@ -1333,7 +1363,7 @@ export class VideoModel extends Model<VideoModel> {
1333 } 1363 }
1334 } 1364 }
1335 1365
1336 return VideoModel.findOne(options) 1366 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(options)
1337 } 1367 }
1338 1368
1339 static loadByUrl (url: string, transaction?: Sequelize.Transaction) { 1369 static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
@@ -1344,7 +1374,7 @@ export class VideoModel extends Model<VideoModel> {
1344 transaction 1374 transaction
1345 } 1375 }
1346 1376
1347 return VideoModel.findOne(query) 1377 return VideoModel.scope(ScopeNames.WITH_THUMBNAILS).findOne(query)
1348 } 1378 }
1349 1379
1350 static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) { 1380 static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) {
@@ -1358,7 +1388,8 @@ export class VideoModel extends Model<VideoModel> {
1358 return VideoModel.scope([ 1388 return VideoModel.scope([
1359 ScopeNames.WITH_ACCOUNT_DETAILS, 1389 ScopeNames.WITH_ACCOUNT_DETAILS,
1360 ScopeNames.WITH_FILES, 1390 ScopeNames.WITH_FILES,
1361 ScopeNames.WITH_STREAMING_PLAYLISTS 1391 ScopeNames.WITH_STREAMING_PLAYLISTS,
1392 ScopeNames.WITH_THUMBNAILS
1362 ]).findOne(query) 1393 ]).findOne(query)
1363 } 1394 }
1364 1395
@@ -1377,7 +1408,8 @@ export class VideoModel extends Model<VideoModel> {
1377 ScopeNames.WITH_ACCOUNT_DETAILS, 1408 ScopeNames.WITH_ACCOUNT_DETAILS,
1378 ScopeNames.WITH_SCHEDULED_UPDATE, 1409 ScopeNames.WITH_SCHEDULED_UPDATE,
1379 ScopeNames.WITH_FILES, 1410 ScopeNames.WITH_FILES,
1380 ScopeNames.WITH_STREAMING_PLAYLISTS 1411 ScopeNames.WITH_STREAMING_PLAYLISTS,
1412 ScopeNames.WITH_THUMBNAILS
1381 ] 1413 ]
1382 1414
1383 if (userId) { 1415 if (userId) {
@@ -1403,6 +1435,7 @@ export class VideoModel extends Model<VideoModel> {
1403 ScopeNames.WITH_BLACKLISTED, 1435 ScopeNames.WITH_BLACKLISTED,
1404 ScopeNames.WITH_ACCOUNT_DETAILS, 1436 ScopeNames.WITH_ACCOUNT_DETAILS,
1405 ScopeNames.WITH_SCHEDULED_UPDATE, 1437 ScopeNames.WITH_SCHEDULED_UPDATE,
1438 ScopeNames.WITH_THUMBNAILS,
1406 { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings 1439 { method: [ ScopeNames.WITH_FILES, true ] } as any, // FIXME: typings
1407 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings 1440 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] } as any // FIXME: typings
1408 ] 1441 ]
@@ -1555,7 +1588,7 @@ export class VideoModel extends Model<VideoModel> {
1555 } 1588 }
1556 1589
1557 // FIXME: typing 1590 // FIXME: typing
1558 const apiScope: any[] = [] 1591 const apiScope: any[] = [ ScopeNames.WITH_THUMBNAILS ]
1559 1592
1560 if (options.user) { 1593 if (options.user) {
1561 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) 1594 apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] })
@@ -1611,18 +1644,37 @@ export class VideoModel extends Model<VideoModel> {
1611 return maxBy(this.VideoFiles, file => file.resolution) 1644 return maxBy(this.VideoFiles, file => file.resolution)
1612 } 1645 }
1613 1646
1647 addThumbnail (thumbnail: ThumbnailModel) {
1648 if (Array.isArray(this.Thumbnails) === false) this.Thumbnails = []
1649
1650 // Already have this thumbnail, skip
1651 if (this.Thumbnails.find(t => t.id === thumbnail.id)) return
1652
1653 this.Thumbnails.push(thumbnail)
1654 }
1655
1614 getVideoFilename (videoFile: VideoFileModel) { 1656 getVideoFilename (videoFile: VideoFileModel) {
1615 return this.uuid + '-' + videoFile.resolution + videoFile.extname 1657 return this.uuid + '-' + videoFile.resolution + videoFile.extname
1616 } 1658 }
1617 1659
1618 getThumbnailName () { 1660 generateThumbnailName () {
1619 const extension = '.jpg' 1661 return this.uuid + '.jpg'
1620 return this.uuid + extension
1621 } 1662 }
1622 1663
1623 getPreviewName () { 1664 getThumbnail () {
1624 const extension = '.jpg' 1665 if (Array.isArray(this.Thumbnails) === false) return undefined
1625 return this.uuid + extension 1666
1667 return this.Thumbnails.find(t => t.type === ThumbnailType.THUMBNAIL)
1668 }
1669
1670 generatePreviewName () {
1671 return this.uuid + '.jpg'
1672 }
1673
1674 getPreview () {
1675 if (Array.isArray(this.Thumbnails) === false) return undefined
1676
1677 return this.Thumbnails.find(t => t.type === ThumbnailType.PREVIEW)
1626 } 1678 }
1627 1679
1628 getTorrentFileName (videoFile: VideoFileModel) { 1680 getTorrentFileName (videoFile: VideoFileModel) {
@@ -1634,24 +1686,6 @@ export class VideoModel extends Model<VideoModel> {
1634 return this.remote === false 1686 return this.remote === false
1635 } 1687 }
1636 1688
1637 createPreview (videoFile: VideoFileModel) {
1638 return generateImageFromVideoFile(
1639 this.getVideoFilePath(videoFile),
1640 CONFIG.STORAGE.PREVIEWS_DIR,
1641 this.getPreviewName(),
1642 PREVIEWS_SIZE
1643 )
1644 }
1645
1646 createThumbnail (videoFile: VideoFileModel) {
1647 return generateImageFromVideoFile(
1648 this.getVideoFilePath(videoFile),
1649 CONFIG.STORAGE.THUMBNAILS_DIR,
1650 this.getThumbnailName(),
1651 THUMBNAILS_SIZE
1652 )
1653 }
1654
1655 getTorrentFilePath (videoFile: VideoFileModel) { 1689 getTorrentFilePath (videoFile: VideoFileModel) {
1656 return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile)) 1690 return join(CONFIG.STORAGE.TORRENTS_DIR, this.getTorrentFileName(videoFile))
1657 } 1691 }
@@ -1692,11 +1726,18 @@ export class VideoModel extends Model<VideoModel> {
1692 } 1726 }
1693 1727
1694 getThumbnailStaticPath () { 1728 getThumbnailStaticPath () {
1695 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()) 1729 const thumbnail = this.getThumbnail()
1730 if (!thumbnail) return null
1731
1732 return join(STATIC_PATHS.THUMBNAILS, thumbnail.filename)
1696 } 1733 }
1697 1734
1698 getPreviewStaticPath () { 1735 getPreviewStaticPath () {
1699 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) 1736 const preview = this.getPreview()
1737 if (!preview) return null
1738
1739 // We use a local cache, so specify our cache endpoint instead of potential remote URL
1740 return join(STATIC_PATHS.PREVIEWS, preview.filename)
1700 } 1741 }
1701 1742
1702 toFormattedJSON (options?: VideoFormattingJSONOptions): Video { 1743 toFormattedJSON (options?: VideoFormattingJSONOptions): Video {
@@ -1732,18 +1773,6 @@ export class VideoModel extends Model<VideoModel> {
1732 return `/api/${API_VERSION}/videos/${this.uuid}/description` 1773 return `/api/${API_VERSION}/videos/${this.uuid}/description`
1733 } 1774 }
1734 1775
1735 removeThumbnail () {
1736 const thumbnailPath = join(CONFIG.STORAGE.THUMBNAILS_DIR, this.getThumbnailName())
1737 return remove(thumbnailPath)
1738 .catch(err => logger.warn('Cannot delete thumbnail %s.', thumbnailPath, { err }))
1739 }
1740
1741 removePreview () {
1742 const previewPath = join(CONFIG.STORAGE.PREVIEWS_DIR + this.getPreviewName())
1743 return remove(previewPath)
1744 .catch(err => logger.warn('Cannot delete preview %s.', previewPath, { err }))
1745 }
1746
1747 removeFile (videoFile: VideoFileModel, isRedundancy = false) { 1776 removeFile (videoFile: VideoFileModel, isRedundancy = false) {
1748 const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR 1777 const baseDir = isRedundancy ? CONFIG.STORAGE.REDUNDANCY_DIR : CONFIG.STORAGE.VIDEOS_DIR
1749 1778
@@ -1816,10 +1845,6 @@ export class VideoModel extends Model<VideoModel> {
1816 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ] 1845 return [ baseUrlWs + '/tracker/socket', baseUrlHttp + '/tracker/announce' ]
1817 } 1846 }
1818 1847
1819 getThumbnailUrl (baseUrlHttp: string) {
1820 return baseUrlHttp + STATIC_PATHS.THUMBNAILS + this.getThumbnailName()
1821 }
1822
1823 getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) { 1848 getTorrentUrl (videoFile: VideoFileModel, baseUrlHttp: string) {
1824 return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile) 1849 return baseUrlHttp + STATIC_PATHS.TORRENTS + this.getTorrentFileName(videoFile)
1825 } 1850 }