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.ts555
1 files changed, 104 insertions, 451 deletions
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 27c631dcd..6c89c16bf 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1,8 +1,8 @@
1import * as Bluebird from 'bluebird' 1import * as Bluebird from 'bluebird'
2import { map, maxBy } from 'lodash' 2import { maxBy } from 'lodash'
3import * as magnetUtil from 'magnet-uri' 3import * as magnetUtil from 'magnet-uri'
4import * as parseTorrent from 'parse-torrent' 4import * as parseTorrent from 'parse-torrent'
5import { extname, join } from 'path' 5import { join } from 'path'
6import * as Sequelize from 'sequelize' 6import * as Sequelize from 'sequelize'
7import { 7import {
8 AllowNull, 8 AllowNull,
@@ -27,7 +27,7 @@ import {
27 Table, 27 Table,
28 UpdatedAt 28 UpdatedAt
29} from 'sequelize-typescript' 29} from 'sequelize-typescript'
30import { ActivityUrlObject, VideoPrivacy, VideoResolution, VideoState } from '../../../shared' 30import { VideoPrivacy, VideoState } from '../../../shared'
31import { VideoTorrentObject } from '../../../shared/models/activitypub/objects' 31import { VideoTorrentObject } from '../../../shared/models/activitypub/objects'
32import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos' 32import { Video, VideoDetails, VideoFile } from '../../../shared/models/videos'
33import { VideoFilter } from '../../../shared/models/videos/video-query.type' 33import { VideoFilter } from '../../../shared/models/videos/video-query.type'
@@ -45,7 +45,7 @@ import {
45 isVideoStateValid, 45 isVideoStateValid,
46 isVideoSupportValid 46 isVideoSupportValid
47} from '../../helpers/custom-validators/videos' 47} from '../../helpers/custom-validators/videos'
48import { generateImageFromVideoFile, getVideoFileFPS, getVideoFileResolution, transcode } from '../../helpers/ffmpeg-utils' 48import { generateImageFromVideoFile, getVideoFileResolution } from '../../helpers/ffmpeg-utils'
49import { logger } from '../../helpers/logger' 49import { logger } from '../../helpers/logger'
50import { getServerActor } from '../../helpers/utils' 50import { getServerActor } from '../../helpers/utils'
51import { 51import {
@@ -59,18 +59,11 @@ import {
59 STATIC_PATHS, 59 STATIC_PATHS,
60 THUMBNAILS_SIZE, 60 THUMBNAILS_SIZE,
61 VIDEO_CATEGORIES, 61 VIDEO_CATEGORIES,
62 VIDEO_EXT_MIMETYPE,
63 VIDEO_LANGUAGES, 62 VIDEO_LANGUAGES,
64 VIDEO_LICENCES, 63 VIDEO_LICENCES,
65 VIDEO_PRIVACIES, 64 VIDEO_PRIVACIES,
66 VIDEO_STATES 65 VIDEO_STATES
67} from '../../initializers' 66} from '../../initializers'
68import {
69 getVideoCommentsActivityPubUrl,
70 getVideoDislikesActivityPubUrl,
71 getVideoLikesActivityPubUrl,
72 getVideoSharesActivityPubUrl
73} from '../../lib/activitypub'
74import { sendDeleteVideo } from '../../lib/activitypub/send' 67import { sendDeleteVideo } from '../../lib/activitypub/send'
75import { AccountModel } from '../account/account' 68import { AccountModel } from '../account/account'
76import { AccountVideoRateModel } from '../account/account-video-rate' 69import { AccountVideoRateModel } from '../account/account-video-rate'
@@ -88,9 +81,17 @@ import { VideoTagModel } from './video-tag'
88import { ScheduleVideoUpdateModel } from './schedule-video-update' 81import { ScheduleVideoUpdateModel } from './schedule-video-update'
89import { VideoCaptionModel } from './video-caption' 82import { VideoCaptionModel } from './video-caption'
90import { VideoBlacklistModel } from './video-blacklist' 83import { VideoBlacklistModel } from './video-blacklist'
91import { copy, remove, rename, stat, writeFile } from 'fs-extra' 84import { remove, writeFile } from 'fs-extra'
92import { VideoViewModel } from './video-views' 85import { VideoViewModel } from './video-views'
93import { VideoRedundancyModel } from '../redundancy/video-redundancy' 86import { VideoRedundancyModel } from '../redundancy/video-redundancy'
87import {
88 videoFilesModelToFormattedJSON,
89 VideoFormattingJSONOptions,
90 videoModelToActivityPubObject,
91 videoModelToFormattedDetailsJSON,
92 videoModelToFormattedJSON
93} from './video-format-utils'
94import * as validator from 'validator'
94 95
95// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation 96// FIXME: Define indexes here because there is an issue with TS and Sequelize.literal when called directly in the annotation
96const indexes: Sequelize.DefineIndexesOptions[] = [ 97const indexes: Sequelize.DefineIndexesOptions[] = [
@@ -221,6 +222,7 @@ type AvailableForListIDsOptions = {
221 }, 222 },
222 [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => { 223 [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => {
223 const query: IFindOptions<VideoModel> = { 224 const query: IFindOptions<VideoModel> = {
225 raw: true,
224 attributes: [ 'id' ], 226 attributes: [ 'id' ],
225 where: { 227 where: {
226 id: { 228 id: {
@@ -387,16 +389,7 @@ type AvailableForListIDsOptions = {
387 } 389 }
388 390
389 if (options.trendingDays) { 391 if (options.trendingDays) {
390 query.include.push({ 392 query.include.push(VideoModel.buildTrendingQuery(options.trendingDays))
391 attributes: [],
392 model: VideoViewModel,
393 required: false,
394 where: {
395 startDate: {
396 [ Sequelize.Op.gte ]: new Date(new Date().getTime() - (24 * 3600 * 1000) * options.trendingDays)
397 }
398 }
399 })
400 393
401 query.subQuery = false 394 query.subQuery = false
402 } 395 }
@@ -474,6 +467,7 @@ type AvailableForListIDsOptions = {
474 required: false, 467 required: false,
475 include: [ 468 include: [
476 { 469 {
470 attributes: [ 'fileUrl' ],
477 model: () => VideoRedundancyModel.unscoped(), 471 model: () => VideoRedundancyModel.unscoped(),
478 required: false 472 required: false
479 } 473 }
@@ -937,7 +931,7 @@ export class VideoModel extends Model<VideoModel> {
937 videoChannelId?: number, 931 videoChannelId?: number,
938 actorId?: number 932 actorId?: number
939 trendingDays?: number 933 trendingDays?: number
940 }) { 934 }, countVideos = true) {
941 const query: IFindOptions<VideoModel> = { 935 const query: IFindOptions<VideoModel> = {
942 offset: options.start, 936 offset: options.start,
943 limit: options.count, 937 limit: options.count,
@@ -970,7 +964,7 @@ export class VideoModel extends Model<VideoModel> {
970 trendingDays 964 trendingDays
971 } 965 }
972 966
973 return VideoModel.getAvailableForApi(query, queryOptions) 967 return VideoModel.getAvailableForApi(query, queryOptions, countVideos)
974 } 968 }
975 969
976 static async searchAndPopulateAccountAndServer (options: { 970 static async searchAndPopulateAccountAndServer (options: {
@@ -1070,41 +1064,34 @@ export class VideoModel extends Model<VideoModel> {
1070 return VideoModel.getAvailableForApi(query, queryOptions) 1064 return VideoModel.getAvailableForApi(query, queryOptions)
1071 } 1065 }
1072 1066
1073 static load (id: number, t?: Sequelize.Transaction) { 1067 static load (id: number | string, t?: Sequelize.Transaction) {
1074 const options = t ? { transaction: t } : undefined 1068 const where = VideoModel.buildWhereIdOrUUID(id)
1075 1069 const options = {
1076 return VideoModel.findById(id, options) 1070 where,
1077 } 1071 transaction: t
1078
1079 static loadByUrlAndPopulateAccount (url: string, t?: Sequelize.Transaction) {
1080 const query: IFindOptions<VideoModel> = {
1081 where: {
1082 url
1083 }
1084 } 1072 }
1085 1073
1086 if (t !== undefined) query.transaction = t 1074 return VideoModel.findOne(options)
1087
1088 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
1089 } 1075 }
1090 1076
1091 static loadAndPopulateAccountAndServerAndTags (id: number) { 1077 static loadOnlyId (id: number | string, t?: Sequelize.Transaction) {
1078 const where = VideoModel.buildWhereIdOrUUID(id)
1079
1092 const options = { 1080 const options = {
1093 order: [ [ 'Tags', 'name', 'ASC' ] ] 1081 attributes: [ 'id' ],
1082 where,
1083 transaction: t
1094 } 1084 }
1095 1085
1096 return VideoModel 1086 return VideoModel.findOne(options)
1097 .scope([
1098 ScopeNames.WITH_TAGS,
1099 ScopeNames.WITH_BLACKLISTED,
1100 ScopeNames.WITH_FILES,
1101 ScopeNames.WITH_ACCOUNT_DETAILS,
1102 ScopeNames.WITH_SCHEDULED_UPDATE
1103 ])
1104 .findById(id, options)
1105 } 1087 }
1106 1088
1107 static loadByUUID (uuid: string) { 1089 static loadWithFile (id: number, t?: Sequelize.Transaction, logging?: boolean) {
1090 return VideoModel.scope(ScopeNames.WITH_FILES)
1091 .findById(id, { transaction: t, logging })
1092 }
1093
1094 static loadByUUIDWithFile (uuid: string) {
1108 const options = { 1095 const options = {
1109 where: { 1096 where: {
1110 uuid 1097 uuid
@@ -1116,12 +1103,34 @@ export class VideoModel extends Model<VideoModel> {
1116 .findOne(options) 1103 .findOne(options)
1117 } 1104 }
1118 1105
1119 static loadByUUIDAndPopulateAccountAndServerAndTags (uuid: string, t?: Sequelize.Transaction) { 1106 static loadByUrl (url: string, transaction?: Sequelize.Transaction) {
1120 const options = { 1107 const query: IFindOptions<VideoModel> = {
1121 order: [ [ 'Tags', 'name', 'ASC' ] ],
1122 where: { 1108 where: {
1123 uuid 1109 url
1110 },
1111 transaction
1112 }
1113
1114 return VideoModel.findOne(query)
1115 }
1116
1117 static loadByUrlAndPopulateAccount (url: string, transaction?: Sequelize.Transaction) {
1118 const query: IFindOptions<VideoModel> = {
1119 where: {
1120 url
1124 }, 1121 },
1122 transaction
1123 }
1124
1125 return VideoModel.scope([ ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_FILES ]).findOne(query)
1126 }
1127
1128 static loadAndPopulateAccountAndServerAndTags (id: number | string, t?: Sequelize.Transaction) {
1129 const where = VideoModel.buildWhereIdOrUUID(id)
1130
1131 const options = {
1132 order: [ [ 'Tags', 'name', 'ASC' ] ],
1133 where,
1125 transaction: t 1134 transaction: t
1126 } 1135 }
1127 1136
@@ -1169,7 +1178,14 @@ export class VideoModel extends Model<VideoModel> {
1169 } 1178 }
1170 1179
1171 // threshold corresponds to how many video the field should have to be returned 1180 // threshold corresponds to how many video the field should have to be returned
1172 static getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) { 1181 static async getRandomFieldSamples (field: 'category' | 'channelId', threshold: number, count: number) {
1182 const actorId = (await getServerActor()).id
1183
1184 const scopeOptions = {
1185 actorId,
1186 includeLocalVideos: true
1187 }
1188
1173 const query: IFindOptions<VideoModel> = { 1189 const query: IFindOptions<VideoModel> = {
1174 attributes: [ field ], 1190 attributes: [ field ],
1175 limit: count, 1191 limit: count,
@@ -1177,20 +1193,28 @@ export class VideoModel extends Model<VideoModel> {
1177 having: Sequelize.where(Sequelize.fn('COUNT', Sequelize.col(field)), { 1193 having: Sequelize.where(Sequelize.fn('COUNT', Sequelize.col(field)), {
1178 [ Sequelize.Op.gte ]: threshold 1194 [ Sequelize.Op.gte ]: threshold
1179 }) as any, // FIXME: typings 1195 }) as any, // FIXME: typings
1180 where: {
1181 [ field ]: {
1182 [ Sequelize.Op.not ]: null
1183 },
1184 privacy: VideoPrivacy.PUBLIC,
1185 state: VideoState.PUBLISHED
1186 },
1187 order: [ this.sequelize.random() ] 1196 order: [ this.sequelize.random() ]
1188 } 1197 }
1189 1198
1190 return VideoModel.findAll(query) 1199 return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST_IDS, scopeOptions ] })
1200 .findAll(query)
1191 .then(rows => rows.map(r => r[ field ])) 1201 .then(rows => rows.map(r => r[ field ]))
1192 } 1202 }
1193 1203
1204 static buildTrendingQuery (trendingDays: number) {
1205 return {
1206 attributes: [],
1207 subQuery: false,
1208 model: VideoViewModel,
1209 required: false,
1210 where: {
1211 startDate: {
1212 [ Sequelize.Op.gte ]: new Date(new Date().getTime() - (24 * 3600 * 1000) * trendingDays)
1213 }
1214 }
1215 }
1216 }
1217
1194 private static buildActorWhereWithFilter (filter?: VideoFilter) { 1218 private static buildActorWhereWithFilter (filter?: VideoFilter) {
1195 if (filter && filter === 'local') { 1219 if (filter && filter === 'local') {
1196 return { 1220 return {
@@ -1201,7 +1225,7 @@ export class VideoModel extends Model<VideoModel> {
1201 return {} 1225 return {}
1202 } 1226 }
1203 1227
1204 private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions) { 1228 private static async getAvailableForApi (query: IFindOptions<VideoModel>, options: AvailableForListIDsOptions, countVideos = true) {
1205 const idsScope = { 1229 const idsScope = {
1206 method: [ 1230 method: [
1207 ScopeNames.AVAILABLE_FOR_LIST_IDS, options 1231 ScopeNames.AVAILABLE_FOR_LIST_IDS, options
@@ -1218,7 +1242,7 @@ export class VideoModel extends Model<VideoModel> {
1218 } 1242 }
1219 1243
1220 const [ count, rowsId ] = await Promise.all([ 1244 const [ count, rowsId ] = await Promise.all([
1221 VideoModel.scope(countScope).count(countQuery), 1245 countVideos ? VideoModel.scope(countScope).count(countQuery) : Promise.resolve(undefined),
1222 VideoModel.scope(idsScope).findAll(query) 1246 VideoModel.scope(idsScope).findAll(query)
1223 ]) 1247 ])
1224 const ids = rowsId.map(r => r.id) 1248 const ids = rowsId.map(r => r.id)
@@ -1247,26 +1271,30 @@ export class VideoModel extends Model<VideoModel> {
1247 } 1271 }
1248 } 1272 }
1249 1273
1250 private static getCategoryLabel (id: number) { 1274 static getCategoryLabel (id: number) {
1251 return VIDEO_CATEGORIES[ id ] || 'Misc' 1275 return VIDEO_CATEGORIES[ id ] || 'Misc'
1252 } 1276 }
1253 1277
1254 private static getLicenceLabel (id: number) { 1278 static getLicenceLabel (id: number) {
1255 return VIDEO_LICENCES[ id ] || 'Unknown' 1279 return VIDEO_LICENCES[ id ] || 'Unknown'
1256 } 1280 }
1257 1281
1258 private static getLanguageLabel (id: string) { 1282 static getLanguageLabel (id: string) {
1259 return VIDEO_LANGUAGES[ id ] || 'Unknown' 1283 return VIDEO_LANGUAGES[ id ] || 'Unknown'
1260 } 1284 }
1261 1285
1262 private static getPrivacyLabel (id: number) { 1286 static getPrivacyLabel (id: number) {
1263 return VIDEO_PRIVACIES[ id ] || 'Unknown' 1287 return VIDEO_PRIVACIES[ id ] || 'Unknown'
1264 } 1288 }
1265 1289
1266 private static getStateLabel (id: number) { 1290 static getStateLabel (id: number) {
1267 return VIDEO_STATES[ id ] || 'Unknown' 1291 return VIDEO_STATES[ id ] || 'Unknown'
1268 } 1292 }
1269 1293
1294 static buildWhereIdOrUUID (id: number | string) {
1295 return validator.isInt('' + id) ? { id } : { uuid: id }
1296 }
1297
1270 getOriginalFile () { 1298 getOriginalFile () {
1271 if (Array.isArray(this.VideoFiles) === false) return undefined 1299 if (Array.isArray(this.VideoFiles) === false) return undefined
1272 1300
@@ -1359,273 +1387,20 @@ export class VideoModel extends Model<VideoModel> {
1359 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName()) 1387 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
1360 } 1388 }
1361 1389
1362 toFormattedJSON (options?: { 1390 toFormattedJSON (options?: VideoFormattingJSONOptions): Video {
1363 additionalAttributes: { 1391 return videoModelToFormattedJSON(this, options)
1364 state?: boolean,
1365 waitTranscoding?: boolean,
1366 scheduledUpdate?: boolean,
1367 blacklistInfo?: boolean
1368 }
1369 }): Video {
1370 const formattedAccount = this.VideoChannel.Account.toFormattedJSON()
1371 const formattedVideoChannel = this.VideoChannel.toFormattedJSON()
1372
1373 const videoObject: Video = {
1374 id: this.id,
1375 uuid: this.uuid,
1376 name: this.name,
1377 category: {
1378 id: this.category,
1379 label: VideoModel.getCategoryLabel(this.category)
1380 },
1381 licence: {
1382 id: this.licence,
1383 label: VideoModel.getLicenceLabel(this.licence)
1384 },
1385 language: {
1386 id: this.language,
1387 label: VideoModel.getLanguageLabel(this.language)
1388 },
1389 privacy: {
1390 id: this.privacy,
1391 label: VideoModel.getPrivacyLabel(this.privacy)
1392 },
1393 nsfw: this.nsfw,
1394 description: this.getTruncatedDescription(),
1395 isLocal: this.isOwned(),
1396 duration: this.duration,
1397 views: this.views,
1398 likes: this.likes,
1399 dislikes: this.dislikes,
1400 thumbnailPath: this.getThumbnailStaticPath(),
1401 previewPath: this.getPreviewStaticPath(),
1402 embedPath: this.getEmbedStaticPath(),
1403 createdAt: this.createdAt,
1404 updatedAt: this.updatedAt,
1405 publishedAt: this.publishedAt,
1406 account: {
1407 id: formattedAccount.id,
1408 uuid: formattedAccount.uuid,
1409 name: formattedAccount.name,
1410 displayName: formattedAccount.displayName,
1411 url: formattedAccount.url,
1412 host: formattedAccount.host,
1413 avatar: formattedAccount.avatar
1414 },
1415 channel: {
1416 id: formattedVideoChannel.id,
1417 uuid: formattedVideoChannel.uuid,
1418 name: formattedVideoChannel.name,
1419 displayName: formattedVideoChannel.displayName,
1420 url: formattedVideoChannel.url,
1421 host: formattedVideoChannel.host,
1422 avatar: formattedVideoChannel.avatar
1423 }
1424 }
1425
1426 if (options) {
1427 if (options.additionalAttributes.state === true) {
1428 videoObject.state = {
1429 id: this.state,
1430 label: VideoModel.getStateLabel(this.state)
1431 }
1432 }
1433
1434 if (options.additionalAttributes.waitTranscoding === true) {
1435 videoObject.waitTranscoding = this.waitTranscoding
1436 }
1437
1438 if (options.additionalAttributes.scheduledUpdate === true && this.ScheduleVideoUpdate) {
1439 videoObject.scheduledUpdate = {
1440 updateAt: this.ScheduleVideoUpdate.updateAt,
1441 privacy: this.ScheduleVideoUpdate.privacy || undefined
1442 }
1443 }
1444
1445 if (options.additionalAttributes.blacklistInfo === true) {
1446 videoObject.blacklisted = !!this.VideoBlacklist
1447 videoObject.blacklistedReason = this.VideoBlacklist ? this.VideoBlacklist.reason : null
1448 }
1449 }
1450
1451 return videoObject
1452 } 1392 }
1453 1393
1454 toFormattedDetailsJSON (): VideoDetails { 1394 toFormattedDetailsJSON (): VideoDetails {
1455 const formattedJson = this.toFormattedJSON({ 1395 return videoModelToFormattedDetailsJSON(this)
1456 additionalAttributes: {
1457 scheduledUpdate: true,
1458 blacklistInfo: true
1459 }
1460 })
1461
1462 const detailsJson = {
1463 support: this.support,
1464 descriptionPath: this.getDescriptionPath(),
1465 channel: this.VideoChannel.toFormattedJSON(),
1466 account: this.VideoChannel.Account.toFormattedJSON(),
1467 tags: map(this.Tags, 'name'),
1468 commentsEnabled: this.commentsEnabled,
1469 waitTranscoding: this.waitTranscoding,
1470 state: {
1471 id: this.state,
1472 label: VideoModel.getStateLabel(this.state)
1473 },
1474 files: []
1475 }
1476
1477 // Format and sort video files
1478 detailsJson.files = this.getFormattedVideoFilesJSON()
1479
1480 return Object.assign(formattedJson, detailsJson)
1481 } 1396 }
1482 1397
1483 getFormattedVideoFilesJSON (): VideoFile[] { 1398 getFormattedVideoFilesJSON (): VideoFile[] {
1484 const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() 1399 return videoFilesModelToFormattedJSON(this, this.VideoFiles)
1485
1486 return this.VideoFiles
1487 .map(videoFile => {
1488 let resolutionLabel = videoFile.resolution + 'p'
1489
1490 return {
1491 resolution: {
1492 id: videoFile.resolution,
1493 label: resolutionLabel
1494 },
1495 magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
1496 size: videoFile.size,
1497 fps: videoFile.fps,
1498 torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp),
1499 torrentDownloadUrl: this.getTorrentDownloadUrl(videoFile, baseUrlHttp),
1500 fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp),
1501 fileDownloadUrl: this.getVideoFileDownloadUrl(videoFile, baseUrlHttp)
1502 } as VideoFile
1503 })
1504 .sort((a, b) => {
1505 if (a.resolution.id < b.resolution.id) return 1
1506 if (a.resolution.id === b.resolution.id) return 0
1507 return -1
1508 })
1509 } 1400 }
1510 1401
1511 toActivityPubObject (): VideoTorrentObject { 1402 toActivityPubObject (): VideoTorrentObject {
1512 const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() 1403 return videoModelToActivityPubObject(this)
1513 if (!this.Tags) this.Tags = []
1514
1515 const tag = this.Tags.map(t => ({
1516 type: 'Hashtag' as 'Hashtag',
1517 name: t.name
1518 }))
1519
1520 let language
1521 if (this.language) {
1522 language = {
1523 identifier: this.language,
1524 name: VideoModel.getLanguageLabel(this.language)
1525 }
1526 }
1527
1528 let category
1529 if (this.category) {
1530 category = {
1531 identifier: this.category + '',
1532 name: VideoModel.getCategoryLabel(this.category)
1533 }
1534 }
1535
1536 let licence
1537 if (this.licence) {
1538 licence = {
1539 identifier: this.licence + '',
1540 name: VideoModel.getLicenceLabel(this.licence)
1541 }
1542 }
1543
1544 const url: ActivityUrlObject[] = []
1545 for (const file of this.VideoFiles) {
1546 url.push({
1547 type: 'Link',
1548 mimeType: VIDEO_EXT_MIMETYPE[ file.extname ] as any,
1549 href: this.getVideoFileUrl(file, baseUrlHttp),
1550 height: file.resolution,
1551 size: file.size,
1552 fps: file.fps
1553 })
1554
1555 url.push({
1556 type: 'Link',
1557 mimeType: 'application/x-bittorrent' as 'application/x-bittorrent',
1558 href: this.getTorrentUrl(file, baseUrlHttp),
1559 height: file.resolution
1560 })
1561
1562 url.push({
1563 type: 'Link',
1564 mimeType: 'application/x-bittorrent;x-scheme-handler/magnet' as 'application/x-bittorrent;x-scheme-handler/magnet',
1565 href: this.generateMagnetUri(file, baseUrlHttp, baseUrlWs),
1566 height: file.resolution
1567 })
1568 }
1569
1570 // Add video url too
1571 url.push({
1572 type: 'Link',
1573 mimeType: 'text/html',
1574 href: CONFIG.WEBSERVER.URL + '/videos/watch/' + this.uuid
1575 })
1576
1577 const subtitleLanguage = []
1578 for (const caption of this.VideoCaptions) {
1579 subtitleLanguage.push({
1580 identifier: caption.language,
1581 name: VideoCaptionModel.getLanguageLabel(caption.language)
1582 })
1583 }
1584
1585 return {
1586 type: 'Video' as 'Video',
1587 id: this.url,
1588 name: this.name,
1589 duration: this.getActivityStreamDuration(),
1590 uuid: this.uuid,
1591 tag,
1592 category,
1593 licence,
1594 language,
1595 views: this.views,
1596 sensitive: this.nsfw,
1597 waitTranscoding: this.waitTranscoding,
1598 state: this.state,
1599 commentsEnabled: this.commentsEnabled,
1600 published: this.publishedAt.toISOString(),
1601 updated: this.updatedAt.toISOString(),
1602 mediaType: 'text/markdown',
1603 content: this.getTruncatedDescription(),
1604 support: this.support,
1605 subtitleLanguage,
1606 icon: {
1607 type: 'Image',
1608 url: this.getThumbnailUrl(baseUrlHttp),
1609 mediaType: 'image/jpeg',
1610 width: THUMBNAILS_SIZE.width,
1611 height: THUMBNAILS_SIZE.height
1612 },
1613 url,
1614 likes: getVideoLikesActivityPubUrl(this),
1615 dislikes: getVideoDislikesActivityPubUrl(this),
1616 shares: getVideoSharesActivityPubUrl(this),
1617 comments: getVideoCommentsActivityPubUrl(this),
1618 attributedTo: [
1619 {
1620 type: 'Person',
1621 id: this.VideoChannel.Account.Actor.url
1622 },
1623 {
1624 type: 'Group',
1625 id: this.VideoChannel.Actor.url
1626 }
1627 ]
1628 }
1629 } 1404 }
1630 1405
1631 getTruncatedDescription () { 1406 getTruncatedDescription () {
@@ -1635,130 +1410,13 @@ export class VideoModel extends Model<VideoModel> {
1635 return peertubeTruncate(this.description, maxLength) 1410 return peertubeTruncate(this.description, maxLength)
1636 } 1411 }
1637 1412
1638 async optimizeOriginalVideofile () {
1639 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
1640 const newExtname = '.mp4'
1641 const inputVideoFile = this.getOriginalFile()
1642 const videoInputPath = join(videosDirectory, this.getVideoFilename(inputVideoFile))
1643 const videoTranscodedPath = join(videosDirectory, this.id + '-transcoded' + newExtname)
1644
1645 const transcodeOptions = {
1646 inputPath: videoInputPath,
1647 outputPath: videoTranscodedPath
1648 }
1649
1650 // Could be very long!
1651 await transcode(transcodeOptions)
1652
1653 try {
1654 await remove(videoInputPath)
1655
1656 // Important to do this before getVideoFilename() to take in account the new file extension
1657 inputVideoFile.set('extname', newExtname)
1658
1659 const videoOutputPath = this.getVideoFilePath(inputVideoFile)
1660 await rename(videoTranscodedPath, videoOutputPath)
1661 const stats = await stat(videoOutputPath)
1662 const fps = await getVideoFileFPS(videoOutputPath)
1663
1664 inputVideoFile.set('size', stats.size)
1665 inputVideoFile.set('fps', fps)
1666
1667 await this.createTorrentAndSetInfoHash(inputVideoFile)
1668 await inputVideoFile.save()
1669
1670 } catch (err) {
1671 // Auto destruction...
1672 this.destroy().catch(err => logger.error('Cannot destruct video after transcoding failure.', { err }))
1673
1674 throw err
1675 }
1676 }
1677
1678 async transcodeOriginalVideofile (resolution: VideoResolution, isPortraitMode: boolean) {
1679 const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
1680 const extname = '.mp4'
1681
1682 // We are sure it's x264 in mp4 because optimizeOriginalVideofile was already executed
1683 const videoInputPath = join(videosDirectory, this.getVideoFilename(this.getOriginalFile()))
1684
1685 const newVideoFile = new VideoFileModel({
1686 resolution,
1687 extname,
1688 size: 0,
1689 videoId: this.id
1690 })
1691 const videoOutputPath = join(videosDirectory, this.getVideoFilename(newVideoFile))
1692
1693 const transcodeOptions = {
1694 inputPath: videoInputPath,
1695 outputPath: videoOutputPath,
1696 resolution,
1697 isPortraitMode
1698 }
1699
1700 await transcode(transcodeOptions)
1701
1702 const stats = await stat(videoOutputPath)
1703 const fps = await getVideoFileFPS(videoOutputPath)
1704
1705 newVideoFile.set('size', stats.size)
1706 newVideoFile.set('fps', fps)
1707
1708 await this.createTorrentAndSetInfoHash(newVideoFile)
1709
1710 await newVideoFile.save()
1711
1712 this.VideoFiles.push(newVideoFile)
1713 }
1714
1715 async importVideoFile (inputFilePath: string) {
1716 const { videoFileResolution } = await getVideoFileResolution(inputFilePath)
1717 const { size } = await stat(inputFilePath)
1718 const fps = await getVideoFileFPS(inputFilePath)
1719
1720 let updatedVideoFile = new VideoFileModel({
1721 resolution: videoFileResolution,
1722 extname: extname(inputFilePath),
1723 size,
1724 fps,
1725 videoId: this.id
1726 })
1727
1728 const currentVideoFile = this.VideoFiles.find(videoFile => videoFile.resolution === updatedVideoFile.resolution)
1729
1730 if (currentVideoFile) {
1731 // Remove old file and old torrent
1732 await this.removeFile(currentVideoFile)
1733 await this.removeTorrent(currentVideoFile)
1734 // Remove the old video file from the array
1735 this.VideoFiles = this.VideoFiles.filter(f => f !== currentVideoFile)
1736
1737 // Update the database
1738 currentVideoFile.set('extname', updatedVideoFile.extname)
1739 currentVideoFile.set('size', updatedVideoFile.size)
1740 currentVideoFile.set('fps', updatedVideoFile.fps)
1741
1742 updatedVideoFile = currentVideoFile
1743 }
1744
1745 const outputPath = this.getVideoFilePath(updatedVideoFile)
1746 await copy(inputFilePath, outputPath)
1747
1748 await this.createTorrentAndSetInfoHash(updatedVideoFile)
1749
1750 await updatedVideoFile.save()
1751
1752 this.VideoFiles.push(updatedVideoFile)
1753 }
1754
1755 getOriginalFileResolution () { 1413 getOriginalFileResolution () {
1756 const originalFilePath = this.getVideoFilePath(this.getOriginalFile()) 1414 const originalFilePath = this.getVideoFilePath(this.getOriginalFile())
1757 1415
1758 return getVideoFileResolution(originalFilePath) 1416 return getVideoFileResolution(originalFilePath)
1759 } 1417 }
1760 1418
1761 getDescriptionPath () { 1419 getDescriptionAPIPath () {
1762 return `/api/${API_VERSION}/videos/${this.uuid}/description` 1420 return `/api/${API_VERSION}/videos/${this.uuid}/description`
1763 } 1421 }
1764 1422
@@ -1786,11 +1444,6 @@ export class VideoModel extends Model<VideoModel> {
1786 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err })) 1444 .catch(err => logger.warn('Cannot delete torrent %s.', torrentPath, { err }))
1787 } 1445 }
1788 1446
1789 getActivityStreamDuration () {
1790 // https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
1791 return 'PT' + this.duration + 'S'
1792 }
1793
1794 isOutdated () { 1447 isOutdated () {
1795 if (this.isOwned()) return false 1448 if (this.isOwned()) return false
1796 1449