diff --git a/Gruntfile.js b/Gruntfile.js index a04ff21..8384cf0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -100,6 +100,13 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-webstore-upload'); + // watcher + grunt.registerTask('default', [ 'watch' ]); + // pack & run dev version locally + grunt.registerTask('pack', [ 'rollup', 'zip' ]); + + // CI stuff below + // Alias task for release grunt.registerTask('makeRelease', function (type) { type = type ? type : 'patch'; // Default release type @@ -109,10 +116,8 @@ module.exports = function(grunt) { grunt.task.run('exec:push_release'); }); - grunt.registerTask('default', [ 'watch' ]); // to make release run this one grunt.registerTask('build', [ 'makeRelease' ]); - grunt.registerTask('pack', [ 'rollup', 'zip' ]); // only should be run by CI, not manually grunt.registerTask('deploy', ['pack', 'webstore_upload']); }; diff --git a/src/background/models/ProvidersList.js b/src/background/models/ProvidersList.js index 273749e..c357007 100644 --- a/src/background/models/ProvidersList.js +++ b/src/background/models/ProvidersList.js @@ -28,13 +28,10 @@ export const ProvidersList = [ 'musicforprogramming.net', 'muzebra.com', 'netflix.com', - 'new.vk.com', 'open.spotify.com', 'play.google.com', - 'play.mubert.com', 'play.spotify.com', 'player.vimeo.com', - 'pleer.net', 'podcasts.apple.com', 'promodj.com', 'radio.garden', diff --git a/src/content/Control.Strategies.js b/src/content/Control.Strategies.js new file mode 100644 index 0000000..f8ff800 --- /dev/null +++ b/src/content/Control.Strategies.js @@ -0,0 +1,86 @@ + +import { Status } from './Status.Types.js'; + +export class BaseControlStrategy { + static play() {} + static pause() {} +} + + +export class clickSelector extends BaseControlStrategy { + static play(className) { + let element = document.querySelector(className); + if (!element) { + return; + } + + element.click(); + } + + static pause(className) { + clickSelector.play(className); + } +} + +/* class for standard audio/video media elements */ +export class mediaToggle extends BaseControlStrategy { + static play(className) { + let element = document.querySelector(className); + element && element.paused && element.play(); + } + + static pause(className) { + let element = document.querySelector(className); + element && !element.paused && element.pause(); + } +} + +/* clicking the storedSelector, injected by the Service */ +export class clickStoredSelector extends BaseControlStrategy { + static play() { + if (this.selector) { + this.selector.click(); + } + } + + static pause() { + clickStoredSelector.play.call(this); + } +} + +/* custom for jouele, based on its buttons position */ +export class joueleStoredSelector extends BaseControlStrategy { + static pause() { + clickStoredSelector.pause.call(this); + } + + static play() { + if (this.selector) { + this.selector.previousSibling.click(); + } + } +} + + +export class oneOfTheVideos extends BaseControlStrategy { + static getVideosArray() { + return Array.from(document.getElementsByTagName('video')); + } + + static pause() { + oneOfTheVideos.getVideosArray() + .filter((player) => !player.paused) + .forEach((player) => { + player.pause(); + }); + } + + static play() { + oneOfTheVideos.getVideosArray() + .filter((player) => player.paused && player.played.length > 0) + .forEach((player) => { + player.play(); + }); + } + +} diff --git a/src/content/Service.js b/src/content/Service.js new file mode 100644 index 0000000..9da3263 --- /dev/null +++ b/src/content/Service.js @@ -0,0 +1,26 @@ +/* Base Service class */ +export class Service { + /** + @param options {Object} + @param options.statusStrategy {Class} + @param options.statusArgs {Array} (optional) + @param options.controlStrategy {Class} + @param options.playArgs {Array} (optional) + @param options.pauseArgs {Array} (optional) + */ + constructor(options) { + this.options = options; + } + + getStatus() { + return this.options.statusStrategy.getStatus.apply(this, this.options.statusArgs); + } + + play() { + this.options.controlStrategy.play.apply(this, this.options.playArgs); + } + + pause() { + this.options.controlStrategy.pause.apply(this, this.options.pauseArgs); + } +} diff --git a/src/content/ServicesRegistry.js b/src/content/ServicesRegistry.js new file mode 100644 index 0000000..e29b187 --- /dev/null +++ b/src/content/ServicesRegistry.js @@ -0,0 +1,219 @@ +import { Service } from './Service.js'; +import * as StatusStrategies from './Status.Strategies.js'; +import * as ControlStrategies from './Control.Strategies.js'; + +// #TODO: move it to build-time +function oneSelectorHelper(hosts, statusArgs, playArgs, pauseArgs) { + return { + hosts, + options: { + statusStrategy: StatusStrategies.checkSelector, + statusArgs: [ statusArgs ], + controlStrategy: ControlStrategies.clickSelector, + playArgs: [ playArgs ], + pauseArgs: [ pauseArgs ] + } + }; +} + +function getOneSelector() { + return [ + [ + [ 'vimeo.com', 'player.vimeo.com' ], + '.play.state-playing', + '.play.state-paused', + '.play.state-playing' + ], [ + [ 'vk.com' ], + '.top_audio_player.top_audio_player_playing', + '.top_audio_player_play', + '.top_audio_player_play' + ], [ + [ 'muzebra.com' ], + '.player.jp-state-playing', + '.player.jp-play', + '.player.jp-pause' + ], [ + [ 'music.yandex.ru', 'music.yandex.ua' ], + '.player-controls__btn_play.player-controls__btn_pause', + '.player-controls__btn_play', + '.player-controls__btn_pause' + ], [ + [ 'mixcloud.com' ], + '.player-control.pause-state', + '.player-control', + '.player-control' + ], [ + [ 'soundcloud.com' ], + '.playControl.playing', + '.playControl', + '.playControl.playing' + ], [ + [ 'jazzradio.com', 'rockradio.com', 'radiotunes.com', 'classicalradio.com', 'zenradio.com' ], + '#play-button .icon-pause', + '#play-button .ctl', + '#play-button .ctl' + ], [ + [ 'v5player.slipstreamradio.com', 'accuradio.com' ], + '#playerPauseButton', + '#playerPlayButton', + '#playerPauseButton' + ], [ + [ 'open.spotify.com' ], + ".control-button[class*='pause']", + ".control-button[class*='play']", + ".control-button[class*='pause']" + ], [ + [ 'bandcamp.com' ], + '.inline_player .playbutton.playing', + '.inline_player .playbutton', + '.inline_player .playbutton.playing' + ], [ + [ 'promodj.com' ], + '.playerr_bigplaybutton .playerr_bigpausebutton', + '.playerr_bigplaybutton .playerr_bigplaybutton', + '.playerr_bigplaybutton .playerr_bigpausebutton' + ], [ + [ 'courses.prometheus.org.ua' ], + '.video-controls .video_control.pause', + '.video-controls .video_control.play', + '.video-controls .video_control.pause' + ], [ + [ 'coursera.org' ], + '.c-video-control.vjs-control.vjs-playing', + '.c-video-control.vjs-control.vjs-paused', + '.c-video-control.vjs-control.vjs-playing' + ], [ + [ 'di.fm' ], + '#webplayer-region .controls .icon-pause', + '#webplayer-region .controls .icon-play', + '#webplayer-region .controls .icon-pause' + ], [ + [ 'audible.ca', 'audible.com', 'audible.com.au' ], + '#adbl-cloud-player-controls .adblPauseButton:not(.bc-hidden)', + '#adbl-cloud-player-controls .adblPauseButton:not(.bc-hidden)', + '#adbl-cloud-player-controls .adblPauseButton:not(.bc-hidden)' + ], [ + [ 'coub.com' ], + '.coub.active[play-state="playing"]', + '.coub.active .viewer__replay', + '.coub.active .viewer__click', + ], [ + [ 'livestream.com' ], + '.playback-control .play-holder.lsp-hidden', + '.playback-control .play-holder', + '.playback-control .pause-holder' + ], [ + [ 'beatport.com' ], + '#Player__pause-button', + '#Player__play-button', + '#Player__pause-button' + ], [ + [ 'radio.garden' ], + '.icon-toggle.mod-mute .icon-button.mod-sound', + '.icon-toggle.mod-mute .icon-button.mod-muted', + '.icon-toggle.mod-mute .icon-button.mod-sound' + ] + ].map(item => oneSelectorHelper.apply(null, item)); + +} + +export const servicesRegistry = () => { + + return getOneSelector().concat([ + { + hosts: [ 'radiolist.com.ua' ], + options: { + statusStrategy: StatusStrategies.checkSelectorAndStore, + statusArgs: [ '.jouele-status-playing .jouele-info-control-button-icon_pause' ], + controlStrategy: ControlStrategies.joueleStoredSelector + } + }, { + hosts: [ 'megogo.net' ], + options: { + statusStrategy: StatusStrategies.mediaSelector, + statusArgs: [ 'video[class*="player:video"]' ], + controlStrategy: ControlStrategies.mediaToggle, + playArgs: [ 'video[class*="player:video"]' ], + pauseArgs: [ 'video[class*="player:video"]' ] + } + }, { + hosts: [ 'dailymotion.com' ], + options: { + statusStrategy: StatusStrategies.mediaSelector, + statusArgs: [ '#dmp_Video' ], + controlStrategy: ControlStrategies.mediaToggle, + playArgs: [ '#dmp_Video' ], + pauseArgs: [ '#dmp_Video' ] + } + }, { + hosts: [ 'netflix.com' ], + options: { + statusStrategy: StatusStrategies.mediaSelector, + statusArgs: [ '.VideoContainer video' ], + controlStrategy: ControlStrategies.mediaToggle, + playArgs: [ '.VideoContainer video' ], + pauseArgs: [ '.VideoContainer video' ] + } + }, { + hosts: [ 'egghead.io' ], + options: { + statusStrategy: StatusStrategies.mediaSelector, + statusArgs: [ '.bitmovinplayer-container video' ], + controlStrategy: ControlStrategies.mediaToggle, + playArgs: [ '.bitmovinplayer-container video' ], + pauseArgs: [ '.bitmovinplayer-container video' ] + } + }, { + hosts: [ 'udemy.com' ], + options: { + statusStrategy: StatusStrategies.mediaSelector, + statusArgs: [ 'video' ], + controlStrategy: ControlStrategies.mediaToggle, + playArgs: [ 'video' ], + pauseArgs: [ 'video' ] + } + }, { + hosts: [ 'musicforprogramming.net' ], + options: { + statusStrategy: StatusStrategies.mediaSelector, + statusArgs: [ '#player' ], + controlStrategy: ControlStrategies.mediaToggle, + playArgs: [ '#player' ], + pauseArgs: [ '#player' ] + } + }, { + hosts: [ 'netflix.com' ], + options: { + statusStrategy: StatusStrategies.mediaSelector, + statusArgs: [ '.VideoContainer video' ], + controlStrategy: ControlStrategies.mediaToggle, + playArgs: [ '.VideoContainer video' ], + pauseArgs: [ '.VideoContainer video' ] + } + }, { + hosts: [ 'hearthis.at' ], + options: { + statusStrategy: StatusStrategies.checkSelector, + statusArgs: [ 'body.play' ], + controlStrategy: { /* custom */}, + } + }, { + hosts: ['ted.com', 'facebook.com', 'kickstarter.com', 'music.youtube.com' ], + options: { + statusStrategy: StatusStrategies.oneOfTheVideosPlaying, + controlStrategy: ControlStrategies.oneOfTheVideos, + } + }, + ]) +}; + +export function getService(domain) { + const matchedService = servicesRegistry().find(serviceConfig => serviceConfig.hosts.includes(domain)); + + if (!matchedService) { + return; + } + + return new Service(matchedService.options); +} diff --git a/src/content/Status.Strategies.js b/src/content/Status.Strategies.js new file mode 100644 index 0000000..c479066 --- /dev/null +++ b/src/content/Status.Strategies.js @@ -0,0 +1,49 @@ +import { Status } from './Status.Types.js'; + +export class BaseStatusStrategy { + static getStatus() {} +} + +/* simple check for selector */ +export class checkSelector extends BaseStatusStrategy { + static getStatus(className) { + let el = document.querySelector(className); + return el ? Status.PLAYING : Status.PAUSED; + } +} + +/* simple check for media state */ +export class mediaSelector extends BaseStatusStrategy { + static getStatus(className) { + let el = document.querySelector(className); + if (el && el.paused === false) { + return Status.PLAYING; + } + return Status.PAUSED; + } +} + +/* check for selector and store it */ +export class checkSelectorAndStore extends BaseStatusStrategy { + static getStatus(className, storeSelector) { + let el = document.querySelector(className); + if (el) { + // this is the Service context + this.selector = el; + } + return el ? Status.PLAYING : Status.PAUSED; + } +} + +/* when there are video tags on page */ +export class oneOfTheVideosPlaying extends BaseStatusStrategy { + static getStatus() { + let status = Status.PAUSED; + const videos = document.getElementsByTagName("video"); + if (videos.length > 0) { + const hasPlayingVideo = Array.from(videos).some((player) => !player.paused); + status = hasPlayingVideo ? Status.PLAYING : Status.PAUSED; + } + return status; + } +} diff --git a/src/content/Status.Types.js b/src/content/Status.Types.js new file mode 100644 index 0000000..5b18cf3 --- /dev/null +++ b/src/content/Status.Types.js @@ -0,0 +1,5 @@ +/* Status Types */ +export const Status = { + PAUSED: "paused", + PLAYING: "playing" +}; diff --git a/src/content/index.js b/src/content/index.js index 8e43ea0..2e33f8a 100644 --- a/src/content/index.js +++ b/src/content/index.js @@ -1,5 +1,7 @@ /* StoPlay Content JS */ import { CheckTimer } from './CheckTimer.js'; +import { getService } from './ServicesRegistry.js'; +import { Status } from './Status.Types.js'; function safeGetElementTextContentByQuery(query) { try { @@ -25,11 +27,6 @@ const StoPlay = { let button = null; -const Status = { - PAUSED: "paused", - PLAYING: "playing" -}; - const CHECK_TIMEOUT = 1000; const TITLE_TIMEOUT = 10000; @@ -48,6 +45,8 @@ class Provider { this.customLastPlayerSelector = null; this.customLastPauseSelector = null; + this.service = null; + chrome.storage.sync.get({ enabled: true, providers: [] @@ -55,7 +54,15 @@ class Provider { this.timer = new CheckTimer({ delay: CHECK_TIMEOUT, - callback: this.checkStatus.bind(this), + callback: () => { + let status; + if (this.service) { + status = this.service.getStatus(); + } else { + status = this.checkStatus(); + } + this.__changeState(status); + }, recursive: true }); this.checkTitleInterval = new CheckTimer({ @@ -112,7 +119,8 @@ class Provider { } _detectProviderAndStartCheckInterval() { - if (this.detectProvider()) { + this.detectProvider(); + if (this.providerAllowed()) { this.timer.start(); this.checkTitleInterval.start(); @@ -165,7 +173,12 @@ class Provider { clearSubDomains = "bandcamp.com"; } if (clearSubDomains) this.host = clearSubDomains; + this.service = getService(this.host); + + return this.host; + } + providerAllowed() { return (this.allowed.indexOf(this.host) >= 0); } @@ -560,13 +573,20 @@ class Provider { break; } - status && this.__changeState(status); + return status; } pause() { let p, selector, selectorQuery, playerPauseButton; if (this.status === Status.PLAYING) { + if (this.service) { + this.service.pause(); + // #TODO: remove duplication for POC + this.__changeState(Status.PAUSED); + return; + } + switch(this.host) { case "radiolist.com.ua": if (this.customLastPlayerSelector) { @@ -850,6 +870,14 @@ class Provider { let p, selector, selectorQuery, playerPlayButton; if (this.status !== Status.PLAYING) { + if (this.service) { + this.service.play(); + // #TODO: remove duplication for POC + this.__changeState(Status.PLAYING); + return; + + } + switch(this.host) { case "radiolist.com.ua": if (this.customLastPlayerSelector) {