aboutsummaryrefslogtreecommitdiffhomepage
path: root/server/controllers
diff options
context:
space:
mode:
Diffstat (limited to 'server/controllers')
-rw-r--r--server/controllers/api/clients.js8
-rw-r--r--server/controllers/api/index.js8
-rw-r--r--server/controllers/api/pods.js34
-rw-r--r--server/controllers/api/remote.js86
-rw-r--r--server/controllers/api/remote/index.js16
-rw-r--r--server/controllers/api/remote/videos.js328
-rw-r--r--server/controllers/api/requests.js10
-rw-r--r--server/controllers/api/users.js72
-rw-r--r--server/controllers/api/videos.js382
-rw-r--r--server/controllers/client.js11
10 files changed, 694 insertions, 261 deletions
diff --git a/server/controllers/api/clients.js b/server/controllers/api/clients.js
index 7755f6c2b..cf83cb835 100644
--- a/server/controllers/api/clients.js
+++ b/server/controllers/api/clients.js
@@ -1,13 +1,11 @@
1'use strict' 1'use strict'
2 2
3const express = require('express') 3const express = require('express')
4const mongoose = require('mongoose')
5 4
6const constants = require('../../initializers/constants') 5const constants = require('../../initializers/constants')
6const db = require('../../initializers/database')
7const logger = require('../../helpers/logger') 7const logger = require('../../helpers/logger')
8 8
9const Client = mongoose.model('OAuthClient')
10
11const router = express.Router() 9const router = express.Router()
12 10
13router.get('/local', getLocalClient) 11router.get('/local', getLocalClient)
@@ -27,12 +25,12 @@ function getLocalClient (req, res, next) {
27 return res.type('json').status(403).end() 25 return res.type('json').status(403).end()
28 } 26 }
29 27
30 Client.loadFirstClient(function (err, client) { 28 db.OAuthClient.loadFirstClient(function (err, client) {
31 if (err) return next(err) 29 if (err) return next(err)
32 if (!client) return next(new Error('No client available.')) 30 if (!client) return next(new Error('No client available.'))
33 31
34 res.json({ 32 res.json({
35 client_id: client._id, 33 client_id: client.clientId,
36 client_secret: client.clientSecret 34 client_secret: client.clientSecret
37 }) 35 })
38 }) 36 })
diff --git a/server/controllers/api/index.js b/server/controllers/api/index.js
index 4cb65ed55..f13ff922c 100644
--- a/server/controllers/api/index.js
+++ b/server/controllers/api/index.js
@@ -2,6 +2,8 @@
2 2
3const express = require('express') 3const express = require('express')
4 4
5const utils = require('../../helpers/utils')
6
5const router = express.Router() 7const router = express.Router()
6 8
7const clientsController = require('./clients') 9const clientsController = require('./clients')
@@ -18,7 +20,7 @@ router.use('/requests', requestsController)
18router.use('/users', usersController) 20router.use('/users', usersController)
19router.use('/videos', videosController) 21router.use('/videos', videosController)
20router.use('/ping', pong) 22router.use('/ping', pong)
21router.use('/*', badRequest) 23router.use('/*', utils.badRequest)
22 24
23// --------------------------------------------------------------------------- 25// ---------------------------------------------------------------------------
24 26
@@ -29,7 +31,3 @@ module.exports = router
29function pong (req, res, next) { 31function pong (req, res, next) {
30 return res.send('pong').status(200).end() 32 return res.send('pong').status(200).end()
31} 33}
32
33function badRequest (req, res, next) {
34 res.type('json').status(400).end()
35}
diff --git a/server/controllers/api/pods.js b/server/controllers/api/pods.js
index 7857fcee0..38702face 100644
--- a/server/controllers/api/pods.js
+++ b/server/controllers/api/pods.js
@@ -1,10 +1,11 @@
1'use strict' 1'use strict'
2 2
3const express = require('express') 3const express = require('express')
4const mongoose = require('mongoose')
5const waterfall = require('async/waterfall') 4const waterfall = require('async/waterfall')
6 5
6const db = require('../../initializers/database')
7const logger = require('../../helpers/logger') 7const logger = require('../../helpers/logger')
8const utils = require('../../helpers/utils')
8const friends = require('../../lib/friends') 9const friends = require('../../lib/friends')
9const middlewares = require('../../middlewares') 10const middlewares = require('../../middlewares')
10const admin = middlewares.admin 11const admin = middlewares.admin
@@ -15,7 +16,6 @@ const validators = middlewares.validators.pods
15const signatureValidator = middlewares.validators.remote.signature 16const signatureValidator = middlewares.validators.remote.signature
16 17
17const router = express.Router() 18const router = express.Router()
18const Pod = mongoose.model('Pod')
19 19
20router.get('/', listPods) 20router.get('/', listPods)
21router.post('/', 21router.post('/',
@@ -37,7 +37,7 @@ router.get('/quitfriends',
37) 37)
38// Post because this is a secured request 38// Post because this is a secured request
39router.post('/remove', 39router.post('/remove',
40 signatureValidator, 40 signatureValidator.signature,
41 checkSignature, 41 checkSignature,
42 removePods 42 removePods
43) 43)
@@ -53,15 +53,15 @@ function addPods (req, res, next) {
53 53
54 waterfall([ 54 waterfall([
55 function addPod (callback) { 55 function addPod (callback) {
56 const pod = new Pod(informations) 56 const pod = db.Pod.build(informations)
57 pod.save(function (err, podCreated) { 57 pod.save().asCallback(function (err, podCreated) {
58 // Be sure about the number of parameters for the callback 58 // Be sure about the number of parameters for the callback
59 return callback(err, podCreated) 59 return callback(err, podCreated)
60 }) 60 })
61 }, 61 },
62 62
63 function sendMyVideos (podCreated, callback) { 63 function sendMyVideos (podCreated, callback) {
64 friends.sendOwnedVideosToPod(podCreated._id) 64 friends.sendOwnedVideosToPod(podCreated.id)
65 65
66 callback(null) 66 callback(null)
67 }, 67 },
@@ -84,10 +84,10 @@ function addPods (req, res, next) {
84} 84}
85 85
86function listPods (req, res, next) { 86function listPods (req, res, next) {
87 Pod.list(function (err, podsList) { 87 db.Pod.list(function (err, podsList) {
88 if (err) return next(err) 88 if (err) return next(err)
89 89
90 res.json(getFormatedPods(podsList)) 90 res.json(utils.getFormatedObjects(podsList, podsList.length))
91 }) 91 })
92} 92}
93 93
@@ -111,11 +111,11 @@ function removePods (req, res, next) {
111 111
112 waterfall([ 112 waterfall([
113 function loadPod (callback) { 113 function loadPod (callback) {
114 Pod.loadByHost(host, callback) 114 db.Pod.loadByHost(host, callback)
115 }, 115 },
116 116
117 function removePod (pod, callback) { 117 function deletePod (pod, callback) {
118 pod.remove(callback) 118 pod.destroy().asCallback(callback)
119 } 119 }
120 ], function (err) { 120 ], function (err) {
121 if (err) return next(err) 121 if (err) return next(err)
@@ -131,15 +131,3 @@ function quitFriends (req, res, next) {
131 res.type('json').status(204).end() 131 res.type('json').status(204).end()
132 }) 132 })
133} 133}
134
135// ---------------------------------------------------------------------------
136
137function getFormatedPods (pods) {
138 const formatedPods = []
139
140 pods.forEach(function (pod) {
141 formatedPods.push(pod.toFormatedJSON())
142 })
143
144 return formatedPods
145}
diff --git a/server/controllers/api/remote.js b/server/controllers/api/remote.js
deleted file mode 100644
index f1046c534..000000000
--- a/server/controllers/api/remote.js
+++ /dev/null
@@ -1,86 +0,0 @@
1'use strict'
2
3const each = require('async/each')
4const eachSeries = require('async/eachSeries')
5const express = require('express')
6const mongoose = require('mongoose')
7
8const middlewares = require('../../middlewares')
9const secureMiddleware = middlewares.secure
10const validators = middlewares.validators.remote
11const logger = require('../../helpers/logger')
12
13const router = express.Router()
14const Video = mongoose.model('Video')
15
16router.post('/videos',
17 validators.signature,
18 secureMiddleware.checkSignature,
19 validators.remoteVideos,
20 remoteVideos
21)
22
23// ---------------------------------------------------------------------------
24
25module.exports = router
26
27// ---------------------------------------------------------------------------
28
29function remoteVideos (req, res, next) {
30 const requests = req.body.data
31 const fromHost = req.body.signature.host
32
33 // We need to process in the same order to keep consistency
34 // TODO: optimization
35 eachSeries(requests, function (request, callbackEach) {
36 const videoData = request.data
37
38 if (request.type === 'add') {
39 addRemoteVideo(videoData, fromHost, callbackEach)
40 } else if (request.type === 'remove') {
41 removeRemoteVideo(videoData, fromHost, callbackEach)
42 } else {
43 logger.error('Unkown remote request type %s.', request.type)
44 }
45 }, function (err) {
46 if (err) logger.error('Error managing remote videos.', { error: err })
47 })
48
49 // We don't need to keep the other pod waiting
50 return res.type('json').status(204).end()
51}
52
53function addRemoteVideo (videoToCreateData, fromHost, callback) {
54 logger.debug('Adding remote video "%s".', videoToCreateData.name)
55
56 const video = new Video(videoToCreateData)
57 video.podHost = fromHost
58 Video.generateThumbnailFromBase64(video, videoToCreateData.thumbnailBase64, function (err) {
59 if (err) {
60 logger.error('Cannot generate thumbnail from base 64 data.', { error: err })
61 return callback(err)
62 }
63
64 video.save(callback)
65 })
66}
67
68function removeRemoteVideo (videoToRemoveData, fromHost, callback) {
69 // We need the list because we have to remove some other stuffs (thumbnail etc)
70 Video.listByHostAndRemoteId(fromHost, videoToRemoveData.remoteId, function (err, videosList) {
71 if (err) {
72 logger.error('Cannot list videos from host and magnets.', { error: err })
73 return callback(err)
74 }
75
76 if (videosList.length === 0) {
77 logger.error('No remote video was found for this pod.', { magnetUri: videoToRemoveData.magnetUri, podHost: fromHost })
78 }
79
80 each(videosList, function (video, callbackEach) {
81 logger.debug('Removing remote video %s.', video.magnetUri)
82
83 video.remove(callbackEach)
84 }, callback)
85 })
86}
diff --git a/server/controllers/api/remote/index.js b/server/controllers/api/remote/index.js
new file mode 100644
index 000000000..2947632d5
--- /dev/null
+++ b/server/controllers/api/remote/index.js
@@ -0,0 +1,16 @@
1'use strict'
2
3const express = require('express')
4
5const utils = require('../../../helpers/utils')
6
7const router = express.Router()
8
9const videosRemoteController = require('./videos')
10
11router.use('/videos', videosRemoteController)
12router.use('/*', utils.badRequest)
13
14// ---------------------------------------------------------------------------
15
16module.exports = router
diff --git a/server/controllers/api/remote/videos.js b/server/controllers/api/remote/videos.js
new file mode 100644
index 000000000..c45a86dbb
--- /dev/null
+++ b/server/controllers/api/remote/videos.js
@@ -0,0 +1,328 @@
1'use strict'
2
3const eachSeries = require('async/eachSeries')
4const express = require('express')
5const waterfall = require('async/waterfall')
6
7const db = require('../../../initializers/database')
8const middlewares = require('../../../middlewares')
9const secureMiddleware = middlewares.secure
10const videosValidators = middlewares.validators.remote.videos
11const signatureValidators = middlewares.validators.remote.signature
12const logger = require('../../../helpers/logger')
13const utils = require('../../../helpers/utils')
14
15const router = express.Router()
16
17router.post('/',
18 signatureValidators.signature,
19 secureMiddleware.checkSignature,
20 videosValidators.remoteVideos,
21 remoteVideos
22)
23
24// ---------------------------------------------------------------------------
25
26module.exports = router
27
28// ---------------------------------------------------------------------------
29
30function remoteVideos (req, res, next) {
31 const requests = req.body.data
32 const fromPod = res.locals.secure.pod
33
34 // We need to process in the same order to keep consistency
35 // TODO: optimization
36 eachSeries(requests, function (request, callbackEach) {
37 const data = request.data
38
39 switch (request.type) {
40 case 'add':
41 addRemoteVideoRetryWrapper(data, fromPod, callbackEach)
42 break
43
44 case 'update':
45 updateRemoteVideoRetryWrapper(data, fromPod, callbackEach)
46 break
47
48 case 'remove':
49 removeRemoteVideo(data, fromPod, callbackEach)
50 break
51
52 case 'report-abuse':
53 reportAbuseRemoteVideo(data, fromPod, callbackEach)
54 break
55
56 default:
57 logger.error('Unkown remote request type %s.', request.type)
58 }
59 }, function (err) {
60 if (err) logger.error('Error managing remote videos.', { error: err })
61 })
62
63 // We don't need to keep the other pod waiting
64 return res.type('json').status(204).end()
65}
66
67// Handle retries on fail
68function addRemoteVideoRetryWrapper (videoToCreateData, fromPod, finalCallback) {
69 utils.transactionRetryer(
70 function (callback) {
71 return addRemoteVideo(videoToCreateData, fromPod, callback)
72 },
73 function (err) {
74 if (err) {
75 logger.error('Cannot insert the remote video with many retries.', { error: err })
76 }
77
78 // Do not return the error, continue the process
79 return finalCallback(null)
80 }
81 )
82}
83
84function addRemoteVideo (videoToCreateData, fromPod, finalCallback) {
85 logger.debug('Adding remote video "%s".', videoToCreateData.remoteId)
86
87 waterfall([
88
89 function startTransaction (callback) {
90 db.sequelize.transaction({ isolationLevel: 'SERIALIZABLE' }).asCallback(function (err, t) {
91 return callback(err, t)
92 })
93 },
94
95 function findOrCreateAuthor (t, callback) {
96 const name = videoToCreateData.author
97 const podId = fromPod.id
98 // This author is from another pod so we do not associate a user
99 const userId = null
100
101 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
102 return callback(err, t, authorInstance)
103 })
104 },
105
106 function findOrCreateTags (t, author, callback) {
107 const tags = videoToCreateData.tags
108
109 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
110 return callback(err, t, author, tagInstances)
111 })
112 },
113
114 function createVideoObject (t, author, tagInstances, callback) {
115 const videoData = {
116 name: videoToCreateData.name,
117 remoteId: videoToCreateData.remoteId,
118 extname: videoToCreateData.extname,
119 infoHash: videoToCreateData.infoHash,
120 description: videoToCreateData.description,
121 authorId: author.id,
122 duration: videoToCreateData.duration,
123 createdAt: videoToCreateData.createdAt,
124 // FIXME: updatedAt does not seems to be considered by Sequelize
125 updatedAt: videoToCreateData.updatedAt
126 }
127
128 const video = db.Video.build(videoData)
129
130 return callback(null, t, tagInstances, video)
131 },
132
133 function generateThumbnail (t, tagInstances, video, callback) {
134 db.Video.generateThumbnailFromData(video, videoToCreateData.thumbnailData, function (err) {
135 if (err) {
136 logger.error('Cannot generate thumbnail from data.', { error: err })
137 return callback(err)
138 }
139
140 return callback(err, t, tagInstances, video)
141 })
142 },
143
144 function insertVideoIntoDB (t, tagInstances, video, callback) {
145 const options = {
146 transaction: t
147 }
148
149 video.save(options).asCallback(function (err, videoCreated) {
150 return callback(err, t, tagInstances, videoCreated)
151 })
152 },
153
154 function associateTagsToVideo (t, tagInstances, video, callback) {
155 const options = { transaction: t }
156
157 video.setTags(tagInstances, options).asCallback(function (err) {
158 return callback(err, t)
159 })
160 }
161
162 ], function (err, t) {
163 if (err) {
164 // This is just a debug because we will retry the insert
165 logger.debug('Cannot insert the remote video.', { error: err })
166
167 // Abort transaction?
168 if (t) t.rollback()
169
170 return finalCallback(err)
171 }
172
173 // Commit transaction
174 t.commit().asCallback(function (err) {
175 if (err) return finalCallback(err)
176
177 logger.info('Remote video %s inserted.', videoToCreateData.name)
178 return finalCallback(null)
179 })
180 })
181}
182
183// Handle retries on fail
184function updateRemoteVideoRetryWrapper (videoAttributesToUpdate, fromPod, finalCallback) {
185 utils.transactionRetryer(
186 function (callback) {
187 return updateRemoteVideo(videoAttributesToUpdate, fromPod, callback)
188 },
189 function (err) {
190 if (err) {
191 logger.error('Cannot update the remote video with many retries.', { error: err })
192 }
193
194 // Do not return the error, continue the process
195 return finalCallback(null)
196 }
197 )
198}
199
200function updateRemoteVideo (videoAttributesToUpdate, fromPod, finalCallback) {
201 logger.debug('Updating remote video "%s".', videoAttributesToUpdate.remoteId)
202
203 waterfall([
204
205 function startTransaction (callback) {
206 db.sequelize.transaction({ isolationLevel: 'SERIALIZABLE' }).asCallback(function (err, t) {
207 return callback(err, t)
208 })
209 },
210
211 function findVideo (t, callback) {
212 fetchVideo(fromPod.host, videoAttributesToUpdate.remoteId, function (err, videoInstance) {
213 return callback(err, t, videoInstance)
214 })
215 },
216
217 function findOrCreateTags (t, videoInstance, callback) {
218 const tags = videoAttributesToUpdate.tags
219
220 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
221 return callback(err, t, videoInstance, tagInstances)
222 })
223 },
224
225 function updateVideoIntoDB (t, videoInstance, tagInstances, callback) {
226 const options = { transaction: t }
227
228 videoInstance.set('name', videoAttributesToUpdate.name)
229 videoInstance.set('description', videoAttributesToUpdate.description)
230 videoInstance.set('infoHash', videoAttributesToUpdate.infoHash)
231 videoInstance.set('duration', videoAttributesToUpdate.duration)
232 videoInstance.set('createdAt', videoAttributesToUpdate.createdAt)
233 videoInstance.set('updatedAt', videoAttributesToUpdate.updatedAt)
234 videoInstance.set('extname', videoAttributesToUpdate.extname)
235
236 videoInstance.save(options).asCallback(function (err) {
237 return callback(err, t, videoInstance, tagInstances)
238 })
239 },
240
241 function associateTagsToVideo (t, videoInstance, tagInstances, callback) {
242 const options = { transaction: t }
243
244 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
245 return callback(err, t)
246 })
247 }
248
249 ], function (err, t) {
250 if (err) {
251 // This is just a debug because we will retry the insert
252 logger.debug('Cannot update the remote video.', { error: err })
253
254 // Abort transaction?
255 if (t) t.rollback()
256
257 return finalCallback(err)
258 }
259
260 // Commit transaction
261 t.commit().asCallback(function (err) {
262 if (err) return finalCallback(err)
263
264 logger.info('Remote video %s updated', videoAttributesToUpdate.name)
265 return finalCallback(null)
266 })
267 })
268}
269
270function removeRemoteVideo (videoToRemoveData, fromPod, callback) {
271 // We need the instance because we have to remove some other stuffs (thumbnail etc)
272 fetchVideo(fromPod.host, videoToRemoveData.remoteId, function (err, video) {
273 // Do not return the error, continue the process
274 if (err) return callback(null)
275
276 logger.debug('Removing remote video %s.', video.remoteId)
277 video.destroy().asCallback(function (err) {
278 // Do not return the error, continue the process
279 if (err) {
280 logger.error('Cannot remove remote video with id %s.', videoToRemoveData.remoteId, { error: err })
281 }
282
283 return callback(null)
284 })
285 })
286}
287
288function reportAbuseRemoteVideo (reportData, fromPod, callback) {
289 db.Video.load(reportData.videoRemoteId, function (err, video) {
290 if (err || !video) {
291 if (!err) err = new Error('video not found')
292
293 logger.error('Cannot load video from id.', { error: err, id: reportData.videoRemoteId })
294 // Do not return the error, continue the process
295 return callback(null)
296 }
297
298 logger.debug('Reporting remote abuse for video %s.', video.id)
299
300 const videoAbuseData = {
301 reporterUsername: reportData.reporterUsername,
302 reason: reportData.reportReason,
303 reporterPodId: fromPod.id,
304 videoId: video.id
305 }
306
307 db.VideoAbuse.create(videoAbuseData).asCallback(function (err) {
308 if (err) {
309 logger.error('Cannot create remote abuse video.', { error: err })
310 }
311
312 return callback(null)
313 })
314 })
315}
316
317function fetchVideo (podHost, remoteId, callback) {
318 db.Video.loadByHostAndRemoteId(podHost, remoteId, function (err, video) {
319 if (err || !video) {
320 if (!err) err = new Error('video not found')
321
322 logger.error('Cannot load video from host and remote id.', { error: err, podHost, remoteId })
323 return callback(err)
324 }
325
326 return callback(null, video)
327 })
328}
diff --git a/server/controllers/api/requests.js b/server/controllers/api/requests.js
index 52aad6997..1f9193fc8 100644
--- a/server/controllers/api/requests.js
+++ b/server/controllers/api/requests.js
@@ -1,15 +1,13 @@
1'use strict' 1'use strict'
2 2
3const express = require('express') 3const express = require('express')
4const mongoose = require('mongoose')
5 4
6const constants = require('../../initializers/constants') 5const constants = require('../../initializers/constants')
6const db = require('../../initializers/database')
7const middlewares = require('../../middlewares') 7const middlewares = require('../../middlewares')
8const admin = middlewares.admin 8const admin = middlewares.admin
9const oAuth = middlewares.oauth 9const oAuth = middlewares.oauth
10 10
11const Request = mongoose.model('Request')
12
13const router = express.Router() 11const router = express.Router()
14 12
15router.get('/stats', 13router.get('/stats',
@@ -25,13 +23,13 @@ module.exports = router
25// --------------------------------------------------------------------------- 23// ---------------------------------------------------------------------------
26 24
27function getStatsRequests (req, res, next) { 25function getStatsRequests (req, res, next) {
28 Request.list(function (err, requests) { 26 db.Request.countTotalRequests(function (err, totalRequests) {
29 if (err) return next(err) 27 if (err) return next(err)
30 28
31 return res.json({ 29 return res.json({
32 requests: requests, 30 totalRequests: totalRequests,
33 maxRequestsInParallel: constants.REQUESTS_IN_PARALLEL, 31 maxRequestsInParallel: constants.REQUESTS_IN_PARALLEL,
34 remainingMilliSeconds: Request.remainingMilliSeconds(), 32 remainingMilliSeconds: db.Request.remainingMilliSeconds(),
35 milliSecondsInterval: constants.REQUESTS_INTERVAL 33 milliSecondsInterval: constants.REQUESTS_INTERVAL
36 }) 34 })
37 }) 35 })
diff --git a/server/controllers/api/users.js b/server/controllers/api/users.js
index b4d687312..6cd0e84f7 100644
--- a/server/controllers/api/users.js
+++ b/server/controllers/api/users.js
@@ -1,13 +1,12 @@
1'use strict' 1'use strict'
2 2
3const each = require('async/each')
4const express = require('express') 3const express = require('express')
5const mongoose = require('mongoose')
6const waterfall = require('async/waterfall') 4const waterfall = require('async/waterfall')
7 5
8const constants = require('../../initializers/constants') 6const constants = require('../../initializers/constants')
9const friends = require('../../lib/friends') 7const db = require('../../initializers/database')
10const logger = require('../../helpers/logger') 8const logger = require('../../helpers/logger')
9const utils = require('../../helpers/utils')
11const middlewares = require('../../middlewares') 10const middlewares = require('../../middlewares')
12const admin = middlewares.admin 11const admin = middlewares.admin
13const oAuth = middlewares.oauth 12const oAuth = middlewares.oauth
@@ -17,9 +16,6 @@ const validatorsPagination = middlewares.validators.pagination
17const validatorsSort = middlewares.validators.sort 16const validatorsSort = middlewares.validators.sort
18const validatorsUsers = middlewares.validators.users 17const validatorsUsers = middlewares.validators.users
19 18
20const User = mongoose.model('User')
21const Video = mongoose.model('Video')
22
23const router = express.Router() 19const router = express.Router()
24 20
25router.get('/me', oAuth.authenticate, getUserInformation) 21router.get('/me', oAuth.authenticate, getUserInformation)
@@ -62,13 +58,13 @@ module.exports = router
62// --------------------------------------------------------------------------- 58// ---------------------------------------------------------------------------
63 59
64function createUser (req, res, next) { 60function createUser (req, res, next) {
65 const user = new User({ 61 const user = db.User.build({
66 username: req.body.username, 62 username: req.body.username,
67 password: req.body.password, 63 password: req.body.password,
68 role: constants.USER_ROLES.USER 64 role: constants.USER_ROLES.USER
69 }) 65 })
70 66
71 user.save(function (err, createdUser) { 67 user.save().asCallback(function (err, createdUser) {
72 if (err) return next(err) 68 if (err) return next(err)
73 69
74 return res.type('json').status(204).end() 70 return res.type('json').status(204).end()
@@ -76,7 +72,7 @@ function createUser (req, res, next) {
76} 72}
77 73
78function getUserInformation (req, res, next) { 74function getUserInformation (req, res, next) {
79 User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) { 75 db.User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) {
80 if (err) return next(err) 76 if (err) return next(err)
81 77
82 return res.json(user.toFormatedJSON()) 78 return res.json(user.toFormatedJSON())
@@ -84,48 +80,21 @@ function getUserInformation (req, res, next) {
84} 80}
85 81
86function listUsers (req, res, next) { 82function listUsers (req, res, next) {
87 User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) { 83 db.User.listForApi(req.query.start, req.query.count, req.query.sort, function (err, usersList, usersTotal) {
88 if (err) return next(err) 84 if (err) return next(err)
89 85
90 res.json(getFormatedUsers(usersList, usersTotal)) 86 res.json(utils.getFormatedObjects(usersList, usersTotal))
91 }) 87 })
92} 88}
93 89
94function removeUser (req, res, next) { 90function removeUser (req, res, next) {
95 waterfall([ 91 waterfall([
96 function getUser (callback) { 92 function loadUser (callback) {
97 User.loadById(req.params.id, callback) 93 db.User.loadById(req.params.id, callback)
98 },
99
100 function getVideos (user, callback) {
101 Video.listOwnedByAuthor(user.username, function (err, videos) {
102 return callback(err, user, videos)
103 })
104 },
105
106 function removeVideosFromDB (user, videos, callback) {
107 each(videos, function (video, callbackEach) {
108 video.remove(callbackEach)
109 }, function (err) {
110 return callback(err, user, videos)
111 })
112 },
113
114 function sendInformationToFriends (user, videos, callback) {
115 videos.forEach(function (video) {
116 const params = {
117 name: video.name,
118 magnetUri: video.magnetUri
119 }
120
121 friends.removeVideoToFriends(params)
122 })
123
124 return callback(null, user)
125 }, 94 },
126 95
127 function removeUserFromDB (user, callback) { 96 function deleteUser (user, callback) {
128 user.remove(callback) 97 user.destroy().asCallback(callback)
129 } 98 }
130 ], function andFinally (err) { 99 ], function andFinally (err) {
131 if (err) { 100 if (err) {
@@ -138,11 +107,11 @@ function removeUser (req, res, next) {
138} 107}
139 108
140function updateUser (req, res, next) { 109function updateUser (req, res, next) {
141 User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) { 110 db.User.loadByUsername(res.locals.oauth.token.user.username, function (err, user) {
142 if (err) return next(err) 111 if (err) return next(err)
143 112
144 user.password = req.body.password 113 user.password = req.body.password
145 user.save(function (err) { 114 user.save().asCallback(function (err) {
146 if (err) return next(err) 115 if (err) return next(err)
147 116
148 return res.sendStatus(204) 117 return res.sendStatus(204)
@@ -153,18 +122,3 @@ function updateUser (req, res, next) {
153function success (req, res, next) { 122function success (req, res, next) {
154 res.end() 123 res.end()
155} 124}
156
157// ---------------------------------------------------------------------------
158
159function getFormatedUsers (users, usersTotal) {
160 const formatedUsers = []
161
162 users.forEach(function (user) {
163 formatedUsers.push(user.toFormatedJSON())
164 })
165
166 return {
167 total: usersTotal,
168 data: formatedUsers
169 }
170}
diff --git a/server/controllers/api/videos.js b/server/controllers/api/videos.js
index daf452573..2c4af520e 100644
--- a/server/controllers/api/videos.js
+++ b/server/controllers/api/videos.js
@@ -2,15 +2,16 @@
2 2
3const express = require('express') 3const express = require('express')
4const fs = require('fs') 4const fs = require('fs')
5const mongoose = require('mongoose')
6const multer = require('multer') 5const multer = require('multer')
7const path = require('path') 6const path = require('path')
8const waterfall = require('async/waterfall') 7const waterfall = require('async/waterfall')
9 8
10const constants = require('../../initializers/constants') 9const constants = require('../../initializers/constants')
10const db = require('../../initializers/database')
11const logger = require('../../helpers/logger') 11const logger = require('../../helpers/logger')
12const friends = require('../../lib/friends') 12const friends = require('../../lib/friends')
13const middlewares = require('../../middlewares') 13const middlewares = require('../../middlewares')
14const admin = middlewares.admin
14const oAuth = middlewares.oauth 15const oAuth = middlewares.oauth
15const pagination = middlewares.pagination 16const pagination = middlewares.pagination
16const validators = middlewares.validators 17const validators = middlewares.validators
@@ -22,7 +23,6 @@ const sort = middlewares.sort
22const utils = require('../../helpers/utils') 23const utils = require('../../helpers/utils')
23 24
24const router = express.Router() 25const router = express.Router()
25const Video = mongoose.model('Video')
26 26
27// multer configuration 27// multer configuration
28const storage = multer.diskStorage({ 28const storage = multer.diskStorage({
@@ -44,6 +44,21 @@ const storage = multer.diskStorage({
44 44
45const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }]) 45const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCount: 1 }])
46 46
47router.get('/abuse',
48 oAuth.authenticate,
49 admin.ensureIsAdmin,
50 validatorsPagination.pagination,
51 validatorsSort.videoAbusesSort,
52 sort.setVideoAbusesSort,
53 pagination.setPagination,
54 listVideoAbuses
55)
56router.post('/:id/abuse',
57 oAuth.authenticate,
58 validatorsVideos.videoAbuseReport,
59 reportVideoAbuseRetryWrapper
60)
61
47router.get('/', 62router.get('/',
48 validatorsPagination.pagination, 63 validatorsPagination.pagination,
49 validatorsSort.videosSort, 64 validatorsSort.videosSort,
@@ -51,11 +66,17 @@ router.get('/',
51 pagination.setPagination, 66 pagination.setPagination,
52 listVideos 67 listVideos
53) 68)
69router.put('/:id',
70 oAuth.authenticate,
71 reqFiles,
72 validatorsVideos.videosUpdate,
73 updateVideoRetryWrapper
74)
54router.post('/', 75router.post('/',
55 oAuth.authenticate, 76 oAuth.authenticate,
56 reqFiles, 77 reqFiles,
57 validatorsVideos.videosAdd, 78 validatorsVideos.videosAdd,
58 addVideo 79 addVideoRetryWrapper
59) 80)
60router.get('/:id', 81router.get('/:id',
61 validatorsVideos.videosGet, 82 validatorsVideos.videosGet,
@@ -82,117 +103,264 @@ module.exports = router
82 103
83// --------------------------------------------------------------------------- 104// ---------------------------------------------------------------------------
84 105
85function addVideo (req, res, next) { 106// Wrapper to video add that retry the function if there is a database error
86 const videoFile = req.files.videofile[0] 107// We need this because we run the transaction in SERIALIZABLE isolation that can fail
108function addVideoRetryWrapper (req, res, next) {
109 utils.transactionRetryer(
110 function (callback) {
111 return addVideo(req, res, req.files.videofile[0], callback)
112 },
113 function (err) {
114 if (err) {
115 logger.error('Cannot insert the video with many retries.', { error: err })
116 return next(err)
117 }
118
119 // TODO : include Location of the new video -> 201
120 return res.type('json').status(204).end()
121 }
122 )
123}
124
125function addVideo (req, res, videoFile, callback) {
87 const videoInfos = req.body 126 const videoInfos = req.body
88 127
89 waterfall([ 128 waterfall([
90 function createVideoObject (callback) {
91 const id = mongoose.Types.ObjectId()
92 129
130 function startTransaction (callbackWaterfall) {
131 db.sequelize.transaction({ isolationLevel: 'SERIALIZABLE' }).asCallback(function (err, t) {
132 return callbackWaterfall(err, t)
133 })
134 },
135
136 function findOrCreateAuthor (t, callbackWaterfall) {
137 const user = res.locals.oauth.token.User
138
139 const name = user.username
140 // null because it is OUR pod
141 const podId = null
142 const userId = user.id
143
144 db.Author.findOrCreateAuthor(name, podId, userId, t, function (err, authorInstance) {
145 return callbackWaterfall(err, t, authorInstance)
146 })
147 },
148
149 function findOrCreateTags (t, author, callbackWaterfall) {
150 const tags = videoInfos.tags
151
152 db.Tag.findOrCreateTags(tags, t, function (err, tagInstances) {
153 return callbackWaterfall(err, t, author, tagInstances)
154 })
155 },
156
157 function createVideoObject (t, author, tagInstances, callbackWaterfall) {
93 const videoData = { 158 const videoData = {
94 _id: id,
95 name: videoInfos.name, 159 name: videoInfos.name,
96 remoteId: null, 160 remoteId: null,
97 extname: path.extname(videoFile.filename), 161 extname: path.extname(videoFile.filename),
98 description: videoInfos.description, 162 description: videoInfos.description,
99 author: res.locals.oauth.token.user.username,
100 duration: videoFile.duration, 163 duration: videoFile.duration,
101 tags: videoInfos.tags 164 authorId: author.id
102 } 165 }
103 166
104 const video = new Video(videoData) 167 const video = db.Video.build(videoData)
105 168
106 return callback(null, video) 169 return callbackWaterfall(null, t, author, tagInstances, video)
107 }, 170 },
108 171
109 // Set the videoname the same as the MongoDB id 172 // Set the videoname the same as the id
110 function renameVideoFile (video, callback) { 173 function renameVideoFile (t, author, tagInstances, video, callbackWaterfall) {
111 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR 174 const videoDir = constants.CONFIG.STORAGE.VIDEOS_DIR
112 const source = path.join(videoDir, videoFile.filename) 175 const source = path.join(videoDir, videoFile.filename)
113 const destination = path.join(videoDir, video.getVideoFilename()) 176 const destination = path.join(videoDir, video.getVideoFilename())
114 177
115 fs.rename(source, destination, function (err) { 178 fs.rename(source, destination, function (err) {
116 return callback(err, video) 179 if (err) return callbackWaterfall(err)
180
181 // This is important in case if there is another attempt
182 videoFile.filename = video.getVideoFilename()
183 return callbackWaterfall(null, t, author, tagInstances, video)
184 })
185 },
186
187 function insertVideoIntoDB (t, author, tagInstances, video, callbackWaterfall) {
188 const options = { transaction: t }
189
190 // Add tags association
191 video.save(options).asCallback(function (err, videoCreated) {
192 if (err) return callbackWaterfall(err)
193
194 // Do not forget to add Author informations to the created video
195 videoCreated.Author = author
196
197 return callbackWaterfall(err, t, tagInstances, videoCreated)
117 }) 198 })
118 }, 199 },
119 200
120 function insertIntoDB (video, callback) { 201 function associateTagsToVideo (t, tagInstances, video, callbackWaterfall) {
121 video.save(function (err, video) { 202 const options = { transaction: t }
122 // Assert there are only one argument sent to the next function (video) 203
123 return callback(err, video) 204 video.setTags(tagInstances, options).asCallback(function (err) {
205 video.Tags = tagInstances
206
207 return callbackWaterfall(err, t, video)
124 }) 208 })
125 }, 209 },
126 210
127 function sendToFriends (video, callback) { 211 function sendToFriends (t, video, callbackWaterfall) {
128 video.toRemoteJSON(function (err, remoteVideo) { 212 video.toAddRemoteJSON(function (err, remoteVideo) {
129 if (err) return callback(err) 213 if (err) return callbackWaterfall(err)
130 214
131 // Now we'll add the video's meta data to our friends 215 // Now we'll add the video's meta data to our friends
132 friends.addVideoToFriends(remoteVideo) 216 friends.addVideoToFriends(remoteVideo, t, function (err) {
133 217 return callbackWaterfall(err, t)
134 return callback(null) 218 })
135 }) 219 })
136 } 220 }
137 221
138 ], function andFinally (err) { 222 ], function andFinally (err, t) {
139 if (err) { 223 if (err) {
140 logger.error('Cannot insert the video.') 224 // This is just a debug because we will retry the insert
141 return next(err) 225 logger.debug('Cannot insert the video.', { error: err })
226
227 // Abort transaction?
228 if (t) t.rollback()
229
230 return callback(err)
142 } 231 }
143 232
144 // TODO : include Location of the new video -> 201 233 // Commit transaction
145 return res.type('json').status(204).end() 234 t.commit().asCallback(function (err) {
235 if (err) return callback(err)
236
237 logger.info('Video with name %s created.', videoInfos.name)
238 return callback(null)
239 })
146 }) 240 })
147} 241}
148 242
149function getVideo (req, res, next) { 243function updateVideoRetryWrapper (req, res, next) {
150 Video.load(req.params.id, function (err, video) { 244 utils.transactionRetryer(
151 if (err) return next(err) 245 function (callback) {
246 return updateVideo(req, res, callback)
247 },
248 function (err) {
249 if (err) {
250 logger.error('Cannot update the video with many retries.', { error: err })
251 return next(err)
252 }
152 253
153 if (!video) { 254 // TODO : include Location of the new video -> 201
154 return res.type('json').status(204).end() 255 return res.type('json').status(204).end()
155 } 256 }
156 257 )
157 res.json(video.toFormatedJSON())
158 })
159} 258}
160 259
161function listVideos (req, res, next) { 260function updateVideo (req, res, finalCallback) {
162 Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) { 261 const videoInstance = res.locals.video
163 if (err) return next(err) 262 const videoFieldsSave = videoInstance.toJSON()
263 const videoInfosToUpdate = req.body
164 264
165 res.json(getFormatedVideos(videosList, videosTotal)) 265 waterfall([
166 })
167}
168 266
169function removeVideo (req, res, next) { 267 function startTransaction (callback) {
170 const videoId = req.params.id 268 db.sequelize.transaction({ isolationLevel: 'SERIALIZABLE' }).asCallback(function (err, t) {
269 return callback(err, t)
270 })
271 },
171 272
172 waterfall([ 273 function findOrCreateTags (t, callback) {
173 function getVideo (callback) { 274 if (videoInfosToUpdate.tags) {
174 Video.load(videoId, callback) 275 db.Tag.findOrCreateTags(videoInfosToUpdate.tags, t, function (err, tagInstances) {
276 return callback(err, t, tagInstances)
277 })
278 } else {
279 return callback(null, t, null)
280 }
175 }, 281 },
176 282
177 function removeFromDB (video, callback) { 283 function updateVideoIntoDB (t, tagInstances, callback) {
178 video.remove(function (err) { 284 const options = {
179 if (err) return callback(err) 285 transaction: t
286 }
287
288 if (videoInfosToUpdate.name) videoInstance.set('name', videoInfosToUpdate.name)
289 if (videoInfosToUpdate.description) videoInstance.set('description', videoInfosToUpdate.description)
180 290
181 return callback(null, video) 291 videoInstance.save(options).asCallback(function (err) {
292 return callback(err, t, tagInstances)
182 }) 293 })
183 }, 294 },
184 295
185 function sendInformationToFriends (video, callback) { 296 function associateTagsToVideo (t, tagInstances, callback) {
186 const params = { 297 if (tagInstances) {
187 name: video.name, 298 const options = { transaction: t }
188 remoteId: video._id 299
300 videoInstance.setTags(tagInstances, options).asCallback(function (err) {
301 videoInstance.Tags = tagInstances
302
303 return callback(err, t)
304 })
305 } else {
306 return callback(null, t)
189 } 307 }
308 },
190 309
191 friends.removeVideoToFriends(params) 310 function sendToFriends (t, callback) {
311 const json = videoInstance.toUpdateRemoteJSON()
192 312
193 return callback(null) 313 // Now we'll update the video's meta data to our friends
314 friends.updateVideoToFriends(json, t, function (err) {
315 return callback(err, t)
316 })
317 }
318
319 ], function andFinally (err, t) {
320 if (err) {
321 logger.debug('Cannot update the video.', { error: err })
322
323 // Abort transaction?
324 if (t) t.rollback()
325
326 // Force fields we want to update
327 // If the transaction is retried, sequelize will think the object has not changed
328 // So it will skip the SQL request, even if the last one was ROLLBACKed!
329 Object.keys(videoFieldsSave).forEach(function (key) {
330 const value = videoFieldsSave[key]
331 videoInstance.set(key, value)
332 })
333
334 return finalCallback(err)
194 } 335 }
195 ], function andFinally (err) { 336
337 // Commit transaction
338 t.commit().asCallback(function (err) {
339 if (err) return finalCallback(err)
340
341 logger.info('Video with name %s updated.', videoInfosToUpdate.name)
342 return finalCallback(null)
343 })
344 })
345}
346
347function getVideo (req, res, next) {
348 const videoInstance = res.locals.video
349 res.json(videoInstance.toFormatedJSON())
350}
351
352function listVideos (req, res, next) {
353 db.Video.listForApi(req.query.start, req.query.count, req.query.sort, function (err, videosList, videosTotal) {
354 if (err) return next(err)
355
356 res.json(utils.getFormatedObjects(videosList, videosTotal))
357 })
358}
359
360function removeVideo (req, res, next) {
361 const videoInstance = res.locals.video
362
363 videoInstance.destroy().asCallback(function (err) {
196 if (err) { 364 if (err) {
197 logger.error('Errors when removed the video.', { error: err }) 365 logger.error('Errors when removed the video.', { error: err })
198 return next(err) 366 return next(err)
@@ -203,25 +371,97 @@ function removeVideo (req, res, next) {
203} 371}
204 372
205function searchVideos (req, res, next) { 373function searchVideos (req, res, next) {
206 Video.search(req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort, 374 db.Video.searchAndPopulateAuthorAndPodAndTags(
207 function (err, videosList, videosTotal) { 375 req.params.value, req.query.field, req.query.start, req.query.count, req.query.sort,
376 function (err, videosList, videosTotal) {
377 if (err) return next(err)
378
379 res.json(utils.getFormatedObjects(videosList, videosTotal))
380 }
381 )
382}
383
384function listVideoAbuses (req, res, next) {
385 db.VideoAbuse.listForApi(req.query.start, req.query.count, req.query.sort, function (err, abusesList, abusesTotal) {
208 if (err) return next(err) 386 if (err) return next(err)
209 387
210 res.json(getFormatedVideos(videosList, videosTotal)) 388 res.json(utils.getFormatedObjects(abusesList, abusesTotal))
211 }) 389 })
212} 390}
213 391
214// --------------------------------------------------------------------------- 392function reportVideoAbuseRetryWrapper (req, res, next) {
393 utils.transactionRetryer(
394 function (callback) {
395 return reportVideoAbuse(req, res, callback)
396 },
397 function (err) {
398 if (err) {
399 logger.error('Cannot report abuse to the video with many retries.', { error: err })
400 return next(err)
401 }
215 402
216function getFormatedVideos (videos, videosTotal) { 403 return res.type('json').status(204).end()
217 const formatedVideos = [] 404 }
405 )
406}
218 407
219 videos.forEach(function (video) { 408function reportVideoAbuse (req, res, finalCallback) {
220 formatedVideos.push(video.toFormatedJSON()) 409 const videoInstance = res.locals.video
221 }) 410 const reporterUsername = res.locals.oauth.token.User.username
222 411
223 return { 412 const abuse = {
224 total: videosTotal, 413 reporterUsername,
225 data: formatedVideos 414 reason: req.body.reason,
415 videoId: videoInstance.id,
416 reporterPodId: null // This is our pod that reported this abuse
226 } 417 }
418
419 waterfall([
420
421 function startTransaction (callback) {
422 db.sequelize.transaction().asCallback(function (err, t) {
423 return callback(err, t)
424 })
425 },
426
427 function createAbuse (t, callback) {
428 db.VideoAbuse.create(abuse).asCallback(function (err, abuse) {
429 return callback(err, t, abuse)
430 })
431 },
432
433 function sendToFriendsIfNeeded (t, abuse, callback) {
434 // We send the information to the destination pod
435 if (videoInstance.isOwned() === false) {
436 const reportData = {
437 reporterUsername,
438 reportReason: abuse.reason,
439 videoRemoteId: videoInstance.remoteId
440 }
441
442 friends.reportAbuseVideoToFriend(reportData, videoInstance)
443 }
444
445 return callback(null, t)
446 }
447
448 ], function andFinally (err, t) {
449 if (err) {
450 logger.debug('Cannot update the video.', { error: err })
451
452 // Abort transaction?
453 if (t) t.rollback()
454
455 return finalCallback(err)
456 }
457
458 // Commit transaction
459 t.commit().asCallback(function (err) {
460 if (err) return finalCallback(err)
461
462 logger.info('Abuse report for video %s created.', videoInstance.name)
463 return finalCallback(null)
464 })
465 })
227} 466}
467
diff --git a/server/controllers/client.js b/server/controllers/client.js
index 572db6133..8c242af07 100644
--- a/server/controllers/client.js
+++ b/server/controllers/client.js
@@ -3,13 +3,12 @@
3const parallel = require('async/parallel') 3const parallel = require('async/parallel')
4const express = require('express') 4const express = require('express')
5const fs = require('fs') 5const fs = require('fs')
6const mongoose = require('mongoose')
7const path = require('path') 6const path = require('path')
8const validator = require('express-validator').validator 7const validator = require('express-validator').validator
9 8
10const constants = require('../initializers/constants') 9const constants = require('../initializers/constants')
10const db = require('../initializers/database')
11 11
12const Video = mongoose.model('Video')
13const router = express.Router() 12const router = express.Router()
14 13
15const opengraphComment = '<!-- opengraph tags -->' 14const opengraphComment = '<!-- opengraph tags -->'
@@ -45,14 +44,14 @@ function addOpenGraphTags (htmlStringPage, video) {
45 if (video.isOwned()) { 44 if (video.isOwned()) {
46 basePreviewUrlHttp = constants.CONFIG.WEBSERVER.URL 45 basePreviewUrlHttp = constants.CONFIG.WEBSERVER.URL
47 } else { 46 } else {
48 basePreviewUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + video.podHost 47 basePreviewUrlHttp = constants.REMOTE_SCHEME.HTTP + '://' + video.Author.Pod.host
49 } 48 }
50 49
51 // We fetch the remote preview (bigger than the thumbnail) 50 // We fetch the remote preview (bigger than the thumbnail)
52 // This should not overhead the remote server since social websites put in a cache the OpenGraph tags 51 // This should not overhead the remote server since social websites put in a cache the OpenGraph tags
53 // We can't use the thumbnail because these social websites want bigger images (> 200x200 for Facebook for example) 52 // We can't use the thumbnail because these social websites want bigger images (> 200x200 for Facebook for example)
54 const previewUrl = basePreviewUrlHttp + constants.STATIC_PATHS.PREVIEWS + video.getPreviewName() 53 const previewUrl = basePreviewUrlHttp + constants.STATIC_PATHS.PREVIEWS + video.getPreviewName()
55 const videoUrl = constants.CONFIG.WEBSERVER.URL + '/videos/watch/' + video._id 54 const videoUrl = constants.CONFIG.WEBSERVER.URL + '/videos/watch/' + video.id
56 55
57 const metaTags = { 56 const metaTags = {
58 'og:type': 'video', 57 'og:type': 'video',
@@ -86,7 +85,7 @@ function generateWatchHtmlPage (req, res, next) {
86 const videoId = req.params.id 85 const videoId = req.params.id
87 86
88 // Let Angular application handle errors 87 // Let Angular application handle errors
89 if (!validator.isMongoId(videoId)) return res.sendFile(indexPath) 88 if (!validator.isUUID(videoId, 4)) return res.sendFile(indexPath)
90 89
91 parallel({ 90 parallel({
92 file: function (callback) { 91 file: function (callback) {
@@ -94,7 +93,7 @@ function generateWatchHtmlPage (req, res, next) {
94 }, 93 },
95 94
96 video: function (callback) { 95 video: function (callback) {
97 Video.load(videoId, callback) 96 db.Video.loadAndPopulateAuthorAndPodAndTags(videoId, callback)
98 } 97 }
99 }, function (err, results) { 98 }, function (err, results) {
100 if (err) return next(err) 99 if (err) return next(err)