Skip to content

Commit

Permalink
Implement [YouTube] badge (#5132)
Browse files Browse the repository at this point in the history
* Implement [YouTube] badge

* Update production-hosting.md with account owner

* Add votes badge variant

* Add links to tests

* Switch to social badge style
  • Loading branch information
PyvesB authored Jun 10, 2020
1 parent 4582ea1 commit 0fd557d
Show file tree
Hide file tree
Showing 13 changed files with 318 additions and 0 deletions.
1 change: 1 addition & 0 deletions config/custom-environment-variables.yml
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,4 @@ private:
wheelmap_token: 'WHEELMAP_TOKEN'
influx_username: 'INFLUX_USERNAME'
influx_password: 'INFLUX_PASSWORD'
youtube_api_key: 'YOUTUBE_API_KEY'
1 change: 1 addition & 0 deletions config/local-shields-io-production.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ private:
twitch_client_id: ...
twitch_client_secret: ...
wheelmap_token: ...
youtube_api_key: ...
1 change: 1 addition & 0 deletions config/local.template.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ private:
twitch_client_id: '...'
twitch_client_secret: '...'
wheelmap_token: '...'
youtube_api_key: '...'
1 change: 1 addition & 0 deletions core/server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions doc/production-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
10 changes: 10 additions & 0 deletions doc/server-secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
72 changes: 72 additions & 0 deletions services/youtube/youtube-base.js
Original file line number Diff line number Diff line change
@@ -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 changes: 36 additions & 0 deletions services/youtube/youtube-comments.service.js
Original file line number Diff line number Diff line change
@@ -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 changes: 25 additions & 0 deletions services/youtube/youtube-comments.tester.js
Original file line number Diff line number Diff line change
@@ -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 changes: 71 additions & 0 deletions services/youtube/youtube-likes.service.js
Original file line number Diff line number Diff line change
@@ -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 changes: 38 additions & 0 deletions services/youtube/youtube-likes.tester.js
Original file line number Diff line number Diff line change
@@ -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 changes: 36 additions & 0 deletions services/youtube/youtube-views.service.js
Original file line number Diff line number Diff line change
@@ -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 changes: 25 additions & 0 deletions services/youtube/youtube-views.tester.js
Original file line number Diff line number Diff line change
@@ -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',
})

0 comments on commit 0fd557d

Please sign in to comment.