Implement [YouTube] badge (#5132)
* Implement [YouTube] badge * Update production-hosting.md with account owner * Add votes badge variant * Add links to tests * Switch to social badge style
This commit is contained in:
@@ -98,3 +98,4 @@ private:
|
||||
wheelmap_token: 'WHEELMAP_TOKEN'
|
||||
influx_username: 'INFLUX_USERNAME'
|
||||
influx_password: 'INFLUX_PASSWORD'
|
||||
youtube_api_key: 'YOUTUBE_API_KEY'
|
||||
|
||||
@@ -10,3 +10,4 @@ private:
|
||||
twitch_client_id: ...
|
||||
twitch_client_secret: ...
|
||||
wheelmap_token: ...
|
||||
youtube_api_key: ...
|
||||
|
||||
@@ -8,3 +8,4 @@ private:
|
||||
twitch_client_id: '...'
|
||||
twitch_client_secret: '...'
|
||||
wheelmap_token: '...'
|
||||
youtube_api_key: '...'
|
||||
|
||||
@@ -180,6 +180,7 @@ const privateConfigSchema = Joi.object({
|
||||
wheelmap_token: Joi.string(),
|
||||
influx_username: Joi.string(),
|
||||
influx_password: Joi.string(),
|
||||
youtube_api_key: Joi.string(),
|
||||
}).required()
|
||||
const privateMetricsInfluxConfigSchema = privateConfigSchema.append({
|
||||
influx_username: Joi.string().required(),
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
| Cloudflare | Admin access | @espadrine, @paulmelnikow |
|
||||
| GitHub | OAuth app | @espadrine ([could be transferred to the badges org][oauth transfer]) |
|
||||
| Twitch | OAuth app | @PyvesB |
|
||||
| YouTube | Account owner | @PyvesB |
|
||||
| OpenStreetMap (for Wheelmap) | Account owner | @paulmelnikow |
|
||||
| DNS | Account owner | @olivierlacan |
|
||||
| DNS | Read-only account access | @espadrine, @paulmelnikow, @chris48s |
|
||||
|
||||
@@ -228,6 +228,16 @@ displayed on your profile page.
|
||||
|
||||
[wheelmap token]: http://classic.wheelmap.org/en/users/sign_in
|
||||
|
||||
### YouTube
|
||||
|
||||
- `YOUTUBE_API_KEY` (yml: `private.youtube_api_key`)
|
||||
|
||||
The YouTube API requires authentication. To obtain an API key,
|
||||
log in to a Google account, go to the [credentials page][youtube credentials],
|
||||
and create an API key for the YouTube Data API v3.
|
||||
|
||||
[youtube credentials]: https://console.developers.google.com/apis/credentials
|
||||
|
||||
## Error reporting
|
||||
|
||||
- `SENTRY_DSN` (yml: `private.sentry_dsn`)
|
||||
|
||||
72
services/youtube/youtube-base.js
Normal file
72
services/youtube/youtube-base.js
Normal file
@@ -0,0 +1,72 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { BaseJsonService, NotFound } = require('..')
|
||||
const { metric } = require('../text-formatters')
|
||||
const { nonNegativeInteger } = require('../validators')
|
||||
|
||||
const schema = Joi.object({
|
||||
items: Joi.array()
|
||||
.items(
|
||||
Joi.object({
|
||||
statistics: Joi.object({
|
||||
viewCount: nonNegativeInteger,
|
||||
likeCount: nonNegativeInteger,
|
||||
dislikeCount: nonNegativeInteger,
|
||||
commentCount: nonNegativeInteger,
|
||||
}).required(),
|
||||
})
|
||||
)
|
||||
.required(),
|
||||
}).required()
|
||||
|
||||
module.exports = class YouTubeBase extends BaseJsonService {
|
||||
static get category() {
|
||||
return 'social'
|
||||
}
|
||||
|
||||
static get auth() {
|
||||
return {
|
||||
passKey: 'youtube_api_key',
|
||||
authorizedOrigins: ['https://www.googleapis.com'],
|
||||
isRequired: true,
|
||||
}
|
||||
}
|
||||
|
||||
static get defaultBadgeData() {
|
||||
return { label: 'youtube', color: 'red', namedLogo: 'youtube' }
|
||||
}
|
||||
|
||||
static renderSingleStat({ statistics, statisticName, videoId }) {
|
||||
return {
|
||||
label: `${statisticName}s`,
|
||||
message: metric(statistics[`${statisticName}Count`]),
|
||||
style: 'social',
|
||||
link: `https://www.youtube.com/watch?v=${encodeURIComponent(videoId)}`,
|
||||
}
|
||||
}
|
||||
|
||||
async fetch({ videoId }) {
|
||||
return this._requestJson(
|
||||
this.authHelper.withQueryStringAuth(
|
||||
{ passKey: 'key' },
|
||||
{
|
||||
schema,
|
||||
url: 'https://www.googleapis.com/youtube/v3/videos',
|
||||
options: {
|
||||
qs: { id: videoId, part: 'statistics' },
|
||||
},
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async handle({ videoId }, queryParams) {
|
||||
const json = await this.fetch({ videoId })
|
||||
if (json.items.length === 0) {
|
||||
throw new NotFound({ prettyMessage: 'video not found' })
|
||||
}
|
||||
const statistics = json.items[0].statistics
|
||||
return this.constructor.render({ statistics, videoId }, queryParams)
|
||||
}
|
||||
}
|
||||
36
services/youtube/youtube-comments.service.js
Normal file
36
services/youtube/youtube-comments.service.js
Normal file
@@ -0,0 +1,36 @@
|
||||
'use strict'
|
||||
|
||||
const YouTubeBase = require('./youtube-base')
|
||||
|
||||
module.exports = class YouTubeComments extends YouTubeBase {
|
||||
static get route() {
|
||||
return {
|
||||
base: 'youtube/comments',
|
||||
pattern: ':videoId',
|
||||
}
|
||||
}
|
||||
|
||||
static get examples() {
|
||||
const preview = this.render({
|
||||
statistics: { commentCount: 209 },
|
||||
videoId: 'wGJHwc5ksMA',
|
||||
})
|
||||
// link[] is not allowed in examples
|
||||
delete preview.link
|
||||
return [
|
||||
{
|
||||
title: 'YouTube Video Comments',
|
||||
namedParams: { videoId: 'wGJHwc5ksMA' },
|
||||
staticPreview: preview,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
static render({ statistics, videoId }) {
|
||||
return super.renderSingleStat({
|
||||
statistics,
|
||||
statisticName: 'comment',
|
||||
videoId,
|
||||
})
|
||||
}
|
||||
}
|
||||
25
services/youtube/youtube-comments.tester.js
Normal file
25
services/youtube/youtube-comments.tester.js
Normal file
@@ -0,0 +1,25 @@
|
||||
'use strict'
|
||||
|
||||
const t = (module.exports = require('../tester').createServiceTester())
|
||||
const { noToken } = require('../test-helpers')
|
||||
const { isMetric } = require('../test-validators')
|
||||
const noYouTubeToken = noToken(require('./youtube-comments.service'))
|
||||
|
||||
t.create('video comment count')
|
||||
.skipWhen(noYouTubeToken)
|
||||
.get('/wGJHwc5ksMA.json')
|
||||
.expectBadge({
|
||||
label: 'comments',
|
||||
message: isMetric,
|
||||
color: 'red',
|
||||
link: ['https://www.youtube.com/watch?v=wGJHwc5ksMA'],
|
||||
})
|
||||
|
||||
t.create('video not found')
|
||||
.skipWhen(noYouTubeToken)
|
||||
.get('/doesnotexist.json')
|
||||
.expectBadge({
|
||||
label: 'youtube',
|
||||
message: 'video not found',
|
||||
color: 'red',
|
||||
})
|
||||
71
services/youtube/youtube-likes.service.js
Normal file
71
services/youtube/youtube-likes.service.js
Normal file
@@ -0,0 +1,71 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const { metric } = require('../text-formatters')
|
||||
const YouTubeBase = require('./youtube-base')
|
||||
|
||||
const queryParamSchema = Joi.object({
|
||||
withDislikes: Joi.equal(''),
|
||||
}).required()
|
||||
|
||||
module.exports = class YouTubeLikes extends YouTubeBase {
|
||||
static get route() {
|
||||
return {
|
||||
base: 'youtube/likes',
|
||||
pattern: ':videoId',
|
||||
queryParamSchema,
|
||||
}
|
||||
}
|
||||
|
||||
static get examples() {
|
||||
const previewLikes = this.render({
|
||||
statistics: { likeCount: 7 },
|
||||
videoId: 'abBdk8bSPKU',
|
||||
})
|
||||
const previewVotes = this.render(
|
||||
{
|
||||
statistics: { likeCount: 10236, dislikeCount: 396 },
|
||||
videoId: 'pU9Q6oiQNd0',
|
||||
},
|
||||
{
|
||||
withDislikes: '',
|
||||
}
|
||||
)
|
||||
// link[] is not allowed in examples
|
||||
delete previewLikes.link
|
||||
delete previewVotes.link
|
||||
return [
|
||||
{
|
||||
title: 'YouTube Video Likes',
|
||||
namedParams: { videoId: 'abBdk8bSPKU' },
|
||||
staticPreview: previewLikes,
|
||||
},
|
||||
{
|
||||
title: 'YouTube Video Votes',
|
||||
namedParams: { videoId: 'pU9Q6oiQNd0' },
|
||||
staticPreview: previewVotes,
|
||||
queryParams: {
|
||||
withDislikes: null,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
static render({ statistics, videoId }, queryParams) {
|
||||
if (queryParams && typeof queryParams.withDislikes !== 'undefined') {
|
||||
return {
|
||||
label: 'votes',
|
||||
message: `${metric(statistics.likeCount)} 👍 ${metric(
|
||||
statistics.dislikeCount
|
||||
)} 👎`,
|
||||
style: 'social',
|
||||
link: `https://www.youtube.com/watch?v=${encodeURIComponent(videoId)}`,
|
||||
}
|
||||
}
|
||||
return super.renderSingleStat({
|
||||
statistics,
|
||||
statisticName: 'like',
|
||||
videoId,
|
||||
})
|
||||
}
|
||||
}
|
||||
38
services/youtube/youtube-likes.tester.js
Normal file
38
services/youtube/youtube-likes.tester.js
Normal file
@@ -0,0 +1,38 @@
|
||||
'use strict'
|
||||
|
||||
const Joi = require('@hapi/joi')
|
||||
const t = (module.exports = require('../tester').createServiceTester())
|
||||
const { noToken } = require('../test-helpers')
|
||||
const { isMetric } = require('../test-validators')
|
||||
const noYouTubeToken = noToken(require('./youtube-likes.service'))
|
||||
|
||||
t.create('video like count')
|
||||
.skipWhen(noYouTubeToken)
|
||||
.get('/pU9Q6oiQNd0.json')
|
||||
.expectBadge({
|
||||
label: 'likes',
|
||||
message: isMetric,
|
||||
color: 'red',
|
||||
link: ['https://www.youtube.com/watch?v=pU9Q6oiQNd0'],
|
||||
})
|
||||
|
||||
t.create('video vote count')
|
||||
.skipWhen(noYouTubeToken)
|
||||
.get('/pU9Q6oiQNd0.json?withDislikes')
|
||||
.expectBadge({
|
||||
label: 'votes',
|
||||
message: Joi.string().regex(
|
||||
/^([1-9][0-9]*[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY]) 👍 ([1-9][0-9]*[kMGTPEZY]?|[1-9]\.[1-9][kMGTPEZY]) 👎$/
|
||||
),
|
||||
color: 'red',
|
||||
link: ['https://www.youtube.com/watch?v=pU9Q6oiQNd0'],
|
||||
})
|
||||
|
||||
t.create('video not found')
|
||||
.skipWhen(noYouTubeToken)
|
||||
.get('/doesnotexist.json?withDislikes')
|
||||
.expectBadge({
|
||||
label: 'youtube',
|
||||
message: 'video not found',
|
||||
color: 'red',
|
||||
})
|
||||
36
services/youtube/youtube-views.service.js
Normal file
36
services/youtube/youtube-views.service.js
Normal file
@@ -0,0 +1,36 @@
|
||||
'use strict'
|
||||
|
||||
const YouTubeBase = require('./youtube-base')
|
||||
|
||||
module.exports = class YouTubeViews extends YouTubeBase {
|
||||
static get route() {
|
||||
return {
|
||||
base: 'youtube/views',
|
||||
pattern: ':videoId',
|
||||
}
|
||||
}
|
||||
|
||||
static get examples() {
|
||||
const preview = this.render({
|
||||
statistics: { viewCount: 14577 },
|
||||
videoId: 'abBdk8bSPKU',
|
||||
})
|
||||
// link[] is not allowed in examples
|
||||
delete preview.link
|
||||
return [
|
||||
{
|
||||
title: 'YouTube Video Views',
|
||||
namedParams: { videoId: 'abBdk8bSPKU' },
|
||||
staticPreview: preview,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
static render({ statistics, videoId }) {
|
||||
return super.renderSingleStat({
|
||||
statistics,
|
||||
statisticName: 'view',
|
||||
videoId,
|
||||
})
|
||||
}
|
||||
}
|
||||
25
services/youtube/youtube-views.tester.js
Normal file
25
services/youtube/youtube-views.tester.js
Normal file
@@ -0,0 +1,25 @@
|
||||
'use strict'
|
||||
|
||||
const t = (module.exports = require('../tester').createServiceTester())
|
||||
const { noToken } = require('../test-helpers')
|
||||
const { isMetric } = require('../test-validators')
|
||||
const noYouTubeToken = noToken(require('./youtube-views.service'))
|
||||
|
||||
t.create('video view count')
|
||||
.skipWhen(noYouTubeToken)
|
||||
.get('/abBdk8bSPKU.json')
|
||||
.expectBadge({
|
||||
label: 'views',
|
||||
message: isMetric,
|
||||
color: 'red',
|
||||
link: ['https://www.youtube.com/watch?v=abBdk8bSPKU'],
|
||||
})
|
||||
|
||||
t.create('video not found')
|
||||
.skipWhen(noYouTubeToken)
|
||||
.get('/doesnotexist.json')
|
||||
.expectBadge({
|
||||
label: 'youtube',
|
||||
message: 'video not found',
|
||||
color: 'red',
|
||||
})
|
||||
Reference in New Issue
Block a user