diff --git a/app/browser/api/ledger.js b/app/browser/api/ledger.js index 0662750c659..27e14967964 100644 --- a/app/browser/api/ledger.js +++ b/app/browser/api/ledger.js @@ -315,6 +315,7 @@ const getPublisherData = (result, scorekeeper) => { verified: result.options.verified || false, exclude: result.options.exclude || false, publisherKey: result.publisherKey, + providerName: result.providerName, siteName: result.publisherKey, views: result.visits, duration: duration, @@ -1387,7 +1388,7 @@ const roundtrip = (params, options, callback) => { : typeof params.server !== 'undefined' ? params.server : typeof options.server === 'string' ? urlParse(options.server) : options.server const binaryP = options.binaryP - const rawP = binaryP || options.rawP + const rawP = binaryP || options.rawP || options.scrapeP if (!params.method) params.method = 'GET' parts = underscore.extend(underscore.pick(parts, ['protocol', 'hostname', 'port']), @@ -2399,10 +2400,15 @@ const onMediaRequest = (state, xhr, type, tabId) => { const parsed = ledgerUtil.getMediaData(xhr, type) const mediaId = ledgerUtil.getMediaId(parsed, type) + + if (mediaId == null) { + return state + } + const mediaKey = ledgerUtil.getMediaKey(mediaId, type) - let duration = ledgerUtil.getMediaDuration(parsed, type) + let duration = ledgerUtil.getMediaDuration(state, parsed, mediaKey, type) - if (mediaId == null || duration == null || mediaKey == null) { + if (duration == null || mediaKey == null) { return state } @@ -2422,9 +2428,14 @@ const onMediaRequest = (state, xhr, type, tabId) => { currentMediaKey = mediaKey } + const stateData = ledgerUtil.generateMediaCacheData(parsed, type) const cache = ledgerVideoCache.getDataByVideoId(state, mediaKey) if (!cache.isEmpty()) { + if (!stateData.isEmpty()) { + state = ledgerVideoCache.mergeCacheByVideoId(state, mediaKey, stateData) + } + const publisherKey = cache.get('publisher') const publisher = ledgerState.getPublisher(state, publisherKey) if (!publisher.isEmpty() && publisher.has('providerName')) { @@ -2436,6 +2447,10 @@ const onMediaRequest = (state, xhr, type, tabId) => { } } + if (!stateData.isEmpty()) { + state = ledgerVideoCache.setCacheByVideoId(state, mediaKey, stateData) + } + const options = underscore.extend({roundtrip: module.exports.roundtrip}, clientOptions) const mediaProps = { mediaId, @@ -2513,7 +2528,7 @@ const onMediaPublisher = (state, mediaKey, response, duration, revisited) => { .set('publisher', publisherKey) // Add to cache - state = ledgerVideoCache.setCacheByVideoId(state, mediaKey, cacheObject) + state = ledgerVideoCache.mergeCacheByVideoId(state, mediaKey, cacheObject) state = module.exports.saveVisit(state, publisherKey, { duration, diff --git a/app/common/cache/ledgerVideoCache.js b/app/common/cache/ledgerVideoCache.js index c7f0ae3fcb1..adab8a5f561 100644 --- a/app/common/cache/ledgerVideoCache.js +++ b/app/common/cache/ledgerVideoCache.js @@ -33,7 +33,24 @@ const setCacheByVideoId = (state, key, data) => { return state.setIn(['cache', 'ledgerVideos', key], data) } +const mergeCacheByVideoId = (state, key, data) => { + state = validateState(state) + + if (key == null || data == null) { + return state + } + + data = makeImmutable(data) + + if (data.isEmpty()) { + return state + } + + return state.mergeIn(['cache', 'ledgerVideos', key], data) +} + module.exports = { getDataByVideoId, - setCacheByVideoId + setCacheByVideoId, + mergeCacheByVideoId } diff --git a/app/common/constants/ledgerMediaProviders.js b/app/common/constants/ledgerMediaProviders.js index 84ef2d1af80..b8d241eb3d2 100644 --- a/app/common/constants/ledgerMediaProviders.js +++ b/app/common/constants/ledgerMediaProviders.js @@ -3,7 +3,8 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const providers = { - YOUTUBE: 'youtube' + YOUTUBE: 'youtube', + TWITCH: 'twitch' } module.exports = providers diff --git a/app/common/constants/twitchEvents.js b/app/common/constants/twitchEvents.js new file mode 100644 index 00000000000..6d94e753264 --- /dev/null +++ b/app/common/constants/twitchEvents.js @@ -0,0 +1,12 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const events = { + MINUTE_WATCHED: 'minute-watched', + START: 'video-play', + PLAY_PAUSE: 'player_click_playpause', + SEEK: 'vod_seek' +} + +module.exports = events diff --git a/app/common/lib/ledgerUtil.js b/app/common/lib/ledgerUtil.js index 6b02863b09a..bf34147fbb3 100644 --- a/app/common/lib/ledgerUtil.js +++ b/app/common/lib/ledgerUtil.js @@ -13,16 +13,19 @@ const queryString = require('querystring') // State const siteSettingsState = require('../state/siteSettingsState') const ledgerState = require('../state/ledgerState') +const ledgerVideoCache = require('../cache/ledgerVideoCache') // Constants const settings = require('../../../js/constants/settings') const ledgerMediaProviders = require('../constants/ledgerMediaProviders') +const twitchEvents = require('../constants/twitchEvents') // Utils const {responseHasContent} = require('./httpUtil') const urlUtil = require('../../../js/lib/urlutil') const getSetting = require('../../../js/settings').getSetting const urlParse = require('../urlParse') + /** * Is page an actual page being viewed by the user? (not an error page, etc) * If the page is invalid, we don't want to collect usage info. @@ -221,6 +224,26 @@ const getMediaId = (data, type) => { id = data.docid break } + case ledgerMediaProviders.TWITCH: + { + if ( + ([ + twitchEvents.MINUTE_WATCHED, + twitchEvents.START, + twitchEvents.PLAY_PAUSE, + twitchEvents.SEEK + ].includes(data.event)) && + data.properties + ) { + id = data.properties.channel + let vod = data.properties.vod + + if (vod) { + vod = vod.replace('v', '') + id += `_vod_${vod}` + } + } + } } return id @@ -241,16 +264,39 @@ const getMediaData = (xhr, type) => { return result } + const parsedUrl = urlParse(xhr) + const query = parsedUrl && parsedUrl.query + + if (!parsedUrl || !query) { + return null + } + switch (type) { case ledgerMediaProviders.YOUTUBE: { - const parsedUrl = urlParse(xhr) - let query = null + result = queryString.parse(parsedUrl.query) + break + } + case ledgerMediaProviders.TWITCH: + { + result = queryString.parse(parsedUrl.query) + if (!result.data) { + result = null + break + } - if (parsedUrl && parsedUrl.query) { - query = queryString.parse(parsedUrl.query) + let obj = Buffer.from(result.data, 'base64').toString('utf8') + if (obj == null) { + result = null + break + } + + try { + result = JSON.parse(obj) + } catch (error) { + result = null + console.error(error.toString()) } - result = query break } } @@ -258,18 +304,141 @@ const getMediaData = (xhr, type) => { return result } -const getMediaDuration = (data, type) => { +const getMediaDuration = (state, data, mediaKey, type) => { let duration = 0 + + if (data == null) { + return duration + } + switch (type) { - case ledgerMediaProviders.YOUTUBE: { - duration = getYouTubeDuration(data) - break - } + case ledgerMediaProviders.YOUTUBE: + { + duration = getYouTubeDuration(data) + break + } + case ledgerMediaProviders.TWITCH: + { + duration = getTwitchDuration(state, data, mediaKey) + break + } } return duration } +const generateMediaCacheData = (parsed, type) => { + let data = Immutable.Map() + + if (parsed == null) { + return data + } + + switch (type) { + case ledgerMediaProviders.TWITCH: + { + data = generateTwitchCacheData(parsed) + break + } + } + + return data +} + +const generateTwitchCacheData = (parsed) => { + if (parsed == null) { + return Immutable.Map() + } + + if (parsed.properties) { + return Immutable.fromJS({ + event: parsed.event, + time: parsed.properties.time + }) + } + + return Immutable.fromJS({ + event: parsed.event + }) +} + +const getDefaultMediaFavicon = (providerName) => { + let image = null + + if (!providerName) { + return image + } + + providerName = providerName.toLowerCase() + + switch (providerName) { + case ledgerMediaProviders.YOUTUBE: + { + image = require('../../../img/mediaProviders/youtube.png') + break + } + case ledgerMediaProviders.TWITCH: + { + image = require('../../../img/mediaProviders/twitch.svg') + break + } + } + + return image +} + +const getTwitchDuration = (state, data, mediaKey) => { + if (data == null || mediaKey == null || !data.properties) { + return 0 + } + + const previousData = ledgerVideoCache.getDataByVideoId(state, mediaKey) + const twitchMinimumSeconds = 10 + + if (previousData.isEmpty() && data.event === twitchEvents.START) { + return twitchMinimumSeconds * milliseconds.second + } + + const oldMedia = ledgerVideoCache.getDataByVideoId(state, mediaKey) + let time = 0 + const currentTime = parseFloat(data.properties.time) + const oldTime = parseFloat(previousData.get('time')) + + if ( + data.event === twitchEvents.PLAY_PAUSE && + oldMedia.get('event') !== twitchEvents.PLAY_PAUSE + ) { + // User paused a video + time = currentTime - oldTime + } else if (previousData.get('event') === twitchEvents.START) { + // From video play event to x event + time = currentTime - oldTime - twitchMinimumSeconds + } else if (data.event === twitchEvents.MINUTE_WATCHED) { + // Minute watched event + time = currentTime - oldTime + } else if (data.event === twitchEvents.SEEK && oldMedia.get('event') !== twitchEvents.PLAY_PAUSE) { + // Vod seek event + time = currentTime - oldTime + } + + if (isNaN(time)) { + return 0 + } + + if (time < 0) { + return 0 + } + + if (time > 120) { + time = 120 // 2 minutes + } + + // we get seconds back, so we need to convert it into ms + time = time * milliseconds.second + + return Math.floor(time) +} + const getYouTubeDuration = (data) => { let time = 0 @@ -294,7 +463,7 @@ const getYouTubeDuration = (data) => { return parseInt(time) } -const getMediaProvider = (url) => { +const getMediaProvider = (url, firstPartyUrl, referrer) => { let provider = null if (url == null) { @@ -303,7 +472,18 @@ const getMediaProvider = (url) => { // Youtube if (url.startsWith('https://www.youtube.com/api/stats/watchtime?')) { - provider = ledgerMediaProviders.YOUTUBE + return ledgerMediaProviders.YOUTUBE + } + + // Twitch + if ( + ( + (firstPartyUrl && firstPartyUrl.startsWith('https://www.twitch.tv/')) || + (referrer && referrer.startsWith('https://player.twitch.tv/')) + ) && + url.startsWith('https://api.mixpanel.com/') + ) { + return ledgerMediaProviders.TWITCH } return provider @@ -339,14 +519,18 @@ const getMethods = () => { getMediaData, getMediaKey, milliseconds, - defaultMonthlyAmounts + defaultMonthlyAmounts, + getDefaultMediaFavicon, + generateMediaCacheData } let privateMethods = {} if (process.env.NODE_ENV === 'test') { privateMethods = { - getYouTubeDuration + getYouTubeDuration, + getTwitchDuration, + generateTwitchCacheData } } diff --git a/app/filtering.js b/app/filtering.js index dd3ce3f5bd2..a863d6a0d34 100644 --- a/app/filtering.js +++ b/app/filtering.js @@ -115,12 +115,21 @@ function registerForBeforeRequest (session, partition) { } const firstPartyUrl = module.exports.getMainFrameUrl(details) + const url = details.url // this can happen if the tab is closed and the webContents is no longer available if (!firstPartyUrl) { muonCb({ cancel: true }) return } + if (!isPrivate && module.exports.isResourceEnabled('ledger') && module.exports.isResourceEnabled('ledgerMedia')) { + // Ledger media + const provider = ledgerUtil.getMediaProvider(url, firstPartyUrl, details.referrer) + if (provider) { + appActions.onLedgerMediaData(url, provider, details.tabId) + } + } + for (let i = 0; i < beforeRequestFilteringFns.length; i++) { let results = beforeRequestFilteringFns[i](details, isPrivate) const isAdBlock = (results.resourceName === appConfig.resourceNames.ADBLOCK) || @@ -201,7 +210,6 @@ function registerForBeforeRequest (session, partition) { } } // Redirect to non-script version of DDG when it's blocked - const url = details.url if (details.resourceType === 'mainFrame' && url.startsWith('https://duckduckgo.com/?q') && module.exports.isResourceEnabled('noScript', url, isPrivate)) { @@ -209,14 +217,6 @@ function registerForBeforeRequest (session, partition) { } else { muonCb({}) } - - if (module.exports.isResourceEnabled('ledger') && module.exports.isResourceEnabled('ledgerMedia')) { - // Ledger media - const provider = ledgerUtil.getMediaProvider(url) - if (provider) { - appActions.onLedgerMediaData(url, provider, details.tabId) - } - } }) } diff --git a/app/renderer/components/preferences/payment/ledgerTable.js b/app/renderer/components/preferences/payment/ledgerTable.js index ec10f2287a5..00dceb3cac3 100644 --- a/app/renderer/components/preferences/payment/ledgerTable.js +++ b/app/renderer/components/preferences/payment/ledgerTable.js @@ -27,6 +27,7 @@ const aboutActions = require('../../../../../js/about/aboutActions') const urlUtil = require('../../../../../js/lib/urlutil') const {SettingCheckbox, SiteSettingCheckbox} = require('../../common/settings') const locale = require('../../../../../js/l10n') +const ledgerUtil = require('../../../../common/lib/ledgerUtil') class LedgerTable extends ImmutableComponent { get synopsis () { @@ -153,6 +154,25 @@ class LedgerTable extends ImmutableComponent { ] } + getImage (faviconURL, providerName, publisherKey) { + if (!faviconURL && providerName) { + faviconURL = ledgerUtil.getDefaultMediaFavicon(providerName) + } + + if (!faviconURL) { + return + + + } + + return + } + getRow (synopsis) { const faviconURL = synopsis.get('faviconURL') const views = synopsis.get('views') @@ -162,6 +182,7 @@ class LedgerTable extends ImmutableComponent { const publisherURL = synopsis.get('publisherURL') const percentage = pinned ? this.pinPercentageValue(synopsis) : synopsis.get('percentage') const publisherKey = synopsis.get('publisherKey') + const providerName = synopsis.get('providerName') const siteName = synopsis.get('siteName') const defaultAutoInclude = this.enabledForSite(synopsis) @@ -178,11 +199,7 @@ class LedgerTable extends ImmutableComponent { { html:
- { - faviconURL - ? - : - } + { this.getImage(faviconURL, providerName, publisherKey) } {siteName}
, diff --git a/docs/state.md b/docs/state.md index 565e7d91923..aa5f8bbc5b9 100644 --- a/docs/state.md +++ b/docs/state.md @@ -93,6 +93,7 @@ AppStore ledgerVideos: { [mediaKey]: { publisher: string // publisher key + beatData: object // data that we get from a heartbeat } } } @@ -346,6 +347,7 @@ AppStore publishers: { [publisherId]: { duration: number, + faviconName: string, faviconURL: string, options: { exclude: boolean, @@ -355,6 +357,8 @@ AppStore }, pinPercentage: number, protocol: string, + publisherURL: string, + providerName: string, scores: { concave: number, visits: number diff --git a/img/mediaProviders/twitch.svg b/img/mediaProviders/twitch.svg new file mode 100644 index 00000000000..905e364f500 --- /dev/null +++ b/img/mediaProviders/twitch.svg @@ -0,0 +1 @@ +Glitch \ No newline at end of file diff --git a/img/mediaProviders/youtube.png b/img/mediaProviders/youtube.png new file mode 100644 index 00000000000..b0c05d07169 Binary files /dev/null and b/img/mediaProviders/youtube.png differ diff --git a/test/unit/app/browser/api/ledgerTest.js b/test/unit/app/browser/api/ledgerTest.js index ef4d114fb16..54f79f3dcd8 100644 --- a/test/unit/app/browser/api/ledgerTest.js +++ b/test/unit/app/browser/api/ledgerTest.js @@ -803,11 +803,7 @@ describe('ledger api unit tests', function () { cache: { ledgerVideos: { 'youtube_kLiLOkzLetE': { - publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg', - faviconName: 'Brave', - providerName: 'Youtube', - faviconURL: 'data:image/jpeg;base64,...', - publisherURL: 'https://brave.com' + publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg' } } }, @@ -846,11 +842,7 @@ describe('ledger api unit tests', function () { cache: { ledgerVideos: { 'youtube_kLiLOkzLetE': { - publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg', - faviconName: 'Brave', - providerName: 'Youtube', - faviconURL: 'data:image/jpeg;base64,...', - publisherURL: 'https://brave.com' + publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg' } } }, diff --git a/test/unit/app/common/cache/ledgerVideoCacheTest.js b/test/unit/app/common/cache/ledgerVideoCacheTest.js index feadd27153c..71b4cd6907c 100644 --- a/test/unit/app/common/cache/ledgerVideoCacheTest.js +++ b/test/unit/app/common/cache/ledgerVideoCacheTest.js @@ -17,6 +17,10 @@ const stateWithData = Immutable.fromJS({ ledgerVideos: { 'youtube_kLiLOkzLetE': { publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg' + }, + 'twitch_test': { + publisher: 'twitch#author:test', + time: 1234 } } } @@ -52,9 +56,40 @@ describe('ledgerVideoCache unit test', function () { const state = ledgerVideoCache.setCacheByVideoId(baseState, 'youtube_kLiLOkzLetE', Immutable.fromJS({ publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg' })) - const expectedState = state.setIn(['cache', 'ledgerVideos', 'youtube_kLiLOkzLetE'], Immutable.fromJS({ - publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg' - })) + const expectedState = state + .setIn(['cache', 'ledgerVideos', 'youtube_kLiLOkzLetE'], Immutable.fromJS({ + publisher: 'youtube#channel:UCFNTTISby1c_H-rm5Ww5rZg' + })) + assert.deepEqual(state.toJS(), expectedState.toJS()) + }) + }) + + describe('mergeCacheByVideoId', function () { + it('null case', function () { + const state = ledgerVideoCache.mergeCacheByVideoId(baseState) + assert.deepEqual(state.toJS(), baseState.toJS()) + }) + + it('old data is missing', function () { + const state = ledgerVideoCache.mergeCacheByVideoId(baseState, 'twitch_test', {someData: 'test'}) + const expectedState = baseState + .setIn(['cache', 'ledgerVideos', 'twitch_test'], Immutable.fromJS({someData: 'test'})) + assert.deepEqual(state.toJS(), expectedState.toJS()) + }) + + it('new data is null', function () { + const state = ledgerVideoCache.mergeCacheByVideoId(stateWithData, 'twitch_test') + assert.deepEqual(state.toJS(), stateWithData.toJS()) + }) + + it('old and new data are present', function () { + const state = ledgerVideoCache.mergeCacheByVideoId(stateWithData, 'twitch_test', {someData: 'test'}) + const expectedState = stateWithData + .setIn(['cache', 'ledgerVideos', 'twitch_test'], Immutable.fromJS({ + publisher: 'twitch#author:test', + time: 1234, + someData: 'test' + })) assert.deepEqual(state.toJS(), expectedState.toJS()) }) }) diff --git a/test/unit/app/common/lib/ledgerUtilTest.js b/test/unit/app/common/lib/ledgerUtilTest.js index 8adfd00266d..33662a5d145 100644 --- a/test/unit/app/common/lib/ledgerUtilTest.js +++ b/test/unit/app/common/lib/ledgerUtilTest.js @@ -6,6 +6,23 @@ require('../../../braveUnit') const settings = require('../../../../../js/constants/settings') const ledgerMediaProviders = require('../../../../../app/common/constants/ledgerMediaProviders') +const baseState = Immutable.fromJS({ + cache: { + ledgerVideos: {} + } +}) +const stateWithData = Immutable.fromJS({ + cache: { + ledgerVideos: { + 'twitch_test': { + publisher: 'twitch#author:test', + event: 'video-play', + time: 1519279886 + } + } + } +}) + describe('ledgerUtil unit test', function () { let ledgerUtil let fakeLevel @@ -28,6 +45,8 @@ describe('ledgerUtil unit test', function () { mockery.registerMock('electron', fakeElectron) mockery.registerMock('ad-block', fakeAdBlock) mockery.registerMock('level', fakeLevel) + mockery.registerMock('../../../img/mediaProviders/youtube.png', 'youtube.png') + mockery.registerMock('../../../img/mediaProviders/twitch.svg', 'twitch.svg') mockery.registerMock('../../../js/settings', { getSetting: (settingKey) => { @@ -380,7 +399,7 @@ describe('ledgerUtil unit test', function () { }) it('unknown type', function () { - const result = ledgerUtil.getMediaData({}, 'test') + const result = ledgerUtil.getMediaId({}, 'test') assert.equal(result, null) }) @@ -395,6 +414,78 @@ describe('ledgerUtil unit test', function () { assert.equal(result, 'kLiLOkzLetE') }) }) + + describe('Twitch', function () { + it('null case', function () { + const result = ledgerUtil.getMediaId(null, ledgerMediaProviders.TWITCH) + assert.equal(result, null) + }) + + it('event is not correct', function () { + const result = ledgerUtil.getMediaId({ + event: 'wrong' + }, ledgerMediaProviders.TWITCH) + assert.equal(result, null) + }) + + it('properties are missing', function () { + const result = ledgerUtil.getMediaId({ + event: 'minute-watched' + }, ledgerMediaProviders.TWITCH) + assert.equal(result, null) + }) + + it('content is a live stream', function () { + const result = ledgerUtil.getMediaId({ + event: 'minute-watched', + properties: { + channel: 'tchannel' + } + }, ledgerMediaProviders.TWITCH) + assert.equal(result, 'tchannel') + }) + + it('content is a vod', function () { + const result = ledgerUtil.getMediaId({ + event: 'minute-watched', + properties: { + channel: 'tchannel', + vod: 'v12343234' + } + }, ledgerMediaProviders.TWITCH) + assert.equal(result, 'tchannel_vod_12343234') + }) + + it('event is video-play', function () { + const result = ledgerUtil.getMediaId({ + event: 'video-play', + properties: { + channel: 'tchannel' + } + }, ledgerMediaProviders.TWITCH) + assert.equal(result, 'tchannel') + }) + + it('event is player_click_playpause', function () { + const result = ledgerUtil.getMediaId({ + event: 'player_click_playpause', + properties: { + channel: 'tchannel' + } + }, ledgerMediaProviders.TWITCH) + assert.equal(result, 'tchannel') + }) + + it('event is vod_seek', function () { + const result = ledgerUtil.getMediaId({ + event: 'vod_seek', + properties: { + channel: 'tchannel' + } + }, ledgerMediaProviders.TWITCH) + assert.equal(result, 'tchannel') + }) + }) }) describe('getMediaKey', function () { @@ -408,14 +499,16 @@ describe('ledgerUtil unit test', function () { assert.equal(result, null) }) - it('id is null', function () { - const result = ledgerUtil.getMediaKey(null, ledgerMediaProviders.YOUTUBE) - assert.equal(result, null) - }) + describe('YouTube', function () { + it('id is null', function () { + const result = ledgerUtil.getMediaKey(null, ledgerMediaProviders.YOUTUBE) + assert.equal(result, null) + }) - it('data is ok', function () { - const result = ledgerUtil.getMediaKey('kLiLOkzLetE', ledgerMediaProviders.YOUTUBE) - assert.equal(result, 'youtube_kLiLOkzLetE') + it('data is ok', function () { + const result = ledgerUtil.getMediaKey('kLiLOkzLetE', ledgerMediaProviders.YOUTUBE) + assert.equal(result, 'youtube_kLiLOkzLetE') + }) }) }) @@ -430,17 +523,17 @@ describe('ledgerUtil unit test', function () { assert.equal(result, null) }) + it('query is not present', function () { + const result = ledgerUtil.getMediaData('https://youtube.com', ledgerMediaProviders.YOUTUBE) + assert.equal(result, null) + }) + describe('Youtube', function () { it('null case', function () { const result = ledgerUtil.getMediaData(null, ledgerMediaProviders.YOUTUBE) assert.equal(result, null) }) - it('query is not present', function () { - const result = ledgerUtil.getMediaData('https://youtube.com', ledgerMediaProviders.YOUTUBE) - assert.equal(result, null) - }) - it('query is present', function () { const result = ledgerUtil.getMediaData('https://www.youtube.com/api/stats/watchtime?docid=kLiLOkzLetE&st=11.338&et=21.339', ledgerMediaProviders.YOUTUBE) assert.deepEqual(result, { @@ -450,6 +543,33 @@ describe('ledgerUtil unit test', function () { }) }) }) + + describe('Twitch', function () { + it('null case', function () { + const result = ledgerUtil.getMediaData(null, ledgerMediaProviders.TWITCH) + assert.equal(result, null) + }) + + it('data is missing', function () { + const result = ledgerUtil.getMediaData('https://api.mixpanel.com', ledgerMediaProviders.TWITCH) + assert.equal(result, null) + }) + + it('data is empty string', function () { + const result = ledgerUtil.getMediaData('https://api.mixpanel.com?data=', ledgerMediaProviders.TWITCH) + assert.equal(result, null) + }) + + it('obj is parsed correctly', function () { + const result = ledgerUtil.getMediaData('https://api.mixpanel.com?data=eyJldmVudCI6Im1pbnV0ZS13YXRjaGVkIiwicHJvcGVydGllcyI6eyJjaGFubmVsIjoidHcifX0=', ledgerMediaProviders.TWITCH) + assert.deepEqual(result, { + event: 'minute-watched', + properties: { + channel: 'tw' + } + }) + }) + }) }) describe('getYouTubeDuration', function () { @@ -490,6 +610,30 @@ describe('ledgerUtil unit test', function () { const result = ledgerUtil.getMediaProvider('https://www.youtube.com/api/stats/watchtime?docid=kLiLOkzLetE&st=11.338&et=21.339') assert.equal(result, ledgerMediaProviders.YOUTUBE) }) + + describe('twitch', function () { + it('we only have url', function () { + const result = ledgerUtil.getMediaProvider('https://api.mixpanel.com/?data=lll') + assert.equal(result, null) + }) + + it('video is on twitch.tv', function () { + const result = ledgerUtil.getMediaProvider( + 'https://api.mixpanel.com/?data=lll', + 'https://www.twitch.tv/' + ) + assert.equal(result, ledgerMediaProviders.TWITCH) + }) + + it('video is embeded', function () { + const result = ledgerUtil.getMediaProvider( + 'https://api.mixpanel.com/?data=lll', + 'https://www.site.tv/', + 'https://player.twitch.tv/' + ) + assert.equal(result, ledgerMediaProviders.TWITCH) + }) + }) }) describe('milliseconds', function () { @@ -529,4 +673,169 @@ describe('ledgerUtil unit test', function () { assert.deepEqual(ledgerUtil.defaultMonthlyAmounts.toJS(), [5.0, 7.5, 10.0, 17.5, 25.0, 50.0, 75.0, 100.0]) }) }) + + describe('getDefaultMediaFavicon', function () { + it('null case', function () { + const result = ledgerUtil.getDefaultMediaFavicon() + assert.equal(result, null) + }) + + it('youtube', function () { + const result = ledgerUtil.getDefaultMediaFavicon('YouTube') + assert.equal(result, 'youtube.png') + }) + + it('twitch', function () { + const result = ledgerUtil.getDefaultMediaFavicon('Twitch') + assert.equal(result, 'twitch.svg') + }) + }) + + describe('generateTwitchCacheData', function () { + it('null check', function () { + const result = ledgerUtil.generateTwitchCacheData() + assert.deepEqual(result.toJS(), {}) + }) + + it('properties are missing', function () { + const result = ledgerUtil.generateTwitchCacheData({ + event: 'video-play', + channel: 'test' + }) + assert.deepEqual(result.toJS(), { + event: 'video-play' + }) + }) + + it('properties are present', function () { + const result = ledgerUtil.generateTwitchCacheData({ + event: 'video-play', + properties: { + time: 100, + minute_logged: 1 + }, + channel: 'test' + }) + assert.deepEqual(result.toJS(), { + event: 'video-play', + time: 100 + }) + }) + }) + + describe('getTwitchDuration', function () { + it('null case', function () { + const result = ledgerUtil.getTwitchDuration() + assert.deepEqual(result, 0) + }) + + it('we just video playing', function () { + const result = ledgerUtil.getTwitchDuration(baseState, { + event: 'video-play', + properties: { + time: '1223fa' + } + }, 'twitch_test') + assert.deepEqual(result, 10000) + }) + + it('properties are missing', function () { + const result = ledgerUtil.getTwitchDuration(baseState, { + event: 'minute-watched', + properties: { + time: '1223fa' + } + }, 'twitch_test') + assert.deepEqual(result, 0) + }) + + it('current time is not a number', function () { + const result = ledgerUtil.getTwitchDuration(stateWithData, { + event: 'minute-watched', + properties: { + time: '1223fa' + } + }, 'twitch_test') + assert.deepEqual(result, 0) + }) + + it('user paused a video', function () { + const result = ledgerUtil.getTwitchDuration(stateWithData, { + event: 'player_click_playpause', + properties: { + time: 1519279926 + } + }, 'twitch_test') + assert.deepEqual(result, 40000) + }) + + it('first minute watched', function () { + const result = ledgerUtil.getTwitchDuration(stateWithData, { + event: 'minute-watched', + properties: { + time: 1519279926 + } + }, 'twitch_test') + assert.deepEqual(result, 30000) + }) + + it('second minute watched', function () { + const state = stateWithData + .setIn(['cache', 'ledgerVideos', 'twitch_test', 'event'], 'minute-watched') + + const result = ledgerUtil.getTwitchDuration(state, { + event: 'minute-watched', + properties: { + time: 1519279926 + } + }, 'twitch_test') + assert.deepEqual(result, 40000) + }) + + it('vod seeked', function () { + const state = stateWithData + .setIn(['cache', 'ledgerVideos', 'twitch_test', 'event'], 'minute-watched') + + const result = ledgerUtil.getTwitchDuration(state, { + event: 'vod_seek', + properties: { + time: 1519279926 + } + }, 'twitch_test') + assert.deepEqual(result, 40000) + }) + + it('end time is negative', function () { + const result = ledgerUtil.getTwitchDuration(stateWithData, { + event: 'minute-watched', + properties: { + time: 1519249926 + } + }, 'twitch_test') + assert.deepEqual(result, 0) + }) + + it('end time is more then 2 minutes', function () { + const result = ledgerUtil.getTwitchDuration(stateWithData, { + event: 'minute-watched', + properties: { + time: 1519449926 + } + }, 'twitch_test') + assert.deepEqual(result, 120000) + }) + + it('we need to floor end time', function () { + const state = stateWithData + .setIn(['cache', 'ledgerVideos', 'twitch_test', 'event'], 'minute-watched') + + const result = ledgerUtil.getTwitchDuration(state, { + event: 'minute-watched', + properties: { + time: 1519279926.74353453 + } + }, 'twitch_test') + assert.deepEqual(result, 40743) + }) + }) })