diff options
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 @@ | |||
1 | import { logger } from '@server/helpers/logger' | ||
2 | import { Sequelize, QueryTypes } from 'sequelize' | ||
3 | |||
4 | export 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 @@ | |||
1 | import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder' | ||
2 | import { VideoAttributes } from './video-attributes' | ||
3 | import { VideoModelBuilder } from './video-model-builder' | ||
4 | |||
5 | export 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 @@ | |||
1 | import { QueryTypes, Sequelize, Transaction } from 'sequelize' | ||
2 | import { logger } from '@server/helpers/logger' | ||
3 | |||
4 | export 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 @@ | |||
1 | export 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 @@ | |||
1 | import { pick } from 'lodash' | ||
2 | import { AccountModel } from '@server/models/account/account' | ||
3 | import { ActorModel } from '@server/models/actor/actor' | ||
4 | import { ActorImageModel } from '@server/models/actor/actor-image' | ||
5 | import { VideoRedundancyModel } from '@server/models/redundancy/video-redundancy' | ||
6 | import { ServerModel } from '@server/models/server/server' | ||
7 | import { TrackerModel } from '@server/models/server/tracker' | ||
8 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' | ||
9 | import { ScheduleVideoUpdateModel } from '../../schedule-video-update' | ||
10 | import { TagModel } from '../../tag' | ||
11 | import { ThumbnailModel } from '../../thumbnail' | ||
12 | import { VideoModel } from '../../video' | ||
13 | import { VideoBlacklistModel } from '../../video-blacklist' | ||
14 | import { VideoChannelModel } from '../../video-channel' | ||
15 | import { VideoFileModel } from '../../video-file' | ||
16 | import { VideoLiveModel } from '../../video-live' | ||
17 | import { VideoStreamingPlaylistModel } from '../../video-streaming-playlist' | ||
18 | import { VideoAttributes } from './video-attributes' | ||
19 | |||
20 | export 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 @@ | |||
1 | import { pick } from 'lodash' | ||
2 | import { AccountModel } from '@server/models/account/account' | ||
3 | import { ActorModel } from '@server/models/actor/actor' | ||
4 | import { ActorImageModel } from '@server/models/actor/actor-image' | ||
5 | import { ServerModel } from '@server/models/server/server' | ||
6 | import { UserVideoHistoryModel } from '@server/models/user/user-video-history' | ||
7 | import { ThumbnailModel } from '../thumbnail' | ||
8 | import { VideoModel } from '../video' | ||
9 | import { VideoChannelModel } from '../video-channel' | ||
10 | import { VideoFileModel } from '../video-file' | ||
11 | import { VideoStreamingPlaylistModel } from '../video-streaming-playlist' | ||
12 | |||
13 | function 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 | |||
160 | export { | ||
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 @@ | |||
1 | import { Sequelize, Transaction } from 'sequelize' | ||
2 | import validator from 'validator' | ||
3 | import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder' | ||
4 | |||
5 | export type BuildVideoGetQueryOptions = { | ||
6 | id: number | string | ||
7 | transaction?: Transaction | ||
8 | userId?: number | ||
9 | forGetAPI?: boolean | ||
10 | } | ||
11 | |||
12 | export 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' | |||
4 | import { buildDirectionAndField, createSafeIn } from '@server/models/utils' | 4 | import { buildDirectionAndField, createSafeIn } from '@server/models/utils' |
5 | import { MUserAccountId, MUserId } from '@server/types/models' | 5 | import { MUserAccountId, MUserId } from '@server/types/models' |
6 | import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models' | 6 | import { VideoFilter, VideoPrivacy, VideoState } from '@shared/models' |
7 | import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder' | 7 | import { AbstractVideosQueryBuilder } from './shared/abstract-videos-query-builder' |
8 | 8 | ||
9 | export type BuildVideosListQueryOptions = { | 9 | export type BuildVideosListQueryOptions = { |
10 | attributes?: string[] | 10 | attributes?: string[] |
@@ -57,11 +57,12 @@ export type BuildVideosListQueryOptions = { | |||
57 | } | 57 | } |
58 | 58 | ||
59 | export class VideosIdListQueryBuilder extends AbstractVideosQueryBuilder { | 59 | export 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 | |||
2 | import { MUserId } from '@server/types/models' | ||
3 | import { Sequelize } from 'sequelize' | 1 | import { Sequelize } from 'sequelize' |
4 | import { AbstractVideosQueryBuilder } from './abstract-videos-query-builder' | 2 | import { AbstractVideosModelQueryBuilder } from './shared/abstract-videos-model-query-builder' |
5 | import { buildVideosFromRows } from './video-model-builder' | ||
6 | import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './videos-id-list-query-builder' | 3 | import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './videos-id-list-query-builder' |
7 | 4 | ||
8 | export class VideosModelListQueryBuilder extends AbstractVideosQueryBuilder { | 5 | export 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' |
120 | import { ScheduleVideoUpdateModel } from './schedule-video-update' | 120 | import { ScheduleVideoUpdateModel } from './schedule-video-update' |
121 | import { VideosModelGetQueryBuilder } from './sql/video-model-get-query-builder' | ||
121 | import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder' | 122 | import { BuildVideosListQueryOptions, VideosIdListQueryBuilder } from './sql/videos-id-list-query-builder' |
122 | import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder' | 123 | import { VideosModelListQueryBuilder } from './sql/videos-model-list-query-builder' |
123 | import { TagModel } from './tag' | 124 | import { 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 () { |