diff --git a/lib/commands/implementations/youtube.js b/lib/commands/implementations/youtube.js index f7518a3..d6fefa3 100644 --- a/lib/commands/implementations/youtube.js +++ b/lib/commands/implementations/youtube.js @@ -3,15 +3,15 @@ const Command = require('../command-interface'); const CommandOutput = require('../command-output'); function getLatestVideo(input, services) { - return services.youtube + return services.dggApi .getLatestUploadedVideo() .then( (latestVideo) => new CommandOutput( null, - `"${latestVideo.snippet.title}" posted ${moment( - latestVideo.snippet.publishedAt, - ).fromNow()} https://youtu.be/${latestVideo.snippet.resourceId.videoId}`, + `"${latestVideo.title}" posted ${moment( + latestVideo.publishDate, + ).fromNow()} https://youtu.be/${latestVideo.id}`, ), ) .catch((err) => new CommandOutput(err, "Oops. Something didn't work. Check the logs.")); diff --git a/lib/configuration/sample.config.json b/lib/configuration/sample.config.json index 06db62f..3e22222 100644 --- a/lib/configuration/sample.config.json +++ b/lib/configuration/sample.config.json @@ -119,18 +119,13 @@ "username": "yourUsername", "password": "yourPassword" }, - "youtube": { - "YOUTUBE_API_KEY": "", - "YOUTUBE_CHANNEL": "", - "liveViewerCountTimeToLiveSeconds": 300 - }, "googleCalendar": { "GOOGLE_CALENDAR_API_KEY": "xxxxxxxxxxxx", "GOOGLE_CALENDAR_ID": "xxxxxxxxxxxxx", "SCHEDULE_LINK": "https://destiny.gg/schedule" }, "dggApi": { - "url": "https://www.destiny.gg/api/info/stream" + "baseUrl": "https://www.destiny.gg" }, "redditVote": { "enabled": false, diff --git a/lib/services/dgg-api.js b/lib/services/dgg-api.js index 1591fdc..b08c7e8 100644 --- a/lib/services/dgg-api.js +++ b/lib/services/dgg-api.js @@ -9,7 +9,7 @@ class DggApi { getStreamInfo() { return axios - .get(this.config.url, { + .get(`${this.config.baseUrl}/api/info/stream`, { headers: { Accept: 'application/json', }, @@ -20,6 +20,18 @@ class DggApi { }) .catch((err) => this.log.error('Error retrieving data from dgg api.', err)); } + + getListOfUploadedVideos() { + return axios + .get(`${this.config.baseUrl}/api/info/videos`, { + headers: { Accept: 'application/json' }, + }) + .then((res) => res.data.data); + } + + getLatestUploadedVideo() { + return this.getListOfUploadedVideos().then((videos) => videos[0]); + } } module.exports = DggApi; diff --git a/lib/services/service-index.js b/lib/services/service-index.js index 4721582..a253aa5 100644 --- a/lib/services/service-index.js +++ b/lib/services/service-index.js @@ -9,7 +9,6 @@ const SpamDetection = require('./spam-detection'); const ScheduledCommands = require('./message-scheduler'); const gulagService = require('./gulag'); const LastFm = require('./lastfm'); -const YouTube = require('./youtube.js'); const GoogleCal = require('./schedule.js'); const RoleCache = require('./role-cache.js'); const DggApi = require('./dgg-api'); @@ -38,7 +37,6 @@ class Services { this.scheduledCommands = new ScheduledCommands(serviceConfigurations.schedule); this.gulag = gulagService; this.lastfm = new LastFm(serviceConfigurations.lastFm); - this.youtube = new YouTube(serviceConfigurations.youtube); this.schedule = new GoogleCal(serviceConfigurations.googleCalendar); this.fakeScheduler = new FakeScheduler(serviceConfigurations.schedule); this.dggApi = new DggApi(serviceConfigurations.dggApi, this.logger); diff --git a/lib/services/youtube.js b/lib/services/youtube.js deleted file mode 100644 index 0347362..0000000 --- a/lib/services/youtube.js +++ /dev/null @@ -1,160 +0,0 @@ -const _ = require('lodash'); -const { google } = require('googleapis'); -const moment = require('moment'); - -class YouTube { - constructor(configuration) { - this.configuration = configuration; - this.scopes = ['https://www.googleapis.com/auth/youtube.readonly']; - this.youtube = google.youtube({ - version: 'v3', - auth: this.configuration.YOUTUBE_API_KEY, - }); - this.etags = {}; - } - - getLatestUploadedVideo() { - return this.getListOfUploadedVideos().then((response) => response.items[0]); - } - - getListOfUploadedVideos() { - const key = 'getListOfUploadedVideos'; - return this.getChannelsUploadedPlaylistId(this.configuration.YOUTUBE_CHANNEL) - .then((playlistId) => - this.youtube.playlistItems.list( - { - part: ['snippet', 'contentDetails'], - playlistId, - }, - { headers: this.addEtagHeader(key) }, - ), - ) - .then((response) => this.cacheResponse(key, response)) - .then((response) => response.data); - } - - getActiveLiveBroadcastsVideoId() { - const key = 'getActiveLiveBroadcastsVideoId'; - return this.getChannelIdFromUsername(this.configuration.YOUTUBE_CHANNEL) - .then((channelId) => - this.youtube.search.list( - { - channelId, - eventType: 'live', - type: ['video'], - part: ['snippet'], - }, - { headers: this.addEtagHeader(key) }, - ), - ) - .then((response) => this.cacheResponse(key, response)) - .then((response) => _.find(response.data.items, ['kind', 'youtube#searchResult'])) - .then((searchResult) => { - if (searchResult && searchResult.id && searchResult.id.videoId) { - return searchResult.id.videoId; - } - return null; - }); - } - - getChannelStatus() { - const key = 'getChannelStatus'; - return this.getActiveLiveBroadcastsVideoId() - .then((id) => - this.youtube.videos.list( - { - id: [id], - part: ['liveStreamingDetails'], - }, - { headers: this.addEtagHeader(key) }, - ), - ) - .then((response) => this.cacheResponse(key, response)) - .then((response) => _.find(response.data.items, ['kind', 'youtube#video'])) - .then((searchResult) => { - if ( - searchResult && - searchResult.liveStreamingDetails && - searchResult.liveStreamingDetails.concurrentViewers - ) { - return { - timestamp: moment().unix(), - isLive: true, - viewers: searchResult.liveStreamingDetails.concurrentViewers, - started: moment( - searchResult.liveStreamingDetails.actualStartTime, - 'YYYY-MM-DDTHH:mm:ssZ', - ), - }; - } - return { - timestamp: moment().unix(), - isLive: false, - }; - }); - } - - getChannelsUploadedPlaylistId(user) { - const key = 'getChannelsUploadedPlaylistId'; - return this.youtube.channels - .list( - { - forUsername: user, - part: ['contentDetails'], - }, - { headers: this.addEtagHeader(key) }, - ) - .then((response) => this.cacheResponse(key, response)) - .then( - (response) => - _.find(response.data.items, ['kind', 'youtube#channel']).contentDetails.relatedPlaylists - .uploads, - ); - } - - getChannelIdFromUsername(user) { - const key = 'getChannelIdFromUsername'; - return this.youtube.channels - .list( - { - forUsername: user, - part: ['id'], - }, - { headers: this.addEtagHeader(key) }, - ) - .then((response) => this.cacheResponse(key, response)) - .then((response) => _.find(response.data.items, ['kind', 'youtube#channel']).id); - } - - /** - * @param {string} key - * @param {T} response - * @returns {T} - * @template {import("gaxios").GaxiosResponse} T - */ - cacheResponse(key, response) { - if (response.status === 304) { - return this.etags[key]; - } - this.etags[key] = response; - return response; - } - - getEtag(key) { - const response = this.etags[key]; - if (response && response.data && response.data.etag) { - return response.data.etag; - } - return null; - } - - addEtagHeader(key, headers = {}) { - const etag = this.getEtag(key); - if (etag) { - return Object.assign(headers, { 'If-None-Match': etag }); - } - return headers; - } -} - -module.exports = YouTube; diff --git a/tests/lib/services/dgg-api.test.js b/tests/lib/services/dgg-api.test.js new file mode 100644 index 0000000..e9b7fc5 --- /dev/null +++ b/tests/lib/services/dgg-api.test.js @@ -0,0 +1,34 @@ +const assert = require('assert'); +const sinon = require('sinon'); +const proxyquire = require('proxyquire').noCallThru(); +const mockResponses = require('./mocks/dgg-api-responses.json'); + +describe('DggApi Tests', () => { + const config = { baseUrl: 'https://www.destiny.gg' }; + + let axiosGet; + let DggApi; + let dggApi; + + beforeEach(function () { + axiosGet = sinon.stub(); + DggApi = proxyquire('../../../lib/services/dgg-api', { + axios: { default: { get: axiosGet } }, + }); + dggApi = new DggApi(config); + }); + + it('Gets the channel\'s uploaded videos', function () { + axiosGet.withArgs(`${config.baseUrl}/api/info/videos`).resolves({ data: mockResponses.videos }); + return dggApi.getListOfUploadedVideos().then((response) => { + assert.deepStrictEqual(response, mockResponses.videos.data); + }); + }); + + it('Gets the latest uploaded video', function () { + axiosGet.withArgs(`${config.baseUrl}/api/info/videos`).resolves({ data: mockResponses.videos }); + return dggApi.getLatestUploadedVideo().then((response) => { + assert.deepStrictEqual(response, mockResponses.videos.data[0]); + }); + }); +}); diff --git a/tests/lib/services/mocks/dgg-api-responses.json b/tests/lib/services/mocks/dgg-api-responses.json new file mode 100644 index 0000000..0f7c14a --- /dev/null +++ b/tests/lib/services/mocks/dgg-api-responses.json @@ -0,0 +1,33 @@ +{ + "videos": { + "status": "success", + "data": [ + { + "id": "uenf3uYDKBE", + "title": "POLITICON - DESTINY REACTS", + "mediumThumbnailUrl": "https://i.ytimg.com/vi/uenf3uYDKBE/mqdefault.jpg", + "highThumbnailUrl": "https://i.ytimg.com/vi/uenf3uYDKBE/hqdefault.jpg", + "streamViewers": null, + "streamStartTime": null, + "streamEndTime": null, + "url": "https://www.youtube.com/watch?v=uenf3uYDKBE", + "thumbnailHref": "https://i.ytimg.com/vi/uenf3uYDKBE/hqdefault.jpg", + "publishDate": "2018-10-22T11:10:49+00:00", + "embedUrl": "/bigscreen?vid=uenf3uYDKBE" + }, + { + "id": "abcdef12345", + "title": "Second video", + "mediumThumbnailUrl": "", + "highThumbnailUrl": "", + "streamViewers": null, + "streamStartTime": null, + "streamEndTime": null, + "url": "https://www.youtube.com/watch?v=abcdef12345", + "thumbnailHref": "", + "publishDate": "2018-10-21T11:10:49+00:00", + "embedUrl": "/bigscreen?vid=abcdef12345" + } + ] + } +} diff --git a/tests/lib/services/mocks/youtube-responses.json b/tests/lib/services/mocks/youtube-responses.json deleted file mode 100644 index 820bd34..0000000 --- a/tests/lib/services/mocks/youtube-responses.json +++ /dev/null @@ -1,179 +0,0 @@ -{ - "getChannelsUploadedPlaylistId": { - "kind": "youtube#channelListResponse", - "etag": "\"XI7nbFXulYBIpL0ayR_gDh3eu1k/Q4EBb0pKv0kjPKAhlunmv6Hg6Oc\"", - "pageInfo": { - "totalResults": 1, - "resultsPerPage": 5 - }, - "items": [ - { - "kind": "youtube#channel", - "etag": "\"XI7nbFXulYBIpL0ayR_gDh3eu1k/rGM6d0AXM0SXx7D5bD4F7l6wSy4\"", - "id": "UC554eY5jNUfDq3yDOJYirOQ", - "contentDetails": { - "relatedPlaylists": { - "uploads": "UU554eY5jNUfDq3yDOJYirOQ", - "watchHistory": "HL", - "watchLater": "WL" - } - } - } - ] - }, - "getListOfUploadedVideos": { - "kind": "youtube#playlistItemListResponse", - "etag": "\"XI7nbFXulYBIpL0ayR_gDh3eu1k/ZbgjQFk18ztZ8a29o83MM2a9Fjs\"", - "nextPageToken": "CAEQAA", - "pageInfo": { - "totalResults": 1392, - "resultsPerPage": 1 - }, - "items": [ - { - "kind": "youtube#playlistItem", - "etag": "\"XI7nbFXulYBIpL0ayR_gDh3eu1k/nkOSQq0bh2gw6pzMe5BSLMB87Cc\"", - "id": "VVU1NTRlWTVqTlVmRHEzeURPSllpck9RLnVlbmYzdVlES0JF", - "snippet": { - "publishedAt": "2018-10-22T11:10:49.000Z", - "channelId": "UC554eY5jNUfDq3yDOJYirOQ", - "title": "POLITICON - DESTINY REACTS", - "description": "Date streamed: October 20th, 2018\n\nClick▼\n\n\nFollow Destiny\n►STREAM - http://www.destiny.gg/bigscreen\n►DISCORD - https://discordapp.com/invite/destiny\n►REDDIT - https://www.reddit.com/r/Destiny\n\nUse Destiny's affiliate link to buy stuff! http://www.amazon.com/?tag=des000-20\n\nEdited by https://twitter.com/editormaddog\nThumbnail by https://twitter.com/novusalan\n\nMusic:\n►OUTRO: https://soundcloud.com/osvelit/cc6-mastered-3-conflict", - "thumbnails": { - "default": { - "url": "https://i.ytimg.com/vi/uenf3uYDKBE/default.jpg", - "width": 120, - "height": 90 - }, - "medium": { - "url": "https://i.ytimg.com/vi/uenf3uYDKBE/mqdefault.jpg", - "width": 320, - "height": 180 - }, - "high": { - "url": "https://i.ytimg.com/vi/uenf3uYDKBE/hqdefault.jpg", - "width": 480, - "height": 360 - }, - "standard": { - "url": "https://i.ytimg.com/vi/uenf3uYDKBE/sddefault.jpg", - "width": 640, - "height": 480 - } - }, - "channelTitle": "Destiny", - "playlistId": "UU554eY5jNUfDq3yDOJYirOQ", - "position": 0, - "resourceId": { - "kind": "youtube#video", - "videoId": "uenf3uYDKBE" - } - }, - "contentDetails": { - "videoId": "uenf3uYDKBE", - "videoPublishedAt": "2018-10-22T11:10:49.000Z" - } - } - ] - }, - "getChannelIdFromUsername": { - "kind": "youtube#channelListResponse", - "etag": "HDb0rTGfEHM-71GKrEi-ppIjOBU", - "pageInfo": { - "totalResults": 1, - "resultsPerPage": 5 - }, - "items": [ - { - "kind": "youtube#channel", - "etag": "68OLRYpJC3FdmRmtCbYPgCz1jIs", - "id": "UC554eY5jNUfDq3yDOJYirOQ" - } - ] - }, - "getActiveLiveBroadcastsVideoId": { - "kind": "youtube#searchListResponse", - "etag": "ltkmVel3nQbC33R4NfZoYfKYjoQ", - "regionCode": "US", - "pageInfo": { - "totalResults": 1, - "resultsPerPage": 5 - }, - "items": [ - { - "kind": "youtube#searchResult", - "etag": "lPm4OW84dH30emAhxIF_QUVHz0U", - "id": { - "kind": "youtube#video", - "videoId": "qif_XUayrWY" - }, - "snippet": { - "publishedAt": "2020-10-10T19:04:09Z", - "channelId": "UC554eY5jNUfDq3yDOJYirOQ", - "title": "Setting up stream stuffs, Starcraft 2, then Wastelands 3 later tonight", - "description": "Powered by Restream https://restream.io/ Setting up stream stuffs, Starcraft 2, then Wastelands 3 later tonight.", - "thumbnails": { - "default": { - "url": "https://i.ytimg.com/vi/qif_XUayrWY/default_live.jpg", - "width": 120, - "height": 90 - }, - "medium": { - "url": "https://i.ytimg.com/vi/qif_XUayrWY/mqdefault_live.jpg", - "width": 320, - "height": 180 - }, - "high": { - "url": "https://i.ytimg.com/vi/qif_XUayrWY/hqdefault_live.jpg", - "width": 480, - "height": 360 - } - }, - "channelTitle": "Destiny", - "liveBroadcastContent": "live", - "publishTime": "2020-10-10T19:04:09Z" - } - } - ] - }, - "getActiveLiveBroadcastsVideoIdOffline": { - "kind": "youtube#searchListResponse", - "etag": "ltkmVel3nQbC33R4NfZoYfKYjoQ", - "regionCode": "US", - "pageInfo": { - "totalResults": 1, - "resultsPerPage": 5 - }, - "items": [] - }, - "getConcurrentViewers": { - "kind": "youtube#videoListResponse", - "etag": "asdBmhAo0sk8pRVHMVH5dBpnzW0", - "items": [ - { - "kind": "youtube#video", - "etag": "d7_SxNUwnmRN7geQibeTHhMIF2Y", - "id": "qif_XUayrWY", - "liveStreamingDetails": { - "actualStartTime": "2020-10-10T19:04:28Z", - "scheduledStartTime": "2020-10-10T19:04:25Z", - "concurrentViewers": "1785", - "activeLiveChatId": "Cg0KC3FpZl9YVWF5cldZKicKGFVDNTU0ZVk1ak5VZkRxM3lET0pZaXJPURILcWlmX1hVYXlyV1k" - } - } - ], - "pageInfo": { - "totalResults": 1, - "resultsPerPage": 1 - } - }, - "getConcurrentViewersOffline": { - "kind": "youtube#videoListResponse", - "etag": "asdBmhAo0sk8pRVHMVH5dBpnzW0", - "items": [], - "pageInfo": { - "totalResults": 1, - "resultsPerPage": 1 - } - } -} \ No newline at end of file diff --git a/tests/lib/services/youtube.test.js b/tests/lib/services/youtube.test.js deleted file mode 100644 index b5d1f33..0000000 --- a/tests/lib/services/youtube.test.js +++ /dev/null @@ -1,149 +0,0 @@ -const assert = require('assert'); -const sinon = require('sinon'); -const proxyquire = require('proxyquire').noCallThru(); -const mockResponses = require('./mocks/youtube-responses.json') -const moment = require('moment'); - -describe('Youtube Tests', () => { - - const config = { - YOUTUBE_API_KEY: 'TEST123', - YOUTUBE_CHANNEL: 'Destiny', - liveViewerCountTimeToLiveSeconds: 300 - }; - - const pathStub = function(){ - } - pathStub.youtube = function(config){ - let searchCalled = 0; - let videosCalled = 0; - return { - channels: { - list: function(payload){ - switch(payload.part.join(',')) { - case 'contentDetails': - return Promise.resolve({ data: mockResponses.getChannelsUploadedPlaylistId }); - case 'id': - return Promise.resolve({ data: mockResponses.getChannelIdFromUsername }); - default: - return Promise.reject('YOU FUCKED UP'); - } - } - }, - playlistItems: { - list: function(){ - return Promise.resolve({ - data: mockResponses.getListOfUploadedVideos - }); - } - }, - search: { - list: function(){ - searchCalled += 1; - - if (searchCalled % 2 === 1) { - return Promise.resolve({ data: mockResponses.getActiveLiveBroadcastsVideoId }); - } - if (searchCalled % 2 === 0) { - return Promise.resolve({ data: mockResponses.getActiveLiveBroadcastsVideoIdOffline }); - } - } - }, - videos: { - list: function(){ - videosCalled += 1; - - if (videosCalled === 2) { - return Promise.resolve({ data: mockResponses.getConcurrentViewersOffline }); - } - return Promise.resolve({ data: mockResponses.getConcurrentViewers }); - } - } - } - } - - const youtubeProxy = proxyquire('../../../lib/services/youtube', { 'googleapis': { google: pathStub }}) - const yt = new youtubeProxy(config); - - beforeEach(function () { - this.clock = sinon.useFakeTimers(1603155310586); - }); - - afterEach(function () { - this.clock.restore(); - yt.liveViewerCache = {}; - }); - - it('Gets a Channels Uploaded Playlist Id', function () { - - return yt.getChannelsUploadedPlaylistId(config.YOUTUBE_CHANNEL) - .then(function (response) { - return assert.strictEqual(response, "UU554eY5jNUfDq3yDOJYirOQ"); - }); - }); - - it('Gets a Channels Uploaded Playlist Id and caches it', function () { - - return yt.getChannelsUploadedPlaylistId(config.YOUTUBE_CHANNEL) - .then(function () { - return assert.strictEqual(yt.etags['getChannelsUploadedPlaylistId'].data.items[0].contentDetails.relatedPlaylists.uploads, 'UU554eY5jNUfDq3yDOJYirOQ') - }); - }); - - it('Gets a Channels Latest Uploaded Videos Playlist', function () { - - return yt.getListOfUploadedVideos() - .then(function (response) { - return assert.equal(response, mockResponses.getListOfUploadedVideos); - }); - }); - it('Gets a Channels Latest Uploaded Video', function () { - - return yt.getLatestUploadedVideo() - .then(function (response) { - return assert.equal(response, mockResponses.getListOfUploadedVideos.items[0]); - }); - }); - - it('Gets the channel id from username', function() { - return yt.getChannelIdFromUsername(config.YOUTUBE_CHANNEL) - .then(function (response) { - return assert.strictEqual(response, 'UC554eY5jNUfDq3yDOJYirOQ') - }); - }); - - it('Gets the channel id from username and caches it', function() { - return yt.getChannelIdFromUsername(config.YOUTUBE_CHANNEL) - .then(function () { - return assert.strictEqual(yt.etags['getChannelIdFromUsername'].data.items[0].id, 'UC554eY5jNUfDq3yDOJYirOQ') - }); -}); - - it('Gets live broadcast details from channel id', function() { - return yt.getActiveLiveBroadcastsVideoId() - .then(function (response) { - return assert.strictEqual(response, 'qif_XUayrWY'); - }); - }); - - it('Gets live broadcast details from channel id when offline', function() { - return yt.getActiveLiveBroadcastsVideoId() - .then(function (response) { - return assert.strictEqual(response, null); - }); - }); - - it('Gets live broadcast concurrent viewers count', function() { - return yt.getChannelStatus() - .then(function (response) { - return assert.deepStrictEqual(response, { timestamp: 1603155310, isLive: true, viewers: '1785', started: moment('2020-10-10T19:04:28Z', 'YYYY-MM-DDTHH:mm:ssZ') }); - }); - }); - - it('Gets live broadcast concurrent viewers count when offline', function() { - return yt.getChannelStatus() - .then(function (response) { - return assert.deepStrictEqual(response, { timestamp: 1603155310, isLive: false }); - }); - }); -});