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:
Pierre-Yves B
2020-06-10 18:44:21 +02:00
committed by GitHub
parent 4582ea1c56
commit 0fd557d7bb
13 changed files with 318 additions and 0 deletions

View File

@@ -98,3 +98,4 @@ private:
wheelmap_token: 'WHEELMAP_TOKEN'
influx_username: 'INFLUX_USERNAME'
influx_password: 'INFLUX_PASSWORD'
youtube_api_key: 'YOUTUBE_API_KEY'

View File

@@ -10,3 +10,4 @@ private:
twitch_client_id: ...
twitch_client_secret: ...
wheelmap_token: ...
youtube_api_key: ...

View File

@@ -8,3 +8,4 @@ private:
twitch_client_id: '...'
twitch_client_secret: '...'
wheelmap_token: '...'
youtube_api_key: '...'

View File

@@ -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(),

View File

@@ -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 |

View File

@@ -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`)

View 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)
}
}

View 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,
})
}
}

View 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',
})

View 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,
})
}
}

View 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',
})

View 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,
})
}
}

View 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',
})