diff options
author | Chocobozzz <me@florianbigard.com> | 2019-04-17 10:07:00 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2019-04-24 16:25:52 +0200 |
commit | e8bafea35bc930cb8ac5b2d521a188642a1adffe (patch) | |
tree | 7537f957ed7307b464e3c90b71b813d992acaade /server/models/video/video.ts | |
parent | 94565d52bb2883e09f16d1363170ac9c0dccb7a1 (diff) | |
download | PeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.tar.gz PeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.tar.zst PeerTube-e8bafea35bc930cb8ac5b2d521a188642a1adffe.zip |
Create a dedicated table to track video thumbnails
Diffstat (limited to 'server/models/video/video.ts')
-rw-r--r-- | server/models/video/video.ts | 155 |
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' | |||
107 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' | 107 | import { VideoStreamingPlaylistModel } from './video-streaming-playlist' |
108 | import { VideoPlaylistElementModel } from './video-playlist-element' | 108 | import { VideoPlaylistElementModel } from './video-playlist-element' |
109 | import { CONFIG } from '../../initializers/config' | 109 | import { CONFIG } from '../../initializers/config' |
110 | import { ThumbnailModel } from './thumbnail' | ||
111 | import { 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 |
112 | const indexes: Sequelize.DefineIndexesOptions[] = [ | 114 | const 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 | ||
187 | type ForAPIOptions = { | 190 | type 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 | } |