Fix trending videos count

pull/1011/head
Chocobozzz 2018-09-03 18:05:12 +02:00
parent 74d6346935
commit 8ea6f49ad7
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
1 changed files with 104 additions and 92 deletions

View File

@ -113,7 +113,7 @@ const indexes: Sequelize.DefineIndexesOptions[] = [
unique: true unique: true
}, },
{ {
fields: [ 'url'], fields: [ 'url' ],
unique: true unique: true
} }
] ]
@ -150,7 +150,7 @@ type AvailableForListIDsOptions = {
} }
@Scopes({ @Scopes({
[ScopeNames.FOR_API]: (options: ForAPIOptions) => { [ ScopeNames.FOR_API ]: (options: ForAPIOptions) => {
const accountInclude = { const accountInclude = {
attributes: [ 'id', 'name' ], attributes: [ 'id', 'name' ],
model: AccountModel.unscoped(), model: AccountModel.unscoped(),
@ -203,7 +203,7 @@ type AvailableForListIDsOptions = {
const query: IFindOptions<VideoModel> = { const query: IFindOptions<VideoModel> = {
where: { where: {
id: { id: {
[Sequelize.Op.any]: options.ids [ Sequelize.Op.any ]: options.ids
} }
}, },
include: [ videoChannelInclude ] include: [ videoChannelInclude ]
@ -218,12 +218,12 @@ type AvailableForListIDsOptions = {
return query return query
}, },
[ScopeNames.AVAILABLE_FOR_LIST_IDS]: (options: AvailableForListIDsOptions) => { [ ScopeNames.AVAILABLE_FOR_LIST_IDS ]: (options: AvailableForListIDsOptions) => {
const query: IFindOptions<VideoModel> = { const query: IFindOptions<VideoModel> = {
attributes: [ 'id' ], attributes: [ 'id' ],
where: { where: {
id: { id: {
[Sequelize.Op.and]: [ [ Sequelize.Op.and ]: [
{ {
[ Sequelize.Op.notIn ]: Sequelize.literal( [ Sequelize.Op.notIn ]: Sequelize.literal(
'(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")' '(SELECT "videoBlacklist"."videoId" FROM "videoBlacklist")'
@ -246,7 +246,7 @@ type AvailableForListIDsOptions = {
} }
] ]
}, },
include: [ ] include: []
} }
if (options.filter || options.accountId || options.videoChannelId) { if (options.filter || options.accountId || options.videoChannelId) {
@ -303,27 +303,27 @@ type AvailableForListIDsOptions = {
// Force actorId to be a number to avoid SQL injections // Force actorId to be a number to avoid SQL injections
const actorIdNumber = parseInt(options.actorId.toString(), 10) const actorIdNumber = parseInt(options.actorId.toString(), 10)
query.where['id'][Sequelize.Op.and].push({ query.where[ 'id' ][ Sequelize.Op.and ].push({
[ Sequelize.Op.in ]: Sequelize.literal( [ Sequelize.Op.in ]: Sequelize.literal(
'(' + '(' +
'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' +
'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' +
'WHERE "actorFollow"."actorId" = ' + actorIdNumber + 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
' UNION ALL ' + ' UNION ALL ' +
'SELECT "video"."id" AS "id" FROM "video" ' + 'SELECT "video"."id" AS "id" FROM "video" ' +
'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' +
'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' +
'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' +
'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' + 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' +
'WHERE "actorFollow"."actorId" = ' + actorIdNumber + 'WHERE "actorFollow"."actorId" = ' + actorIdNumber +
localVideosReq + localVideosReq +
')' ')'
) )
}) })
} }
if (options.withFiles === true) { if (options.withFiles === true) {
query.where['id'][Sequelize.Op.and].push({ query.where[ 'id' ][ Sequelize.Op.and ].push({
[ Sequelize.Op.in ]: Sequelize.literal( [ Sequelize.Op.in ]: Sequelize.literal(
'(SELECT "videoId" FROM "videoFile")' '(SELECT "videoId" FROM "videoFile")'
) )
@ -338,8 +338,8 @@ type AvailableForListIDsOptions = {
} }
if (options.tagsOneOf) { if (options.tagsOneOf) {
query.where['id'][Sequelize.Op.and].push({ query.where[ 'id' ][ Sequelize.Op.and ].push({
[Sequelize.Op.in]: Sequelize.literal( [ Sequelize.Op.in ]: Sequelize.literal(
'(' + '(' +
'SELECT "videoId" FROM "videoTag" ' + 'SELECT "videoId" FROM "videoTag" ' +
'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
@ -350,8 +350,8 @@ type AvailableForListIDsOptions = {
} }
if (options.tagsAllOf) { if (options.tagsAllOf) {
query.where['id'][Sequelize.Op.and].push({ query.where[ 'id' ][ Sequelize.Op.and ].push({
[Sequelize.Op.in]: Sequelize.literal( [ Sequelize.Op.in ]: Sequelize.literal(
'(' + '(' +
'SELECT "videoId" FROM "videoTag" ' + 'SELECT "videoId" FROM "videoTag" ' +
'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
@ -364,24 +364,24 @@ type AvailableForListIDsOptions = {
} }
if (options.nsfw === true || options.nsfw === false) { if (options.nsfw === true || options.nsfw === false) {
query.where['nsfw'] = options.nsfw query.where[ 'nsfw' ] = options.nsfw
} }
if (options.categoryOneOf) { if (options.categoryOneOf) {
query.where['category'] = { query.where[ 'category' ] = {
[Sequelize.Op.or]: options.categoryOneOf [ Sequelize.Op.or ]: options.categoryOneOf
} }
} }
if (options.licenceOneOf) { if (options.licenceOneOf) {
query.where['licence'] = { query.where[ 'licence' ] = {
[Sequelize.Op.or]: options.licenceOneOf [ Sequelize.Op.or ]: options.licenceOneOf
} }
} }
if (options.languageOneOf) { if (options.languageOneOf) {
query.where['language'] = { query.where[ 'language' ] = {
[Sequelize.Op.or]: options.languageOneOf [ Sequelize.Op.or ]: options.languageOneOf
} }
} }
@ -402,7 +402,7 @@ type AvailableForListIDsOptions = {
return query return query
}, },
[ScopeNames.WITH_ACCOUNT_DETAILS]: { [ ScopeNames.WITH_ACCOUNT_DETAILS ]: {
include: [ include: [
{ {
model: () => VideoChannelModel.unscoped(), model: () => VideoChannelModel.unscoped(),
@ -454,10 +454,10 @@ type AvailableForListIDsOptions = {
} }
] ]
}, },
[ScopeNames.WITH_TAGS]: { [ ScopeNames.WITH_TAGS ]: {
include: [ () => TagModel ] include: [ () => TagModel ]
}, },
[ScopeNames.WITH_BLACKLISTED]: { [ ScopeNames.WITH_BLACKLISTED ]: {
include: [ include: [
{ {
attributes: [ 'id', 'reason' ], attributes: [ 'id', 'reason' ],
@ -466,7 +466,7 @@ type AvailableForListIDsOptions = {
} }
] ]
}, },
[ScopeNames.WITH_FILES]: { [ ScopeNames.WITH_FILES ]: {
include: [ include: [
{ {
model: () => VideoFileModel.unscoped(), model: () => VideoFileModel.unscoped(),
@ -474,7 +474,7 @@ type AvailableForListIDsOptions = {
} }
] ]
}, },
[ScopeNames.WITH_SCHEDULED_UPDATE]: { [ ScopeNames.WITH_SCHEDULED_UPDATE ]: {
include: [ include: [
{ {
model: () => ScheduleVideoUpdateModel.unscoped(), model: () => ScheduleVideoUpdateModel.unscoped(),
@ -700,7 +700,7 @@ export class VideoModel extends Model<VideoModel> {
}, },
onDelete: 'cascade', onDelete: 'cascade',
hooks: true, hooks: true,
['separate' as any]: true [ 'separate' as any ]: true
}) })
VideoCaptions: VideoCaptionModel[] VideoCaptions: VideoCaptionModel[]
@ -749,9 +749,9 @@ export class VideoModel extends Model<VideoModel> {
// Do not wait video deletion because we could be in a transaction // Do not wait video deletion because we could be in a transaction
Promise.all(tasks) Promise.all(tasks)
.catch(err => { .catch(err => {
logger.error('Some errors when removing files of video %s in before destroy hook.', instance.uuid, { err }) logger.error('Some errors when removing files of video %s in before destroy hook.', instance.uuid, { err })
}) })
return undefined return undefined
} }
@ -783,9 +783,9 @@ export class VideoModel extends Model<VideoModel> {
order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ]), order: getVideoSort('createdAt', [ 'Tags', 'name', 'ASC' ]),
where: { where: {
id: { id: {
[Sequelize.Op.in]: Sequelize.literal('(' + rawQuery + ')') [ Sequelize.Op.in ]: Sequelize.literal('(' + rawQuery + ')')
}, },
[Sequelize.Op.or]: [ [ Sequelize.Op.or ]: [
{ privacy: VideoPrivacy.PUBLIC }, { privacy: VideoPrivacy.PUBLIC },
{ privacy: VideoPrivacy.UNLISTED } { privacy: VideoPrivacy.UNLISTED }
] ]
@ -802,10 +802,10 @@ export class VideoModel extends Model<VideoModel> {
required: false, required: false,
// We only want videos shared by this actor // We only want videos shared by this actor
where: { where: {
[Sequelize.Op.and]: [ [ Sequelize.Op.and ]: [
{ {
id: { id: {
[Sequelize.Op.not]: null [ Sequelize.Op.not ]: null
} }
}, },
{ {
@ -856,8 +856,8 @@ export class VideoModel extends Model<VideoModel> {
// totals: totalVideos + totalVideoShares // totals: totalVideos + totalVideoShares
let totalVideos = 0 let totalVideos = 0
let totalVideoShares = 0 let totalVideoShares = 0
if (totals[0]) totalVideos = parseInt(totals[0].total, 10) if (totals[ 0 ]) totalVideos = parseInt(totals[ 0 ].total, 10)
if (totals[1]) totalVideoShares = parseInt(totals[1].total, 10) if (totals[ 1 ]) totalVideoShares = parseInt(totals[ 1 ].total, 10)
const total = totalVideos + totalVideoShares const total = totalVideos + totalVideoShares
return { return {
@ -982,22 +982,22 @@ export class VideoModel extends Model<VideoModel> {
durationMin?: number // seconds durationMin?: number // seconds
durationMax?: number // seconds durationMax?: number // seconds
}) { }) {
const whereAnd = [ ] const whereAnd = []
if (options.startDate || options.endDate) { if (options.startDate || options.endDate) {
const publishedAtRange = { } const publishedAtRange = {}
if (options.startDate) publishedAtRange[Sequelize.Op.gte] = options.startDate if (options.startDate) publishedAtRange[ Sequelize.Op.gte ] = options.startDate
if (options.endDate) publishedAtRange[Sequelize.Op.lte] = options.endDate if (options.endDate) publishedAtRange[ Sequelize.Op.lte ] = options.endDate
whereAnd.push({ publishedAt: publishedAtRange }) whereAnd.push({ publishedAt: publishedAtRange })
} }
if (options.durationMin || options.durationMax) { if (options.durationMin || options.durationMax) {
const durationRange = { } const durationRange = {}
if (options.durationMin) durationRange[Sequelize.Op.gte] = options.durationMin if (options.durationMin) durationRange[ Sequelize.Op.gte ] = options.durationMin
if (options.durationMax) durationRange[Sequelize.Op.lte] = options.durationMax if (options.durationMax) durationRange[ Sequelize.Op.lte ] = options.durationMax
whereAnd.push({ duration: durationRange }) whereAnd.push({ duration: durationRange })
} }
@ -1011,14 +1011,14 @@ export class VideoModel extends Model<VideoModel> {
id: { id: {
[ Sequelize.Op.in ]: Sequelize.literal( [ Sequelize.Op.in ]: Sequelize.literal(
'(' + '(' +
'SELECT "video"."id" FROM "video" ' + 'SELECT "video"."id" FROM "video" ' +
'WHERE ' + 'WHERE ' +
'lower(immutable_unaccent("video"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' + 'lower(immutable_unaccent("video"."name")) % lower(immutable_unaccent(' + escapedSearch + ')) OR ' +
'lower(immutable_unaccent("video"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' + 'lower(immutable_unaccent("video"."name")) LIKE lower(immutable_unaccent(' + escapedLikeSearch + '))' +
'UNION ALL ' + 'UNION ALL ' +
'SELECT "video"."id" FROM "video" LEFT JOIN "videoTag" ON "videoTag"."videoId" = "video"."id" ' + 'SELECT "video"."id" FROM "video" LEFT JOIN "videoTag" ON "videoTag"."videoId" = "video"."id" ' +
'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' + 'INNER JOIN "tag" ON "tag"."id" = "videoTag"."tagId" ' +
'WHERE "tag"."name" = ' + escapedSearch + 'WHERE "tag"."name" = ' + escapedSearch +
')' ')'
) )
} }
@ -1167,11 +1167,11 @@ export class VideoModel extends Model<VideoModel> {
limit: count, limit: count,
group: field, group: field,
having: Sequelize.where(Sequelize.fn('COUNT', Sequelize.col(field)), { having: Sequelize.where(Sequelize.fn('COUNT', Sequelize.col(field)), {
[Sequelize.Op.gte]: threshold [ Sequelize.Op.gte ]: threshold
}) as any, // FIXME: typings }) as any, // FIXME: typings
where: { where: {
[field]: { [ field ]: {
[Sequelize.Op.not]: null [ Sequelize.Op.not ]: null
}, },
privacy: VideoPrivacy.PUBLIC, privacy: VideoPrivacy.PUBLIC,
state: VideoState.PUBLISHED state: VideoState.PUBLISHED
@ -1180,7 +1180,7 @@ export class VideoModel extends Model<VideoModel> {
} }
return VideoModel.findAll(query) return VideoModel.findAll(query)
.then(rows => rows.map(r => r[field])) .then(rows => rows.map(r => r[ field ]))
} }
private static buildActorWhereWithFilter (filter?: VideoFilter) { private static buildActorWhereWithFilter (filter?: VideoFilter) {
@ -1200,7 +1200,19 @@ export class VideoModel extends Model<VideoModel> {
] ]
} }
const { count, rows: rowsId } = await VideoModel.scope(idsScope).findAndCountAll(query) // Remove trending sort on count, because it uses a group by
const countOptions = Object.assign({}, options, { trendingDays: undefined })
const countQuery = Object.assign({}, query, { attributes: undefined, group: undefined })
const countScope = {
method: [
ScopeNames.AVAILABLE_FOR_LIST_IDS, countOptions
]
}
const [ count, rowsId ] = await Promise.all([
VideoModel.scope(countScope).count(countQuery),
VideoModel.scope(idsScope).findAll(query)
])
const ids = rowsId.map(r => r.id) const ids = rowsId.map(r => r.id)
if (ids.length === 0) return { data: [], total: count } if (ids.length === 0) return { data: [], total: count }
@ -1228,23 +1240,23 @@ export class VideoModel extends Model<VideoModel> {
} }
private static getCategoryLabel (id: number) { private static getCategoryLabel (id: number) {
return VIDEO_CATEGORIES[id] || 'Misc' return VIDEO_CATEGORIES[ id ] || 'Misc'
} }
private static getLicenceLabel (id: number) { private static getLicenceLabel (id: number) {
return VIDEO_LICENCES[id] || 'Unknown' return VIDEO_LICENCES[ id ] || 'Unknown'
} }
private static getLanguageLabel (id: string) { private static getLanguageLabel (id: string) {
return VIDEO_LANGUAGES[id] || 'Unknown' return VIDEO_LANGUAGES[ id ] || 'Unknown'
} }
private static getPrivacyLabel (id: number) { private static getPrivacyLabel (id: number) {
return VIDEO_PRIVACIES[id] || 'Unknown' return VIDEO_PRIVACIES[ id ] || 'Unknown'
} }
private static getStateLabel (id: number) { private static getStateLabel (id: number) {
return VIDEO_STATES[id] || 'Unknown' return VIDEO_STATES[ id ] || 'Unknown'
} }
getOriginalFile () { getOriginalFile () {
@ -1466,28 +1478,28 @@ export class VideoModel extends Model<VideoModel> {
const { baseUrlHttp, baseUrlWs } = this.getBaseUrls() const { baseUrlHttp, baseUrlWs } = this.getBaseUrls()
return this.VideoFiles return this.VideoFiles
.map(videoFile => { .map(videoFile => {
let resolutionLabel = videoFile.resolution + 'p' let resolutionLabel = videoFile.resolution + 'p'
return { return {
resolution: { resolution: {
id: videoFile.resolution, id: videoFile.resolution,
label: resolutionLabel label: resolutionLabel
}, },
magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs), magnetUri: this.generateMagnetUri(videoFile, baseUrlHttp, baseUrlWs),
size: videoFile.size, size: videoFile.size,
fps: videoFile.fps, fps: videoFile.fps,
torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp), torrentUrl: this.getTorrentUrl(videoFile, baseUrlHttp),
torrentDownloadUrl: this.getTorrentDownloadUrl(videoFile, baseUrlHttp), torrentDownloadUrl: this.getTorrentDownloadUrl(videoFile, baseUrlHttp),
fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp), fileUrl: this.getVideoFileUrl(videoFile, baseUrlHttp),
fileDownloadUrl: this.getVideoFileDownloadUrl(videoFile, baseUrlHttp) fileDownloadUrl: this.getVideoFileDownloadUrl(videoFile, baseUrlHttp)
} as VideoFile } as VideoFile
}) })
.sort((a, b) => { .sort((a, b) => {
if (a.resolution.id < b.resolution.id) return 1 if (a.resolution.id < b.resolution.id) return 1
if (a.resolution.id === b.resolution.id) return 0 if (a.resolution.id === b.resolution.id) return 0
return -1 return -1
}) })
} }
toActivityPubObject (): VideoTorrentObject { toActivityPubObject (): VideoTorrentObject {
@ -1527,7 +1539,7 @@ export class VideoModel extends Model<VideoModel> {
for (const file of this.VideoFiles) { for (const file of this.VideoFiles) {
url.push({ url.push({
type: 'Link', type: 'Link',
mimeType: VIDEO_EXT_MIMETYPE[file.extname], mimeType: VIDEO_EXT_MIMETYPE[ file.extname ],
href: this.getVideoFileUrl(file, baseUrlHttp), href: this.getVideoFileUrl(file, baseUrlHttp),
height: file.resolution, height: file.resolution,
size: file.size, size: file.size,