aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/models
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2021-06-10 14:43:55 +0200
committerChocobozzz <me@florianbigard.com>2021-06-10 15:26:18 +0200
commitd9bf974f5df787bbeaab5b04949ca91a2b3ca2a3 (patch)
treeaa02ee0cc28c845432e91da43b1e6de2a2f04039 /server/models
parente5dbd5084e7ae91ce118c0bccd5b84c47b88c55f (diff)
downloadPeerTube-d9bf974f5df787bbeaab5b04949ca91a2b3ca2a3.tar.gz
PeerTube-d9bf974f5df787bbeaab5b04949ca91a2b3ca2a3.tar.zst
PeerTube-d9bf974f5df787bbeaab5b04949ca91a2b3ca2a3.zip
Use raw SQL for video get request
Diffstat (limited to 'server/models')
-rw-r--r--server/models/video/sql/abstract-videos-query-builder.ts15
-rw-r--r--server/models/video/sql/shared/abstract-videos-model-query-builder.ts239
-rw-r--r--server/models/video/sql/shared/abstract-videos-query-builder.ts22
-rw-r--r--server/models/video/sql/shared/video-attributes.ts247
-rw-r--r--server/models/video/sql/shared/video-model-builder.ts268
-rw-r--r--server/models/video/sql/video-model-builder.ts162
-rw-r--r--server/models/video/sql/video-model-get-query-builder.ts86
-rw-r--r--server/models/video/sql/videos-id-list-query-builder.ts7
-rw-r--r--server/models/video/sql/videos-model-list-query-builder.ts189
-rw-r--r--server/models/video/video.ts29
10 files changed, 876 insertions, 388 deletions
diff --git a/server/models/video/sql/abstract-videos-query-builder.ts b/server/models/video/sql/abstract-videos-query-builder.ts
deleted file mode 100644
index 597a02af7..000000000
--- a/server/models/video/sql/abstract-videos-query-builder.ts
+++ /dev/null
@@ -1,15 +0,0 @@
1import { logger } from '@server/helpers/logger'
2import { Sequelize, QueryTypes } from 'sequelize'
3
4export class AbstractVideosQueryBuilder {
5 protected sequelize: Sequelize
6
7 protected query: string
8 protected replacements: any = {}
9
10 protected runQuery (nest?: boolean) {
11 logger.info('Running video query.', { query: this.query, replacements: this.replacements })
12
13 return this.sequelize.query<any>(this.query, { replacements: this.replacements, type: QueryTypes.SELECT, nest })
14 }
15}
diff --git a/server/models/video/sql/shared/abstract-videos-model-query-builder.ts b/server/models/video/sql/shared/abstract-videos-model-query-builder.ts
new file mode 100644
index 000000000..bdf926cbe
--- /dev/null
+++ b/server/models/video/sql/shared/abstract-videos-model-query-builder.ts
@@ -0,0 +1,239 @@
1import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder'
2import { VideoAttributes } from './video-attributes'
3import { VideoModelBuilder } from './video-model-builder'
4
5export class AbstractVideosModelQueryBuilder extends AbstractVideosQueryBuilder {
6 protected attributes: { [key: string]: string } = {}
7 protected joins: string[] = []
8
9 protected videoAttributes: VideoAttributes
10 protected videoModelBuilder: VideoModelBuilder
11
12 constructor (private readonly mode: 'list' | 'get') {
13 super()
14
15 this.videoAttributes = new VideoAttributes(this.mode)
16 this.videoModelBuilder = new VideoModelBuilder(this.mode, this.videoAttributes)
17 }
18
19 protected buildSelect () {
20 return 'SELECT ' + Object.keys(this.attributes).map(key => {
21 const value = this.attributes[key]
22 if (value) return `${key} AS ${value}`
23
24 return key
25 }).join(', ')
26 }
27
28 protected includeChannels () {
29 this.joins.push(
30 'INNER JOIN "videoChannel" AS "VideoChannel" ON "video"."channelId" = "VideoChannel"."id"',
31 'INNER JOIN "actor" AS "VideoChannel->Actor" ON "VideoChannel"."actorId" = "VideoChannel->Actor"."id"',
32
33 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"',
34
35 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' +
36 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"'
37 )
38
39 this.attributes = {
40 ...this.attributes,
41
42 ...this.buildAttributesObject('VideoChannel', this.videoAttributes.getChannelAttributes()),
43 ...this.buildActorInclude('VideoChannel->Actor'),
44 ...this.buildAvatarInclude('VideoChannel->Actor->Avatar'),
45 ...this.buildServerInclude('VideoChannel->Actor->Server')
46 }
47 }
48
49 protected includeAccounts () {
50 this.joins.push(
51 'INNER JOIN "account" AS "VideoChannel->Account" ON "VideoChannel"."accountId" = "VideoChannel->Account"."id"',
52 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"',
53
54 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' +
55 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"',
56
57 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' +
58 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"'
59 )
60
61 this.attributes = {
62 ...this.attributes,
63
64 ...this.buildAttributesObject('VideoChannel->Account', this.videoAttributes.getAccountAttributes()),
65 ...this.buildActorInclude('VideoChannel->Account->Actor'),
66 ...this.buildAvatarInclude('VideoChannel->Account->Actor->Avatar'),
67 ...this.buildServerInclude('VideoChannel->Account->Actor->Server')
68 }
69 }
70
71 protected includeThumbnails () {
72 this.joins.push('LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"')
73
74 this.attributes = {
75 ...this.attributes,
76
77 ...this.buildAttributesObject('Thumbnails', this.videoAttributes.getThumbnailAttributes())
78 }
79 }
80
81 protected includeFiles () {
82 this.joins.push(
83 'LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"',
84
85 'LEFT JOIN "videoStreamingPlaylist" AS "VideoStreamingPlaylists" ON "VideoStreamingPlaylists"."videoId" = "video"."id"',
86
87 'LEFT JOIN "videoFile" AS "VideoStreamingPlaylists->VideoFiles" ' +
88 'ON "VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId" = "VideoStreamingPlaylists"."id"'
89 )
90
91 this.attributes = {
92 ...this.attributes,
93
94 ...this.buildAttributesObject('VideoFiles', this.videoAttributes.getFileAttributes()),
95
96 ...this.buildAttributesObject('VideoStreamingPlaylists', this.videoAttributes.getStreamingPlaylistAttributes()),
97 ...this.buildAttributesObject('VideoStreamingPlaylists->VideoFiles', this.videoAttributes.getFileAttributes())
98 }
99 }
100
101 protected includeUserHistory (userId: number) {
102 this.joins.push(
103 'LEFT OUTER JOIN "userVideoHistory" ' +
104 'ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :userVideoHistoryId'
105 )
106
107 this.replacements.userVideoHistoryId = userId
108
109 this.attributes = {
110 ...this.attributes,
111
112 ...this.buildAttributesObject('userVideoHistory', this.videoAttributes.getUserHistoryAttributes())
113 }
114 }
115
116 protected includePlaylist (playlistId: number) {
117 this.joins.push(
118 'INNER JOIN "videoPlaylistElement" as "VideoPlaylistElement" ON "videoPlaylistElement"."videoId" = "video"."id" ' +
119 'AND "VideoPlaylistElement"."videoPlaylistId" = :videoPlaylistId'
120 )
121
122 this.replacements.videoPlaylistId = playlistId
123
124 this.attributes = {
125 ...this.attributes,
126
127 ...this.buildAttributesObject('VideoPlaylistElement', this.videoAttributes.getPlaylistAttributes())
128 }
129 }
130
131 protected includeTags () {
132 this.joins.push(
133 'LEFT OUTER JOIN (' +
134 '"videoTag" AS "Tags->VideoTagModel" INNER JOIN "tag" AS "Tags" ON "Tags"."id" = "Tags->VideoTagModel"."tagId"' +
135 ') ' +
136 'ON "video"."id" = "Tags->VideoTagModel"."videoId"'
137 )
138
139 this.attributes = {
140 ...this.attributes,
141
142 ...this.buildAttributesObject('Tags', this.videoAttributes.getTagAttributes()),
143 ...this.buildAttributesObject('Tags->VideoTagModel', this.videoAttributes.getVideoTagAttributes())
144 }
145 }
146
147 protected includeBlacklisted () {
148 this.joins.push(
149 'LEFT OUTER JOIN "videoBlacklist" AS "VideoBlacklist" ON "video"."id" = "VideoBlacklist"."videoId"'
150 )
151
152 this.attributes = {
153 ...this.attributes,
154
155 ...this.buildAttributesObject('VideoBlacklist', this.videoAttributes.getBlacklistedAttributes())
156 }
157 }
158
159 protected includeScheduleUpdate () {
160 this.joins.push(
161 'LEFT OUTER JOIN "scheduleVideoUpdate" AS "ScheduleVideoUpdate" ON "video"."id" = "ScheduleVideoUpdate"."videoId"'
162 )
163
164 this.attributes = {
165 ...this.attributes,
166
167 ...this.buildAttributesObject('ScheduleVideoUpdate', this.videoAttributes.getScheduleUpdateAttributes())
168 }
169 }
170
171 protected includeLive () {
172 this.joins.push(
173 'LEFT OUTER JOIN "videoLive" AS "VideoLive" ON "video"."id" = "VideoLive"."videoId"'
174 )
175
176 this.attributes = {
177 ...this.attributes,
178
179 ...this.buildAttributesObject('VideoLive', this.videoAttributes.getLiveAttributes())
180 }
181 }
182
183 protected includeTrackers () {
184 this.joins.push(
185 'LEFT OUTER JOIN (' +
186 '"videoTracker" AS "Trackers->VideoTrackerModel" ' +
187 'INNER JOIN "tracker" AS "Trackers" ON "Trackers"."id" = "Trackers->VideoTrackerModel"."trackerId"' +
188 ') ON "video"."id" = "Trackers->VideoTrackerModel"."videoId"'
189 )
190
191 this.attributes = {
192 ...this.attributes,
193
194 ...this.buildAttributesObject('Trackers', this.videoAttributes.getTrackerAttributes()),
195 ...this.buildAttributesObject('Trackers->VideoTrackerModel', this.videoAttributes.getVideoTrackerAttributes())
196 }
197 }
198
199 protected includeRedundancies () {
200 this.joins.push(
201 'LEFT OUTER JOIN "videoRedundancy" AS "VideoStreamingPlaylists->RedundancyVideos" ' +
202 'ON "VideoStreamingPlaylists"."id" = "VideoStreamingPlaylists->RedundancyVideos"."videoStreamingPlaylistId"',
203
204 'LEFT OUTER JOIN "videoRedundancy" AS "VideoFiles->RedundancyVideos" ON ' +
205 '"VideoFiles"."id" = "VideoFiles->RedundancyVideos"."videoFileId"'
206 )
207
208 this.attributes = {
209 ...this.attributes,
210
211 ...this.buildAttributesObject('VideoFiles->RedundancyVideos', this.videoAttributes.getRedundancyAttributes()),
212 ...this.buildAttributesObject('VideoStreamingPlaylists->RedundancyVideos', this.videoAttributes.getRedundancyAttributes())
213 }
214 }
215
216 protected buildActorInclude (prefixKey: string) {
217 return this.buildAttributesObject(prefixKey, this.videoAttributes.getActorAttributes())
218 }
219
220 protected buildAvatarInclude (prefixKey: string) {
221 return this.buildAttributesObject(prefixKey, this.videoAttributes.getAvatarAttributes())
222 }
223
224 protected buildServerInclude (prefixKey: string) {
225 return this.buildAttributesObject(prefixKey, this.videoAttributes.getServerAttributes())
226 }
227
228 protected buildAttributesObject (prefixKey: string, attributeKeys: string[]) {
229 const result: { [id: string]: string} = {}
230
231 const prefixValue = prefixKey.replace(/->/g, '.')
232
233 for (const attribute of attributeKeys) {
234 result[`"${prefixKey}"."${attribute}"`] = `"${prefixValue}.${attribute}"`
235 }
236
237 return result
238 }
239}
diff --git a/server/models/video/sql/shared/abstract-videos-query-builder.ts b/server/models/video/sql/shared/abstract-videos-query-builder.ts
new file mode 100644
index 000000000..01694e691
--- /dev/null
+++ b/server/models/video/sql/shared/abstract-videos-query-builder.ts
@@ -0,0 +1,22 @@
1import { QueryTypes, Sequelize, Transaction } from 'sequelize'
2import { logger } from '@server/helpers/logger'
3
4export class AbstractVideosQueryBuilder {
5 protected sequelize: Sequelize
6
7 protected query: string
8 protected replacements: any = {}
9
10 protected runQuery (transaction?: Transaction, nest?: boolean) {
11 logger.debug('Running videos query.', { query: this.query, replacements: this.replacements })
12
13 const options = {
14 transaction,
15 replacements: this.replacements,
16 type: QueryTypes.SELECT as QueryTypes.SELECT,
17 nest
18 }
19
20 return this.sequelize.query<any>(this.query, options)
21 }
22}
diff --git a/server/models/video/sql/shared/video-attributes.ts b/server/models/video/sql/shared/video-attributes.ts
new file mode 100644
index 000000000..1a1650dc7
--- /dev/null
+++ b/server/models/video/sql/shared/video-attributes.ts
@@ -0,0 +1,247 @@
1export class VideoAttributes {
2
3 constructor (readonly mode: 'get' | 'list') {
4
5 }
6
7 getChannelAttributes () {
8 let attributeKeys = [
9 'id',
10 'name',
11 'description',
12 'actorId'
13 ]
14
15 if (this.mode === 'get') {
16 attributeKeys = attributeKeys.concat([
17 'support',
18 'createdAt',
19 'updatedAt'
20 ])
21 }
22
23 return attributeKeys
24 }
25
26 getAccountAttributes () {
27 let attributeKeys = [ 'id', 'name', 'actorId' ]
28
29 if (this.mode === 'get') {
30 attributeKeys = attributeKeys.concat([
31 'description',
32 'createdAt',
33 'updatedAt'
34 ])
35 }
36
37 return attributeKeys
38 }
39
40 getThumbnailAttributes () {
41 let attributeKeys = [ 'id', 'type', 'filename' ]
42
43 if (this.mode === 'get') {
44 attributeKeys = attributeKeys.concat([
45 'height',
46 'width',
47 'fileUrl',
48 'automaticallyGenerated',
49 'videoId',
50 'videoPlaylistId',
51 'createdAt',
52 'updatedAt'
53 ])
54 }
55
56 return attributeKeys
57 }
58
59 getFileAttributes () {
60 return [
61 'id',
62 'createdAt',
63 'updatedAt',
64 'resolution',
65 'size',
66 'extname',
67 'filename',
68 'fileUrl',
69 'torrentFilename',
70 'torrentUrl',
71 'infoHash',
72 'fps',
73 'metadataUrl',
74 'videoStreamingPlaylistId',
75 'videoId'
76 ]
77 }
78
79 getStreamingPlaylistAttributes () {
80 let playlistKeys = [ 'id', 'playlistUrl', 'type' ]
81
82 if (this.mode === 'get') {
83 playlistKeys = playlistKeys.concat([
84 'p2pMediaLoaderInfohashes',
85 'p2pMediaLoaderPeerVersion',
86 'segmentsSha256Url',
87 'videoId',
88 'createdAt',
89 'updatedAt'
90 ])
91 }
92
93 return playlistKeys
94 }
95
96 getUserHistoryAttributes () {
97 return [ 'id', 'currentTime' ]
98 }
99
100 getPlaylistAttributes () {
101 return [
102 'createdAt',
103 'updatedAt',
104 'url',
105 'position',
106 'startTimestamp',
107 'stopTimestamp',
108 'videoPlaylistId'
109 ]
110 }
111
112 getTagAttributes () {
113 return [ 'id', 'name' ]
114 }
115
116 getVideoTagAttributes () {
117 return [ 'videoId', 'tagId', 'createdAt', 'updatedAt' ]
118 }
119
120 getBlacklistedAttributes () {
121 return [ 'id', 'reason', 'unfederated' ]
122 }
123
124 getScheduleUpdateAttributes () {
125 return [
126 'id',
127 'updateAt',
128 'privacy',
129 'videoId',
130 'createdAt',
131 'updatedAt'
132 ]
133 }
134
135 getLiveAttributes () {
136 return [
137 'id',
138 'streamKey',
139 'saveReplay',
140 'permanentLive',
141 'videoId',
142 'createdAt',
143 'updatedAt'
144 ]
145 }
146
147 getTrackerAttributes () {
148 return [ 'id', 'url' ]
149 }
150
151 getVideoTrackerAttributes () {
152 return [
153 'videoId',
154 'trackerId',
155 'createdAt',
156 'updatedAt'
157 ]
158 }
159
160 getRedundancyAttributes () {
161 return [ 'id', 'fileUrl' ]
162 }
163
164 getActorAttributes () {
165 let attributeKeys = [
166 'id',
167 'preferredUsername',
168 'url',
169 'serverId',
170 'avatarId'
171 ]
172
173 if (this.mode === 'get') {
174 attributeKeys = attributeKeys.concat([
175 'type',
176 'followersCount',
177 'followingCount',
178 'inboxUrl',
179 'outboxUrl',
180 'sharedInboxUrl',
181 'followersUrl',
182 'followingUrl',
183 'remoteCreatedAt',
184 'createdAt',
185 'updatedAt'
186 ])
187 }
188
189 return attributeKeys
190 }
191
192 getAvatarAttributes () {
193 let attributeKeys = [
194 'id',
195 'filename',
196 'fileUrl',
197 'onDisk',
198 'createdAt',
199 'updatedAt'
200 ]
201
202 if (this.mode === 'get') {
203 attributeKeys = attributeKeys.concat([
204 'height',
205 'width',
206 'type'
207 ])
208 }
209
210 return attributeKeys
211 }
212
213 getServerAttributes () {
214 return [ 'id', 'host' ]
215 }
216
217 getVideoAttributes () {
218 return [
219 'id',
220 'uuid',
221 'name',
222 'category',
223 'licence',
224 'language',
225 'privacy',
226 'nsfw',
227 'description',
228 'support',
229 'duration',
230 'views',
231 'likes',
232 'dislikes',
233 'remote',
234 'isLive',
235 'url',
236 'commentsEnabled',
237 'downloadEnabled',
238 'waitTranscoding',
239 'state',
240 'publishedAt',
241 'originallyPublishedAt',
242 'channelId',
243 'createdAt',
244 'updatedAt'
245 ]
246 }
247}
diff --git a/server/models/video/sql/shared/video-model-builder.ts b/server/models/video/sql/shared/video-model-builder.ts
new file mode 100644
index 000000000..9719f6d2e
--- /dev/null
+++ b/server/models/video/sql/shared/video-model-builder.ts
@@ -0,0 +1,268 @@
1import { pick } from 'lodash'
2import { AccountModel } from '@server/models/account/account'
3import { ActorModel } from '@server/models/actor/actor'
4import { ActorImageModel } from '@server/models/actor/actor-image'
5import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy'
6import { ServerModel } from '@server/models/server/server'
7import { TrackerModel } from '@server/models/server/tracker'
8import { UserVideoHistoryModel } from '@server/models/user/user-video-history'
9import { ScheduleVideoUpdateModel } from '../../schedule-video-update'
10import { TagModel } from '../../tag'
11import { ThumbnailModel } from '../../thumbnail'
12import { VideoModel } from '../../video'
13import { VideoBlacklistModel } from '../../video-blacklist'
14import { VideoChannelModel } from '../../video-channel'
15import { VideoFileModel } from '../../video-file'
16import { VideoLiveModel } from '../../video-live'
17import { VideoStreamingPlaylistModel } from '../../video-streaming-playlist'
18import { VideoAttributes } from './video-attributes'
19
20export class VideoModelBuilder {
21 private videosMemo: { [ id: number ]: VideoModel }
22 private videoStreamingPlaylistMemo: { [ id: number ]: VideoStreamingPlaylistModel }
23 private videoFileMemo: { [ id: number ]: VideoFileModel }
24
25 private thumbnailsDone: Set<number>
26 private historyDone: Set<number>
27 private blacklistDone: Set<number>
28 private liveDone: Set<number>
29 private redundancyDone: Set<number>
30 private scheduleVideoUpdateDone: Set<number>
31
32 private trackersDone: Set<string>
33 private tagsDone: Set<string>
34
35 private videos: VideoModel[]
36
37 private readonly buildOpts = { raw: true, isNewRecord: false }
38
39 constructor (
40 readonly mode: 'get' | 'list',
41 readonly videoAttributes: VideoAttributes
42 ) {
43
44 }
45
46 buildVideosFromRows (rows: any[]) {
47 this.reinit()
48
49 for (const row of rows) {
50 this.buildVideo(row)
51
52 const videoModel = this.videosMemo[row.id]
53
54 this.setUserHistory(row, videoModel)
55 this.addThumbnail(row, videoModel)
56 this.addWebTorrentFile(row, videoModel)
57
58 this.addStreamingPlaylist(row, videoModel)
59 this.addStreamingPlaylistFile(row)
60
61 if (this.mode === 'get') {
62 this.addTag(row, videoModel)
63 this.addTracker(row, videoModel)
64 this.setBlacklisted(row, videoModel)
65 this.setScheduleVideoUpdate(row, videoModel)
66 this.setLive(row, videoModel)
67
68 if (row.VideoFiles.id) {
69 this.addRedundancy(row.VideoFiles.RedundancyVideos, this.videoFileMemo[row.VideoFiles.id])
70 }
71
72 if (row.VideoStreamingPlaylists.id) {
73 this.addRedundancy(row.VideoStreamingPlaylists.RedundancyVideos, this.videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id])
74 }
75 }
76 }
77
78 return this.videos
79 }
80
81 private reinit () {
82 this.videosMemo = {}
83 this.videoStreamingPlaylistMemo = {}
84 this.videoFileMemo = {}
85
86 this.thumbnailsDone = new Set<number>()
87 this.historyDone = new Set<number>()
88 this.blacklistDone = new Set<number>()
89 this.liveDone = new Set<number>()
90 this.redundancyDone = new Set<number>()
91 this.scheduleVideoUpdateDone = new Set<number>()
92
93 this.trackersDone = new Set<string>()
94 this.tagsDone = new Set<string>()
95
96 this.videos = []
97 }
98
99 private buildVideo (row: any) {
100 if (this.videosMemo[row.id]) return
101
102 // Build Channel
103 const channel = row.VideoChannel
104 const channelModel = new VideoChannelModel(pick(channel, this.videoAttributes.getChannelAttributes()), this.buildOpts)
105 channelModel.Actor = this.buildActor(channel.Actor)
106
107 const account = row.VideoChannel.Account
108 const accountModel = new AccountModel(pick(account, this.videoAttributes.getAccountAttributes()), this.buildOpts)
109 accountModel.Actor = this.buildActor(account.Actor)
110
111 channelModel.Account = accountModel
112
113 const videoModel = new VideoModel(pick(row, this.videoAttributes.getVideoAttributes()), this.buildOpts)
114 videoModel.VideoChannel = channelModel
115
116 this.videosMemo[row.id] = videoModel
117
118 videoModel.UserVideoHistories = []
119 videoModel.Thumbnails = []
120 videoModel.VideoFiles = []
121 videoModel.VideoStreamingPlaylists = []
122 videoModel.Tags = []
123 videoModel.Trackers = []
124
125 // Keep rows order
126 this.videos.push(videoModel)
127 }
128
129 private buildActor (rowActor: any) {
130 const avatarModel = rowActor.Avatar.id !== null
131 ? new ActorImageModel(pick(rowActor.Avatar, this.videoAttributes.getAvatarAttributes()), this.buildOpts)
132 : null
133
134 const serverModel = rowActor.Server.id !== null
135 ? new ServerModel(pick(rowActor.Server, this.videoAttributes.getServerAttributes()), this.buildOpts)
136 : null
137
138 const actorModel = new ActorModel(pick(rowActor, this.videoAttributes.getActorAttributes()), this.buildOpts)
139 actorModel.Avatar = avatarModel
140 actorModel.Server = serverModel
141
142 return actorModel
143 }
144
145 private setUserHistory (row: any, videoModel: VideoModel) {
146 if (!row.userVideoHistory?.id || this.historyDone.has(row.userVideoHistory.id)) return
147
148 const attributes = pick(row.userVideoHistory, this.videoAttributes.getUserHistoryAttributes())
149 const historyModel = new UserVideoHistoryModel(attributes, this.buildOpts)
150 videoModel.UserVideoHistories.push(historyModel)
151
152 this.historyDone.add(row.userVideoHistory.id)
153 }
154
155 private addThumbnail (row: any, videoModel: VideoModel) {
156 if (!row.Thumbnails?.id || this.thumbnailsDone.has(row.Thumbnails.id)) return
157
158 const attributes = pick(row.Thumbnails, this.videoAttributes.getThumbnailAttributes())
159 const thumbnailModel = new ThumbnailModel(attributes, this.buildOpts)
160 videoModel.Thumbnails.push(thumbnailModel)
161
162 this.thumbnailsDone.add(row.Thumbnails.id)
163 }
164
165 private addWebTorrentFile (row: any, videoModel: VideoModel) {
166 if (!row.VideoFiles?.id || this.videoFileMemo[row.VideoFiles.id]) return
167
168 const attributes = pick(row.VideoFiles, this.videoAttributes.getFileAttributes())
169 const videoFileModel = new VideoFileModel(attributes, this.buildOpts)
170 videoModel.VideoFiles.push(videoFileModel)
171
172 this.videoFileMemo[row.VideoFiles.id] = videoFileModel
173 }
174
175 private addStreamingPlaylist (row: any, videoModel: VideoModel) {
176 if (!row.VideoStreamingPlaylists?.id || this.videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]) return
177
178 const attributes = pick(row.VideoStreamingPlaylists, this.videoAttributes.getStreamingPlaylistAttributes())
179 const streamingPlaylist = new VideoStreamingPlaylistModel(attributes, this.buildOpts)
180 streamingPlaylist.VideoFiles = []
181
182 videoModel.VideoStreamingPlaylists.push(streamingPlaylist)
183
184 this.videoStreamingPlaylistMemo[streamingPlaylist.id] = streamingPlaylist
185 }
186
187 private addStreamingPlaylistFile (row: any) {
188 if (!row.VideoStreamingPlaylists?.VideoFiles?.id || this.videoFileMemo[row.VideoStreamingPlaylists.VideoFiles.id]) return
189
190 const streamingPlaylist = this.videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]
191
192 const attributes = pick(row.VideoStreamingPlaylists.VideoFiles, this.videoAttributes.getFileAttributes())
193 const videoFileModel = new VideoFileModel(attributes, this.buildOpts)
194 streamingPlaylist.VideoFiles.push(videoFileModel)
195
196 this.videoFileMemo[row.VideoStreamingPlaylists.VideoFiles.id] = videoFileModel
197 }
198
199 private addRedundancy (redundancyRow: any, to: VideoFileModel | VideoStreamingPlaylistModel) {
200 if (!to.RedundancyVideos) to.RedundancyVideos = []
201
202 if (!redundancyRow?.id || this.redundancyDone.has(redundancyRow.id)) return
203
204 const attributes = pick(redundancyRow, this.videoAttributes.getRedundancyAttributes())
205 const redundancyModel = new VideoRedundancyModel(attributes, this.buildOpts)
206 to.RedundancyVideos.push(redundancyModel)
207
208 this.redundancyDone.add(redundancyRow.id)
209 }
210
211 private addTag (row: any, videoModel: VideoModel) {
212 if (!row.Tags?.name) return
213 const association = row.Tags.VideoTagModel
214
215 const key = `${association.videoId}-${association.tagId}`
216 if (this.tagsDone.has(key)) return
217
218 const attributes = pick(row.Tags, this.videoAttributes.getTagAttributes())
219 const tagModel = new TagModel(attributes, this.buildOpts)
220 videoModel.Tags.push(tagModel)
221
222 this.tagsDone.add(key)
223 }
224
225 private addTracker (row: any, videoModel: VideoModel) {
226 if (!row.Trackers?.id) return
227 const association = row.Trackers.VideoTrackerModel
228
229 const key = `${association.videoId}-${association.trackerId}`
230 if (this.trackersDone.has(key)) return
231
232 const attributes = pick(row.Trackers, this.videoAttributes.getTrackerAttributes())
233 const trackerModel = new TrackerModel(attributes, this.buildOpts)
234 videoModel.Trackers.push(trackerModel)
235
236 this.trackersDone.add(key)
237 }
238
239 private setBlacklisted (row: any, videoModel: VideoModel) {
240 if (!row.VideoBlacklist?.id) return
241 if (this.blacklistDone.has(row.VideoBlacklist.id)) return
242
243 const attributes = pick(row.VideoBlacklist, this.videoAttributes.getBlacklistedAttributes())
244 videoModel.VideoBlacklist = new VideoBlacklistModel(attributes, this.buildOpts)
245
246 this.blacklistDone.add(row.VideoBlacklist.id)
247 }
248
249 private setScheduleVideoUpdate (row: any, videoModel: VideoModel) {
250 if (!row.ScheduleVideoUpdate?.id) return
251 if (this.scheduleVideoUpdateDone.has(row.ScheduleVideoUpdate.id)) return
252
253 const attributes = pick(row.ScheduleVideoUpdate, this.videoAttributes.getScheduleUpdateAttributes())
254 videoModel.ScheduleVideoUpdate = new ScheduleVideoUpdateModel(attributes, this.buildOpts)
255
256 this.scheduleVideoUpdateDone.add(row.ScheduleVideoUpdate.id)
257 }
258
259 private setLive (row: any, videoModel: VideoModel) {
260 if (!row.VideoLive?.id) return
261 if (this.liveDone.has(row.VideoLive.id)) return
262
263 const attributes = pick(row.VideoLive, this.videoAttributes.getLiveAttributes())
264 videoModel.VideoLive = new VideoLiveModel(attributes, this.buildOpts)
265
266 this.liveDone.add(row.ScheduleVideoUpdate.id)
267 }
268}
diff --git a/server/models/video/sql/video-model-builder.ts b/server/models/video/sql/video-model-builder.ts
deleted file mode 100644
index c428312fe..000000000
--- a/server/models/video/sql/video-model-builder.ts
+++ /dev/null
@@ -1,162 +0,0 @@
1import { pick } from 'lodash'
2import { AccountModel } from '@server/models/account/account'
3import { ActorModel } from '@server/models/actor/actor'
4import { ActorImageModel } from '@server/models/actor/actor-image'
5import { ServerModel } from '@server/models/server/server'
6import { UserVideoHistoryModel } from '@server/models/user/user-video-history'
7import { ThumbnailModel } from '../thumbnail'
8import { VideoModel } from '../video'
9import { VideoChannelModel } from '../video-channel'
10import { VideoFileModel } from '../video-file'
11import { VideoStreamingPlaylistModel } from '../video-streaming-playlist'
12
13function buildVideosFromRows (rows: any[]) {
14 const videosMemo: { [ id: number ]: VideoModel } = {}
15 const videoStreamingPlaylistMemo: { [ id: number ]: VideoStreamingPlaylistModel } = {}
16
17 const thumbnailsDone = new Set<number>()
18 const historyDone = new Set<number>()
19 const videoFilesDone = new Set<number>()
20
21 const videos: VideoModel[] = []
22
23 const avatarKeys = [ 'id', 'filename', 'fileUrl', 'onDisk', 'createdAt', 'updatedAt' ]
24 const actorKeys = [ 'id', 'preferredUsername', 'url', 'serverId', 'avatarId' ]
25 const serverKeys = [ 'id', 'host' ]
26 const videoFileKeys = [
27 'id',
28 'createdAt',
29 'updatedAt',
30 'resolution',
31 'size',
32 'extname',
33 'filename',
34 'fileUrl',
35 'torrentFilename',
36 'torrentUrl',
37 'infoHash',
38 'fps',
39 'videoId',
40 'videoStreamingPlaylistId'
41 ]
42 const videoStreamingPlaylistKeys = [ 'id', 'type', 'playlistUrl' ]
43 const videoKeys = [
44 'id',
45 'uuid',
46 'name',
47 'category',
48 'licence',
49 'language',
50 'privacy',
51 'nsfw',
52 'description',
53 'support',
54 'duration',
55 'views',
56 'likes',
57 'dislikes',
58 'remote',
59 'isLive',
60 'url',
61 'commentsEnabled',
62 'downloadEnabled',
63 'waitTranscoding',
64 'state',
65 'publishedAt',
66 'originallyPublishedAt',
67 'channelId',
68 'createdAt',
69 'updatedAt'
70 ]
71 const buildOpts = { raw: true }
72
73 function buildActor (rowActor: any) {
74 const avatarModel = rowActor.Avatar.id !== null
75 ? new ActorImageModel(pick(rowActor.Avatar, avatarKeys), buildOpts)
76 : null
77
78 const serverModel = rowActor.Server.id !== null
79 ? new ServerModel(pick(rowActor.Server, serverKeys), buildOpts)
80 : null
81
82 const actorModel = new ActorModel(pick(rowActor, actorKeys), buildOpts)
83 actorModel.Avatar = avatarModel
84 actorModel.Server = serverModel
85
86 return actorModel
87 }
88
89 for (const row of rows) {
90 if (!videosMemo[row.id]) {
91 // Build Channel
92 const channel = row.VideoChannel
93 const channelModel = new VideoChannelModel(pick(channel, [ 'id', 'name', 'description', 'actorId' ]), buildOpts)
94 channelModel.Actor = buildActor(channel.Actor)
95
96 const account = row.VideoChannel.Account
97 const accountModel = new AccountModel(pick(account, [ 'id', 'name' ]), buildOpts)
98 accountModel.Actor = buildActor(account.Actor)
99
100 channelModel.Account = accountModel
101
102 const videoModel = new VideoModel(pick(row, videoKeys), buildOpts)
103 videoModel.VideoChannel = channelModel
104
105 videoModel.UserVideoHistories = []
106 videoModel.Thumbnails = []
107 videoModel.VideoFiles = []
108 videoModel.VideoStreamingPlaylists = []
109
110 videosMemo[row.id] = videoModel
111 // Don't take object value to have a sorted array
112 videos.push(videoModel)
113 }
114
115 const videoModel = videosMemo[row.id]
116
117 if (row.userVideoHistory?.id && !historyDone.has(row.userVideoHistory.id)) {
118 const historyModel = new UserVideoHistoryModel(pick(row.userVideoHistory, [ 'id', 'currentTime' ]), buildOpts)
119 videoModel.UserVideoHistories.push(historyModel)
120
121 historyDone.add(row.userVideoHistory.id)
122 }
123
124 if (row.Thumbnails?.id && !thumbnailsDone.has(row.Thumbnails.id)) {
125 const thumbnailModel = new ThumbnailModel(pick(row.Thumbnails, [ 'id', 'type', 'filename' ]), buildOpts)
126 videoModel.Thumbnails.push(thumbnailModel)
127
128 thumbnailsDone.add(row.Thumbnails.id)
129 }
130
131 if (row.VideoFiles?.id && !videoFilesDone.has(row.VideoFiles.id)) {
132 const videoFileModel = new VideoFileModel(pick(row.VideoFiles, videoFileKeys), buildOpts)
133 videoModel.VideoFiles.push(videoFileModel)
134
135 videoFilesDone.add(row.VideoFiles.id)
136 }
137
138 if (row.VideoStreamingPlaylists?.id && !videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]) {
139 const streamingPlaylist = new VideoStreamingPlaylistModel(pick(row.VideoStreamingPlaylists, videoStreamingPlaylistKeys), buildOpts)
140 streamingPlaylist.VideoFiles = []
141
142 videoModel.VideoStreamingPlaylists.push(streamingPlaylist)
143
144 videoStreamingPlaylistMemo[streamingPlaylist.id] = streamingPlaylist
145 }
146
147 if (row.VideoStreamingPlaylists?.VideoFiles?.id && !videoFilesDone.has(row.VideoStreamingPlaylists.VideoFiles.id)) {
148 const streamingPlaylist = videoStreamingPlaylistMemo[row.VideoStreamingPlaylists.id]
149
150 const videoFileModel = new VideoFileModel(pick(row.VideoStreamingPlaylists.VideoFiles, videoFileKeys), buildOpts)
151 streamingPlaylist.VideoFiles.push(videoFileModel)
152
153 videoFilesDone.add(row.VideoStreamingPlaylists.VideoFiles.id)
154 }
155 }
156
157 return videos
158}
159
160export {
161 buildVideosFromRows
162}
diff --git a/server/models/video/sql/video-model-get-query-builder.ts b/server/models/video/sql/video-model-get-query-builder.ts
new file mode 100644
index 000000000..0a3723e63
--- /dev/null
+++ b/server/models/video/sql/video-model-get-query-builder.ts
@@ -0,0 +1,86 @@
1import { Sequelize, Transaction } from 'sequelize'
2import validator from 'validator'
3import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder'
4
5export type BuildVideoGetQueryOptions = {
6 id: number | string
7 transaction?: Transaction
8 userId?: number
9 forGetAPI?: boolean
10}
11
12export class VideosModelGetQueryBuilder extends AbstractVideosModelQueryBuilder {
13 protected attributes: { [key: string]: string }
14 protected joins: string[] = []
15 protected where: string
16
17 constructor (protected readonly sequelize: Sequelize) {
18 super('get')
19 }
20
21 queryVideos (options: BuildVideoGetQueryOptions) {
22 this.buildGetQuery(options)
23
24 return this.runQuery(options.transaction, true).then(rows => {
25 const videos = this.videoModelBuilder.buildVideosFromRows(rows)
26
27 if (videos.length > 1) {
28 throw new Error('Video results is more than ')
29 }
30
31 if (videos.length === 0) return null
32 return videos[0]
33 })
34 }
35
36 private buildGetQuery (options: BuildVideoGetQueryOptions) {
37 this.attributes = {
38 '"video".*': ''
39 }
40
41 this.includeChannels()
42 this.includeAccounts()
43
44 this.includeTags()
45
46 this.includeThumbnails()
47
48 this.includeFiles()
49
50 this.includeBlacklisted()
51
52 this.includeScheduleUpdate()
53
54 this.includeLive()
55
56 if (options.userId) {
57 this.includeUserHistory(options.userId)
58 }
59
60 if (options.forGetAPI === true) {
61 this.includeTrackers()
62 this.includeRedundancies()
63 }
64
65 this.whereId(options.id)
66
67 const select = this.buildSelect()
68 const order = this.buildOrder()
69
70 this.query = `${select} FROM "video" ${this.joins.join(' ')} ${this.where} ${order}`
71 }
72
73 private whereId (id: string | number) {
74 if (validator.isInt('' + id)) {
75 this.where = 'WHERE "video".id = :videoId'
76 } else {
77 this.where = 'WHERE uuid = :videoId'
78 }
79
80 this.replacements.videoId = id
81 }
82
83 private buildOrder () {
84 return 'ORDER BY "Tags"."name" ASC'
85 }
86}
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts
index 7bb942ea4..6e0d97d9e 100644
--- a/server/models/video/sql/videos-id-list-query-builder.ts
+++ b/server/models/video/sql/videos-id-list-query-builder.ts
@@ -4,7 +4,7 @@ import { exists } from '@server/helpers/custom-validators/misc'
4import { buildDirectionAndField, createSafeIn } from '@server/models/utils' 4import { buildDirectionAndField, createSafeIn } from '@server/models/utils'
5import { MUserAccountId, MUserId } from '@server/types/models' 5import { MUserAccountId, MUserId } from '@server/types/models'
6import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models' 6import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models'
7import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder' 7import { AbstractVideosQueryBuilder } from './shared/abstract-videos-query-builder'
8 8
9export type BuildVideosListQueryOptions = { 9export type BuildVideosListQueryOptions = {
10 attributes?: string[] 10 attributes?: string[]
@@ -57,11 +57,12 @@ export type BuildVideosListQueryOptions = {
57} 57}
58 58
59export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder { 59export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder {
60 protected replacements: any = {}
61
60 private attributes: string[] 62 private attributes: string[]
63 private joins: string[] = []
61 64
62 protected replacements: any = {}
63 private readonly and: string[] = [] 65 private readonly and: string[] = []
64 private joins: string[] = []
65 66
66 private readonly cte: string[] = [] 67 private readonly cte: string[] = []
67 68
diff --git a/server/models/video/sql/videos-model-list-query-builder.ts b/server/models/video/sql/videos-model-list-query-builder.ts
index 4ba9dd878..38b9c91d0 100644
--- a/server/models/video/sql/videos-model-list-query-builder.ts
+++ b/server/models/video/sql/videos-model-list-query-builder.ts
@@ -1,27 +1,23 @@
1
2import { MUserId } from '@server/types/models'
3import { Sequelize } from 'sequelize' 1import { Sequelize } from 'sequelize'
4import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder' 2import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder'
5import { buildVideosFromRows } from './video-model-builder'
6import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './videos-id-list-query-builder' 3import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './videos-id-list-query-builder'
7 4
8export class VideosModelListQueryBuilder extends AbstractVideosQueryBuilder { 5export class VideosModelListQueryBuilder extends AbstractVideosModelQueryBuilder {
9 private attributes: { [key: string]: string } 6 protected attributes: { [key: string]: string }
10 7 protected joins: string[] = []
11 private joins: string[] = []
12 8
13 private innerQuery: string 9 private innerQuery: string
14 private innerSort: string 10 private innerSort: string
15 11
16 constructor (protected readonly sequelize: Sequelize) { 12 constructor (protected readonly sequelize: Sequelize) {
17 super() 13 super('list')
18 } 14 }
19 15
20 queryVideos (options: BuildVideosListQueryOptions) { 16 queryVideos (options: BuildVideosListQueryOptions) {
21 this.buildInnerQuery(options) 17 this.buildInnerQuery(options)
22 this.buildListQueryFromIdsQuery(options) 18 this.buildListQueryFromIdsQuery(options)
23 19
24 return this.runQuery(true).then(rows => buildVideosFromRows(rows)) 20 return this.runQuery(undefined, true).then(rows => this.videoModelBuilder.buildVideosFromRows(rows))
25 } 21 }
26 22
27 private buildInnerQuery (options: BuildVideosListQueryOptions) { 23 private buildInnerQuery (options: BuildVideosListQueryOptions) {
@@ -49,7 +45,7 @@ export class VideosModelListQueryBuilder extends AbstractVideosQueryBuilder {
49 } 45 }
50 46
51 if (options.user) { 47 if (options.user) {
52 this.includeUserHistory(options.user) 48 this.includeUserHistory(options.user.id)
53 } 49 }
54 50
55 if (options.videoPlaylistId) { 51 if (options.videoPlaylistId) {
@@ -60,175 +56,4 @@ export class VideosModelListQueryBuilder extends AbstractVideosQueryBuilder {
60 56
61 this.query = `${select} FROM (${this.innerQuery}) AS "tmp" ${this.joins.join(' ')} ${this.innerSort}` 57 this.query = `${select} FROM (${this.innerQuery}) AS "tmp" ${this.joins.join(' ')} ${this.innerSort}`
62 } 58 }
63
64 private includeChannels () {
65 this.attributes = {
66 ...this.attributes,
67
68 '"VideoChannel"."id"': '"VideoChannel.id"',
69 '"VideoChannel"."name"': '"VideoChannel.name"',
70 '"VideoChannel"."description"': '"VideoChannel.description"',
71 '"VideoChannel"."actorId"': '"VideoChannel.actorId"',
72 '"VideoChannel->Actor"."id"': '"VideoChannel.Actor.id"',
73 '"VideoChannel->Actor"."preferredUsername"': '"VideoChannel.Actor.preferredUsername"',
74 '"VideoChannel->Actor"."url"': '"VideoChannel.Actor.url"',
75 '"VideoChannel->Actor"."serverId"': '"VideoChannel.Actor.serverId"',
76 '"VideoChannel->Actor"."avatarId"': '"VideoChannel.Actor.avatarId"',
77 '"VideoChannel->Actor->Server"."id"': '"VideoChannel.Actor.Server.id"',
78 '"VideoChannel->Actor->Server"."host"': '"VideoChannel.Actor.Server.host"',
79 '"VideoChannel->Actor->Avatar"."id"': '"VideoChannel.Actor.Avatar.id"',
80 '"VideoChannel->Actor->Avatar"."filename"': '"VideoChannel.Actor.Avatar.filename"',
81 '"VideoChannel->Actor->Avatar"."fileUrl"': '"VideoChannel.Actor.Avatar.fileUrl"',
82 '"VideoChannel->Actor->Avatar"."onDisk"': '"VideoChannel.Actor.Avatar.onDisk"',
83 '"VideoChannel->Actor->Avatar"."createdAt"': '"VideoChannel.Actor.Avatar.createdAt"',
84 '"VideoChannel->Actor->Avatar"."updatedAt"': '"VideoChannel.Actor.Avatar.updatedAt"'
85 }
86
87 this.joins = this.joins.concat([
88 'INNER JOIN "videoChannel" AS "VideoChannel" ON "video"."channelId" = "VideoChannel"."id"',
89 'INNER JOIN "actor" AS "VideoChannel->Actor" ON "VideoChannel"."actorId" = "VideoChannel->Actor"."id"',
90
91 'LEFT OUTER JOIN "server" AS "VideoChannel->Actor->Server" ON "VideoChannel->Actor"."serverId" = "VideoChannel->Actor->Server"."id"',
92 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Actor->Avatar" ' +
93 'ON "VideoChannel->Actor"."avatarId" = "VideoChannel->Actor->Avatar"."id"'
94 ])
95 }
96
97 private includeAccounts () {
98 this.attributes = {
99 ...this.attributes,
100
101 '"VideoChannel->Account"."id"': '"VideoChannel.Account.id"',
102 '"VideoChannel->Account"."name"': '"VideoChannel.Account.name"',
103 '"VideoChannel->Account->Actor"."id"': '"VideoChannel.Account.Actor.id"',
104 '"VideoChannel->Account->Actor"."preferredUsername"': '"VideoChannel.Account.Actor.preferredUsername"',
105 '"VideoChannel->Account->Actor"."url"': '"VideoChannel.Account.Actor.url"',
106 '"VideoChannel->Account->Actor"."serverId"': '"VideoChannel.Account.Actor.serverId"',
107 '"VideoChannel->Account->Actor"."avatarId"': '"VideoChannel.Account.Actor.avatarId"',
108 '"VideoChannel->Account->Actor->Server"."id"': '"VideoChannel.Account.Actor.Server.id"',
109 '"VideoChannel->Account->Actor->Server"."host"': '"VideoChannel.Account.Actor.Server.host"',
110 '"VideoChannel->Account->Actor->Avatar"."id"': '"VideoChannel.Account.Actor.Avatar.id"',
111 '"VideoChannel->Account->Actor->Avatar"."filename"': '"VideoChannel.Account.Actor.Avatar.filename"',
112 '"VideoChannel->Account->Actor->Avatar"."fileUrl"': '"VideoChannel.Account.Actor.Avatar.fileUrl"',
113 '"VideoChannel->Account->Actor->Avatar"."onDisk"': '"VideoChannel.Account.Actor.Avatar.onDisk"',
114 '"VideoChannel->Account->Actor->Avatar"."createdAt"': '"VideoChannel.Account.Actor.Avatar.createdAt"',
115 '"VideoChannel->Account->Actor->Avatar"."updatedAt"': '"VideoChannel.Account.Actor.Avatar.updatedAt"'
116 }
117
118 this.joins = this.joins.concat([
119 'INNER JOIN "account" AS "VideoChannel->Account" ON "VideoChannel"."accountId" = "VideoChannel->Account"."id"',
120 'INNER JOIN "actor" AS "VideoChannel->Account->Actor" ON "VideoChannel->Account"."actorId" = "VideoChannel->Account->Actor"."id"',
121
122 'LEFT OUTER JOIN "server" AS "VideoChannel->Account->Actor->Server" ' +
123 'ON "VideoChannel->Account->Actor"."serverId" = "VideoChannel->Account->Actor->Server"."id"',
124
125 'LEFT OUTER JOIN "actorImage" AS "VideoChannel->Account->Actor->Avatar" ' +
126 'ON "VideoChannel->Account->Actor"."avatarId" = "VideoChannel->Account->Actor->Avatar"."id"'
127 ])
128 }
129
130 private includeThumbnails () {
131 this.attributes = {
132 ...this.attributes,
133
134 '"Thumbnails"."id"': '"Thumbnails.id"',
135 '"Thumbnails"."type"': '"Thumbnails.type"',
136 '"Thumbnails"."filename"': '"Thumbnails.filename"'
137 }
138
139 this.joins.push('LEFT OUTER JOIN "thumbnail" AS "Thumbnails" ON "video"."id" = "Thumbnails"."videoId"')
140 }
141
142 private includeFiles () {
143 this.attributes = {
144 ...this.attributes,
145
146 '"VideoFiles"."id"': '"VideoFiles.id"',
147 '"VideoFiles"."createdAt"': '"VideoFiles.createdAt"',
148 '"VideoFiles"."updatedAt"': '"VideoFiles.updatedAt"',
149 '"VideoFiles"."resolution"': '"VideoFiles.resolution"',
150 '"VideoFiles"."size"': '"VideoFiles.size"',
151 '"VideoFiles"."extname"': '"VideoFiles.extname"',
152 '"VideoFiles"."filename"': '"VideoFiles.filename"',
153 '"VideoFiles"."fileUrl"': '"VideoFiles.fileUrl"',
154 '"VideoFiles"."torrentFilename"': '"VideoFiles.torrentFilename"',
155 '"VideoFiles"."torrentUrl"': '"VideoFiles.torrentUrl"',
156 '"VideoFiles"."infoHash"': '"VideoFiles.infoHash"',
157 '"VideoFiles"."fps"': '"VideoFiles.fps"',
158 '"VideoFiles"."videoId"': '"VideoFiles.videoId"',
159
160 '"VideoStreamingPlaylists"."id"': '"VideoStreamingPlaylists.id"',
161 '"VideoStreamingPlaylists"."playlistUrl"': '"VideoStreamingPlaylists.playlistUrl"',
162 '"VideoStreamingPlaylists"."type"': '"VideoStreamingPlaylists.type"',
163 '"VideoStreamingPlaylists->VideoFiles"."id"': '"VideoStreamingPlaylists.VideoFiles.id"',
164 '"VideoStreamingPlaylists->VideoFiles"."createdAt"': '"VideoStreamingPlaylists.VideoFiles.createdAt"',
165 '"VideoStreamingPlaylists->VideoFiles"."updatedAt"': '"VideoStreamingPlaylists.VideoFiles.updatedAt"',
166 '"VideoStreamingPlaylists->VideoFiles"."resolution"': '"VideoStreamingPlaylists.VideoFiles.resolution"',
167 '"VideoStreamingPlaylists->VideoFiles"."size"': '"VideoStreamingPlaylists.VideoFiles.size"',
168 '"VideoStreamingPlaylists->VideoFiles"."extname"': '"VideoStreamingPlaylists.VideoFiles.extname"',
169 '"VideoStreamingPlaylists->VideoFiles"."filename"': '"VideoStreamingPlaylists.VideoFiles.filename"',
170 '"VideoStreamingPlaylists->VideoFiles"."fileUrl"': '"VideoStreamingPlaylists.VideoFiles.fileUrl"',
171 '"VideoStreamingPlaylists->VideoFiles"."torrentFilename"': '"VideoStreamingPlaylists.VideoFiles.torrentFilename"',
172 '"VideoStreamingPlaylists->VideoFiles"."torrentUrl"': '"VideoStreamingPlaylists.VideoFiles.torrentUrl"',
173 '"VideoStreamingPlaylists->VideoFiles"."infoHash"': '"VideoStreamingPlaylists.VideoFiles.infoHash"',
174 '"VideoStreamingPlaylists->VideoFiles"."fps"': '"VideoStreamingPlaylists.VideoFiles.fps"',
175 '"VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId"': '"VideoStreamingPlaylists.VideoFiles.videoStreamingPlaylistId"',
176 '"VideoStreamingPlaylists->VideoFiles"."videoId"': '"VideoStreamingPlaylists.VideoFiles.videoId"'
177 }
178
179 this.joins = this.joins.concat([
180 'LEFT JOIN "videoFile" AS "VideoFiles" ON "VideoFiles"."videoId" = "video"."id"',
181
182 'LEFT JOIN "videoStreamingPlaylist" AS "VideoStreamingPlaylists" ON "VideoStreamingPlaylists"."videoId" = "video"."id"',
183
184 'LEFT JOIN "videoFile" AS "VideoStreamingPlaylists->VideoFiles" ' +
185 'ON "VideoStreamingPlaylists->VideoFiles"."videoStreamingPlaylistId" = "VideoStreamingPlaylists"."id"'
186 ])
187 }
188
189 private includeUserHistory (user: MUserId) {
190 this.attributes = {
191 ...this.attributes,
192
193 '"userVideoHistory"."id"': '"userVideoHistory.id"',
194 '"userVideoHistory"."currentTime"': '"userVideoHistory.currentTime"'
195 }
196
197 this.joins.push(
198 'LEFT OUTER JOIN "userVideoHistory" ' +
199 'ON "video"."id" = "userVideoHistory"."videoId" AND "userVideoHistory"."userId" = :userVideoHistoryId'
200 )
201
202 this.replacements.userVideoHistoryId = user.id
203 }
204
205 private includePlaylist (playlistId: number) {
206 this.attributes = {
207 ...this.attributes,
208
209 '"VideoPlaylistElement"."createdAt"': '"VideoPlaylistElement.createdAt"',
210 '"VideoPlaylistElement"."updatedAt"': '"VideoPlaylistElement.updatedAt"',
211 '"VideoPlaylistElement"."url"': '"VideoPlaylistElement.url"',
212 '"VideoPlaylistElement"."position"': '"VideoPlaylistElement.position"',
213 '"VideoPlaylistElement"."startTimestamp"': '"VideoPlaylistElement.startTimestamp"',
214 '"VideoPlaylistElement"."stopTimestamp"': '"VideoPlaylistElement.stopTimestamp"',
215 '"VideoPlaylistElement"."videoPlaylistId"': '"VideoPlaylistElement.videoPlaylistId"'
216 }
217
218 this.joins.push(
219 'INNER JOIN "videoPlaylistElement" as "VideoPlaylistElement" ON "videoPlaylistElement"."videoId" = "video"."id" ' +
220 'AND "VideoPlaylistElement"."videoPlaylistId" = :videoPlaylistId'
221 )
222
223 this.replacements.videoPlaylistId = playlistId
224 }
225
226 private buildSelect () {
227 return 'SELECT ' + Object.keys(this.attributes).map(key => {
228 const value = this.attributes[key]
229 if (value) return `${key} AS ${value}`
230
231 return key
232 }).join(', ')
233 }
234} 59}
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 4979cee50..9d56eb13c 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -118,6 +118,7 @@ import {
118 videoModelToFormattedJSON 118 videoModelToFormattedJSON
119} from './formatter/video-format-utils' 119} from './formatter/video-format-utils'
120import { ScheduleVideoUpdateModel } from './schedule-video-update' 120import { ScheduleVideoUpdateModel } from './schedule-video-update'
121import { VideosModelGetQueryBuilder } from './sql/video-model-get-query-builder'
121import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder' 122import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder'
122import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder' 123import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder'
123import { TagModel } from './tag' 124import { TagModel } from './tag'
@@ -1475,33 +1476,9 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1475 userId?: number 1476 userId?: number
1476 }): Promise<MVideoDetails> { 1477 }): Promise<MVideoDetails> {
1477 const { id, t, userId } = parameters 1478 const { id, t, userId } = parameters
1478 const where = buildWhereIdOrUUID(id) 1479 const queryBuilder = new VideosModelGetQueryBuilder(VideoModel.sequelize)
1479
1480 const options = {
1481 order: [ [ 'Tags', 'name', 'ASC' ] ] as any, // FIXME: sequelize typings
1482 where,
1483 transaction: t
1484 }
1485 1480
1486 const scopes: (string | ScopeOptions)[] = [ 1481 return queryBuilder.queryVideos({ id, transaction: t, forGetAPI: true, userId })
1487 ScopeNames.WITH_TAGS,
1488 ScopeNames.WITH_BLACKLISTED,
1489 ScopeNames.WITH_ACCOUNT_DETAILS,
1490 ScopeNames.WITH_SCHEDULED_UPDATE,
1491 ScopeNames.WITH_THUMBNAILS,
1492 ScopeNames.WITH_LIVE,
1493 ScopeNames.WITH_TRACKERS,
1494 { method: [ ScopeNames.WITH_WEBTORRENT_FILES, true ] },
1495 { method: [ ScopeNames.WITH_STREAMING_PLAYLISTS, true ] }
1496 ]
1497
1498 if (userId) {
1499 scopes.push({ method: [ ScopeNames.WITH_USER_HISTORY, userId ] })
1500 }
1501
1502 return VideoModel
1503 .scope(scopes)
1504 .findOne(options)
1505 } 1482 }
1506 1483
1507 static async getStats () { 1484 static async getStats () {