From f0379528a41d2f3f3f97a756efe0854b3ff9ef31 Mon Sep 17 00:00:00 2001 From: Tarun Singh Date: Mon, 5 Apr 2021 15:20:49 +0530 Subject: [PATCH] [Youtube] Added channel view count and subscriber count badges (#6333) Co-authored-by: Pierre-Yves B --- services/youtube/youtube-base.js | 57 ++++++++++++------- .../youtube/youtube-channel-views.service.js | 31 ++++++++++ .../youtube/youtube-channel-views.tester.js | 25 ++++++++ services/youtube/youtube-comments.service.js | 14 ++--- services/youtube/youtube-comments.tester.js | 2 +- services/youtube/youtube-likes.service.js | 16 +++--- services/youtube/youtube-likes.tester.js | 4 +- .../youtube/youtube-subscribers.service.js | 35 ++++++++++++ .../youtube/youtube-subscribers.tester.js | 25 ++++++++ services/youtube/youtube-views.service.js | 14 ++--- services/youtube/youtube-views.tester.js | 2 +- 11 files changed, 175 insertions(+), 50 deletions(-) create mode 100644 services/youtube/youtube-channel-views.service.js create mode 100644 services/youtube/youtube-channel-views.tester.js create mode 100644 services/youtube/youtube-subscribers.service.js create mode 100644 services/youtube/youtube-subscribers.tester.js diff --git a/services/youtube/youtube-base.js b/services/youtube/youtube-base.js index 16a9017283..ad2bfd83fc 100644 --- a/services/youtube/youtube-base.js +++ b/services/youtube/youtube-base.js @@ -10,18 +10,26 @@ const documentation = ` https://www.youtube.com/t/terms

` const schema = Joi.object({ - items: Joi.array() - .items( - Joi.object({ - statistics: Joi.object({ + pageInfo: Joi.object({ + totalResults: nonNegativeInteger, + resultsPerPage: nonNegativeInteger, + }).required(), + items: Joi.array().items( + Joi.object({ + statistics: Joi.alternatives( + Joi.object({ viewCount: nonNegativeInteger, likeCount: nonNegativeInteger, dislikeCount: nonNegativeInteger, commentCount: nonNegativeInteger, - }).required(), - }) - ) - .required(), + }), + Joi.object({ + viewCount: nonNegativeInteger, + subscriberCount: nonNegativeInteger, + }) + ), + }) + ), }).required() class YouTubeBase extends BaseJsonService { @@ -39,38 +47,49 @@ class YouTubeBase extends BaseJsonService { namedLogo: 'youtube', } - static renderSingleStat({ statistics, statisticName, videoId }) { + static renderSingleStat({ statistics, statisticName, id }) { return { label: `${statisticName}s`, message: metric(statistics[`${statisticName}Count`]), style: 'social', - link: `https://www.youtube.com/watch?v=${encodeURIComponent(videoId)}`, + link: `https://www.youtube.com/${this.type}/${encodeURIComponent(id)}`, } } - async fetch({ videoId }) { + async fetch({ id }) { return this._requestJson( this.authHelper.withQueryStringAuth( { passKey: 'key' }, { schema, - url: 'https://www.googleapis.com/youtube/v3/videos', + url: `https://www.googleapis.com/youtube/v3/${this.constructor.type}s`, options: { - qs: { id: videoId, part: 'statistics' }, + qs: { id, part: 'statistics' }, }, } ) ) } - async handle({ videoId }, queryParams) { - const json = await this.fetch({ videoId }) - if (json.items.length === 0) { - throw new NotFound({ prettyMessage: 'video not found' }) + async handle({ channelId, videoId }, queryParams) { + const id = channelId || videoId + const json = await this.fetch({ id }) + if (json.pageInfo.totalResults === 0) { + throw new NotFound({ + prettyMessage: `${this.constructor.type} not found`, + }) } const statistics = json.items[0].statistics - return this.constructor.render({ statistics, videoId }, queryParams) + return this.constructor.render({ statistics, id }, queryParams) } } -module.exports = { documentation, YouTubeBase } +class YouTubeVideoBase extends YouTubeBase { + static type = 'video' +} + +class YouTubeChannelBase extends YouTubeBase { + static type = 'channel' +} + +module.exports = { documentation, YouTubeVideoBase, YouTubeChannelBase } diff --git a/services/youtube/youtube-channel-views.service.js b/services/youtube/youtube-channel-views.service.js new file mode 100644 index 0000000000..27712d5c83 --- /dev/null +++ b/services/youtube/youtube-channel-views.service.js @@ -0,0 +1,31 @@ +'use strict' + +const { documentation, YouTubeChannelBase } = require('./youtube-base') + +module.exports = class YouTubeChannelViews extends YouTubeChannelBase { + static route = { + base: 'youtube/channel/views', + pattern: ':channelId', + } + + static get examples() { + const preview = this.render({ + statistics: { viewCount: 30543 }, + id: 'UC8butISFwT-Wl7EV0hUK0BQ', + }) + // link[] is not allowed in examples + delete preview.link + return [ + { + title: 'YouTube Channel Views', + namedParams: { channelId: 'UC8butISFwT-Wl7EV0hUK0BQ' }, + staticPreview: preview, + documentation, + }, + ] + } + + static render({ statistics, id }) { + return super.renderSingleStat({ statistics, statisticName: 'view', id }) + } +} diff --git a/services/youtube/youtube-channel-views.tester.js b/services/youtube/youtube-channel-views.tester.js new file mode 100644 index 0000000000..eab2689fd0 --- /dev/null +++ b/services/youtube/youtube-channel-views.tester.js @@ -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-channel-views.service')) + +t.create('channel view count') + .skipWhen(noYouTubeToken) + .get('/UC8butISFwT-Wl7EV0hUK0BQ.json') + .expectBadge({ + label: 'views', + message: isMetric, + color: 'red', + link: ['https://www.youtube.com/channel/UC8butISFwT-Wl7EV0hUK0BQ'], + }) + +t.create('channel not found') + .skipWhen(noYouTubeToken) + .get('/doesnotexist.json') + .expectBadge({ + label: 'youtube', + message: 'channel not found', + color: 'red', + }) diff --git a/services/youtube/youtube-comments.service.js b/services/youtube/youtube-comments.service.js index 000a03a533..679605684c 100644 --- a/services/youtube/youtube-comments.service.js +++ b/services/youtube/youtube-comments.service.js @@ -1,8 +1,8 @@ 'use strict' -const { documentation, YouTubeBase } = require('./youtube-base') +const { documentation, YouTubeVideoBase } = require('./youtube-base') -module.exports = class YouTubeComments extends YouTubeBase { +module.exports = class YouTubeComments extends YouTubeVideoBase { static route = { base: 'youtube/comments', pattern: ':videoId', @@ -11,7 +11,7 @@ module.exports = class YouTubeComments extends YouTubeBase { static get examples() { const preview = this.render({ statistics: { commentCount: 209 }, - videoId: 'wGJHwc5ksMA', + id: 'wGJHwc5ksMA', }) // link[] is not allowed in examples delete preview.link @@ -25,11 +25,7 @@ module.exports = class YouTubeComments extends YouTubeBase { ] } - static render({ statistics, videoId }) { - return super.renderSingleStat({ - statistics, - statisticName: 'comment', - videoId, - }) + static render({ statistics, id }) { + return super.renderSingleStat({ statistics, statisticName: 'comment', id }) } } diff --git a/services/youtube/youtube-comments.tester.js b/services/youtube/youtube-comments.tester.js index 0d9930a118..dfdbde45d4 100644 --- a/services/youtube/youtube-comments.tester.js +++ b/services/youtube/youtube-comments.tester.js @@ -12,7 +12,7 @@ t.create('video comment count') label: 'comments', message: isMetric, color: 'red', - link: ['https://www.youtube.com/watch?v=wGJHwc5ksMA'], + link: ['https://www.youtube.com/video/wGJHwc5ksMA'], }) t.create('video not found') diff --git a/services/youtube/youtube-likes.service.js b/services/youtube/youtube-likes.service.js index 211a8d60e4..e2133cd392 100644 --- a/services/youtube/youtube-likes.service.js +++ b/services/youtube/youtube-likes.service.js @@ -2,7 +2,7 @@ const Joi = require('joi') const { metric } = require('../text-formatters') -const { documentation, YouTubeBase } = require('./youtube-base') +const { documentation, YouTubeVideoBase } = require('./youtube-base') const documentationWithDislikes = ` ${documentation} @@ -16,7 +16,7 @@ const queryParamSchema = Joi.object({ withDislikes: Joi.equal(''), }).required() -module.exports = class YouTubeLikes extends YouTubeBase { +module.exports = class YouTubeLikes extends YouTubeVideoBase { static route = { base: 'youtube/likes', pattern: ':videoId', @@ -26,16 +26,14 @@ module.exports = class YouTubeLikes extends YouTubeBase { static get examples() { const previewLikes = this.render({ statistics: { likeCount: 7 }, - videoId: 'abBdk8bSPKU', + id: 'abBdk8bSPKU', }) const previewVotes = this.render( { statistics: { likeCount: 10236, dislikeCount: 396 }, - videoId: 'pU9Q6oiQNd0', + id: 'pU9Q6oiQNd0', }, - { - withDislikes: '', - } + { withDislikes: '' } ) // link[] is not allowed in examples delete previewLikes.link @@ -59,11 +57,11 @@ module.exports = class YouTubeLikes extends YouTubeBase { ] } - static render({ statistics, videoId }, queryParams) { + static render({ statistics, id }, queryParams) { let renderedBadge = super.renderSingleStat({ statistics, statisticName: 'like', - videoId, + id, }) if (queryParams && typeof queryParams.withDislikes !== 'undefined') { renderedBadge = { diff --git a/services/youtube/youtube-likes.tester.js b/services/youtube/youtube-likes.tester.js index cd4adf86b9..5c266e0987 100644 --- a/services/youtube/youtube-likes.tester.js +++ b/services/youtube/youtube-likes.tester.js @@ -13,7 +13,7 @@ t.create('video like count') label: 'likes', message: isMetric, color: 'red', - link: ['https://www.youtube.com/watch?v=pU9Q6oiQNd0'], + link: ['https://www.youtube.com/video/pU9Q6oiQNd0'], }) t.create('video vote count') @@ -25,7 +25,7 @@ t.create('video vote count') /^([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'], + link: ['https://www.youtube.com/video/pU9Q6oiQNd0'], }) t.create('video not found') diff --git a/services/youtube/youtube-subscribers.service.js b/services/youtube/youtube-subscribers.service.js new file mode 100644 index 0000000000..408b451bae --- /dev/null +++ b/services/youtube/youtube-subscribers.service.js @@ -0,0 +1,35 @@ +'use strict' + +const { documentation, YouTubeChannelBase } = require('./youtube-base') + +module.exports = class YouTubeSubscribes extends YouTubeChannelBase { + static route = { + base: 'youtube/channel/subscribers', + pattern: ':channelId', + } + + static get examples() { + const preview = this.render({ + statistics: { subscriberCount: 14577 }, + id: 'UC8butISFwT-Wl7EV0hUK0BQ', + }) + // link[] is not allowed in examples + delete preview.link + return [ + { + title: 'YouTube Channel Subscribers', + namedParams: { channelId: 'UC8butISFwT-Wl7EV0hUK0BQ' }, + staticPreview: preview, + documentation, + }, + ] + } + + static render({ statistics, id }) { + return super.renderSingleStat({ + statistics, + statisticName: 'subscriber', + id, + }) + } +} diff --git a/services/youtube/youtube-subscribers.tester.js b/services/youtube/youtube-subscribers.tester.js new file mode 100644 index 0000000000..acff789b0c --- /dev/null +++ b/services/youtube/youtube-subscribers.tester.js @@ -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-subscribers.service')) + +t.create('subscriber count') + .skipWhen(noYouTubeToken) + .get('/UC8butISFwT-Wl7EV0hUK0BQ.json') + .expectBadge({ + label: 'subscribers', + message: isMetric, + color: 'red', + link: ['https://www.youtube.com/channel/UC8butISFwT-Wl7EV0hUK0BQ'], + }) + +t.create('channel not found') + .skipWhen(noYouTubeToken) + .get('/doesnotexist.json') + .expectBadge({ + label: 'youtube', + message: 'channel not found', + color: 'red', + }) diff --git a/services/youtube/youtube-views.service.js b/services/youtube/youtube-views.service.js index 2516df780b..5259e6818a 100644 --- a/services/youtube/youtube-views.service.js +++ b/services/youtube/youtube-views.service.js @@ -1,8 +1,8 @@ 'use strict' -const { documentation, YouTubeBase } = require('./youtube-base') +const { documentation, YouTubeVideoBase } = require('./youtube-base') -module.exports = class YouTubeViews extends YouTubeBase { +module.exports = class YouTubeViews extends YouTubeVideoBase { static route = { base: 'youtube/views', pattern: ':videoId', @@ -11,7 +11,7 @@ module.exports = class YouTubeViews extends YouTubeBase { static get examples() { const preview = this.render({ statistics: { viewCount: 14577 }, - videoId: 'abBdk8bSPKU', + id: 'abBdk8bSPKU', }) // link[] is not allowed in examples delete preview.link @@ -25,11 +25,7 @@ module.exports = class YouTubeViews extends YouTubeBase { ] } - static render({ statistics, videoId }) { - return super.renderSingleStat({ - statistics, - statisticName: 'view', - videoId, - }) + static render({ statistics, id }) { + return super.renderSingleStat({ statistics, statisticName: 'view', id }) } } diff --git a/services/youtube/youtube-views.tester.js b/services/youtube/youtube-views.tester.js index 1c44b4b0ae..af7c036daf 100644 --- a/services/youtube/youtube-views.tester.js +++ b/services/youtube/youtube-views.tester.js @@ -12,7 +12,7 @@ t.create('video view count') label: 'views', message: isMetric, color: 'red', - link: ['https://www.youtube.com/watch?v=abBdk8bSPKU'], + link: ['https://www.youtube.com/video/abBdk8bSPKU'], }) t.create('video not found')