aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models/video
diff options
context:
space:
mode:
Diffstat (limited to 'server/models/video')
-rw-r--r--server/models/video/video-channel.ts145
-rw-r--r--server/models/video/video-playlist.ts60
-rw-r--r--server/models/video/video-query-builder.ts5
-rw-r--r--server/models/video/video.ts25
4 files changed, 158 insertions, 77 deletions
diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts
index 178878c55..b7ffbd3b1 100644
--- a/server/models/video/video-channel.ts
+++ b/server/models/video/video-channel.ts
@@ -1,4 +1,4 @@
1import { FindOptions, Includeable, literal, Op, ScopeOptions } from 'sequelize' 1import { FindOptions, Includeable, literal, Op, QueryTypes, ScopeOptions } from 'sequelize'
2import { 2import {
3 AllowNull, 3 AllowNull,
4 BeforeDestroy, 4 BeforeDestroy,
@@ -28,17 +28,16 @@ import {
28import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants' 28import { CONSTRAINTS_FIELDS, WEBSERVER } from '../../initializers/constants'
29import { sendDeleteActor } from '../../lib/activitypub/send' 29import { sendDeleteActor } from '../../lib/activitypub/send'
30import { 30import {
31 MChannelAccountDefault,
32 MChannelActor, 31 MChannelActor,
33 MChannelActorAccountDefaultVideos,
34 MChannelAP, 32 MChannelAP,
33 MChannelBannerAccountDefault,
35 MChannelFormattable, 34 MChannelFormattable,
36 MChannelSummaryFormattable 35 MChannelSummaryFormattable
37} from '../../types/models/video' 36} from '../../types/models/video'
38import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' 37import { AccountModel, ScopeNames as AccountModelScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account'
38import { ActorImageModel } from '../account/actor-image'
39import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor' 39import { ActorModel, unusedActorAttributesForAPI } from '../activitypub/actor'
40import { ActorFollowModel } from '../activitypub/actor-follow' 40import { ActorFollowModel } from '../activitypub/actor-follow'
41import { AvatarModel } from '../avatar/avatar'
42import { ServerModel } from '../server/server' 41import { ServerModel } from '../server/server'
43import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils' 42import { buildServerIdsFollowedBy, buildTrigramSearchIndex, createSimilarityAttribute, getSort, throwIfNotValid } from '../utils'
44import { VideoModel } from './video' 43import { VideoModel } from './video'
@@ -49,6 +48,7 @@ export enum ScopeNames {
49 SUMMARY = 'SUMMARY', 48 SUMMARY = 'SUMMARY',
50 WITH_ACCOUNT = 'WITH_ACCOUNT', 49 WITH_ACCOUNT = 'WITH_ACCOUNT',
51 WITH_ACTOR = 'WITH_ACTOR', 50 WITH_ACTOR = 'WITH_ACTOR',
51 WITH_ACTOR_BANNER = 'WITH_ACTOR_BANNER',
52 WITH_VIDEOS = 'WITH_VIDEOS', 52 WITH_VIDEOS = 'WITH_VIDEOS',
53 WITH_STATS = 'WITH_STATS' 53 WITH_STATS = 'WITH_STATS'
54} 54}
@@ -99,7 +99,14 @@ export type SummaryOptions = {
99 } 99 }
100 } 100 }
101 ] 101 ]
102 } 102 },
103 include: [
104 {
105 model: ActorImageModel,
106 as: 'Banner',
107 required: false
108 }
109 ]
103 }, 110 },
104 { 111 {
105 model: AccountModel, 112 model: AccountModel,
@@ -130,7 +137,8 @@ export type SummaryOptions = {
130 required: false 137 required: false
131 }, 138 },
132 { 139 {
133 model: AvatarModel.unscoped(), 140 model: ActorImageModel.unscoped(),
141 as: 'Avatar',
134 required: false 142 required: false
135 } 143 }
136 ] 144 ]
@@ -167,6 +175,20 @@ export type SummaryOptions = {
167 ActorModel 175 ActorModel
168 ] 176 ]
169 }, 177 },
178 [ScopeNames.WITH_ACTOR_BANNER]: {
179 include: [
180 {
181 model: ActorModel,
182 include: [
183 {
184 model: ActorImageModel,
185 required: false,
186 as: 'Banner'
187 }
188 ]
189 }
190 ]
191 },
170 [ScopeNames.WITH_VIDEOS]: { 192 [ScopeNames.WITH_VIDEOS]: {
171 include: [ 193 include: [
172 VideoModel 194 VideoModel
@@ -316,6 +338,47 @@ export class VideoChannelModel extends Model {
316 return VideoChannelModel.count(query) 338 return VideoChannelModel.count(query)
317 } 339 }
318 340
341 static async getStats () {
342
343 function getActiveVideoChannels (days: number) {
344 const options = {
345 type: QueryTypes.SELECT as QueryTypes.SELECT,
346 raw: true
347 }
348
349 const query = `
350SELECT COUNT(DISTINCT("VideoChannelModel"."id")) AS "count"
351FROM "videoChannel" AS "VideoChannelModel"
352INNER JOIN "video" AS "Videos"
353ON "VideoChannelModel"."id" = "Videos"."channelId"
354AND ("Videos"."publishedAt" > Now() - interval '${days}d')
355INNER JOIN "account" AS "Account"
356ON "VideoChannelModel"."accountId" = "Account"."id"
357INNER JOIN "actor" AS "Account->Actor"
358ON "Account"."actorId" = "Account->Actor"."id"
359AND "Account->Actor"."serverId" IS NULL
360LEFT OUTER JOIN "server" AS "Account->Actor->Server"
361ON "Account->Actor"."serverId" = "Account->Actor->Server"."id"`
362
363 return VideoChannelModel.sequelize.query<{ count: string }>(query, options)
364 .then(r => parseInt(r[0].count, 10))
365 }
366
367 const totalLocalVideoChannels = await VideoChannelModel.count()
368 const totalLocalDailyActiveVideoChannels = await getActiveVideoChannels(1)
369 const totalLocalWeeklyActiveVideoChannels = await getActiveVideoChannels(7)
370 const totalLocalMonthlyActiveVideoChannels = await getActiveVideoChannels(30)
371 const totalHalfYearActiveVideoChannels = await getActiveVideoChannels(180)
372
373 return {
374 totalLocalVideoChannels,
375 totalLocalDailyActiveVideoChannels,
376 totalLocalWeeklyActiveVideoChannels,
377 totalLocalMonthlyActiveVideoChannels,
378 totalHalfYearActiveVideoChannels
379 }
380 }
381
319 static listForApi (parameters: { 382 static listForApi (parameters: {
320 actorId: number 383 actorId: number
321 start: number 384 start: number
@@ -441,7 +504,7 @@ export class VideoChannelModel extends Model {
441 where 504 where
442 } 505 }
443 506
444 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR ] 507 const scopes: string | ScopeOptions | (string | ScopeOptions)[] = [ ScopeNames.WITH_ACTOR_BANNER ]
445 508
446 if (options.withStats === true) { 509 if (options.withStats === true) {
447 scopes.push({ 510 scopes.push({
@@ -457,32 +520,13 @@ export class VideoChannelModel extends Model {
457 }) 520 })
458 } 521 }
459 522
460 static loadByIdAndPopulateAccount (id: number): Promise<MChannelAccountDefault> { 523 static loadAndPopulateAccount (id: number): Promise<MChannelBannerAccountDefault> {
461 return VideoChannelModel.unscoped() 524 return VideoChannelModel.unscoped()
462 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 525 .scope([ ScopeNames.WITH_ACTOR_BANNER, ScopeNames.WITH_ACCOUNT ])
463 .findByPk(id) 526 .findByPk(id)
464 } 527 }
465 528
466 static loadByIdAndAccount (id: number, accountId: number): Promise<MChannelAccountDefault> { 529 static loadByUrlAndPopulateAccount (url: string): Promise<MChannelBannerAccountDefault> {
467 const query = {
468 where: {
469 id,
470 accountId
471 }
472 }
473
474 return VideoChannelModel.unscoped()
475 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
476 .findOne(query)
477 }
478
479 static loadAndPopulateAccount (id: number): Promise<MChannelAccountDefault> {
480 return VideoChannelModel.unscoped()
481 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ])
482 .findByPk(id)
483 }
484
485 static loadByUrlAndPopulateAccount (url: string): Promise<MChannelAccountDefault> {
486 const query = { 530 const query = {
487 include: [ 531 include: [
488 { 532 {
@@ -490,7 +534,14 @@ export class VideoChannelModel extends Model {
490 required: true, 534 required: true,
491 where: { 535 where: {
492 url 536 url
493 } 537 },
538 include: [
539 {
540 model: ActorImageModel,
541 required: false,
542 as: 'Banner'
543 }
544 ]
494 } 545 }
495 ] 546 ]
496 } 547 }
@@ -508,7 +559,7 @@ export class VideoChannelModel extends Model {
508 return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host) 559 return VideoChannelModel.loadByNameAndHostAndPopulateAccount(name, host)
509 } 560 }
510 561
511 static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelAccountDefault> { 562 static loadLocalByNameAndPopulateAccount (name: string): Promise<MChannelBannerAccountDefault> {
512 const query = { 563 const query = {
513 include: [ 564 include: [
514 { 565 {
@@ -517,17 +568,24 @@ export class VideoChannelModel extends Model {
517 where: { 568 where: {
518 preferredUsername: name, 569 preferredUsername: name,
519 serverId: null 570 serverId: null
520 } 571 },
572 include: [
573 {
574 model: ActorImageModel,
575 required: false,
576 as: 'Banner'
577 }
578 ]
521 } 579 }
522 ] 580 ]
523 } 581 }
524 582
525 return VideoChannelModel.unscoped() 583 return VideoChannelModel.unscoped()
526 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 584 .scope([ ScopeNames.WITH_ACCOUNT ])
527 .findOne(query) 585 .findOne(query)
528 } 586 }
529 587
530 static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelAccountDefault> { 588 static loadByNameAndHostAndPopulateAccount (name: string, host: string): Promise<MChannelBannerAccountDefault> {
531 const query = { 589 const query = {
532 include: [ 590 include: [
533 { 591 {
@@ -541,6 +599,11 @@ export class VideoChannelModel extends Model {
541 model: ServerModel, 599 model: ServerModel,
542 required: true, 600 required: true,
543 where: { host } 601 where: { host }
602 },
603 {
604 model: ActorImageModel,
605 required: false,
606 as: 'Banner'
544 } 607 }
545 ] 608 ]
546 } 609 }
@@ -548,22 +611,10 @@ export class VideoChannelModel extends Model {
548 } 611 }
549 612
550 return VideoChannelModel.unscoped() 613 return VideoChannelModel.unscoped()
551 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT ]) 614 .scope([ ScopeNames.WITH_ACCOUNT ])
552 .findOne(query) 615 .findOne(query)
553 } 616 }
554 617
555 static loadAndPopulateAccountAndVideos (id: number): Promise<MChannelActorAccountDefaultVideos> {
556 const options = {
557 include: [
558 VideoModel
559 ]
560 }
561
562 return VideoChannelModel.unscoped()
563 .scope([ ScopeNames.WITH_ACTOR, ScopeNames.WITH_ACCOUNT, ScopeNames.WITH_VIDEOS ])
564 .findByPk(id, options)
565 }
566
567 toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary { 618 toFormattedSummaryJSON (this: MChannelSummaryFormattable): VideoChannelSummary {
568 const actor = this.Actor.toFormattedSummaryJSON() 619 const actor = this.Actor.toFormattedSummaryJSON()
569 620
diff --git a/server/models/video/video-playlist.ts b/server/models/video/video-playlist.ts
index 49a406608..efe5be36d 100644
--- a/server/models/video/video-playlist.ts
+++ b/server/models/video/video-playlist.ts
@@ -54,6 +54,7 @@ import { buildServerIdsFollowedBy, buildWhereIdOrUUID, getPlaylistSort, isOutdat
54import { ThumbnailModel } from './thumbnail' 54import { ThumbnailModel } from './thumbnail'
55import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel' 55import { ScopeNames as VideoChannelScopeNames, VideoChannelModel } from './video-channel'
56import { VideoPlaylistElementModel } from './video-playlist-element' 56import { VideoPlaylistElementModel } from './video-playlist-element'
57import { ActorModel } from '../activitypub/actor'
57 58
58enum ScopeNames { 59enum ScopeNames {
59 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST', 60 AVAILABLE_FOR_LIST = 'AVAILABLE_FOR_LIST',
@@ -65,7 +66,7 @@ enum ScopeNames {
65} 66}
66 67
67type AvailableForListOptions = { 68type AvailableForListOptions = {
68 followerActorId: number 69 followerActorId?: number
69 type?: VideoPlaylistType 70 type?: VideoPlaylistType
70 accountId?: number 71 accountId?: number
71 videoChannelId?: number 72 videoChannelId?: number
@@ -134,20 +135,26 @@ type AvailableForListOptions = {
134 privacy: VideoPlaylistPrivacy.PUBLIC 135 privacy: VideoPlaylistPrivacy.PUBLIC
135 }) 136 })
136 137
137 // Only list local playlists OR playlists that are on an instance followed by actorId 138 // Only list local playlists
138 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId) 139 const whereActorOr: WhereOptions[] = [
140 {
141 serverId: null
142 }
143 ]
139 144
140 whereActor = { 145 // … OR playlists that are on an instance followed by actorId
141 [Op.or]: [ 146 if (options.followerActorId) {
142 { 147 const inQueryInstanceFollow = buildServerIdsFollowedBy(options.followerActorId)
143 serverId: null 148
144 }, 149 whereActorOr.push({
145 { 150 serverId: {
146 serverId: { 151 [Op.in]: literal(inQueryInstanceFollow)
147 [Op.in]: literal(inQueryInstanceFollow)
148 }
149 } 152 }
150 ] 153 })
154 }
155
156 whereActor = {
157 [Op.or]: whereActorOr
151 } 158 }
152 } 159 }
153 160
@@ -495,6 +502,33 @@ export class VideoPlaylistModel extends Model {
495 return '/video-playlists/embed/' + this.uuid 502 return '/video-playlists/embed/' + this.uuid
496 } 503 }
497 504
505 static async getStats () {
506 const totalLocalPlaylists = await VideoPlaylistModel.count({
507 include: [
508 {
509 model: AccountModel,
510 required: true,
511 include: [
512 {
513 model: ActorModel,
514 required: true,
515 where: {
516 serverId: null
517 }
518 }
519 ]
520 }
521 ],
522 where: {
523 privacy: VideoPlaylistPrivacy.PUBLIC
524 }
525 })
526
527 return {
528 totalLocalPlaylists
529 }
530 }
531
498 setAsRefreshed () { 532 setAsRefreshed () {
499 this.changed('updatedAt', true) 533 this.changed('updatedAt', true)
500 534
diff --git a/server/models/video/video-query-builder.ts b/server/models/video/video-query-builder.ts
index 96df0a7f8..4d95ddee2 100644
--- a/server/models/video/video-query-builder.ts
+++ b/server/models/video/video-query-builder.ts
@@ -490,12 +490,13 @@ function wrapForAPIResults (baseQuery: string, replacements: any, options: Build
490 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"', 490 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"',
491 491
492 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"', 492 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"',
493 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Actor->Avatar" ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"', 493 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' +
494 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"',
494 495
495 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' + 496 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' +
496 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"', 497 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"',
497 498
498 'LEFT OUTER JOIN "avatar" AS "VideoChannel->Account->Actor->Avatar" ' + 499 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' +
499 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"', 500 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"',
500 501
501 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"' 502 'LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"'
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 3c4f3d3df..422bf6deb 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -24,7 +24,6 @@ import {
24 Table, 24 Table,
25 UpdatedAt 25 UpdatedAt
26} from 'sequelize-typescript' 26} from 'sequelize-typescript'
27import { v4 as uuidv4 } from 'uuid'
28import { buildNSFWFilter } from '@server/helpers/express-utils' 27import { buildNSFWFilter } from '@server/helpers/express-utils'
29import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video' 28import { getPrivaciesForFederation, isPrivacyForFederation, isStateForFederation } from '@server/helpers/video'
30import { LiveManager } from '@server/lib/live-manager' 29import { LiveManager } from '@server/lib/live-manager'
@@ -100,10 +99,10 @@ import { MVideoFile, MVideoFileStreamingPlaylistVideo } from '../../types/models
100import { VideoAbuseModel } from '../abuse/video-abuse' 99import { VideoAbuseModel } from '../abuse/video-abuse'
101import { AccountModel } from '../account/account' 100import { AccountModel } from '../account/account'
102import { AccountVideoRateModel } from '../account/account-video-rate' 101import { AccountVideoRateModel } from '../account/account-video-rate'
102import { ActorImageModel } from '../account/actor-image'
103import { UserModel } from '../account/user' 103import { UserModel } from '../account/user'
104import { UserVideoHistoryModel } from '../account/user-video-history' 104import { UserVideoHistoryModel } from '../account/user-video-history'
105import { ActorModel } from '../activitypub/actor' 105import { ActorModel } from '../activitypub/actor'
106import { AvatarModel } from '../avatar/avatar'
107import { VideoRedundancyModel } from '../redundancy/video-redundancy' 106import { VideoRedundancyModel } from '../redundancy/video-redundancy'
108import { ServerModel } from '../server/server' 107import { ServerModel } from '../server/server'
109import { TrackerModel } from '../server/tracker' 108import { TrackerModel } from '../server/tracker'
@@ -286,7 +285,8 @@ export type AvailableForListIDsOptions = {
286 required: false 285 required: false
287 }, 286 },
288 { 287 {
289 model: AvatarModel.unscoped(), 288 model: ActorImageModel.unscoped(),
289 as: 'Avatar',
290 required: false 290 required: false
291 } 291 }
292 ] 292 ]
@@ -308,7 +308,8 @@ export type AvailableForListIDsOptions = {
308 required: false 308 required: false
309 }, 309 },
310 { 310 {
311 model: AvatarModel.unscoped(), 311 model: ActorImageModel.unscoped(),
312 as: 'Avatar',
312 required: false 313 required: false
313 } 314 }
314 ] 315 ]
@@ -1703,7 +1704,7 @@ export class VideoModel extends Model {
1703 1704
1704 function buildActor (rowActor: any) { 1705 function buildActor (rowActor: any) {
1705 const avatarModel = rowActor.Avatar.id !== null 1706 const avatarModel = rowActor.Avatar.id !== null
1706 ? new AvatarModel(pick(rowActor.Avatar, avatarKeys), buildOpts) 1707 ? new ActorImageModel(pick(rowActor.Avatar, avatarKeys), buildOpts)
1707 : null 1708 : null
1708 1709
1709 const serverModel = rowActor.Server.id !== null 1710 const serverModel = rowActor.Server.id !== null
@@ -1869,20 +1870,12 @@ export class VideoModel extends Model {
1869 this.Thumbnails.push(savedThumbnail) 1870 this.Thumbnails.push(savedThumbnail)
1870 } 1871 }
1871 1872
1872 generateThumbnailName () {
1873 return uuidv4() + '.jpg'
1874 }
1875
1876 getMiniature () { 1873 getMiniature () {
1877 if (Array.isArray(this.Thumbnails) === false) return undefined 1874 if (Array.isArray(this.Thumbnails) === false) return undefined
1878 1875
1879 return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE) 1876 return this.Thumbnails.find(t => t.type === ThumbnailType.MINIATURE)
1880 } 1877 }
1881 1878
1882 generatePreviewName () {
1883 return uuidv4() + '.jpg'
1884 }
1885
1886 hasPreview () { 1879 hasPreview () {
1887 return !!this.getPreview() 1880 return !!this.getPreview()
1888 } 1881 }
@@ -2034,9 +2027,11 @@ export class VideoModel extends Model {
2034 } 2027 }
2035 2028
2036 setAsRefreshed () { 2029 setAsRefreshed () {
2037 this.changed('updatedAt', true) 2030 const options = {
2031 where: { id: this.id }
2032 }
2038 2033
2039 return this.save() 2034 return VideoModel.update({ updatedAt: new Date() }, options)
2040 } 2035 }
2041 2036
2042 requiresAuth () { 2037 requiresAuth () {