diff --git a/extension/background/content.js b/extension/background/content.js index 9a1ea52f2..085ec43f2 100644 --- a/extension/background/content.js +++ b/extension/background/content.js @@ -4,7 +4,11 @@ this.content = (function() { const NO_RECEIVER_MESSAGE = "Could not establish connection. Receiving end does not exist"; const exports = {}; + exports.lazyInject = async function(tabId, scripts) { + if (!tabId) { + throw new Error(`Invalid tabId: ${tabId}`); + } if (typeof scripts === "string") { scripts = [scripts]; } @@ -43,5 +47,6 @@ this.content = (function() { scriptKey, }); }; + return exports; })(); diff --git a/extension/background/intentParser.js b/extension/background/intentParser.js index 4c0a7eecc..30b00b8b3 100644 --- a/extension/background/intentParser.js +++ b/extension/background/intentParser.js @@ -15,6 +15,7 @@ this.intentParser = (function() { const ENTITY_TYPES = { serviceName: serviceList.allServiceNames(), + musicServiceName: serviceList.musicServiceNames(), }; const Matcher = (exports.Matcher = class Matcher { diff --git a/extension/background/serviceList.js b/extension/background/serviceList.js index c6d8b1f93..017c51f14 100644 --- a/extension/background/serviceList.js +++ b/extension/background/serviceList.js @@ -91,6 +91,12 @@ this.serviceList = (function() { return bang; }; + // Note these are maintained separately from the services in extension/services/*, because + // those are all loaded too late to be used here + exports.musicServiceNames = function() { + return ["youtube", "spotify"]; + }; + exports.Service = class Service { constructor(context) { this.context = context; @@ -181,6 +187,9 @@ this.serviceList = (function() { let bestScore = 0; for (const name in services) { const service = services[name]; + if (service.skipAutodetect) { + continue; + } if (!service.baseUrl) { throw new Error(`Service ${service.name} has no .baseUrl`); } diff --git a/extension/intents/music/music.js b/extension/intents/music/music.js index 640afdd0c..ba04d56bc 100644 --- a/extension/intents/music/music.js +++ b/extension/intents/music/music.js @@ -1,21 +1,45 @@ -/* globals intentRunner, serviceList */ +/* globals intentRunner, serviceList, log */ this.intents.music = (function() { const exports = {}; const SERVICES = {}; exports.register = function(service) { + if (!service.id) { + log.error("Bad music service, no id:", service); + throw new Error("Invalid service: no id"); + } + if (SERVICES[service.id]) { + throw new Error( + `Attempt to register two music services with id ${service.id}` + ); + } SERVICES[service.id] = service; }; + async function getService(context) { + let ServiceClass; + if (context.slots.service) { + ServiceClass = SERVICES[context.slots.service.toLowerCase()]; + if (!ServiceClass) { + throw new Error( + `[service] slot refers to unknown service: ${context.slots.service}` + ); + } + } else { + ServiceClass = await serviceList.getService("music", SERVICES); + } + return new ServiceClass(context); + } + intentRunner.registerIntent({ name: "music.play", examples: ["Play Green Day"], match: ` + play [query] on [service:musicServiceName] play [query] `, async run(context) { - const Service = await serviceList.getService("music", SERVICES); - const service = new Service(context); + const service = await getService(context); await service.playQuery(context.slots.query); }, }); @@ -24,12 +48,12 @@ this.intents.music = (function() { name: "music.pause", examples: ["Pause music"], match: ` + pause [service:musicServiceName] pause music stop music `, async run(context) { - const Service = await serviceList.getService("music", SERVICES); - const service = new Service(context); + const service = await getService(context); await service.pause(); }, }); @@ -38,14 +62,16 @@ this.intents.music = (function() { name: "music.unpause", examples: ["Unpause", "continue music", "play music"], match: ` + unpause [service:musicServiceName] + continue [service:musicServiceName] + play [service:musicServiceName] unpause continue music play music `, priority: "high", async run(context) { - const Service = await serviceList.getService("music", SERVICES); - const service = new Service(context); + const service = await getService(context); await service.unpause(); }, }); @@ -54,13 +80,15 @@ this.intents.music = (function() { name: "music.focus", examples: ["Open music"], match: ` + open [service:musicServiceName] + show [service:musicServiceName] + focus [service:musicServiceName] open music show music focus music `, async run(context) { - const Service = await serviceList.getService("music", SERVICES); - const service = new Service(context); + const service = await getService(context); await service.activateOrOpen(); }, }); diff --git a/extension/services/youtube/player.js b/extension/services/youtube/player.js new file mode 100644 index 000000000..e7cc0585c --- /dev/null +++ b/extension/services/youtube/player.js @@ -0,0 +1,25 @@ +/* globals helpers */ + +this.player = (function() { + class Player extends helpers.Runner { + action_play() { + const button = this.querySelector("button.ytp-large-play-button"); + console.log("clicking button", button); + button.click(); + console.log("clicked"); + } + + action_pause() { + const button = this.querySelector( + "buttonytp-play-button[aria-label^='Pause']" + ); + button.click(); + } + + action_unpause() { + this.action_play(); + } + } + + Player.register(); +})(); diff --git a/extension/services/youtube/youtube.js b/extension/services/youtube/youtube.js new file mode 100644 index 000000000..1846b1240 --- /dev/null +++ b/extension/services/youtube/youtube.js @@ -0,0 +1,34 @@ +/* globals intents, serviceList, searching, content */ + +this.services.youtube = (function() { + class YouTube extends serviceList.Service { + async playQuery(query) { + this.tab = await browser.tabs.create({ + url: searching.googleSearchUrl(`${query} youtube.com`, true), + active: true, + }); + browser.tabs.update(this.tab.id, { muted: true }); + await content.lazyInject(this.tab.id, "/services/youtube/player.js"); + await this.callTab("play"); + } + + async pause() { + await this.initTab("/services/spotify/player.js"); + await this.callTab("pause"); + } + + async unpause() { + await this.initTab("/services/spotify/player.js"); + await this.callTab("unpause"); + } + } + + Object.assign(YouTube, { + id: "youtube", + title: "YouTube", + baseUrl: "https://www.youtube.com", + skipAutodetect: true, + }); + + intents.music.register(YouTube); +})();