Allowed all logging to be viewed by the interface.
It is just much easier this way.
This commit is contained in:
@@ -1,42 +1,258 @@
|
||||
console.log("Hello from Universal Music addon!")
|
||||
let logger = new Logger("background");
|
||||
|
||||
logger.pushUpdate = function (message)
|
||||
{
|
||||
if (interfacePort)
|
||||
{
|
||||
interfacePort.postMessage({
|
||||
"messageNum": -1,
|
||||
"message": message
|
||||
});
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
logger.log("Hello from Universal Music addon!")
|
||||
|
||||
/**
|
||||
* This variable contains a mapping of message IDs to the message promises they correspond to, as
|
||||
* long as those messages are still pending a response.
|
||||
*/
|
||||
const tabMessages = new Map();
|
||||
|
||||
/**
|
||||
* This counter variable is used to generate session-unique message IDs between this background and
|
||||
* the various tabs.
|
||||
*/
|
||||
var numTabMessages = 0;
|
||||
|
||||
/*
|
||||
On startup, connect to the "ping_pong" app.
|
||||
*/
|
||||
var interfacePort = browser.runtime.connectNative("universalmusic");
|
||||
|
||||
var tabPort = null;
|
||||
/**
|
||||
* This variable contains a mapping between tab IDs and resolution functions that are to be called
|
||||
* as soon as a connection port is established.
|
||||
*/
|
||||
let waitingPorts = new Map();
|
||||
|
||||
/**
|
||||
* This variable contains a mapping between tab IDs and respective communication ports.
|
||||
*/
|
||||
let ports = new Map();
|
||||
|
||||
/**
|
||||
* Obtains the host name of a URL
|
||||
*/
|
||||
function getHost(url)
|
||||
{
|
||||
return url.replace(/^\w+:\/{2,3}/, "").replace(/[\/#?:]\w+.+$/, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the domain of a URL
|
||||
*/
|
||||
function getDomain(url)
|
||||
{
|
||||
let host = getHost(url);
|
||||
let parts = host.split(".");
|
||||
return parts[parts.length - 2] + "." + parts[parts.length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads up a tab and returns a promise for when it has completely loaded.
|
||||
*
|
||||
* @param {string} url The URL to load.
|
||||
* @param {boolean} pinned Whether the tab should be pinned or not. Pinned tabs can be reused.
|
||||
* Defaults to true.
|
||||
* @return {Promise} A promise that loads the tab. The resolution function is passed the tab
|
||||
* object upon completion.
|
||||
*/
|
||||
function loadTab(url, pinned)
|
||||
{
|
||||
if (typeof pinned != "boolean" && !pinned)
|
||||
{
|
||||
pinned = true;
|
||||
}
|
||||
let chosen;
|
||||
return new Promise((resolve, reject) => {
|
||||
let domain = getDomain(url);
|
||||
|
||||
browser.tabs.query({
|
||||
url: "*://*." + domain + "/*",
|
||||
muted: false
|
||||
}).then(tabs => {
|
||||
let callback = (tabId, changed, tab) => {
|
||||
if (tab.status == "complete")
|
||||
{
|
||||
logger.debug("Tab %d complete", tab.id);
|
||||
if (browser.tabs.onUpdated.hasListener(callback))
|
||||
{
|
||||
browser.tabs.onUpdated.removeListener(callback)
|
||||
}
|
||||
resolve(tab);
|
||||
}
|
||||
};
|
||||
let navigate = (tabId, changed, tab) => {
|
||||
if (tab.status == "complete")
|
||||
{
|
||||
if (browser.tabs.onUpdated.hasListener(navigate))
|
||||
{
|
||||
browser.tabs.onUpdated.removeListener(navigate)
|
||||
}
|
||||
chosen = tabId;
|
||||
logger.debug("Redirecting tab %d %o", tabId, tab);
|
||||
/*
|
||||
* Reloading the tab disconnects the port. We will have to wait for a
|
||||
* reconnection.
|
||||
*/
|
||||
waitingPorts.set(tab.id, {
|
||||
"resolve": callback,
|
||||
"reject": reject
|
||||
});
|
||||
// browser.tabs.onUpdated.addListener(callback, {
|
||||
// urls: [url],
|
||||
// properties: ["status"]
|
||||
// });
|
||||
browser.tabs.update(tabId, {
|
||||
url: url
|
||||
}).then(() => {}, reject);
|
||||
}
|
||||
};
|
||||
let tab;
|
||||
|
||||
/*
|
||||
* If there are no available tabs (or if we are instructed to create a new tab for this
|
||||
* task), then create a new tab and wait for a connection to be established.
|
||||
*/
|
||||
logger.log("Found %o tabs", tabs);
|
||||
if (tabs.length == 0 || !pinned)
|
||||
{
|
||||
// browser.tabs.onUpdated.addListener(callback, {
|
||||
// urls: [url],
|
||||
// properties: ["status"]
|
||||
// });
|
||||
browser.tabs.create({
|
||||
url: url
|
||||
}).then(tab => {
|
||||
chosen = tab.id;
|
||||
waitingPorts.set(tab.id, {
|
||||
"resolve": callback,
|
||||
"reject": reject
|
||||
});
|
||||
browser.tabs.update(tab.id, {
|
||||
muted: !pinned
|
||||
});
|
||||
}, reject);
|
||||
}
|
||||
/*
|
||||
* Otherwise, use an existing tab and set it to a new URL.
|
||||
*/
|
||||
else
|
||||
{
|
||||
let i = 0;
|
||||
while (tabs[i].mutedInfo.muted)
|
||||
{
|
||||
i++;
|
||||
}
|
||||
tab = tabs[i];
|
||||
if (tab.status == "loading")
|
||||
{
|
||||
/*
|
||||
* Only navigate if the tab has finished its task already.
|
||||
*/
|
||||
browser.tabs.onUpdated.addListener(navigate, {
|
||||
urls: [tab.url],
|
||||
properties: ["status"],
|
||||
tabId: tab.id
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
navigate(tab.id, {}, tab);
|
||||
}
|
||||
}
|
||||
}, reject);
|
||||
}).finally(() => {
|
||||
if (!pinned)
|
||||
{
|
||||
browser.tabs.remove(chosen);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback is called every time a new tab attempts to connect to this background process.
|
||||
*
|
||||
* @param {runtime.Port} port - The port object that the tab will communicate through.
|
||||
* @return {boolean} true if the connection was successful, false otherwise.
|
||||
*/
|
||||
function setupTab(port)
|
||||
{
|
||||
if (port.sender)
|
||||
{
|
||||
console.info("Tab " + port.sender.tab.url + " loaded.");
|
||||
logger.info("Tab " + port.sender.tab.url + " loaded.");
|
||||
}
|
||||
else
|
||||
{
|
||||
console.info("Tab loaded");
|
||||
logger.info("Tab loaded");
|
||||
}
|
||||
tabPort = port;
|
||||
tabPort.onDisconnect.addListener(e => {
|
||||
tabPort = null;
|
||||
ports.set(port.sender.tab.id, port);
|
||||
if (waitingPorts.has(port.sender.tab.id))
|
||||
{
|
||||
console.log("Setting up port %o", port.sender.tab);
|
||||
waitingPorts.get(port.sender.tab.id).resolve(port.sender.tab.id, {}, port.sender.tab);
|
||||
waitingPorts.delete(port.sender.tab.id);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn("We just connected to a tab that we didn't request.");
|
||||
}
|
||||
port.onDisconnect.addListener(e => {
|
||||
ports.delete(e.sender.tab.id);
|
||||
if (e.error)
|
||||
{
|
||||
console.error("Tab error: ", e.error)
|
||||
logger.error("Tab error: %o", e.error)
|
||||
}
|
||||
else
|
||||
{
|
||||
console.info("Tab " + e.sender.tab.url + " disconnected.");
|
||||
logger.info("Tab " + e.sender.tab.url + " disconnected.");
|
||||
}
|
||||
browser.tabs.remove(e.sender.tab.id);
|
||||
// browser.tabs.remove(e.sender.tab.id);
|
||||
});
|
||||
tabPort.onMessage.addListener(message => {
|
||||
|
||||
/**
|
||||
* Listens for messages from the tab and processes their promises
|
||||
*/
|
||||
port.onMessage.addListener(message => {
|
||||
let returnValue;
|
||||
if (typeof message == "object")
|
||||
{
|
||||
if (message.type == "response")
|
||||
{
|
||||
tabMessages.set(message.num, message.data);
|
||||
returnValue = tabMessages.get(message.num);
|
||||
logger.trace("Returning message %d, %o", message.num, returnValue)
|
||||
if (returnValue)
|
||||
{
|
||||
tabMessages.delete(message.num);
|
||||
if (message.data instanceof Error)
|
||||
{
|
||||
returnValue.reject(message.data);
|
||||
}
|
||||
else
|
||||
{
|
||||
returnValue.resolve(message.data);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn("Couldn't find message number %d", message.num);
|
||||
}
|
||||
}
|
||||
else if (message.type == "update")
|
||||
{
|
||||
@@ -44,45 +260,121 @@ function setupTab(port)
|
||||
"messageNum": -1,
|
||||
"message": message.data
|
||||
}
|
||||
console.trace("Sending update ", returnValue)
|
||||
logger.trace("Sending update %o", returnValue)
|
||||
interfacePort.postMessage(returnValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn("Unknown message type %s", message.type);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.warn("Unknown message type %s", typeof message, message);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function queryTab(message)
|
||||
/**
|
||||
* Sends a message to specified tab and handles the response.
|
||||
* @param {id,string,tabs.Tab} tab -The tab to send to. This can be the ID of the tab, the hostname
|
||||
* or domain name of the tab, an array of url matchers, or the tab
|
||||
* object itself.
|
||||
* @return {Promise} A promise that passes the tab's response to the resolver.
|
||||
*/
|
||||
function queryTab(tab, message)
|
||||
{
|
||||
let num = numTabMessages++;
|
||||
let promise = new Promise((resolve, reject) => {
|
||||
tabPort.postMessage({
|
||||
num: num,
|
||||
data: message
|
||||
});
|
||||
(function awaitResponse() {
|
||||
if (tabMessages.has(num))
|
||||
|
||||
let tabId;
|
||||
if (typeof tab == "string")
|
||||
{
|
||||
if (tab.match(/^\w+\.\w+$/))
|
||||
{
|
||||
tab = "*://*." + tab + "/*";
|
||||
}
|
||||
else if (tab.match(/^(\w\.)+\w+\.\w+$/))
|
||||
{
|
||||
tab = "*://" + tab + "/*";
|
||||
}
|
||||
tab = [tab];
|
||||
}
|
||||
else if ("id" in tab)
|
||||
{
|
||||
tab = tab.id;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let query = port => {
|
||||
let messageReturn = {
|
||||
resolve: resolve,
|
||||
reject: reject
|
||||
};
|
||||
tabMessages.set(num, messageReturn);
|
||||
logger.log("Querying tab %o", message);
|
||||
port.postMessage({
|
||||
num: num,
|
||||
data: message
|
||||
})
|
||||
};
|
||||
|
||||
if (typeof tab == "number")
|
||||
{
|
||||
if (ports.has(tab))
|
||||
{
|
||||
response = tabMessages.get(num);
|
||||
tabMessages.delete(num);
|
||||
return resolve(response);
|
||||
query(ports.get(tab));
|
||||
}
|
||||
else
|
||||
{
|
||||
setTimeout(awaitResponse, 30);
|
||||
reject(new ReferenceError("Could not find tab with communications port under " + tab));
|
||||
}
|
||||
})();
|
||||
}
|
||||
else if (tab instanceof Array)
|
||||
{
|
||||
tabs = tabs.query({
|
||||
url: tab,
|
||||
status: "complete"
|
||||
}).then(tabs => {
|
||||
if (tabs && tabs.length > 0)
|
||||
{
|
||||
for (let i = 0; i < tabs.length; i++)
|
||||
{
|
||||
if (ports.has(tabs[i].id))
|
||||
{
|
||||
query(ports.get(tabs[i].id));
|
||||
return;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* We only reach here if we couldn't find a valid tab. While we could just fall
|
||||
* through to the other reject statement, this one can provide a clearer error
|
||||
* message.
|
||||
*/
|
||||
reject(new ReferenceError("Could not find tab with communications under " + tab));
|
||||
}
|
||||
else
|
||||
{
|
||||
reject(new ReferenceError("Could not find tab under " + tab));
|
||||
}
|
||||
}, reject);
|
||||
}
|
||||
});
|
||||
tabMessages.set(numTabMessages, promise);
|
||||
numTabMessages++;
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback for when the native application sends a message.
|
||||
*
|
||||
* @param {object} message - The message to process.
|
||||
* @param {object} returnValue -The value returned by the last message listener. Return either this
|
||||
* or a new value.
|
||||
* @return Either the value passed on returnValue or a new value.
|
||||
*/
|
||||
var listeners = [function (message, returnValue) {
|
||||
if (typeof message == "object" && "type" in message)
|
||||
{
|
||||
let type = message.type;
|
||||
lastPeriod = type.lastIndexOf(".");
|
||||
let lastPeriod = type.lastIndexOf(".");
|
||||
if (lastPeriod != -1)
|
||||
{
|
||||
type = type.substring(lastPeriod + 1);
|
||||
@@ -90,8 +382,28 @@ var listeners = [function (message, returnValue) {
|
||||
switch (type)
|
||||
{
|
||||
case "NumberPing":
|
||||
returnValue = message.number;
|
||||
if (message.url)
|
||||
{
|
||||
let tabId;
|
||||
return loadTab(message.url).then(tab => {
|
||||
tabId = tab.id;
|
||||
return queryTab(tab, message);
|
||||
}).then(data => {
|
||||
// browser.tabs.remove(tabId);
|
||||
return data;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.log("Pinging back %d from background", message.number);
|
||||
returnValue = message.number;
|
||||
}
|
||||
break;
|
||||
case "QuerySongData":
|
||||
return loadTab(message.url, false).then(tab => {
|
||||
logger.log("Tab " + tab.url + " loaded. Querying.");
|
||||
return queryTab(tab, message);
|
||||
});
|
||||
case "CommandLoadSong":
|
||||
returnValue = () => {
|
||||
if (message.song)
|
||||
@@ -101,7 +413,7 @@ var listeners = [function (message, returnValue) {
|
||||
}
|
||||
if (tabPort)
|
||||
{
|
||||
console.log("Replacing tab");
|
||||
logger.log("Replacing tab");
|
||||
/*
|
||||
* Closes the existing tab first
|
||||
*/
|
||||
@@ -109,7 +421,7 @@ var listeners = [function (message, returnValue) {
|
||||
}
|
||||
else
|
||||
{
|
||||
console.log("Opening tab");
|
||||
logger.log("Opening tab");
|
||||
returnValue = returnValue();
|
||||
}
|
||||
break;
|
||||
@@ -129,7 +441,7 @@ var listeners = [function (message, returnValue) {
|
||||
returnValue = quit();
|
||||
break;
|
||||
case "CommandError":
|
||||
console.error("Error message", message);
|
||||
logger.error("Error message %o", message);
|
||||
if (message.forward)
|
||||
{
|
||||
returnValue = queryTab(message);
|
||||
@@ -153,9 +465,14 @@ var listeners = [function (message, returnValue) {
|
||||
return returnValue;
|
||||
}];
|
||||
|
||||
/**
|
||||
* Closes all tabs in the browser, thus closing the entire browser.
|
||||
*
|
||||
* @return {Promise} The promise that closes the tabs.
|
||||
*/
|
||||
function quit()
|
||||
{
|
||||
console.log("Quitting browser");
|
||||
logger.log("Quitting browser");
|
||||
return browser.tabs.query({}).then(tabs => {
|
||||
for (let tab of tabs)
|
||||
{
|
||||
@@ -168,6 +485,15 @@ function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is triggered when the interface sends a message to the browser. This method will
|
||||
* trigger all of the relevant message listeners and return a promise for whichever value they
|
||||
* decide to output.
|
||||
*
|
||||
* @param {object} message -The message to analyze.
|
||||
* @return {Promise} A promise for the method completion, resolving to a value to return to
|
||||
* the interface.
|
||||
*/
|
||||
function handleMessage(message)
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -178,7 +504,7 @@ function handleMessage(message)
|
||||
{
|
||||
returnValue = listeners[index](message, returnValue);
|
||||
}
|
||||
console.debug("Message returned ", returnValue);
|
||||
logger.debug("Message returned: %o", returnValue);
|
||||
if (returnValue instanceof Promise)
|
||||
{
|
||||
returnValue.then(resolve).catch(reject);
|
||||
@@ -199,7 +525,7 @@ function handleMessage(message)
|
||||
* Listen for messages from the app.
|
||||
*/
|
||||
interfacePort.onMessage.addListener((message) => {
|
||||
console.log("Received from interface: ", message);
|
||||
logger.log("Received from interface: %o", message);
|
||||
|
||||
handleMessage(message.message).then((response) => {
|
||||
returnValue = {
|
||||
@@ -214,10 +540,10 @@ interfacePort.onMessage.addListener((message) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("Sending ", returnValue)
|
||||
logger.log("Sending %o", returnValue)
|
||||
interfacePort.postMessage(returnValue);
|
||||
}).catch(error => {
|
||||
// console.error("Error in evaluating message: ", error);
|
||||
// logger.error("Error in evaluating message: %o", error);
|
||||
interfacePort.postMessage({
|
||||
"messageNum": message.messageNum,
|
||||
"message": {
|
||||
@@ -226,7 +552,12 @@ interfacePort.onMessage.addListener((message) => {
|
||||
"confirmation": {
|
||||
type: "edu.regis.universeplayer.browserCommands.CommandConfirmation",
|
||||
message: "Error in executing request",
|
||||
errorCode: JSON.parse(JSON.stringify(error, Object.getOwnPropertyNames(error)))
|
||||
errorCode: {
|
||||
type: "edu.regis.universeplayer.browserCommands.BrowserError",
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -234,3 +565,5 @@ interfacePort.onMessage.addListener((message) => {
|
||||
});
|
||||
|
||||
browser.runtime.onConnect.addListener(setupTab);
|
||||
|
||||
logger.log("Background script setup complete!")
|
||||
|
||||
46
add-on/src/addon/javascript/defs/songs.js
Normal file
46
add-on/src/addon/javascript/defs/songs.js
Normal file
@@ -0,0 +1,46 @@
|
||||
class Album
|
||||
{
|
||||
name;
|
||||
artists;
|
||||
year;
|
||||
genres;
|
||||
totalTracks;
|
||||
totalDiscs;
|
||||
}
|
||||
|
||||
class Song
|
||||
{
|
||||
/**
|
||||
* The name of the song.
|
||||
*/
|
||||
title;
|
||||
/**
|
||||
* Artists who contributed to the song.
|
||||
*/
|
||||
artists;
|
||||
/**
|
||||
* Which track number in the album the song belongs to.
|
||||
*/
|
||||
trackNum;
|
||||
/**
|
||||
* Which disc
|
||||
*/
|
||||
disc;
|
||||
/**
|
||||
* How long the song is in milliseconds.
|
||||
*/
|
||||
duration;
|
||||
|
||||
/**
|
||||
* A reference to the album this song is part of.
|
||||
*/
|
||||
album;
|
||||
}
|
||||
|
||||
class InternetSong extends Song
|
||||
{
|
||||
/**
|
||||
* The location of the song.
|
||||
*/
|
||||
location;
|
||||
}
|
||||
@@ -1,6 +1,31 @@
|
||||
console.debug("Loading foreground.js")
|
||||
let logger = new Logger("foreground");
|
||||
logger.debug("Loading foreground.js");
|
||||
let background;
|
||||
|
||||
/**
|
||||
* Contains functions to call before establishing a connection.
|
||||
*/
|
||||
let preload = [];
|
||||
|
||||
logger.pushUpdate = function (message)
|
||||
{
|
||||
if (background)
|
||||
{
|
||||
sendUpdate(message);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles messages sent from the background script.
|
||||
*
|
||||
* @param {object} message -The object containing the request.
|
||||
* @return {Promise} A promise containing the result of the request.
|
||||
*/
|
||||
function handleMessage(message)
|
||||
{
|
||||
if (typeof message == "object" && "type" in message)
|
||||
@@ -13,6 +38,12 @@ function handleMessage(message)
|
||||
}
|
||||
switch (type)
|
||||
{
|
||||
case "NumberPing":
|
||||
logger.log("Pinging back %d from foreground", message.number);
|
||||
return Promise.resolve(message.number);
|
||||
case "QuerySongData":
|
||||
logger.log("Gathering song data");
|
||||
return getSongData();
|
||||
case "QueryStatus":
|
||||
return getState();
|
||||
case "QueryTime":
|
||||
@@ -37,6 +68,15 @@ function handleMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calling this function will forward playback data to the browser background (and by extension, the
|
||||
* interface) as specified by the parameters.
|
||||
*
|
||||
* @param {string} status The playback status. This can be "PLAYING", "PAUSED", "STOPPED",
|
||||
* "FINISHED", or "EMPTY"
|
||||
* @param {number} time The current playback time in seconds.
|
||||
* @param {object} songData Data on the song currently playing.
|
||||
*/
|
||||
function onStatusUpdate(status, time, songData)
|
||||
{
|
||||
sendUpdate({
|
||||
@@ -47,9 +87,15 @@ function onStatusUpdate(status, time, songData)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards an update to the background (and by extension, the interface).
|
||||
*
|
||||
* @param {object|Promise} response The data to forward. If this data is a Promise, then the data
|
||||
* will be forwarded upon completion.
|
||||
*/
|
||||
function sendUpdate(response)
|
||||
{
|
||||
post = data => background.postMessage({
|
||||
let post = data => background.postMessage({
|
||||
type: "update",
|
||||
data: data
|
||||
});
|
||||
@@ -63,33 +109,128 @@ function sendUpdate(response)
|
||||
}
|
||||
}
|
||||
|
||||
background = browser.runtime.connect({name:"universalMusic"});
|
||||
background.onMessage.addListener(message => {
|
||||
let num = message.num;
|
||||
response = handleMessage(message.data);
|
||||
post = data => background.postMessage({
|
||||
type: "response",
|
||||
num: num,
|
||||
data: data
|
||||
});
|
||||
if (response instanceof Promise)
|
||||
$(function () {
|
||||
while (preload.length > 0)
|
||||
{
|
||||
response.then(post);
|
||||
preload.pop()();
|
||||
}
|
||||
else
|
||||
{
|
||||
post(response);
|
||||
}
|
||||
});
|
||||
background = browser.runtime.connect({name:"universalMusic"});
|
||||
|
||||
if (document.readyState === "complete")
|
||||
{
|
||||
sendUpdate("loaded");
|
||||
}
|
||||
else
|
||||
{
|
||||
sendUpdate("loaded");
|
||||
window.addEventListener("load", () => {
|
||||
sendUpdate("loaded");
|
||||
// sendUpdate("loaded");
|
||||
|
||||
/**
|
||||
* Listens for messages from the browser background and sends responses back.
|
||||
*/
|
||||
background.onMessage.addListener(message => {
|
||||
let num = message.num;
|
||||
post = data => {
|
||||
logger.log("Sending to interface %o", data);
|
||||
background.postMessage({
|
||||
type: "response",
|
||||
num: num,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
try
|
||||
{
|
||||
let response = handleMessage(message.data);
|
||||
if (response instanceof Promise)
|
||||
{
|
||||
logger.log("Posting promised information.");
|
||||
response.then(post);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.log("Posting non-promise information.");
|
||||
post(response);
|
||||
}
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
logger.error(e);
|
||||
post(e);
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
/**
|
||||
* Obtains information on the song currently playing.
|
||||
*
|
||||
* @return {Promise<InternetSong>} A serialized form of the song data for this tab.
|
||||
*/
|
||||
function getSongData()
|
||||
{
|
||||
return Promise.reject(new ReferenceError("getSongData unimplemented for " + location.href));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the current tab's playback state.
|
||||
*
|
||||
* @return {Promise<string>} The playback status. This can be "PLAYING", "PAUSED", "STOPPED",
|
||||
* "FINISHED", or "EMPTY"
|
||||
*/
|
||||
function getState()
|
||||
{
|
||||
return Promise.reject(new ReferenceError("getState unimplemented for " + location.href));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the current playback time.
|
||||
*
|
||||
* @return {Promise<number>} The current song playback time in milliseconds.
|
||||
*/
|
||||
function getTime()
|
||||
{
|
||||
return Promise.reject(new ReferenceError("getTime unimplemented for " + location.href));
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the total length of the song.
|
||||
*
|
||||
* @return {Promise<number>} The total song length, in milliseconds.
|
||||
*/
|
||||
function getLength()
|
||||
{
|
||||
return Promise.reject(new ReferenceError("getLength unimplemented for " + location.href));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables playback of the current song.
|
||||
*
|
||||
* @return {Promise} The success of the command.
|
||||
*/
|
||||
function play()
|
||||
{
|
||||
return Promise.reject(new ReferenceError("play unimplemented for " + location.href));
|
||||
}
|
||||
|
||||
/**
|
||||
* Pauses playback of the current song.
|
||||
*
|
||||
* @return {Promise} The success of the command.
|
||||
*/
|
||||
function pause()
|
||||
{
|
||||
return Promise.reject(new ReferenceError("play unimplemented for " + location.href));
|
||||
}
|
||||
|
||||
/**
|
||||
* Halts playback of the current song.
|
||||
*
|
||||
* @return {Promise} The success of the command.
|
||||
*/
|
||||
function stop()
|
||||
{
|
||||
return Promise.reject(new ReferenceError("stop unimplemented for " + location.href));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the playback to a certain time.
|
||||
*
|
||||
* @param {number} time The time to skip to, in milliseconds.
|
||||
* @return {Promise} The success of the command.
|
||||
*/
|
||||
function seek(time)
|
||||
{
|
||||
return Promise.reject(new ReferenceError("seek unimplemented for " + location.href));
|
||||
}
|
||||
2
add-on/src/addon/javascript/jquery.js
vendored
Normal file
2
add-on/src/addon/javascript/jquery.js
vendored
Normal file
File diff suppressed because one or more lines are too long
123
add-on/src/addon/javascript/logger.js
Normal file
123
add-on/src/addon/javascript/logger.js
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Message data is used internally as a wrapper for a log message that is sent to the interface.
|
||||
*/
|
||||
class MessageData {
|
||||
/**
|
||||
* This is used by the interface parser to determine what type of data this is.
|
||||
*/
|
||||
type = "edu.regis.universeplayer.Log";
|
||||
|
||||
/**
|
||||
* This contains the name of the logger that created the message.
|
||||
*/
|
||||
logger;
|
||||
|
||||
/**
|
||||
* The log level this message was sent at.
|
||||
* @type string
|
||||
*/
|
||||
level;
|
||||
|
||||
/**
|
||||
* The actual message sent, as an array of arguments.
|
||||
* @type array
|
||||
*/
|
||||
message;
|
||||
|
||||
/**
|
||||
* Creates a message package.
|
||||
*
|
||||
* @param {string} level The log level.
|
||||
* @param {array-like} message The message to send, as an array-like object.
|
||||
*/
|
||||
constructor(logger, level, message)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.level = level;
|
||||
this.message = Array.from(message);
|
||||
}
|
||||
}
|
||||
|
||||
class Logger {
|
||||
name;
|
||||
/**
|
||||
* Messages that are scheduled to be sent.
|
||||
*/
|
||||
queuedMessages = [];
|
||||
|
||||
constructor(name)
|
||||
{
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* This will format the message in a way SLF4j can understand the log message before scheduling
|
||||
* it for forwarding.
|
||||
*/
|
||||
passLog(level, message)
|
||||
{
|
||||
let messageData;
|
||||
if (message.length > 1 && typeof message[0] == "string")
|
||||
{
|
||||
/*
|
||||
* Format the initial string in a way that SLF4J can understand it.
|
||||
*/
|
||||
message[0] = message[0].replaceAll(/%[a-z]/gi, "{}");
|
||||
}
|
||||
this.queuedMessages.push(new MessageData(this.name, level, message));
|
||||
message = this.queuedMessages.pop();
|
||||
while (message && this.pushUpdate(message))
|
||||
{
|
||||
message = this.queuedMessages.pop();
|
||||
}
|
||||
/*
|
||||
* If we popped a message but got here, then a message was not sent. Put it back.
|
||||
*/
|
||||
if (message)
|
||||
{
|
||||
this.queuedMessages.unshift(message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to actually push data to the interface. Abstract.
|
||||
*
|
||||
* @param [MessageData] message The message to push.
|
||||
* @return [boolean] Whether or not the push was successful.
|
||||
*/
|
||||
pushUpdate(message)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
trace()
|
||||
{
|
||||
console.trace.apply(console, arguments);
|
||||
this.passLog("trace", arguments);
|
||||
}
|
||||
debug()
|
||||
{
|
||||
console.debug.apply(console, arguments);
|
||||
this.passLog("debug", arguments);
|
||||
}
|
||||
info()
|
||||
{
|
||||
console.info.apply(console, arguments);
|
||||
this.passLog("info", arguments);
|
||||
}
|
||||
log()
|
||||
{
|
||||
console.log.apply(console, arguments);
|
||||
this.passLog("info", arguments);
|
||||
}
|
||||
warn()
|
||||
{
|
||||
console.warn.apply(console, arguments);
|
||||
this.passLog("warn", arguments);
|
||||
}
|
||||
error()
|
||||
{
|
||||
console.error.apply(console, arguments);
|
||||
this.passLog("error", arguments);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,26 @@
|
||||
console.debug("Loading youtube.js")
|
||||
let ylogger = new Logger("youtube");
|
||||
|
||||
logger.pushUpdate = function (message)
|
||||
{
|
||||
if (background)
|
||||
{
|
||||
sendUpdate(message);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
ylogger.debug("Loading youtube.js")
|
||||
var video;
|
||||
|
||||
function onload()
|
||||
{
|
||||
video = document.getElementsByClassName('video-stream html5-main-video')[0];
|
||||
ylogger.info("YouTube loaded");
|
||||
$(".more-button").click();
|
||||
video = $('.video-stream.html5-main-video')[0];
|
||||
statusUpdate = e => {
|
||||
return onStatusUpdate(getState(), e.srcElement.currentTime, getSongData());
|
||||
};
|
||||
@@ -19,16 +36,177 @@ function onload()
|
||||
|
||||
function getSongData()
|
||||
{
|
||||
return {
|
||||
type: "edu.regis.universeplayer.browser.InternetSong",
|
||||
location: "window.location.href",
|
||||
title: getTitle(),
|
||||
artists: getArtists(),
|
||||
trackNum: 0,
|
||||
discNum: 0,
|
||||
duration: parseInt(getLength() * 1000),
|
||||
album: null
|
||||
let song = getAutogeneratedMetadata();
|
||||
if (!song)
|
||||
{
|
||||
song = getDetectedMetadata();
|
||||
if (!song)
|
||||
{
|
||||
song = getBasicMetadata();
|
||||
}
|
||||
}
|
||||
return Promise.resolve(song);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains basic song metadata for if nothing else is detected.
|
||||
*/
|
||||
function getBasicMetadata()
|
||||
{
|
||||
let data = new InternetSong();
|
||||
data.location = window.location.href;
|
||||
data.title = getTitle();
|
||||
data.artists = getArtists();
|
||||
data.duration = getLength();
|
||||
|
||||
data.album = new Album();
|
||||
data.album.art = getArt();
|
||||
|
||||
return Promise.resolve(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains song metadata detected by YouTube.
|
||||
*
|
||||
* @param {string} type The type of metadata to get. Can be "Song", "Artist", "Album",
|
||||
* "Writers", or "Licensed to YouTube by". May be null.
|
||||
* @return {jQuery|InternetSong} If a type is provided, this returns a jQuery array of string
|
||||
* values matching that type. Otherwise, it returns an InternetSong
|
||||
* created from detected metadata.
|
||||
*/
|
||||
function getDetectedMetadata(type)
|
||||
{
|
||||
if (type)
|
||||
{
|
||||
return $("ytd-metadata-row-container-renderer.ytd-video-secondary-info-renderer ytd-metadata-row-renderer:contains('" + type + "') div>yt-formatted-string").map(function () {return $(this).text()});
|
||||
}
|
||||
else
|
||||
{
|
||||
let song = new InternetSong();
|
||||
|
||||
song.title = Array.prototype.join.apply(getDetectedMetadata("Song"), [" / "]);
|
||||
if (!song.title)
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
song.artists = ";".join(getDetectedMetadata("Artist"));
|
||||
song.album.name = getDetectedMetadata("Album")[0];
|
||||
song.album.artists = ";".join(getDetectedMetadata("Artist"));
|
||||
song.location = location.href;
|
||||
song.duration = getLength();
|
||||
song.album.art = getArt();
|
||||
|
||||
return song;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains song metadata detected by YouTube as provided in the description.
|
||||
*
|
||||
* @return {InternetSong} The internet song data detected. This will be undefined if no
|
||||
* autogenerated data was found.
|
||||
*/
|
||||
function getAutogeneratedMetadata()
|
||||
{
|
||||
let desc = $("yt-formatted-string.ytd-video-secondary-info-renderer").text().replaceAll(/\n\w*\n/g, "\n").split("\n");
|
||||
if (!desc || !desc.length || !desc[desc.length - 1].startsWith("Auto-generated"))
|
||||
{
|
||||
return undefined;
|
||||
}
|
||||
let data = new InternetSong();
|
||||
desc.forEach(str => {
|
||||
let provHead = "Provided to YouTube by ";
|
||||
let names;
|
||||
if (str.startsWith(provHead))
|
||||
{
|
||||
data.provider = str.substring(provHead.length);
|
||||
}
|
||||
else if (str.indexOf(" · ") > -1)
|
||||
{
|
||||
names = str.split(" · ");
|
||||
data.title = names[0];
|
||||
data.artists = names.slice(1);
|
||||
if (!data.album.artists)
|
||||
{
|
||||
data.album.artists = data.artists;
|
||||
}
|
||||
}
|
||||
else if (str.startsWith('℗'))
|
||||
{
|
||||
data.album.artists = [str.substr(str.match(/^℗( [\d]{4})? /)[0].length)]
|
||||
names = str.match(/\d{4}/);
|
||||
if (!data.album.year && names)
|
||||
{
|
||||
data.album.year = names[0];
|
||||
}
|
||||
}
|
||||
else if (str.startsWith('Released on: '))
|
||||
{
|
||||
names = str.match(/[\d]{4}/);
|
||||
if (names)
|
||||
{
|
||||
data.album.year = names[0];
|
||||
}
|
||||
}
|
||||
else if (data.title && !data.album.name && str)
|
||||
{
|
||||
ylogger.debug("Album name: %s", str);
|
||||
data.album.name = str;
|
||||
}
|
||||
else
|
||||
{
|
||||
ylogger.debug("Ignoring line %s", str);
|
||||
}
|
||||
});
|
||||
data.location = location.href;
|
||||
data.duration = getLength();
|
||||
data.album.art = getArt();
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the song title.
|
||||
*
|
||||
* @return {string} The song title, or null if one could not be found.
|
||||
*/
|
||||
function getTitle()
|
||||
{
|
||||
return $(".title.ytd-video-primary-info-renderer").text()
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the artists who made this song.
|
||||
*
|
||||
* @return {string[]} The song artists, or an empty array if none could be found.
|
||||
*/
|
||||
function getArtists()
|
||||
{
|
||||
return [$("#channel-name.ytd-video-owner-renderer a:first").text()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the YouTube ID for this song
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
function getSongId()
|
||||
{
|
||||
let match = location.search.match(/v=.+(\&|$)/);
|
||||
if (match)
|
||||
{
|
||||
return match[0].substring(2)
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the album art
|
||||
*
|
||||
* @return {blob|string} Either a blob for the album art or a URL for where it can be downloaded.
|
||||
*/
|
||||
function getArt()
|
||||
{
|
||||
return "https://i.ytimg.com/vi/" + getSongId() + "/maxresdefault.jpg"
|
||||
}
|
||||
|
||||
function getState()
|
||||
@@ -53,22 +231,12 @@ function getState()
|
||||
|
||||
function getTime()
|
||||
{
|
||||
return video.currentTime;
|
||||
return parseInt(video.currentTime * 1000);
|
||||
}
|
||||
|
||||
function getLength()
|
||||
{
|
||||
return video.duration;
|
||||
}
|
||||
|
||||
function getTitle()
|
||||
{
|
||||
return document.getElementsByTagName("meta").title.content;
|
||||
}
|
||||
|
||||
function getArtists()
|
||||
{
|
||||
return [document.getElementById("channel-name").getElementsByTagName("a")[0].text];
|
||||
return parseInt(video.duration * 1000);
|
||||
}
|
||||
|
||||
function play()
|
||||
@@ -101,11 +269,4 @@ function seek(time)
|
||||
return false;
|
||||
}
|
||||
|
||||
if (document.readyState === "complete")
|
||||
{
|
||||
onload();
|
||||
}
|
||||
else
|
||||
{
|
||||
window.addEventListener("load", onload);
|
||||
}
|
||||
preload.push(onload);
|
||||
@@ -19,13 +19,13 @@
|
||||
},
|
||||
|
||||
"background": {
|
||||
"scripts": ["background.js"]
|
||||
"scripts": ["logger.js", "defs/songs.js", "background.js"]
|
||||
},
|
||||
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["*://*.youtube.com/watch?v=*"],
|
||||
"js": ["foreground.js", "youtube.js"]
|
||||
"js": ["jquery.js", "logger.js", "defs/songs.js", "foreground.js", "youtube.js"]
|
||||
}
|
||||
],
|
||||
|
||||
@@ -33,5 +33,5 @@
|
||||
"default_icon": "icon.svg"
|
||||
},
|
||||
|
||||
"permissions": ["nativeMessaging"]
|
||||
"permissions": ["nativeMessaging", "tabs"]
|
||||
}
|
||||
Reference in New Issue
Block a user