aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <florian.bigard@gmail.com>2017-10-16 10:05:49 +0200
committerChocobozzz <florian.bigard@gmail.com>2017-10-16 10:07:26 +0200
commitd8755eed1e452d2efbfc983af0e9d228d152bf6b (patch)
treedb94181e7c993f67919f4ea2bb12f08401c437c2
parent334ddfa47120ae53bc2643792ec5e1065a4d1141 (diff)
downloadPeerTube-d8755eed1e452d2efbfc983af0e9d228d152bf6b.tar.gz
PeerTube-d8755eed1e452d2efbfc983af0e9d228d152bf6b.tar.zst
PeerTube-d8755eed1e452d2efbfc983af0e9d228d152bf6b.zip
Add oembed endpoint
-rw-r--r--client/src/app/videos/+video-watch/video-share.component.ts2
-rw-r--r--client/src/app/videos/shared/video.model.ts5
-rw-r--r--client/src/index.html4
-rwxr-xr-xscripts/upgrade-peertube.sh2
-rw-r--r--server.ts5
-rw-r--r--server/controllers/client.ts32
-rw-r--r--server/controllers/index.ts1
-rw-r--r--server/controllers/services.ts62
-rw-r--r--server/initializers/constants.ts14
-rw-r--r--server/middlewares/validators/index.ts1
-rw-r--r--server/middlewares/validators/oembed.ts63
-rw-r--r--server/models/video/video-interface.ts6
-rw-r--r--server/models/video/video.ts29
-rw-r--r--server/tests/api/check-params/index.ts1
-rw-r--r--server/tests/api/check-params/services.ts159
-rw-r--r--server/tests/api/index.ts1
-rw-r--r--server/tests/api/services.ts85
-rw-r--r--server/tests/client.ts17
-rw-r--r--server/tests/utils/index.ts1
-rw-r--r--server/tests/utils/servers.ts2
-rw-r--r--server/tests/utils/services.ts23
-rw-r--r--shared/models/videos/video.model.ts1
22 files changed, 491 insertions, 25 deletions
diff --git a/client/src/app/videos/+video-watch/video-share.component.ts b/client/src/app/videos/+video-watch/video-share.component.ts
index 133f93498..414ed28c6 100644
--- a/client/src/app/videos/+video-watch/video-share.component.ts
+++ b/client/src/app/videos/+video-watch/video-share.component.ts
@@ -27,7 +27,7 @@ export class VideoShareComponent {
27 27
28 getVideoIframeCode () { 28 getVideoIframeCode () {
29 return '<iframe width="560" height="315" ' + 29 return '<iframe width="560" height="315" ' +
30 'src="' + window.location.origin + '/videos/embed/' + this.video.uuid + '" ' + 30 'src="' + this.video.embedUrl + '" ' +
31 'frameborder="0" allowfullscreen>' + 31 'frameborder="0" allowfullscreen>' +
32 '</iframe>' 32 '</iframe>'
33 } 33 }
diff --git a/client/src/app/videos/shared/video.model.ts b/client/src/app/videos/shared/video.model.ts
index b315e59b1..51c5319ea 100644
--- a/client/src/app/videos/shared/video.model.ts
+++ b/client/src/app/videos/shared/video.model.ts
@@ -26,6 +26,8 @@ export class Video implements VideoServerModel {
26 thumbnailUrl: string 26 thumbnailUrl: string
27 previewPath: string 27 previewPath: string
28 previewUrl: string 28 previewUrl: string
29 embedPath: string
30 embedUrl: string
29 views: number 31 views: number
30 likes: number 32 likes: number
31 dislikes: number 33 dislikes: number
@@ -64,6 +66,7 @@ export class Video implements VideoServerModel {
64 tags: string[], 66 tags: string[],
65 thumbnailPath: string, 67 thumbnailPath: string,
66 previewPath: string, 68 previewPath: string,
69 embedPath: string,
67 views: number, 70 views: number,
68 likes: number, 71 likes: number,
69 dislikes: number, 72 dislikes: number,
@@ -91,6 +94,8 @@ export class Video implements VideoServerModel {
91 this.thumbnailUrl = API_URL + hash.thumbnailPath 94 this.thumbnailUrl = API_URL + hash.thumbnailPath
92 this.previewPath = hash.previewPath 95 this.previewPath = hash.previewPath
93 this.previewUrl = API_URL + hash.previewPath 96 this.previewUrl = API_URL + hash.previewPath
97 this.embedPath = hash.embedPath
98 this.embedUrl = API_URL + hash.embedPath
94 this.views = hash.views 99 this.views = hash.views
95 this.likes = hash.likes 100 this.likes = hash.likes
96 this.dislikes = hash.dislikes 101 this.dislikes = hash.dislikes
diff --git a/client/src/index.html b/client/src/index.html
index 91ed04d17..8e94b903d 100644
--- a/client/src/index.html
+++ b/client/src/index.html
@@ -7,8 +7,8 @@
7 <meta name="viewport" content="width=device-width, initial-scale=1"> 7 <meta name="viewport" content="width=device-width, initial-scale=1">
8 <meta name="description" content="PeerTube, a decentralized video streaming platform using P2P (BitTorrent) directly in the web browser" /> 8 <meta name="description" content="PeerTube, a decentralized video streaming platform using P2P (BitTorrent) directly in the web browser" />
9 9
10 <!-- The following comment is used by the server to prerender OpenGraph tags --> 10 <!-- The following comment is used by the server to prerender OpenGraph and oEmbed tags -->
11 <!-- open graph tags --> 11 <!-- open graph and oembed tags -->
12 <!-- Do not remove it! --> 12 <!-- Do not remove it! -->
13 13
14 <link rel="icon" type="image/png" href="/client/assets/favicon.png" /> 14 <link rel="icon" type="image/png" href="/client/assets/favicon.png" />
diff --git a/scripts/upgrade-peertube.sh b/scripts/upgrade-peertube.sh
index 5186a269f..562a2a618 100755
--- a/scripts/upgrade-peertube.sh
+++ b/scripts/upgrade-peertube.sh
@@ -26,7 +26,7 @@ if ! which yarn > /dev/null; then
26fi 26fi
27 27
28if pgrep peertube > /dev/null; then 28if pgrep peertube > /dev/null; then
29 echo 'PeerTube is running!' 29 echo 'PeerTube is running, please shut it off before upgrading'
30 exit 0 30 exit 0
31fi 31fi
32 32
diff --git a/server.ts b/server.ts
index 3f2d27718..72bb11e74 100644
--- a/server.ts
+++ b/server.ts
@@ -47,7 +47,7 @@ db.init(false).then(() => onDatabaseInitDone())
47// ----------- PeerTube modules ----------- 47// ----------- PeerTube modules -----------
48import { migrate, installApplication } from './server/initializers' 48import { migrate, installApplication } from './server/initializers'
49import { JobScheduler, activateSchedulers, VideosPreviewCache } from './server/lib' 49import { JobScheduler, activateSchedulers, VideosPreviewCache } from './server/lib'
50import { apiRouter, clientsRouter, staticRouter } from './server/controllers' 50import { apiRouter, clientsRouter, staticRouter, servicesRouter } from './server/controllers'
51 51
52// ----------- Command line ----------- 52// ----------- Command line -----------
53 53
@@ -85,6 +85,9 @@ app.use(bodyParser.urlencoded({ extended: false }))
85const apiRoute = '/api/' + API_VERSION 85const apiRoute = '/api/' + API_VERSION
86app.use(apiRoute, apiRouter) 86app.use(apiRoute, apiRouter)
87 87
88// Services (oembed...)
89app.use('/services', servicesRouter)
90
88// Client files 91// Client files
89app.use('/', clientsRouter) 92app.use('/', clientsRouter)
90 93
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index b23f7e1ae..e3c962058 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -8,7 +8,7 @@ import {
8 CONFIG, 8 CONFIG,
9 STATIC_PATHS, 9 STATIC_PATHS,
10 STATIC_MAX_AGE, 10 STATIC_MAX_AGE,
11 OPENGRAPH_COMMENT 11 OPENGRAPH_AND_OEMBED_COMMENT
12} from '../initializers' 12} from '../initializers'
13import { root, readFileBufferPromise } from '../helpers' 13import { root, readFileBufferPromise } from '../helpers'
14import { VideoInstance } from '../models' 14import { VideoInstance } from '../models'
@@ -19,7 +19,7 @@ const distPath = join(root(), 'client', 'dist')
19const embedPath = join(distPath, 'standalone', 'videos', 'embed.html') 19const embedPath = join(distPath, 'standalone', 'videos', 'embed.html')
20const indexPath = join(distPath, 'index.html') 20const indexPath = join(distPath, 'index.html')
21 21
22// Special route that add OpenGraph tags 22// Special route that add OpenGraph and oEmbed tags
23// Do not use a template engine for a so little thing 23// Do not use a template engine for a so little thing
24clientsRouter.use('/videos/watch/:id', generateWatchHtmlPage) 24clientsRouter.use('/videos/watch/:id', generateWatchHtmlPage)
25 25
@@ -43,11 +43,11 @@ export {
43 43
44// --------------------------------------------------------------------------- 44// ---------------------------------------------------------------------------
45 45
46function addOpenGraphTags (htmlStringPage: string, video: VideoInstance) { 46function addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoInstance) {
47 const previewUrl = CONFIG.WEBSERVER.URL + STATIC_PATHS.PREVIEWS + video.getPreviewName() 47 const previewUrl = CONFIG.WEBSERVER.URL + STATIC_PATHS.PREVIEWS + video.getPreviewName()
48 const videoUrl = CONFIG.WEBSERVER.URL + '/videos/watch/' + video.id 48 const videoUrl = CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
49 49
50 const metaTags = { 50 const openGraphMetaTags = {
51 'og:type': 'video', 51 'og:type': 'video',
52 'og:title': video.name, 52 'og:title': video.name,
53 'og:image': previewUrl, 53 'og:image': previewUrl,
@@ -65,14 +65,26 @@ function addOpenGraphTags (htmlStringPage: string, video: VideoInstance) {
65 'twitter:image': previewUrl 65 'twitter:image': previewUrl
66 } 66 }
67 67
68 const oembedLinkTags = [
69 {
70 type: 'application/json+oembed',
71 href: CONFIG.WEBSERVER.URL + '/services/oembed?url=' + encodeURIComponent(videoUrl),
72 title: video.name
73 }
74 ]
75
68 let tagsString = '' 76 let tagsString = ''
69 Object.keys(metaTags).forEach(tagName => { 77 Object.keys(openGraphMetaTags).forEach(tagName => {
70 const tagValue = metaTags[tagName] 78 const tagValue = openGraphMetaTags[tagName]
71 79
72 tagsString += '<meta property="' + tagName + '" content="' + tagValue + '" />' 80 tagsString += `<meta property="${tagName}" content="${tagValue}" />`
73 }) 81 })
74 82
75 return htmlStringPage.replace(OPENGRAPH_COMMENT, tagsString) 83 for (const oembedLinkTag of oembedLinkTags) {
84 tagsString += `<link rel="alternate" type="${oembedLinkTag.type}" href="${oembedLinkTag.href}" title="${oembedLinkTag.title}" />`
85 }
86
87 return htmlStringPage.replace(OPENGRAPH_AND_OEMBED_COMMENT, tagsString)
76} 88}
77 89
78function generateWatchHtmlPage (req: express.Request, res: express.Response, next: express.NextFunction) { 90function generateWatchHtmlPage (req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -101,7 +113,7 @@ function generateWatchHtmlPage (req: express.Request, res: express.Response, nex
101 // Let Angular application handle errors 113 // Let Angular application handle errors
102 if (!video) return res.sendFile(indexPath) 114 if (!video) return res.sendFile(indexPath)
103 115
104 const htmlStringPageWithTags = addOpenGraphTags(html, video) 116 const htmlStringPageWithTags = addOpenGraphAndOEmbedTags(html, video)
105 res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags) 117 res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags)
106 }) 118 })
107 .catch(err => next(err)) 119 .catch(err => next(err))
diff --git a/server/controllers/index.ts b/server/controllers/index.ts
index 0223a98f1..51cb480a3 100644
--- a/server/controllers/index.ts
+++ b/server/controllers/index.ts
@@ -1,3 +1,4 @@
1export * from './static' 1export * from './static'
2export * from './client' 2export * from './client'
3export * from './services'
3export * from './api' 4export * from './api'
diff --git a/server/controllers/services.ts b/server/controllers/services.ts
new file mode 100644
index 000000000..3ce6bd526
--- /dev/null
+++ b/server/controllers/services.ts
@@ -0,0 +1,62 @@
1import * as express from 'express'
2
3import { CONFIG, THUMBNAILS_SIZE } from '../initializers'
4import { oembedValidator } from '../middlewares'
5import { VideoInstance } from '../models'
6
7const servicesRouter = express.Router()
8
9servicesRouter.use('/oembed', oembedValidator, generateOEmbed)
10
11// ---------------------------------------------------------------------------
12
13export {
14 servicesRouter
15}
16
17// ---------------------------------------------------------------------------
18
19function generateOEmbed (req: express.Request, res: express.Response, next: express.NextFunction) {
20 const video = res.locals.video as VideoInstance
21 const webserverUrl = CONFIG.WEBSERVER.URL
22 const maxHeight = parseInt(req.query.maxheight, 10)
23 const maxWidth = parseInt(req.query.maxwidth, 10)
24
25 const embedUrl = webserverUrl + video.getEmbedPath()
26 let thumbnailUrl = webserverUrl + video.getThumbnailPath()
27 let embedWidth = 560
28 let embedHeight = 315
29
30 if (maxHeight < embedHeight) embedHeight = maxHeight
31 if (maxWidth < embedWidth) embedWidth = maxWidth
32
33 // Our thumbnail is too big for the consumer
34 if (
35 (maxHeight !== undefined && maxHeight < THUMBNAILS_SIZE.height) ||
36 (maxWidth !== undefined && maxWidth < THUMBNAILS_SIZE.width)
37 ) {
38 thumbnailUrl = undefined
39 }
40
41 const html = `<iframe width="${embedWidth}" height="${embedHeight}" src="${embedUrl}" frameborder="0" allowfullscreen></iframe>`
42
43 const json: any = {
44 type: 'video',
45 version: '1.0',
46 html,
47 width: embedWidth,
48 height: embedHeight,
49 title: video.name,
50 author_name: video.Author.name,
51 provider_name: 'PeerTube',
52 provider_url: webserverUrl
53 }
54
55 if (thumbnailUrl !== undefined) {
56 json.thumbnail_url = thumbnailUrl
57 json.thumbnail_width = THUMBNAILS_SIZE.width
58 json.thumbnail_height = THUMBNAILS_SIZE.height
59 }
60
61 return res.json(json)
62}
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index b11575b34..6218644cf 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -295,8 +295,14 @@ const STATIC_PATHS = {
295let STATIC_MAX_AGE = '30d' 295let STATIC_MAX_AGE = '30d'
296 296
297// Videos thumbnail size 297// Videos thumbnail size
298const THUMBNAILS_SIZE = '200x110' 298const THUMBNAILS_SIZE = {
299const PREVIEWS_SIZE = '640x480' 299 width: 200,
300 height: 110
301}
302const PREVIEWS_SIZE = {
303 width: 640,
304 height: 480
305}
300 306
301// Sub folders of cache directory 307// Sub folders of cache directory
302const CACHE = { 308const CACHE = {
@@ -314,7 +320,7 @@ const USER_ROLES: { [ id: string ]: UserRole } = {
314 320
315// --------------------------------------------------------------------------- 321// ---------------------------------------------------------------------------
316 322
317const OPENGRAPH_COMMENT = '<!-- open graph tags -->' 323const OPENGRAPH_AND_OEMBED_COMMENT = '<!-- open graph and oembed tags -->'
318 324
319// --------------------------------------------------------------------------- 325// ---------------------------------------------------------------------------
320 326
@@ -344,7 +350,7 @@ export {
344 JOBS_FETCHING_INTERVAL, 350 JOBS_FETCHING_INTERVAL,
345 LAST_MIGRATION_VERSION, 351 LAST_MIGRATION_VERSION,
346 OAUTH_LIFETIME, 352 OAUTH_LIFETIME,
347 OPENGRAPH_COMMENT, 353 OPENGRAPH_AND_OEMBED_COMMENT,
348 PAGINATION_COUNT_DEFAULT, 354 PAGINATION_COUNT_DEFAULT,
349 PODS_SCORE, 355 PODS_SCORE,
350 PREVIEWS_SIZE, 356 PREVIEWS_SIZE,
diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts
index 418fa5f1d..068c41b24 100644
--- a/server/middlewares/validators/index.ts
+++ b/server/middlewares/validators/index.ts
@@ -1,3 +1,4 @@
1export * from './oembed'
1export * from './remote' 2export * from './remote'
2export * from './pagination' 3export * from './pagination'
3export * from './pods' 4export * from './pods'
diff --git a/server/middlewares/validators/oembed.ts b/server/middlewares/validators/oembed.ts
new file mode 100644
index 000000000..4b8c03faf
--- /dev/null
+++ b/server/middlewares/validators/oembed.ts
@@ -0,0 +1,63 @@
1import { query } from 'express-validator/check'
2import * as express from 'express'
3import { join } from 'path'
4
5import { checkErrors } from './utils'
6import { CONFIG } from '../../initializers'
7import { logger } from '../../helpers'
8import { checkVideoExists, isVideoIdOrUUIDValid } from '../../helpers/custom-validators/videos'
9import { isTestInstance } from '../../helpers/core-utils'
10
11const urlShouldStartWith = CONFIG.WEBSERVER.SCHEME + '://' + join(CONFIG.WEBSERVER.HOST, 'videos', 'watch') + '/'
12const videoWatchRegex = new RegExp('([^/]+)$')
13const isURLOptions = {
14 require_host: true,
15 require_tld: true
16}
17
18// We validate 'localhost', so we don't have the top level domain
19if (isTestInstance()) {
20 isURLOptions.require_tld = false
21}
22
23const oembedValidator = [
24 query('url').isURL(isURLOptions).withMessage('Should have a valid url'),
25 query('maxwidth').optional().isInt().withMessage('Should have a valid max width'),
26 query('maxheight').optional().isInt().withMessage('Should have a valid max height'),
27 query('format').optional().isIn([ 'xml', 'json' ]).withMessage('Should have a valid format'),
28
29 (req: express.Request, res: express.Response, next: express.NextFunction) => {
30 logger.debug('Checking oembed parameters', { parameters: req.query })
31
32 checkErrors(req, res, () => {
33 if (req.query.format !== undefined && req.query.format !== 'json') {
34 return res.status(501)
35 .json({ error: 'Requested format is not implemented on server.' })
36 .end()
37 }
38
39 const startIsOk = req.query.url.startsWith(urlShouldStartWith)
40 const matches = videoWatchRegex.exec(req.query.url)
41 if (startIsOk === false || matches === null) {
42 return res.status(400)
43 .json({ error: 'Invalid url.' })
44 .end()
45 }
46
47 const videoId = matches[1]
48 if (isVideoIdOrUUIDValid(videoId) === false) {
49 return res.status(400)
50 .json({ error: 'Invalid video id.' })
51 .end()
52 }
53
54 checkVideoExists(videoId, res, next)
55 })
56 }
57]
58
59// ---------------------------------------------------------------------------
60
61export {
62 oembedValidator
63}
diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts
index 6a3db4f3e..1402df26a 100644
--- a/server/models/video/video-interface.ts
+++ b/server/models/video/video-interface.ts
@@ -32,6 +32,9 @@ export namespace VideoMethods {
32 export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise<void> 32 export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise<void>
33 export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise<void> 33 export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise<void>
34 export type GetOriginalFileHeight = (this: VideoInstance) => Promise<number> 34 export type GetOriginalFileHeight = (this: VideoInstance) => Promise<number>
35 export type GetEmbedPath = (this: VideoInstance) => string
36 export type GetThumbnailPath = (this: VideoInstance) => string
37 export type GetPreviewPath = (this: VideoInstance) => string
35 38
36 // Return thumbnail name 39 // Return thumbnail name
37 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string> 40 export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
@@ -107,7 +110,9 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
107 getOriginalFile: VideoMethods.GetOriginalFile 110 getOriginalFile: VideoMethods.GetOriginalFile
108 generateMagnetUri: VideoMethods.GenerateMagnetUri 111 generateMagnetUri: VideoMethods.GenerateMagnetUri
109 getPreviewName: VideoMethods.GetPreviewName 112 getPreviewName: VideoMethods.GetPreviewName
113 getPreviewPath: VideoMethods.GetPreviewPath
110 getThumbnailName: VideoMethods.GetThumbnailName 114 getThumbnailName: VideoMethods.GetThumbnailName
115 getThumbnailPath: VideoMethods.GetThumbnailPath
111 getTorrentFileName: VideoMethods.GetTorrentFileName 116 getTorrentFileName: VideoMethods.GetTorrentFileName
112 getVideoFilename: VideoMethods.GetVideoFilename 117 getVideoFilename: VideoMethods.GetVideoFilename
113 getVideoFilePath: VideoMethods.GetVideoFilePath 118 getVideoFilePath: VideoMethods.GetVideoFilePath
@@ -122,6 +127,7 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
122 optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile 127 optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
123 transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile 128 transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
124 getOriginalFileHeight: VideoMethods.GetOriginalFileHeight 129 getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
130 getEmbedPath: VideoMethods.GetEmbedPath
125 131
126 setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string> 132 setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
127 addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string> 133 addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string>
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 2ba6cf25f..0d0048b4a 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -54,7 +54,9 @@ let getOriginalFile: VideoMethods.GetOriginalFile
54let generateMagnetUri: VideoMethods.GenerateMagnetUri 54let generateMagnetUri: VideoMethods.GenerateMagnetUri
55let getVideoFilename: VideoMethods.GetVideoFilename 55let getVideoFilename: VideoMethods.GetVideoFilename
56let getThumbnailName: VideoMethods.GetThumbnailName 56let getThumbnailName: VideoMethods.GetThumbnailName
57let getThumbnailPath: VideoMethods.GetThumbnailPath
57let getPreviewName: VideoMethods.GetPreviewName 58let getPreviewName: VideoMethods.GetPreviewName
59let getPreviewPath: VideoMethods.GetPreviewPath
58let getTorrentFileName: VideoMethods.GetTorrentFileName 60let getTorrentFileName: VideoMethods.GetTorrentFileName
59let isOwned: VideoMethods.IsOwned 61let isOwned: VideoMethods.IsOwned
60let toFormattedJSON: VideoMethods.ToFormattedJSON 62let toFormattedJSON: VideoMethods.ToFormattedJSON
@@ -67,6 +69,7 @@ let createThumbnail: VideoMethods.CreateThumbnail
67let getVideoFilePath: VideoMethods.GetVideoFilePath 69let getVideoFilePath: VideoMethods.GetVideoFilePath
68let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash 70let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
69let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight 71let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
72let getEmbedPath: VideoMethods.GetEmbedPath
70 73
71let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData 74let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
72let list: VideoMethods.List 75let list: VideoMethods.List
@@ -252,7 +255,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
252 createTorrentAndSetInfoHash, 255 createTorrentAndSetInfoHash,
253 generateMagnetUri, 256 generateMagnetUri,
254 getPreviewName, 257 getPreviewName,
258 getPreviewPath,
255 getThumbnailName, 259 getThumbnailName,
260 getThumbnailPath,
256 getTorrentFileName, 261 getTorrentFileName,
257 getVideoFilename, 262 getVideoFilename,
258 getVideoFilePath, 263 getVideoFilePath,
@@ -267,7 +272,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
267 toUpdateRemoteJSON, 272 toUpdateRemoteJSON,
268 optimizeOriginalVideofile, 273 optimizeOriginalVideofile,
269 transcodeOriginalVideofile, 274 transcodeOriginalVideofile,
270 getOriginalFileHeight 275 getOriginalFileHeight,
276 getEmbedPath
271 ] 277 ]
272 addMethodsToModel(Video, classMethods, instanceMethods) 278 addMethodsToModel(Video, classMethods, instanceMethods)
273 279
@@ -375,11 +381,13 @@ createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
375} 381}
376 382
377createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) { 383createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
384 const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
385
378 return generateImageFromVideoFile( 386 return generateImageFromVideoFile(
379 this.getVideoFilePath(videoFile), 387 this.getVideoFilePath(videoFile),
380 CONFIG.STORAGE.THUMBNAILS_DIR, 388 CONFIG.STORAGE.THUMBNAILS_DIR,
381 this.getThumbnailName(), 389 this.getThumbnailName(),
382 THUMBNAILS_SIZE 390 imageSize
383 ) 391 )
384} 392}
385 393
@@ -438,6 +446,18 @@ generateMagnetUri = function (this: VideoInstance, videoFile: VideoFileInstance)
438 return magnetUtil.encode(magnetHash) 446 return magnetUtil.encode(magnetHash)
439} 447}
440 448
449getEmbedPath = function (this: VideoInstance) {
450 return '/videos/embed/' + this.uuid
451}
452
453getThumbnailPath = function (this: VideoInstance) {
454 return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
455}
456
457getPreviewPath = function (this: VideoInstance) {
458 return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
459}
460
441toFormattedJSON = function (this: VideoInstance) { 461toFormattedJSON = function (this: VideoInstance) {
442 let podHost 462 let podHost
443 463
@@ -480,8 +500,9 @@ toFormattedJSON = function (this: VideoInstance) {
480 likes: this.likes, 500 likes: this.likes,
481 dislikes: this.dislikes, 501 dislikes: this.dislikes,
482 tags: map<TagInstance, string>(this.Tags, 'name'), 502 tags: map<TagInstance, string>(this.Tags, 'name'),
483 thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()), 503 thumbnailPath: this.getThumbnailPath(),
484 previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()), 504 previewPath: this.getPreviewPath(),
505 embedPath: this.getEmbedPath(),
485 createdAt: this.createdAt, 506 createdAt: this.createdAt,
486 updatedAt: this.updatedAt, 507 updatedAt: this.updatedAt,
487 files: [] 508 files: []
diff --git a/server/tests/api/check-params/index.ts b/server/tests/api/check-params/index.ts
index 399a05bc3..954b206e9 100644
--- a/server/tests/api/check-params/index.ts
+++ b/server/tests/api/check-params/index.ts
@@ -3,6 +3,7 @@ import './pods'
3import './remotes' 3import './remotes'
4import './users' 4import './users'
5import './request-schedulers' 5import './request-schedulers'
6import './services'
6import './videos' 7import './videos'
7import './video-abuses' 8import './video-abuses'
8import './video-blacklist' 9import './video-blacklist'
diff --git a/server/tests/api/check-params/services.ts b/server/tests/api/check-params/services.ts
new file mode 100644
index 000000000..780254df5
--- /dev/null
+++ b/server/tests/api/check-params/services.ts
@@ -0,0 +1,159 @@
1/* tslint:disable:no-unused-expression */
2
3import * as request from 'supertest'
4import 'mocha'
5
6import {
7 flushTests,
8 runServer,
9 setAccessTokensToServers,
10 killallServers
11} from '../../utils'
12import { getVideosList, uploadVideo } from '../../utils/videos'
13
14describe('Test services API validators', function () {
15 let server
16
17 // ---------------------------------------------------------------
18
19 before(async function () {
20 this.timeout(60000)
21
22 await flushTests()
23
24 server = await runServer(1)
25 await setAccessTokensToServers([ server ])
26
27 const videoAttributes = {
28 name: 'my super name'
29 }
30 await uploadVideo(server.url, server.accessToken, videoAttributes)
31
32 const res = await getVideosList(server.url)
33 server.video = res.body.data[0]
34 })
35
36 describe('Test oEmbed API validators', function () {
37 const path = '/services/oembed'
38
39 it('Should fail with an invalid url', async function () {
40 const embedUrl = 'hello.com'
41
42 await request(server.url)
43 .get(path)
44 .query({ url: embedUrl })
45 .set('Accept', 'application/json')
46 .set('Authorization', 'Bearer ' + server.accessToken)
47 .expect(400)
48 })
49
50 it('Should fail with an invalid host', async function () {
51 const embedUrl = 'http://hello.com/videos/watch/' + server.video.uuid
52
53 await request(server.url)
54 .get(path)
55 .query({ url: embedUrl })
56 .set('Accept', 'application/json')
57 .set('Authorization', 'Bearer ' + server.accessToken)
58 .expect(400)
59 })
60
61 it('Should fail with an invalid video id', async function () {
62 const embedUrl = 'http://localhost:9001/videos/watch/blabla'
63
64 await request(server.url)
65 .get(path)
66 .query({ url: embedUrl })
67 .set('Accept', 'application/json')
68 .set('Authorization', 'Bearer ' + server.accessToken)
69 .expect(400)
70 })
71
72 it('Should fail with an unknown video', async function () {
73 const embedUrl = 'http://localhost:9001/videos/watch/88fc0165-d1f0-4a35-a51a-3b47f668689c'
74
75 await request(server.url)
76 .get(path)
77 .query({ url: embedUrl })
78 .set('Accept', 'application/json')
79 .set('Authorization', 'Bearer ' + server.accessToken)
80 .expect(404)
81 })
82
83 it('Should fail with an invalid path', async function () {
84 const embedUrl = 'http://localhost:9001/videos/watchs/' + server.video.uuid
85
86 await request(server.url)
87 .get(path)
88 .query({ url: embedUrl })
89 .set('Accept', 'application/json')
90 .set('Authorization', 'Bearer ' + server.accessToken)
91 .expect(400)
92 })
93
94 it('Should fail with an invalid max height', async function () {
95 const embedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
96
97 await request(server.url)
98 .get(path)
99 .query({
100 url: embedUrl,
101 maxheight: 'hello'
102 })
103 .set('Accept', 'application/json')
104 .set('Authorization', 'Bearer ' + server.accessToken)
105 .expect(400)
106 })
107
108 it('Should fail with an invalid max width', async function () {
109 const embedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
110
111 await request(server.url)
112 .get(path)
113 .query({
114 url: embedUrl,
115 maxwidth: 'hello'
116 })
117 .set('Accept', 'application/json')
118 .set('Authorization', 'Bearer ' + server.accessToken)
119 .expect(400)
120 })
121
122 it('Should fail with an invalid format', async function () {
123 const embedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
124
125 await request(server.url)
126 .get(path)
127 .query({
128 url: embedUrl,
129 format: 'blabla'
130 })
131 .set('Accept', 'application/json')
132 .set('Authorization', 'Bearer ' + server.accessToken)
133 .expect(400)
134 })
135
136 it('Should fail with a non supported format', async function () {
137 const embedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
138
139 await request(server.url)
140 .get(path)
141 .query({
142 url: embedUrl,
143 format: 'xml'
144 })
145 .set('Accept', 'application/json')
146 .set('Authorization', 'Bearer ' + server.accessToken)
147 .expect(501)
148 })
149 })
150
151 after(async function () {
152 killallServers([ server ])
153
154 // Keep the logs if the test failed
155 if (this['ok']) {
156 await flushTests()
157 }
158 })
159})
diff --git a/server/tests/api/index.ts b/server/tests/api/index.ts
index 03711e68a..e50e65049 100644
--- a/server/tests/api/index.ts
+++ b/server/tests/api/index.ts
@@ -8,6 +8,7 @@ import './video-abuse'
8import './video-blacklist' 8import './video-blacklist'
9import './video-blacklist-management' 9import './video-blacklist-management'
10import './multiple-pods' 10import './multiple-pods'
11import './services'
11import './request-schedulers' 12import './request-schedulers'
12import './friends-advanced' 13import './friends-advanced'
13import './video-transcoder' 14import './video-transcoder'
diff --git a/server/tests/api/services.ts b/server/tests/api/services.ts
new file mode 100644
index 000000000..b396ea582
--- /dev/null
+++ b/server/tests/api/services.ts
@@ -0,0 +1,85 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4import * as chai from 'chai'
5const expect = chai.expect
6
7import {
8 ServerInfo,
9 flushTests,
10 uploadVideo,
11 getVideosList,
12 setAccessTokensToServers,
13 killallServers,
14 getOEmbed
15} from '../utils'
16import { runServer } from '../utils/servers'
17
18describe('Test services', function () {
19 let server: ServerInfo = null
20
21 before(async function () {
22 this.timeout(120000)
23
24 await flushTests()
25
26 server = await runServer(1)
27
28 await setAccessTokensToServers([ server ])
29
30 const videoAttributes = {
31 name: 'my super name'
32 }
33 await uploadVideo(server.url, server.accessToken, videoAttributes)
34
35 const res = await getVideosList(server.url)
36 server.video = res.body.data[0]
37 })
38
39 it('Should have a valid oEmbed response', async function () {
40 const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
41
42 const res = await getOEmbed(server.url, oembedUrl)
43 const expectedHtml = `<iframe width="560" height="315" src="http://localhost:9001/videos/embed/${server.video.uuid}" ` +
44 'frameborder="0" allowfullscreen></iframe>'
45 const expectedThumbnailUrl = 'http://localhost:9001/static/thumbnails/' + server.video.uuid + '.jpg'
46
47 expect(res.body.html).to.equal(expectedHtml)
48 expect(res.body.title).to.equal(server.video.name)
49 expect(res.body.author_name).to.equal(server.video.author)
50 expect(res.body.height).to.equal(315)
51 expect(res.body.width).to.equal(560)
52 expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl)
53 expect(res.body.thumbnail_width).to.equal(200)
54 expect(res.body.thumbnail_height).to.equal(110)
55 })
56
57 it('Should have a valid oEmbed response with small max height query', async function () {
58 const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
59 const format = 'json'
60 const maxHeight = 50
61 const maxWidth = 50
62
63 const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth)
64 const expectedHtml = `<iframe width="50" height="50" src="http://localhost:9001/videos/embed/${server.video.uuid}" ` +
65 'frameborder="0" allowfullscreen></iframe>'
66
67 expect(res.body.html).to.equal(expectedHtml)
68 expect(res.body.title).to.equal(server.video.name)
69 expect(res.body.author_name).to.equal(server.video.author)
70 expect(res.body.height).to.equal(50)
71 expect(res.body.width).to.equal(50)
72 expect(res.body).to.not.have.property('thumbnail_url')
73 expect(res.body).to.not.have.property('thumbnail_width')
74 expect(res.body).to.not.have.property('thumbnail_height')
75 })
76
77 after(async function () {
78 killallServers([ server ])
79
80 // Keep the logs if the test failed
81 if (this['ok']) {
82 await flushTests()
83 }
84 })
85})
diff --git a/server/tests/client.ts b/server/tests/client.ts
index 5e5abba5a..5f947ed2b 100644
--- a/server/tests/client.ts
+++ b/server/tests/client.ts
@@ -39,7 +39,7 @@ describe('Test a client controllers', function () {
39 server.video = videos[0] 39 server.video = videos[0]
40 }) 40 })
41 41
42 it('It should have valid Open Graph tags on the watch page with video id', async function () { 42 it('Should have valid Open Graph tags on the watch page with video id', async function () {
43 const res = await request(server.url) 43 const res = await request(server.url)
44 .get('/videos/watch/' + server.video.id) 44 .get('/videos/watch/' + server.video.id)
45 .expect(200) 45 .expect(200)
@@ -48,7 +48,7 @@ describe('Test a client controllers', function () {
48 expect(res.text).to.contain('<meta property="og:description" content="my super description for pod 1" />') 48 expect(res.text).to.contain('<meta property="og:description" content="my super description for pod 1" />')
49 }) 49 })
50 50
51 it('It should have valid Open Graph tags on the watch page with video uuid', async function () { 51 it('Should have valid Open Graph tags on the watch page with video uuid', async function () {
52 const res = await request(server.url) 52 const res = await request(server.url)
53 .get('/videos/watch/' + server.video.uuid) 53 .get('/videos/watch/' + server.video.uuid)
54 .expect(200) 54 .expect(200)
@@ -57,6 +57,19 @@ describe('Test a client controllers', function () {
57 expect(res.text).to.contain('<meta property="og:description" content="my super description for pod 1" />') 57 expect(res.text).to.contain('<meta property="og:description" content="my super description for pod 1" />')
58 }) 58 })
59 59
60 it('Should have valid oEmbed discovery tags', async function () {
61 const path = '/videos/watch/' + server.video.uuid
62 const res = await request(server.url)
63 .get(path)
64 .expect(200)
65
66 const expectedLink = '<link rel="alternate" type="application/json+oembed" href="http://localhost:9001/services/oembed?' +
67 `url=http%3A%2F%2Flocalhost%3A9001%2Fvideos%2Fwatch%2F${server.video.uuid}" ` +
68 `title="${server.video.name}" />`
69
70 expect(res.text).to.contain(expectedLink)
71 })
72
60 after(async function () { 73 after(async function () {
61 process.kill(-server.app.pid) 74 process.kill(-server.app.pid)
62 75
diff --git a/server/tests/utils/index.ts b/server/tests/utils/index.ts
index 99c445887..90ee2d515 100644
--- a/server/tests/utils/index.ts
+++ b/server/tests/utils/index.ts
@@ -7,6 +7,7 @@ export * from './pods'
7export * from './request-schedulers' 7export * from './request-schedulers'
8export * from './requests' 8export * from './requests'
9export * from './servers' 9export * from './servers'
10export * from './services'
10export * from './users' 11export * from './users'
11export * from './video-abuses' 12export * from './video-abuses'
12export * from './video-blacklist' 13export * from './video-blacklist'
diff --git a/server/tests/utils/servers.ts b/server/tests/utils/servers.ts
index 88027f74e..3526ffa51 100644
--- a/server/tests/utils/servers.ts
+++ b/server/tests/utils/servers.ts
@@ -23,6 +23,8 @@ interface ServerInfo {
23 video?: { 23 video?: {
24 id: number 24 id: number
25 uuid: string 25 uuid: string
26 name: string
27 author: string
26 } 28 }
27 29
28 remoteVideo?: { 30 remoteVideo?: {
diff --git a/server/tests/utils/services.ts b/server/tests/utils/services.ts
new file mode 100644
index 000000000..1a53dd4cf
--- /dev/null
+++ b/server/tests/utils/services.ts
@@ -0,0 +1,23 @@
1import * as request from 'supertest'
2
3function getOEmbed (url: string, oembedUrl: string, format?: string, maxHeight?: number, maxWidth?: number) {
4 const path = '/services/oembed'
5 const query = {
6 url: oembedUrl,
7 format,
8 maxheight: maxHeight,
9 maxwidth: maxWidth
10 }
11
12 return request(url)
13 .get(path)
14 .query(query)
15 .set('Accept', 'application/json')
16 .expect(200)
17}
18
19// ---------------------------------------------------------------------------
20
21export {
22 getOEmbed
23}
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts
index 75070bfd6..bbcada845 100644
--- a/shared/models/videos/video.model.ts
+++ b/shared/models/videos/video.model.ts
@@ -25,6 +25,7 @@ export interface Video {
25 tags: string[] 25 tags: string[]
26 thumbnailPath: string 26 thumbnailPath: string
27 previewPath: string 27 previewPath: string
28 embedPath: string
28 views: number 29 views: number
29 likes: number 30 likes: number
30 dislikes: number 31 dislikes: number