diff options
Diffstat (limited to 'server/models/video/video.ts')
-rw-r--r-- | server/models/video/video.ts | 157 |
1 files changed, 120 insertions, 37 deletions
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 217ca8e50..7f94e834a 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -1,5 +1,5 @@ | |||
1 | import * as Bluebird from 'bluebird' | 1 | import * as Bluebird from 'bluebird' |
2 | import { maxBy, minBy } from 'lodash' | 2 | import { maxBy, minBy, pick } from 'lodash' |
3 | import { join } from 'path' | 3 | import { join } from 'path' |
4 | import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' | 4 | import { FindOptions, IncludeOptions, Op, QueryTypes, ScopeOptions, Sequelize, Transaction, WhereOptions } from 'sequelize' |
5 | import { | 5 | import { |
@@ -124,7 +124,7 @@ import { MThumbnail } from '../../typings/models/video/thumbnail' | |||
124 | import { VideoFile } from '@shared/models/videos/video-file.model' | 124 | import { VideoFile } from '@shared/models/videos/video-file.model' |
125 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' | 125 | import { getHLSDirectory, getTorrentFileName, getTorrentFilePath, getVideoFilename, getVideoFilePath } from '@server/lib/video-paths' |
126 | import { ModelCache } from '@server/models/model-cache' | 126 | import { ModelCache } from '@server/models/model-cache' |
127 | import { buildListQuery, BuildVideosQueryOptions } from './video-query-builder' | 127 | import { buildListQuery, BuildVideosQueryOptions, wrapForAPIResults } from './video-query-builder' |
128 | 128 | ||
129 | export enum ScopeNames { | 129 | export enum ScopeNames { |
130 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', | 130 | AVAILABLE_FOR_LIST_IDS = 'AVAILABLE_FOR_LIST_IDS', |
@@ -1408,19 +1408,25 @@ export class VideoModel extends Model<VideoModel> { | |||
1408 | options: BuildVideosQueryOptions, | 1408 | options: BuildVideosQueryOptions, |
1409 | countVideos = true | 1409 | countVideos = true |
1410 | ) { | 1410 | ) { |
1411 | const { query, replacements } = buildListQuery(VideoModel, options) | 1411 | function getCount () { |
1412 | const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, Object.assign({}, options, { isCount: true })) | 1412 | if (countVideos !== true) return Promise.resolve(undefined) |
1413 | 1413 | ||
1414 | const [ count, rows ] = await Promise.all([ | 1414 | const countOptions = Object.assign({}, options, { isCount: true }) |
1415 | countVideos | 1415 | const { query: queryCount, replacements: replacementsCount } = buildListQuery(VideoModel, countOptions) |
1416 | ? this.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT }) | ||
1417 | .then(rows => rows.length !== 0 ? rows[0].total : 0) | ||
1418 | : Promise.resolve<number>(undefined), | ||
1419 | 1416 | ||
1420 | this.sequelize.query<any>(query, { replacements, type: QueryTypes.SELECT }) | 1417 | return VideoModel.sequelize.query<any>(queryCount, { replacements: replacementsCount, type: QueryTypes.SELECT }) |
1421 | .then(rows => rows.map(r => r.id)) | 1418 | .then(rows => rows.length !== 0 ? rows[0].total : 0) |
1422 | .then(ids => VideoModel.loadCompleteVideosForApi(ids, options)) | 1419 | } |
1423 | ]) | 1420 | |
1421 | function getModels () { | ||
1422 | const { query, replacements, order } = buildListQuery(VideoModel, options) | ||
1423 | const queryModels = wrapForAPIResults(query, replacements, options, order) | ||
1424 | |||
1425 | return VideoModel.sequelize.query<any>(queryModels, { replacements, type: QueryTypes.SELECT, nest: true }) | ||
1426 | .then(rows => VideoModel.buildAPIResult(rows)) | ||
1427 | } | ||
1428 | |||
1429 | const [ count, rows ] = await Promise.all([ getCount(), getModels() ]) | ||
1424 | 1430 | ||
1425 | return { | 1431 | return { |
1426 | data: rows, | 1432 | data: rows, |
@@ -1428,36 +1434,113 @@ export class VideoModel extends Model<VideoModel> { | |||
1428 | } | 1434 | } |
1429 | } | 1435 | } |
1430 | 1436 | ||
1431 | private static loadCompleteVideosForApi (ids: number[], options: BuildVideosQueryOptions) { | 1437 | private static buildAPIResult (rows: any[]) { |
1432 | if (ids.length === 0) return [] | 1438 | const memo: { [ id: number ]: VideoModel } = {} |
1439 | |||
1440 | const thumbnailsDone = new Set<number>() | ||
1441 | const historyDone = new Set<number>() | ||
1442 | const videoFilesDone = new Set<number>() | ||
1443 | |||
1444 | const videos: VideoModel[] = [] | ||
1445 | |||
1446 | const avatarKeys = [ 'id', 'filename', 'fileUrl', 'onDisk', 'createdAt', 'updatedAt' ] | ||
1447 | const actorKeys = [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ] | ||
1448 | const serverKeys = [ 'id', 'host' ] | ||
1449 | const videoFileKeys = [ 'id', 'createdAt', 'updatedAt', 'resolution', 'size', 'extname', 'infoHash', 'fps', 'videoId' ] | ||
1450 | const videoKeys = [ | ||
1451 | 'id', | ||
1452 | 'uuid', | ||
1453 | 'name', | ||
1454 | 'category', | ||
1455 | 'licence', | ||
1456 | 'language', | ||
1457 | 'privacy', | ||
1458 | 'nsfw', | ||
1459 | 'description', | ||
1460 | 'support', | ||
1461 | 'duration', | ||
1462 | 'views', | ||
1463 | 'likes', | ||
1464 | 'dislikes', | ||
1465 | 'remote', | ||
1466 | 'url', | ||
1467 | 'commentsEnabled', | ||
1468 | 'downloadEnabled', | ||
1469 | 'waitTranscoding', | ||
1470 | 'state', | ||
1471 | 'publishedAt', | ||
1472 | 'originallyPublishedAt', | ||
1473 | 'channelId', | ||
1474 | 'createdAt', | ||
1475 | 'updatedAt' | ||
1476 | ] | ||
1433 | 1477 | ||
1434 | const secondQuery: FindOptions = { | 1478 | function buildActor (rowActor: any) { |
1435 | offset: 0, | 1479 | const avatarModel = rowActor.Avatar.id !== null |
1436 | limit: options.count, | 1480 | ? new AvatarModel(pick(rowActor.Avatar, avatarKeys)) |
1437 | order: [ // Keep original order | 1481 | : null |
1438 | Sequelize.literal( | ||
1439 | ids.map(id => `"VideoModel".id = ${id} DESC`).join(', ') | ||
1440 | ) | ||
1441 | ] | ||
1442 | } | ||
1443 | 1482 | ||
1444 | const apiScope: (string | ScopeOptions)[] = [] | 1483 | const serverModel = rowActor.Server.id !== null |
1484 | ? new ServerModel(pick(rowActor.Server, serverKeys)) | ||
1485 | : null | ||
1445 | 1486 | ||
1446 | if (options.user) { | 1487 | const actorModel = new ActorModel(pick(rowActor, actorKeys)) |
1447 | apiScope.push({ method: [ ScopeNames.WITH_USER_HISTORY, options.user.id ] }) | 1488 | actorModel.Avatar = avatarModel |
1489 | actorModel.Server = serverModel | ||
1490 | |||
1491 | return actorModel | ||
1448 | } | 1492 | } |
1449 | 1493 | ||
1450 | apiScope.push({ | 1494 | for (const row of rows) { |
1451 | method: [ | 1495 | if (!memo[row.id]) { |
1452 | ScopeNames.FOR_API, { | 1496 | // Build Channel |
1453 | ids, | 1497 | const channel = row.VideoChannel |
1454 | withFiles: options.withFiles, | 1498 | const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ])) |
1455 | videoPlaylistId: options.videoPlaylistId | 1499 | channelModel.Actor = buildActor(channel.Actor) |
1456 | } as ForAPIOptions | 1500 | |
1457 | ] | 1501 | const account = row.VideoChannel.Account |
1458 | }) | 1502 | const accountModel = new AccountModel(pick(account, [ 'id', 'name' ])) |
1503 | accountModel.Actor = buildActor(account.Actor) | ||
1504 | |||
1505 | channelModel.Account = accountModel | ||
1506 | |||
1507 | const videoModel = new VideoModel(pick(row, videoKeys)) | ||
1508 | videoModel.VideoChannel = channelModel | ||
1509 | |||
1510 | videoModel.UserVideoHistories = [] | ||
1511 | videoModel.Thumbnails = [] | ||
1512 | videoModel.VideoFiles = [] | ||
1513 | |||
1514 | memo[row.id] = videoModel | ||
1515 | // Don't take object value to have a sorted array | ||
1516 | videos.push(videoModel) | ||
1517 | } | ||
1518 | |||
1519 | const videoModel = memo[row.id] | ||
1520 | |||
1521 | if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) { | ||
1522 | const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ])) | ||
1523 | videoModel.UserVideoHistories.push(historyModel) | ||
1524 | |||
1525 | historyDone.add(row.userVideoHistory.id) | ||
1526 | } | ||
1527 | |||
1528 | if (row.Thumbnails?.id && !thumbnailsDone.has(row.Thumbnails.id)) { | ||
1529 | const thumbnailModel = new ThumbnailModel(pick(row.Thumbnails, [ 'id', 'type', 'filename' ])) | ||
1530 | videoModel.Thumbnails.push(thumbnailModel) | ||
1531 | |||
1532 | thumbnailsDone.add(row.Thumbnails.id) | ||
1533 | } | ||
1534 | |||
1535 | if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) { | ||
1536 | const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys)) | ||
1537 | videoModel.VideoFiles.push(videoFileModel) | ||
1538 | |||
1539 | videoFilesDone.add(row.VideoFiles.id) | ||
1540 | } | ||
1541 | } | ||
1459 | 1542 | ||
1460 | return VideoModel.scope(apiScope).findAll(secondQuery) | 1543 | return videos |
1461 | } | 1544 | } |
1462 | 1545 | ||
1463 | private static isPrivacyForFederation (privacy: VideoPrivacy) { | 1546 | private static isPrivacyForFederation (privacy: VideoPrivacy) { |