getVideoIframeCode () {
return '<iframe width="560" height="315" ' +
- 'src="' + window.location.origin + '/videos/embed/' + this.video.uuid + '" ' +
+ 'src="' + this.video.embedUrl + '" ' +
'frameborder="0" allowfullscreen>' +
'</iframe>'
}
thumbnailUrl: string
previewPath: string
previewUrl: string
+ embedPath: string
+ embedUrl: string
views: number
likes: number
dislikes: number
tags: string[],
thumbnailPath: string,
previewPath: string,
+ embedPath: string,
views: number,
likes: number,
dislikes: number,
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
<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" />
fi
if pgrep peertube > /dev/null; then
- echo 'PeerTube is running!'
+ echo 'PeerTube is running, please shut it off before upgrading'
exit 0
fi
// ----------- 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 -----------
const apiRoute = '/api/' + API_VERSION
app.use(apiRoute, apiRouter)
+// Services (oembed...)
+app.use('/services', servicesRouter)
+
// Client files
app.use('/', clientsRouter)
CONFIG,
STATIC_PATHS,
STATIC_MAX_AGE,
- OPENGRAPH_COMMENT
+ OPENGRAPH_AND_OEMBED_COMMENT
} from '../initializers'
import { root, readFileBufferPromise } from '../helpers'
import { VideoInstance } from '../models'
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)
// ---------------------------------------------------------------------------
-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,
'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) {
// 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))
export * from './static'
export * from './client'
+export * from './services'
export * from './api'
--- /dev/null
+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)
+}
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 = {
// ---------------------------------------------------------------------------
-const OPENGRAPH_COMMENT = '<!-- open graph tags -->'
+const OPENGRAPH_AND_OEMBED_COMMENT = '<!-- open graph and oembed tags -->'
// ---------------------------------------------------------------------------
JOBS_FETCHING_INTERVAL,
LAST_MIGRATION_VERSION,
OAUTH_LIFETIME,
- OPENGRAPH_COMMENT,
+ OPENGRAPH_AND_OEMBED_COMMENT,
PAGINATION_COUNT_DEFAULT,
PODS_SCORE,
PREVIEWS_SIZE,
+export * from './oembed'
export * from './remote'
export * from './pagination'
export * from './pods'
--- /dev/null
+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
+}
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>
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
optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile
transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile
getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
+ getEmbedPath: VideoMethods.GetEmbedPath
setTags: Sequelize.HasManySetAssociationsMixin<TagAttributes, string>
addVideoFile: Sequelize.HasManyAddAssociationMixin<VideoFileAttributes, string>
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
let getVideoFilePath: VideoMethods.GetVideoFilePath
let createTorrentAndSetInfoHash: VideoMethods.CreateTorrentAndSetInfoHash
let getOriginalFileHeight: VideoMethods.GetOriginalFileHeight
+let getEmbedPath: VideoMethods.GetEmbedPath
let generateThumbnailFromData: VideoMethods.GenerateThumbnailFromData
let list: VideoMethods.List
createTorrentAndSetInfoHash,
generateMagnetUri,
getPreviewName,
+ getPreviewPath,
getThumbnailName,
+ getThumbnailPath,
getTorrentFileName,
getVideoFilename,
getVideoFilePath,
toUpdateRemoteJSON,
optimizeOriginalVideofile,
transcodeOriginalVideofile,
- getOriginalFileHeight
+ getOriginalFileHeight,
+ getEmbedPath
]
addMethodsToModel(Video, classMethods, instanceMethods)
}
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
)
}
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
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: []
import './remotes'
import './users'
import './request-schedulers'
+import './services'
import './videos'
import './video-abuses'
import './video-blacklist'
--- /dev/null
+/* 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()
+ }
+ })
+})
import './video-blacklist'
import './video-blacklist-management'
import './multiple-pods'
+import './services'
import './request-schedulers'
import './friends-advanced'
import './video-transcoder'
--- /dev/null
+/* 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()
+ }
+ })
+})
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)
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)
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)
export * from './request-schedulers'
export * from './requests'
export * from './servers'
+export * from './services'
export * from './users'
export * from './video-abuses'
export * from './video-blacklist'
video?: {
id: number
uuid: string
+ name: string
+ author: string
}
remoteVideo?: {
--- /dev/null
+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
+}
tags: string[]
thumbnailPath: string
previewPath: string
+ embedPath: string
views: number
likes: number
dislikes: number