Add oembed endpoint
authorChocobozzz <florian.bigard@gmail.com>
Mon, 16 Oct 2017 08:05:49 +0000 (10:05 +0200)
committerChocobozzz <florian.bigard@gmail.com>
Mon, 16 Oct 2017 08:07:26 +0000 (10:07 +0200)
22 files changed:
client/src/app/videos/+video-watch/video-share.component.ts
client/src/app/videos/shared/video.model.ts
client/src/index.html
scripts/upgrade-peertube.sh
server.ts
server/controllers/client.ts
server/controllers/index.ts
server/controllers/services.ts [new file with mode: 0644]
server/initializers/constants.ts
server/middlewares/validators/index.ts
server/middlewares/validators/oembed.ts [new file with mode: 0644]
server/models/video/video-interface.ts
server/models/video/video.ts
server/tests/api/check-params/index.ts
server/tests/api/check-params/services.ts [new file with mode: 0644]
server/tests/api/index.ts
server/tests/api/services.ts [new file with mode: 0644]
server/tests/client.ts
server/tests/utils/index.ts
server/tests/utils/servers.ts
server/tests/utils/services.ts [new file with mode: 0644]
shared/models/videos/video.model.ts

index 133f9349809d5e0b4efbb958f055d24656aa2e0a..414ed28c6a79325f8592fb24454779214a545a24 100644 (file)
@@ -27,7 +27,7 @@ export class VideoShareComponent {
 
   getVideoIframeCode () {
     return '<iframe width="560" height="315" ' +
-           'src="' + window.location.origin + '/videos/embed/' + this.video.uuid + '" ' +
+           'src="' + this.video.embedUrl + '" ' +
            'frameborder="0" allowfullscreen>' +
            '</iframe>'
   }
index b315e59b184451990927505140fe070d7e14e91e..51c5319eacae0bc0d5b3d72b257efeb0ba0d1b64 100644 (file)
@@ -26,6 +26,8 @@ export class Video implements VideoServerModel {
   thumbnailUrl: string
   previewPath: string
   previewUrl: string
+  embedPath: string
+  embedUrl: string
   views: number
   likes: number
   dislikes: number
@@ -64,6 +66,7 @@ export class Video implements VideoServerModel {
     tags: string[],
     thumbnailPath: string,
     previewPath: string,
+    embedPath: string,
     views: number,
     likes: number,
     dislikes: number,
@@ -91,6 +94,8 @@ export class Video implements VideoServerModel {
     this.thumbnailUrl = API_URL + hash.thumbnailPath
     this.previewPath = hash.previewPath
     this.previewUrl = API_URL + hash.previewPath
+    this.embedPath = hash.embedPath
+    this.embedUrl = API_URL + hash.embedPath
     this.views = hash.views
     this.likes = hash.likes
     this.dislikes = hash.dislikes
index 91ed04d177bd6b0d6c285590bb3a512d910a201a..8e94b903d5de51eb796304d731ec2e29024e64d1 100644 (file)
@@ -7,8 +7,8 @@
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <meta name="description" content="PeerTube, a decentralized video streaming platform using P2P (BitTorrent) directly in the web browser" />
 
-    <!-- The following comment is used by the server to prerender OpenGraph tags -->
-    <!-- open graph tags -->
+    <!-- The following comment is used by the server to prerender OpenGraph and oEmbed tags -->
+    <!-- open graph and oembed tags -->
     <!-- Do not remove it! -->
 
     <link rel="icon" type="image/png" href="/client/assets/favicon.png" />
index 5186a269fa7d66c42708452bedd31ba4632c14a2..562a2a618a9c0da351132cea9cc176bec3ceaaa2 100755 (executable)
@@ -26,7 +26,7 @@ if ! which yarn > /dev/null; then
 fi
 
 if pgrep peertube > /dev/null; then
-  echo 'PeerTube is running!'
+  echo 'PeerTube is running, please shut it off before upgrading'
   exit 0
 fi
 
index 3f2d27718187c0e001ced9ffc8832a5af76b351a..72bb11e747bc8ccf35e3cf4d2511852af0bb46d5 100644 (file)
--- a/server.ts
+++ b/server.ts
@@ -47,7 +47,7 @@ db.init(false).then(() => onDatabaseInitDone())
 // ----------- PeerTube modules -----------
 import { migrate, installApplication } from './server/initializers'
 import { JobScheduler, activateSchedulers, VideosPreviewCache } from './server/lib'
-import { apiRouter, clientsRouter, staticRouter } from './server/controllers'
+import { apiRouter, clientsRouter, staticRouter, servicesRouter } from './server/controllers'
 
 // ----------- Command line -----------
 
@@ -85,6 +85,9 @@ app.use(bodyParser.urlencoded({ extended: false }))
 const apiRoute = '/api/' + API_VERSION
 app.use(apiRoute, apiRouter)
 
+// Services (oembed...)
+app.use('/services', servicesRouter)
+
 // Client files
 app.use('/', clientsRouter)
 
index b23f7e1aeb4fc6add8e640e7b55b563567325800..e3c9620588f3ae368b9a8965ad8b037edf325646 100644 (file)
@@ -8,7 +8,7 @@ import {
   CONFIG,
   STATIC_PATHS,
   STATIC_MAX_AGE,
-  OPENGRAPH_COMMENT
+  OPENGRAPH_AND_OEMBED_COMMENT
 } from '../initializers'
 import { root, readFileBufferPromise } from '../helpers'
 import { VideoInstance } from '../models'
@@ -19,7 +19,7 @@ const distPath = join(root(), 'client', 'dist')
 const embedPath = join(distPath, 'standalone', 'videos', 'embed.html')
 const indexPath = join(distPath, 'index.html')
 
-// Special route that add OpenGraph tags
+// Special route that add OpenGraph and oEmbed tags
 // Do not use a template engine for a so little thing
 clientsRouter.use('/videos/watch/:id', generateWatchHtmlPage)
 
@@ -43,11 +43,11 @@ export {
 
 // ---------------------------------------------------------------------------
 
-function addOpenGraphTags (htmlStringPage: string, video: VideoInstance) {
+function addOpenGraphAndOEmbedTags (htmlStringPage: string, video: VideoInstance) {
   const previewUrl = CONFIG.WEBSERVER.URL + STATIC_PATHS.PREVIEWS + video.getPreviewName()
-  const videoUrl = CONFIG.WEBSERVER.URL + '/videos/watch/' + video.id
+  const videoUrl = CONFIG.WEBSERVER.URL + '/videos/watch/' + video.uuid
 
-  const metaTags = {
+  const openGraphMetaTags = {
     'og:type': 'video',
     'og:title': video.name,
     'og:image': previewUrl,
@@ -65,14 +65,26 @@ function addOpenGraphTags (htmlStringPage: string, video: VideoInstance) {
     'twitter:image': previewUrl
   }
 
+  const oembedLinkTags = [
+    {
+      type: 'application/json+oembed',
+      href: CONFIG.WEBSERVER.URL + '/services/oembed?url=' + encodeURIComponent(videoUrl),
+      title: video.name
+    }
+  ]
+
   let tagsString = ''
-  Object.keys(metaTags).forEach(tagName => {
-    const tagValue = metaTags[tagName]
+  Object.keys(openGraphMetaTags).forEach(tagName => {
+    const tagValue = openGraphMetaTags[tagName]
 
-    tagsString += '<meta property="' + tagName + '" content="' + tagValue + '" />'
+    tagsString += `<meta property="${tagName}" content="${tagValue}" />`
   })
 
-  return htmlStringPage.replace(OPENGRAPH_COMMENT, tagsString)
+  for (const oembedLinkTag of oembedLinkTags) {
+    tagsString += `<link rel="alternate" type="${oembedLinkTag.type}" href="${oembedLinkTag.href}" title="${oembedLinkTag.title}" />`
+  }
+
+  return htmlStringPage.replace(OPENGRAPH_AND_OEMBED_COMMENT, tagsString)
 }
 
 function generateWatchHtmlPage (req: express.Request, res: express.Response, next: express.NextFunction) {
@@ -101,7 +113,7 @@ function generateWatchHtmlPage (req: express.Request, res: express.Response, nex
     // Let Angular application handle errors
     if (!video) return res.sendFile(indexPath)
 
-    const htmlStringPageWithTags = addOpenGraphTags(html, video)
+    const htmlStringPageWithTags = addOpenGraphAndOEmbedTags(html, video)
     res.set('Content-Type', 'text/html; charset=UTF-8').send(htmlStringPageWithTags)
   })
   .catch(err => next(err))
index 0223a98f119556a6912f3744b7ac6154f8d2803e..51cb480a3f54f76f0ef08198d0f792638849da61 100644 (file)
@@ -1,3 +1,4 @@
 export * from './static'
 export * from './client'
+export * from './services'
 export * from './api'
diff --git a/server/controllers/services.ts b/server/controllers/services.ts
new file mode 100644 (file)
index 0000000..3ce6bd5
--- /dev/null
@@ -0,0 +1,62 @@
+import * as express from 'express'
+
+import { CONFIG, THUMBNAILS_SIZE } from '../initializers'
+import { oembedValidator } from '../middlewares'
+import { VideoInstance } from '../models'
+
+const servicesRouter = express.Router()
+
+servicesRouter.use('/oembed', oembedValidator, generateOEmbed)
+
+// ---------------------------------------------------------------------------
+
+export {
+  servicesRouter
+}
+
+// ---------------------------------------------------------------------------
+
+function generateOEmbed (req: express.Request, res: express.Response, next: express.NextFunction) {
+  const video = res.locals.video as VideoInstance
+  const webserverUrl = CONFIG.WEBSERVER.URL
+  const maxHeight = parseInt(req.query.maxheight, 10)
+  const maxWidth = parseInt(req.query.maxwidth, 10)
+
+  const embedUrl = webserverUrl + video.getEmbedPath()
+  let thumbnailUrl = webserverUrl + video.getThumbnailPath()
+  let embedWidth = 560
+  let embedHeight = 315
+
+  if (maxHeight < embedHeight) embedHeight = maxHeight
+  if (maxWidth < embedWidth) embedWidth = maxWidth
+
+  // Our thumbnail is too big for the consumer
+  if (
+    (maxHeight !== undefined && maxHeight < THUMBNAILS_SIZE.height) ||
+    (maxWidth !== undefined && maxWidth < THUMBNAILS_SIZE.width)
+  ) {
+    thumbnailUrl = undefined
+  }
+
+  const html = `<iframe width="${embedWidth}" height="${embedHeight}" src="${embedUrl}" frameborder="0" allowfullscreen></iframe>`
+
+  const json: any = {
+    type: 'video',
+    version: '1.0',
+    html,
+    width: embedWidth,
+    height: embedHeight,
+    title: video.name,
+    author_name: video.Author.name,
+    provider_name: 'PeerTube',
+    provider_url: webserverUrl
+  }
+
+  if (thumbnailUrl !== undefined) {
+    json.thumbnail_url = thumbnailUrl
+    json.thumbnail_width = THUMBNAILS_SIZE.width
+    json.thumbnail_height = THUMBNAILS_SIZE.height
+  }
+
+  return res.json(json)
+}
index b11575b346ebbb15c845403ba15c4980ed0951db..6218644cfbaa83ab9fff024b6f4c27d3d60be20e 100644 (file)
@@ -295,8 +295,14 @@ const STATIC_PATHS = {
 let STATIC_MAX_AGE = '30d'
 
 // Videos thumbnail size
-const THUMBNAILS_SIZE = '200x110'
-const PREVIEWS_SIZE = '640x480'
+const THUMBNAILS_SIZE = {
+  width: 200,
+  height: 110
+}
+const PREVIEWS_SIZE = {
+  width: 640,
+  height: 480
+}
 
 // Sub folders of cache directory
 const CACHE = {
@@ -314,7 +320,7 @@ const USER_ROLES: { [ id: string ]: UserRole } = {
 
 // ---------------------------------------------------------------------------
 
-const OPENGRAPH_COMMENT = '<!-- open graph tags -->'
+const OPENGRAPH_AND_OEMBED_COMMENT = '<!-- open graph and oembed tags -->'
 
 // ---------------------------------------------------------------------------
 
@@ -344,7 +350,7 @@ export {
   JOBS_FETCHING_INTERVAL,
   LAST_MIGRATION_VERSION,
   OAUTH_LIFETIME,
-  OPENGRAPH_COMMENT,
+  OPENGRAPH_AND_OEMBED_COMMENT,
   PAGINATION_COUNT_DEFAULT,
   PODS_SCORE,
   PREVIEWS_SIZE,
index 418fa5f1daf2437662e8a0ec47f5ee9f8a3c5693..068c41b240e763e34c28ee69cea4309223665bbe 100644 (file)
@@ -1,3 +1,4 @@
+export * from './oembed'
 export * from './remote'
 export * from './pagination'
 export * from './pods'
diff --git a/server/middlewares/validators/oembed.ts b/server/middlewares/validators/oembed.ts
new file mode 100644 (file)
index 0000000..4b8c03f
--- /dev/null
@@ -0,0 +1,63 @@
+import { query } from 'express-validator/check'
+import * as express from 'express'
+import { join } from 'path'
+
+import { checkErrors } from './utils'
+import { CONFIG } from '../../initializers'
+import { logger } from '../../helpers'
+import { checkVideoExists, isVideoIdOrUUIDValid } from '../../helpers/custom-validators/videos'
+import { isTestInstance } from '../../helpers/core-utils'
+
+const urlShouldStartWith = CONFIG.WEBSERVER.SCHEME + '://' + join(CONFIG.WEBSERVER.HOST, 'videos', 'watch') + '/'
+const videoWatchRegex = new RegExp('([^/]+)$')
+const isURLOptions = {
+  require_host: true,
+  require_tld: true
+}
+
+// We validate 'localhost', so we don't have the top level domain
+if (isTestInstance()) {
+  isURLOptions.require_tld = false
+}
+
+const oembedValidator = [
+  query('url').isURL(isURLOptions).withMessage('Should have a valid url'),
+  query('maxwidth').optional().isInt().withMessage('Should have a valid max width'),
+  query('maxheight').optional().isInt().withMessage('Should have a valid max height'),
+  query('format').optional().isIn([ 'xml', 'json' ]).withMessage('Should have a valid format'),
+
+  (req: express.Request, res: express.Response, next: express.NextFunction) => {
+    logger.debug('Checking oembed parameters', { parameters: req.query })
+
+    checkErrors(req, res, () => {
+      if (req.query.format !== undefined && req.query.format !== 'json') {
+        return res.status(501)
+                  .json({ error: 'Requested format is not implemented on server.' })
+                  .end()
+      }
+
+      const startIsOk = req.query.url.startsWith(urlShouldStartWith)
+      const matches = videoWatchRegex.exec(req.query.url)
+      if (startIsOk === false || matches === null) {
+        return res.status(400)
+                  .json({ error: 'Invalid url.' })
+                  .end()
+      }
+
+      const videoId = matches[1]
+      if (isVideoIdOrUUIDValid(videoId) === false) {
+        return res.status(400)
+                  .json({ error: 'Invalid video id.' })
+                  .end()
+      }
+
+      checkVideoExists(videoId, res, next)
+    })
+  }
+]
+
+// ---------------------------------------------------------------------------
+
+export {
+  oembedValidator
+}
index 6a3db4f3efcf5957055054a81842a88a1c276b87..1402df26a2aaf20f44403b6e16937d1c06f1df36 100644 (file)
@@ -32,6 +32,9 @@ export namespace VideoMethods {
   export type OptimizeOriginalVideofile = (this: VideoInstance) => Promise<void>
   export type TranscodeOriginalVideofile = (this: VideoInstance, resolution: number) => Promise<void>
   export type GetOriginalFileHeight = (this: VideoInstance) => Promise<number>
+  export type GetEmbedPath = (this: VideoInstance) => string
+  export type GetThumbnailPath = (this: VideoInstance) => string
+  export type GetPreviewPath = (this: VideoInstance) => string
 
   // Return thumbnail name
   export type GenerateThumbnailFromData = (video: VideoInstance, thumbnailData: string) => Promise<string>
@@ -107,7 +110,9 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
   getOriginalFile: VideoMethods.GetOriginalFile
   generateMagnetUri: VideoMethods.GenerateMagnetUri
   getPreviewName: VideoMethods.GetPreviewName
+  getPreviewPath: VideoMethods.GetPreviewPath
   getThumbnailName: VideoMethods.GetThumbnailName
+  getThumbnailPath: VideoMethods.GetThumbnailPath
   getTorrentFileName: VideoMethods.GetTorrentFileName
   getVideoFilename: VideoMethods.GetVideoFilename
   getVideoFilePath: VideoMethods.GetVideoFilePath
@@ -122,6 +127,7 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In
   optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
   transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
   getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
+  getEmbedPath: VideoMethods.GetEmbedPath
 
   setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
   addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string>
index 2ba6cf25f0cc4daa8b137f0c6e8045a506633f64..0d0048b4a6220bc8a13a2d35cc12a8fea0e38433 100644 (file)
@@ -54,7 +54,9 @@ let getOriginalFile: VideoMethods.GetOriginalFile
 let generateMagnetUri: VideoMethods.GenerateMagnetUri
 let getVideoFilename: VideoMethods.GetVideoFilename
 let getThumbnailName: VideoMethods.GetThumbnailName
+let getThumbnailPath: VideoMethods.GetThumbnailPath
 let getPreviewName: VideoMethods.GetPreviewName
+let getPreviewPath: VideoMethods.GetPreviewPath
 let getTorrentFileName: VideoMethods.GetTorrentFileName
 let isOwned: VideoMethods.IsOwned
 let toFormattedJSON: VideoMethods.ToFormattedJSON
@@ -67,6 +69,7 @@ let createThumbnail: VideoMethods.CreateThumbnail
 let getVideoFilePath: VideoMethods.GetVideoFilePath
 let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
 let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
+let getEmbedPath: VideoMethods.GetEmbedPath
 
 let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
 let list: VideoMethods.List
@@ -252,7 +255,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     createTorrentAndSetInfoHash,
     generateMagnetUri,
     getPreviewName,
+    getPreviewPath,
     getThumbnailName,
+    getThumbnailPath,
     getTorrentFileName,
     getVideoFilename,
     getVideoFilePath,
@@ -267,7 +272,8 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da
     toUpdateRemoteJSON,
     optimizeOriginalVideofile,
     transcodeOriginalVideofile,
-    getOriginalFileHeight
+    getOriginalFileHeight,
+    getEmbedPath
   ]
   addMethodsToModel(Video, classMethods, instanceMethods)
 
@@ -375,11 +381,13 @@ createPreview = function (this: VideoInstance, videoFile: VideoFileInstance) {
 }
 
 createThumbnail = function (this: VideoInstance, videoFile: VideoFileInstance) {
+  const imageSize = THUMBNAILS_SIZE.width + 'x' + THUMBNAILS_SIZE.height
+
   return generateImageFromVideoFile(
     this.getVideoFilePath(videoFile),
     CONFIG.STORAGE.THUMBNAILS_DIR,
     this.getThumbnailName(),
-    THUMBNAILS_SIZE
+    imageSize
   )
 }
 
@@ -438,6 +446,18 @@ generateMagnetUri = function (this: VideoInstance, videoFile: VideoFileInstance)
   return magnetUtil.encode(magnetHash)
 }
 
+getEmbedPath = function (this: VideoInstance) {
+  return '/videos/embed/' + this.uuid
+}
+
+getThumbnailPath = function (this: VideoInstance) {
+  return join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName())
+}
+
+getPreviewPath = function (this: VideoInstance) {
+  return join(STATIC_PATHS.PREVIEWS, this.getPreviewName())
+}
+
 toFormattedJSON = function (this: VideoInstance) {
   let podHost
 
@@ -480,8 +500,9 @@ toFormattedJSON = function (this: VideoInstance) {
     likes: this.likes,
     dislikes: this.dislikes,
     tags: map<TagInstance, string>(this.Tags, 'name'),
-    thumbnailPath: join(STATIC_PATHS.THUMBNAILS, this.getThumbnailName()),
-    previewPath: join(STATIC_PATHS.PREVIEWS, this.getPreviewName()),
+    thumbnailPath: this.getThumbnailPath(),
+    previewPath: this.getPreviewPath(),
+    embedPath: this.getEmbedPath(),
     createdAt: this.createdAt,
     updatedAt: this.updatedAt,
     files: []
index 399a05bc347ebe2dd6293b14a4236f7f734b8986..954b206e916a4d0b8ed6bb5fd852ad15171d911e 100644 (file)
@@ -3,6 +3,7 @@ import './pods'
 import './remotes'
 import './users'
 import './request-schedulers'
+import './services'
 import './videos'
 import './video-abuses'
 import './video-blacklist'
diff --git a/server/tests/api/check-params/services.ts b/server/tests/api/check-params/services.ts
new file mode 100644 (file)
index 0000000..780254d
--- /dev/null
@@ -0,0 +1,159 @@
+/* tslint:disable:no-unused-expression */
+
+import * as request from 'supertest'
+import 'mocha'
+
+import {
+  flushTests,
+  runServer,
+  setAccessTokensToServers,
+  killallServers
+} from '../../utils'
+import { getVideosList, uploadVideo } from '../../utils/videos'
+
+describe('Test services API validators', function () {
+  let server
+
+  // ---------------------------------------------------------------
+
+  before(async function () {
+    this.timeout(60000)
+
+    await flushTests()
+
+    server = await runServer(1)
+    await setAccessTokensToServers([ server ])
+
+    const videoAttributes = {
+      name: 'my super name'
+    }
+    await uploadVideo(server.url, server.accessToken, videoAttributes)
+
+    const res = await getVideosList(server.url)
+    server.video = res.body.data[0]
+  })
+
+  describe('Test oEmbed API validators', function () {
+    const path = '/services/oembed'
+
+    it('Should fail with an invalid url', async function () {
+      const embedUrl = 'hello.com'
+
+      await request(server.url)
+        .get(path)
+        .query({ url: embedUrl })
+        .set('Accept', 'application/json')
+        .set('Authorization', 'Bearer ' + server.accessToken)
+        .expect(400)
+    })
+
+    it('Should fail with an invalid host', async function () {
+      const embedUrl = 'http://hello.com/videos/watch/' + server.video.uuid
+
+      await request(server.url)
+        .get(path)
+        .query({ url: embedUrl })
+        .set('Accept', 'application/json')
+        .set('Authorization', 'Bearer ' + server.accessToken)
+        .expect(400)
+    })
+
+    it('Should fail with an invalid video id', async function () {
+      const embedUrl = 'http://localhost:9001/videos/watch/blabla'
+
+      await request(server.url)
+        .get(path)
+        .query({ url: embedUrl })
+        .set('Accept', 'application/json')
+        .set('Authorization', 'Bearer ' + server.accessToken)
+        .expect(400)
+    })
+
+    it('Should fail with an unknown video', async function () {
+      const embedUrl = 'http://localhost:9001/videos/watch/88fc0165-d1f0-4a35-a51a-3b47f668689c'
+
+      await request(server.url)
+        .get(path)
+        .query({ url: embedUrl })
+        .set('Accept', 'application/json')
+        .set('Authorization', 'Bearer ' + server.accessToken)
+        .expect(404)
+    })
+
+    it('Should fail with an invalid path', async function () {
+      const embedUrl = 'http://localhost:9001/videos/watchs/' + server.video.uuid
+
+      await request(server.url)
+        .get(path)
+        .query({ url: embedUrl })
+        .set('Accept', 'application/json')
+        .set('Authorization', 'Bearer ' + server.accessToken)
+        .expect(400)
+    })
+
+    it('Should fail with an invalid max height', async function () {
+      const embedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
+
+      await request(server.url)
+        .get(path)
+        .query({
+          url: embedUrl,
+          maxheight: 'hello'
+        })
+        .set('Accept', 'application/json')
+        .set('Authorization', 'Bearer ' + server.accessToken)
+        .expect(400)
+    })
+
+    it('Should fail with an invalid max width', async function () {
+      const embedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
+
+      await request(server.url)
+        .get(path)
+        .query({
+          url: embedUrl,
+          maxwidth: 'hello'
+        })
+        .set('Accept', 'application/json')
+        .set('Authorization', 'Bearer ' + server.accessToken)
+        .expect(400)
+    })
+
+    it('Should fail with an invalid format', async function () {
+      const embedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
+
+      await request(server.url)
+        .get(path)
+        .query({
+          url: embedUrl,
+          format: 'blabla'
+        })
+        .set('Accept', 'application/json')
+        .set('Authorization', 'Bearer ' + server.accessToken)
+        .expect(400)
+    })
+
+    it('Should fail with a non supported format', async function () {
+      const embedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
+
+      await request(server.url)
+        .get(path)
+        .query({
+          url: embedUrl,
+          format: 'xml'
+        })
+        .set('Accept', 'application/json')
+        .set('Authorization', 'Bearer ' + server.accessToken)
+        .expect(501)
+    })
+  })
+
+  after(async function () {
+    killallServers([ server ])
+
+    // Keep the logs if the test failed
+    if (this['ok']) {
+      await flushTests()
+    }
+  })
+})
index 03711e68aa2da45419ec212f1f06b6d78e78a930..e50e65049b2b2f173ebe8c345bfae5fa2b393fb1 100644 (file)
@@ -8,6 +8,7 @@ import './video-abuse'
 import './video-blacklist'
 import './video-blacklist-management'
 import './multiple-pods'
+import './services'
 import './request-schedulers'
 import './friends-advanced'
 import './video-transcoder'
diff --git a/server/tests/api/services.ts b/server/tests/api/services.ts
new file mode 100644 (file)
index 0000000..b396ea5
--- /dev/null
@@ -0,0 +1,85 @@
+/* tslint:disable:no-unused-expression */
+
+import 'mocha'
+import * as chai from 'chai'
+const expect = chai.expect
+
+import {
+  ServerInfo,
+  flushTests,
+  uploadVideo,
+  getVideosList,
+  setAccessTokensToServers,
+  killallServers,
+  getOEmbed
+} from '../utils'
+import { runServer } from '../utils/servers'
+
+describe('Test services', function () {
+  let server: ServerInfo = null
+
+  before(async function () {
+    this.timeout(120000)
+
+    await flushTests()
+
+    server = await runServer(1)
+
+    await setAccessTokensToServers([ server ])
+
+    const videoAttributes = {
+      name: 'my super name'
+    }
+    await uploadVideo(server.url, server.accessToken, videoAttributes)
+
+    const res = await getVideosList(server.url)
+    server.video = res.body.data[0]
+  })
+
+  it('Should have a valid oEmbed response', async function () {
+    const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
+
+    const res = await getOEmbed(server.url, oembedUrl)
+    const expectedHtml = `<iframe width="560" height="315" src="http://localhost:9001/videos/embed/${server.video.uuid}" ` +
+                         'frameborder="0" allowfullscreen></iframe>'
+    const expectedThumbnailUrl = 'http://localhost:9001/static/thumbnails/' + server.video.uuid + '.jpg'
+
+    expect(res.body.html).to.equal(expectedHtml)
+    expect(res.body.title).to.equal(server.video.name)
+    expect(res.body.author_name).to.equal(server.video.author)
+    expect(res.body.height).to.equal(315)
+    expect(res.body.width).to.equal(560)
+    expect(res.body.thumbnail_url).to.equal(expectedThumbnailUrl)
+    expect(res.body.thumbnail_width).to.equal(200)
+    expect(res.body.thumbnail_height).to.equal(110)
+  })
+
+  it('Should have a valid oEmbed response with small max height query', async function () {
+    const oembedUrl = 'http://localhost:9001/videos/watch/' + server.video.uuid
+    const format = 'json'
+    const maxHeight = 50
+    const maxWidth = 50
+
+    const res = await getOEmbed(server.url, oembedUrl, format, maxHeight, maxWidth)
+    const expectedHtml = `<iframe width="50" height="50" src="http://localhost:9001/videos/embed/${server.video.uuid}" ` +
+                         'frameborder="0" allowfullscreen></iframe>'
+
+    expect(res.body.html).to.equal(expectedHtml)
+    expect(res.body.title).to.equal(server.video.name)
+    expect(res.body.author_name).to.equal(server.video.author)
+    expect(res.body.height).to.equal(50)
+    expect(res.body.width).to.equal(50)
+    expect(res.body).to.not.have.property('thumbnail_url')
+    expect(res.body).to.not.have.property('thumbnail_width')
+    expect(res.body).to.not.have.property('thumbnail_height')
+  })
+
+  after(async function () {
+    killallServers([ server ])
+
+    // Keep the logs if the test failed
+    if (this['ok']) {
+      await flushTests()
+    }
+  })
+})
index 5e5abba5aa1277e3de9528c1b02aa1546099722f..5f947ed2b98e632fa9520d59ce598d0a911847c1 100644 (file)
@@ -39,7 +39,7 @@ describe('Test a client controllers', function () {
     server.video = videos[0]
   })
 
-  it('It should have valid Open Graph tags on the watch page with video id', async function () {
+  it('Should have valid Open Graph tags on the watch page with video id', async function () {
     const res = await request(server.url)
                         .get('/videos/watch/' + server.video.id)
                         .expect(200)
@@ -48,7 +48,7 @@ describe('Test a client controllers', function () {
     expect(res.text).to.contain('<meta property="og:description" content="my super description for pod 1" />')
   })
 
-  it('It should have valid Open Graph tags on the watch page with video uuid', async function () {
+  it('Should have valid Open Graph tags on the watch page with video uuid', async function () {
     const res = await request(server.url)
                         .get('/videos/watch/' + server.video.uuid)
                         .expect(200)
@@ -57,6 +57,19 @@ describe('Test a client controllers', function () {
     expect(res.text).to.contain('<meta property="og:description" content="my super description for pod 1" />')
   })
 
+  it('Should have valid oEmbed discovery tags', async function () {
+    const path = '/videos/watch/' + server.video.uuid
+    const res = await request(server.url)
+      .get(path)
+      .expect(200)
+
+    const expectedLink = '<link rel="alternate" type="application/json+oembed" href="http://localhost:9001/services/oembed?' +
+      `url=http%3A%2F%2Flocalhost%3A9001%2Fvideos%2Fwatch%2F${server.video.uuid}" ` +
+      `title="${server.video.name}" />`
+
+    expect(res.text).to.contain(expectedLink)
+  })
+
   after(async function () {
     process.kill(-server.app.pid)
 
index 99c44588747b45800e054d598a3c17318ecb8316..90ee2d515c8594bac941a6364b814acfe486e303 100644 (file)
@@ -7,6 +7,7 @@ export * from './pods'
 export * from './request-schedulers'
 export * from './requests'
 export * from './servers'
+export * from './services'
 export * from './users'
 export * from './video-abuses'
 export * from './video-blacklist'
index 88027f74e557dc71eb0551bcd4277499cd7d7472..3526ffa519cb9b1cdb73b8124f8576432f4d5367 100644 (file)
@@ -23,6 +23,8 @@ interface ServerInfo {
   video?: {
     id: number
     uuid: string
+    name: string
+    author: string
   }
 
   remoteVideo?: {
diff --git a/server/tests/utils/services.ts b/server/tests/utils/services.ts
new file mode 100644 (file)
index 0000000..1a53dd4
--- /dev/null
@@ -0,0 +1,23 @@
+import * as request from 'supertest'
+
+function getOEmbed (url: string, oembedUrl: string, format?: string, maxHeight?: number, maxWidth?: number) {
+  const path = '/services/oembed'
+  const query = {
+    url: oembedUrl,
+    format,
+    maxheight: maxHeight,
+    maxwidth: maxWidth
+  }
+
+  return request(url)
+          .get(path)
+          .query(query)
+          .set('Accept', 'application/json')
+          .expect(200)
+}
+
+// ---------------------------------------------------------------------------
+
+export {
+  getOEmbed
+}
index 75070bfd6892ba9e81a2722c135cbdaab0209fe2..bbcada845412a48b5187410eb86c111afcaab80c 100644 (file)
@@ -25,6 +25,7 @@ export interface Video {
   tags: string[]
   thumbnailPath: string
   previewPath: string
+  embedPath: string
   views: number
   likes: number
   dislikes: number