Allowed all logging to be viewed by the interface.

It is just much easier this way.
This commit is contained in:
Markil3
2021-10-09 15:52:34 -06:00
parent 72ccfbc444
commit a92d686ddb
17 changed files with 1087 additions and 138 deletions

View File

@@ -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(); 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; var numTabMessages = 0;
/* /*
On startup, connect to the "ping_pong" app. On startup, connect to the "ping_pong" app.
*/ */
var interfacePort = browser.runtime.connectNative("universalmusic"); 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) function setupTab(port)
{ {
if (port.sender) if (port.sender)
{ {
console.info("Tab " + port.sender.tab.url + " loaded."); logger.info("Tab " + port.sender.tab.url + " loaded.");
} }
else else
{ {
console.info("Tab loaded"); logger.info("Tab loaded");
} }
tabPort = port; ports.set(port.sender.tab.id, port);
tabPort.onDisconnect.addListener(e => { if (waitingPorts.has(port.sender.tab.id))
tabPort = null; {
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) if (e.error)
{ {
console.error("Tab error: ", e.error) logger.error("Tab error: %o", e.error)
} }
else 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 (typeof message == "object")
{ {
if (message.type == "response") 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") else if (message.type == "update")
{ {
@@ -44,45 +260,121 @@ function setupTab(port)
"messageNum": -1, "messageNum": -1,
"message": message.data "message": message.data
} }
console.trace("Sending update ", returnValue) logger.trace("Sending update %o", returnValue)
interfacePort.postMessage(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; 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 num = numTabMessages++;
let promise = new Promise((resolve, reject) => {
tabPort.postMessage({ let tabId;
num: num, if (typeof tab == "string")
data: message {
}); if (tab.match(/^\w+\.\w+$/))
(function awaitResponse() { {
if (tabMessages.has(num)) 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); query(ports.get(tab));
tabMessages.delete(num);
return resolve(response);
} }
else 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) { var listeners = [function (message, returnValue) {
if (typeof message == "object" && "type" in message) if (typeof message == "object" && "type" in message)
{ {
let type = message.type; let type = message.type;
lastPeriod = type.lastIndexOf("."); let lastPeriod = type.lastIndexOf(".");
if (lastPeriod != -1) if (lastPeriod != -1)
{ {
type = type.substring(lastPeriod + 1); type = type.substring(lastPeriod + 1);
@@ -90,8 +382,28 @@ var listeners = [function (message, returnValue) {
switch (type) switch (type)
{ {
case "NumberPing": 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; break;
case "QuerySongData":
return loadTab(message.url, false).then(tab => {
logger.log("Tab " + tab.url + " loaded. Querying.");
return queryTab(tab, message);
});
case "CommandLoadSong": case "CommandLoadSong":
returnValue = () => { returnValue = () => {
if (message.song) if (message.song)
@@ -101,7 +413,7 @@ var listeners = [function (message, returnValue) {
} }
if (tabPort) if (tabPort)
{ {
console.log("Replacing tab"); logger.log("Replacing tab");
/* /*
* Closes the existing tab first * Closes the existing tab first
*/ */
@@ -109,7 +421,7 @@ var listeners = [function (message, returnValue) {
} }
else else
{ {
console.log("Opening tab"); logger.log("Opening tab");
returnValue = returnValue(); returnValue = returnValue();
} }
break; break;
@@ -129,7 +441,7 @@ var listeners = [function (message, returnValue) {
returnValue = quit(); returnValue = quit();
break; break;
case "CommandError": case "CommandError":
console.error("Error message", message); logger.error("Error message %o", message);
if (message.forward) if (message.forward)
{ {
returnValue = queryTab(message); returnValue = queryTab(message);
@@ -153,9 +465,14 @@ var listeners = [function (message, returnValue) {
return returnValue; return returnValue;
}]; }];
/**
* Closes all tabs in the browser, thus closing the entire browser.
*
* @return {Promise} The promise that closes the tabs.
*/
function quit() function quit()
{ {
console.log("Quitting browser"); logger.log("Quitting browser");
return browser.tabs.query({}).then(tabs => { return browser.tabs.query({}).then(tabs => {
for (let tab of tabs) for (let tab of tabs)
{ {
@@ -168,6 +485,15 @@ function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, 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) function handleMessage(message)
{ {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@@ -178,7 +504,7 @@ function handleMessage(message)
{ {
returnValue = listeners[index](message, returnValue); returnValue = listeners[index](message, returnValue);
} }
console.debug("Message returned ", returnValue); logger.debug("Message returned: %o", returnValue);
if (returnValue instanceof Promise) if (returnValue instanceof Promise)
{ {
returnValue.then(resolve).catch(reject); returnValue.then(resolve).catch(reject);
@@ -199,7 +525,7 @@ function handleMessage(message)
* Listen for messages from the app. * Listen for messages from the app.
*/ */
interfacePort.onMessage.addListener((message) => { interfacePort.onMessage.addListener((message) => {
console.log("Received from interface: ", message); logger.log("Received from interface: %o", message);
handleMessage(message.message).then((response) => { handleMessage(message.message).then((response) => {
returnValue = { returnValue = {
@@ -214,10 +540,10 @@ interfacePort.onMessage.addListener((message) => {
} }
} }
} }
console.log("Sending ", returnValue) logger.log("Sending %o", returnValue)
interfacePort.postMessage(returnValue); interfacePort.postMessage(returnValue);
}).catch(error => { }).catch(error => {
// console.error("Error in evaluating message: ", error); // logger.error("Error in evaluating message: %o", error);
interfacePort.postMessage({ interfacePort.postMessage({
"messageNum": message.messageNum, "messageNum": message.messageNum,
"message": { "message": {
@@ -226,7 +552,12 @@ interfacePort.onMessage.addListener((message) => {
"confirmation": { "confirmation": {
type: "edu.regis.universeplayer.browserCommands.CommandConfirmation", type: "edu.regis.universeplayer.browserCommands.CommandConfirmation",
message: "Error in executing request", 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); browser.runtime.onConnect.addListener(setupTab);
logger.log("Background script setup complete!")

View 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;
}

View File

@@ -1,6 +1,31 @@
console.debug("Loading foreground.js") let logger = new Logger("foreground");
logger.debug("Loading foreground.js");
let background; 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) function handleMessage(message)
{ {
if (typeof message == "object" && "type" in message) if (typeof message == "object" && "type" in message)
@@ -13,6 +38,12 @@ function handleMessage(message)
} }
switch (type) 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": case "QueryStatus":
return getState(); return getState();
case "QueryTime": 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) function onStatusUpdate(status, time, songData)
{ {
sendUpdate({ 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) function sendUpdate(response)
{ {
post = data => background.postMessage({ let post = data => background.postMessage({
type: "update", type: "update",
data: data data: data
}); });
@@ -63,33 +109,128 @@ function sendUpdate(response)
} }
} }
background = browser.runtime.connect({name:"universalMusic"}); $(function () {
background.onMessage.addListener(message => { while (preload.length > 0)
let num = message.num;
response = handleMessage(message.data);
post = data => background.postMessage({
type: "response",
num: num,
data: data
});
if (response instanceof Promise)
{ {
response.then(post); preload.pop()();
} }
else background = browser.runtime.connect({name:"universalMusic"});
{
post(response);
}
});
if (document.readyState === "complete") // sendUpdate("loaded");
{
sendUpdate("loaded"); /**
} * Listens for messages from the browser background and sends responses back.
else */
{ background.onMessage.addListener(message => {
sendUpdate("loaded"); let num = message.num;
window.addEventListener("load", () => { post = data => {
sendUpdate("loaded"); 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

File diff suppressed because one or more lines are too long

View 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);
}
}

View File

@@ -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; var video;
function onload() 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 => { statusUpdate = e => {
return onStatusUpdate(getState(), e.srcElement.currentTime, getSongData()); return onStatusUpdate(getState(), e.srcElement.currentTime, getSongData());
}; };
@@ -19,16 +36,177 @@ function onload()
function getSongData() function getSongData()
{ {
return { let song = getAutogeneratedMetadata();
type: "edu.regis.universeplayer.browser.InternetSong", if (!song)
location: "window.location.href", {
title: getTitle(), song = getDetectedMetadata();
artists: getArtists(), if (!song)
trackNum: 0, {
discNum: 0, song = getBasicMetadata();
duration: parseInt(getLength() * 1000), }
album: null
} }
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() function getState()
@@ -53,22 +231,12 @@ function getState()
function getTime() function getTime()
{ {
return video.currentTime; return parseInt(video.currentTime * 1000);
} }
function getLength() function getLength()
{ {
return video.duration; return parseInt(video.duration * 1000);
}
function getTitle()
{
return document.getElementsByTagName("meta").title.content;
}
function getArtists()
{
return [document.getElementById("channel-name").getElementsByTagName("a")[0].text];
} }
function play() function play()
@@ -101,11 +269,4 @@ function seek(time)
return false; return false;
} }
if (document.readyState === "complete") preload.push(onload);
{
onload();
}
else
{
window.addEventListener("load", onload);
}

View File

@@ -19,13 +19,13 @@
}, },
"background": { "background": {
"scripts": ["background.js"] "scripts": ["logger.js", "defs/songs.js", "background.js"]
}, },
"content_scripts": [ "content_scripts": [
{ {
"matches": ["*://*.youtube.com/watch?v=*"], "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" "default_icon": "icon.svg"
}, },
"permissions": ["nativeMessaging"] "permissions": ["nativeMessaging", "tabs"]
} }

View File

@@ -63,3 +63,9 @@ artifacts {
} }
install(distTar) install(distTar)
} }
processResources {
filesMatching("**/log4j2.xml") {
expand(rootProject.properties)
}
}

View File

@@ -1,4 +1,4 @@
package edu.regis.universeplayer.addon; package edu.regis.universeplayer;
import org.apache.logging.log4j.core.*; import org.apache.logging.log4j.core.*;
import org.apache.logging.log4j.core.appender.AbstractAppender; import org.apache.logging.log4j.core.appender.AbstractAppender;
@@ -9,7 +9,6 @@ import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
import java.io.Serializable; import java.io.Serializable;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* This appender will store all messages sent to it, for the interface link to * This appender will store all messages sent to it, for the interface link to
@@ -18,25 +17,25 @@ import java.util.stream.Collectors;
* @author William Hubbard * @author William Hubbard
*/ */
@Plugin( @Plugin(
name = "Socket", name = "Queue",
category = Core.CATEGORY_NAME, category = Core.CATEGORY_NAME,
elementType = Appender.ELEMENT_TYPE) elementType = Appender.ELEMENT_TYPE)
public class SocketAppender extends AbstractAppender public class QueueAppender extends AbstractAppender
{ {
private static final LinkedList<LogEvent> events = new LinkedList<>(); private static final LinkedList<LogEvent> events = new LinkedList<>();
/** /**
* Builds FileAppender instances. * Builds QueueAppender instances.
* *
* @param <B> The type to build * @param <B> The type to build
*/ */
public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B> public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
implements org.apache.logging.log4j.core.util.Builder<SocketAppender> implements org.apache.logging.log4j.core.util.Builder<QueueAppender>
{ {
@Override @Override
public SocketAppender build() public QueueAppender build()
{ {
return new SocketAppender(getName(), getFilter(), getOrCreateLayout(), isIgnoreExceptions(), getPropertyArray()); return new QueueAppender(getName(), getFilter(), getOrCreateLayout(), isIgnoreExceptions(), getPropertyArray());
} }
} }
@@ -46,8 +45,8 @@ public class SocketAppender extends AbstractAppender
return new Builder<B>().asBuilder(); return new Builder<B>().asBuilder();
} }
public SocketAppender(final String name, final Filter filter, final Layout<? extends Serializable> layout, public QueueAppender(final String name, final Filter filter, final Layout<? extends Serializable> layout,
final boolean ignoreExceptions, final Property[] properties) final boolean ignoreExceptions, final Property[] properties)
{ {
super(name, filter, layout, ignoreExceptions, properties); super(name, filter, layout, ignoreExceptions, properties);
} }

View File

@@ -4,6 +4,7 @@
package edu.regis.universeplayer.addon; package edu.regis.universeplayer.addon;
import edu.regis.universeplayer.QueueAppender;
import edu.regis.universeplayer.browserCommands.BrowserConstants; import edu.regis.universeplayer.browserCommands.BrowserConstants;
import edu.regis.universeplayer.browserCommands.MessageHandler; import edu.regis.universeplayer.browserCommands.MessageHandler;
import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.LogEvent;
@@ -57,10 +58,10 @@ public class Main
/* /*
* Make sure that it is active. * Make sure that it is active.
*/ */
if (SocketAppender.hasLogs()) if (QueueAppender.hasLogs())
{ {
lastPing = System.currentTimeMillis(); lastPing = System.currentTimeMillis();
for (LogEvent event : SocketAppender.retrieveLogEvents()) for (LogEvent event : QueueAppender.retrieveLogEvents())
{ {
this.sendUpdate(event); this.sendUpdate(event);
} }

View File

@@ -2,16 +2,29 @@
<!-- <!--
~ Copyright (c) 2021 William Hubbard. All Rights Reserved. ~ Copyright (c) 2021 William Hubbard. All Rights Reserved.
--> -->
<Configuration status="WARN"> <Configuration packages="edu.regis.universeplayer" status="WARN">
<Appenders> <Appenders>
<Console name="Console" target="SYSTEM_ERR"> <Console name="Console" target="SYSTEM_ERR">
<PatternLayout pattern="%d%c%L %-5level - %msg%n"/> <PatternLayout pattern="%d%c%L %-5level - %msg%n"/>
</Console> </Console>
<File name="File" fileName="addonInter.log" append="false"> <RollingFile name="File" fileName="$LOG_DIR/addonInter.log"
filePattern="$LOG_DIR/addonInter.%i.log.gz"
ignoreExceptions="false">
<PatternLayout> <PatternLayout>
<Pattern>%d{HH:mm:ss.SSS} - %c.%M(%F:%L) - %-5level - %msg%n</Pattern> <Pattern>%d{HH:mm:ss.SSS} - %c.%M(%F:%L) - %-5level - %msg%n
</Pattern>
</PatternLayout> </PatternLayout>
</File> <Policies>
<OnStartupTriggeringPolicy/>
</Policies>
<DefaultRolloverStrategy max="5"/>
</RollingFile>
<Queue name="Queue"/>
<Async name="Async">
<AppenderRef ref="File"/>
<AppenderRef ref="Console"/>
<AppenderRef ref="Queue"/>
</Async>
</Appenders> </Appenders>
<Loggers> <Loggers>
<Root level="debug"> <Root level="debug">

View File

@@ -4,6 +4,9 @@
package edu.regis.universeplayer.browser; package edu.regis.universeplayer.browser;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.core.DefaultLoggerContextAccessor;
import org.apache.logging.log4j.core.LogEvent;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -11,20 +14,27 @@ import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.io.PrintStream; import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.ConnectException; import java.net.ConnectException;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.ServerSocket; import java.net.ServerSocket;
import java.net.Socket; import java.net.Socket;
import java.util.Arrays;
import java.util.Scanner; import java.util.Scanner;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import edu.regis.universeplayer.ConfigManager; import edu.regis.universeplayer.ConfigManager;
import edu.regis.universeplayer.Log;
import edu.regis.universeplayer.browserCommands.BrowserConstants; import edu.regis.universeplayer.browserCommands.BrowserConstants;
import edu.regis.universeplayer.browserCommands.MessageRunner; import edu.regis.universeplayer.browserCommands.MessageRunner;
import edu.regis.universeplayer.browserCommands.UpdateListener;
public class Browser extends MessageRunner public class Browser extends MessageRunner
{ {
private static final Logger logger = LoggerFactory.getLogger(Browser.class); private static final Logger logger = LoggerFactory.getLogger(Browser.class);
private static final Logger browserLogger =
LoggerFactory.getLogger("browser");
private static Browser INSTANCE; private static Browser INSTANCE;
private static final AtomicBoolean instanceWaiter = new AtomicBoolean(); private static final AtomicBoolean instanceWaiter = new AtomicBoolean();
@@ -116,6 +126,72 @@ public class Browser extends MessageRunner
this.socket = socket; this.socket = socket;
this.server = server; this.server = server;
this.process = process; this.process = process;
/*
* Sends browser logs to the main log.
*/
this.addUpdateListener((object, runner) ->
{
Log log;
LogEvent logEvent;
if (object instanceof Log)
{
log = (Log) object;
String methodName;
Class<?>[] types = null;
Object[] params = null;
if (log.message.length == 1)
{
types = new Class<?>[]{String.class};
params = new Object[]{log.message[0]};
}
else if (log.message.length == 2)
{
types = new Class<?>[]{String.class, Object.class};
params = new Object[]{log.message[0], log.message[1]};
}
else if (log.message.length == 3)
{
types = new Class<?>[]{String.class, Object.class,
Object[].class};
params = new Object[]{log.message[0], log.message[1],
log.message[1]};
}
else if (log.message.length > 4)
{
types = new Class<?>[]{String.class, Object[].class};
params = new Object[]{log.message[0],
Arrays.copyOfRange(log.message, 1,
log.message.length)};
}
methodName = log.level.toLowerCase();
if (types != null)
{
Method method = null;
try
{
method = Logger.class.getMethod(methodName, types);
method.invoke(LoggerFactory.getLogger(log.logger), params);
}
catch (NoSuchMethodException e)
{
/*
* This can happen if a zero-argument message was
* received. This is expected.
*/
}
catch (IllegalAccessException | InvocationTargetException e)
{
logger.error("Could not process logger message {}", log,
e);
}
}
}
else if (object instanceof LogEvent)
{
DefaultLoggerContextAccessor.INSTANCE.getLoggerContext().getRootLogger().get().log((LogEvent) object);
}
});
} }
@Override @Override

View File

@@ -0,0 +1,18 @@
package edu.regis.universeplayer;
import java.io.Serializable;
import java.util.Arrays;
public class Log implements Serializable
{
public String logger;
public String level;
public Object[] message;
@Override
public String toString()
{
return "LOG " + this.level.toUpperCase() + ": " +
Arrays.toString(message);
}
}

View File

@@ -1,21 +1,30 @@
package edu.regis.universeplayer; package edu.regis.universeplayer;
import java.io.Serializable; import java.io.Serializable;
import java.net.URL;
/** /**
* A test class for pinging the browser. * A test class for pinging the browser.
*/ */
public class NumberPing implements Serializable public class NumberPing implements Serializable
{ {
public int number; private final URL url;
public double number;
public NumberPing() public NumberPing()
{ {
this(0); this(0);
} }
public NumberPing(int number) public NumberPing(double number)
{ {
this.url = null;
this.number = number;
}
public NumberPing(URL url, double number)
{
this.url = url;
this.number = number; this.number = number;
} }
} }

View File

@@ -1,9 +1,20 @@
import net.harawata.appdirs.AppDirsFactory
import java.util.stream.Collectors import java.util.stream.Collectors
/* /*
* Copyright (c) 2021 William Hubbard. All Rights Reserved. * Copyright (c) 2021 William Hubbard. All Rights Reserved.
*/ */
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'net.harawata:appdirs:1.2.1'
}
}
/* /*
* This build file was generated by the Gradle 'init' task. * This build file was generated by the Gradle 'init' task.
* *
@@ -64,3 +75,5 @@ task buildscriptNix(type: Copy) {
file("${buildDir}/install.sh").append mergeApps.archiveFile.get().asFile.bytes file("${buildDir}/install.sh").append mergeApps.archiveFile.get().asFile.bytes
} }
} }
ext.LOG_DIR = AppDirsFactory.getInstance().getUserLogDir("universemusic", "0.1", "edu.regis");

View File

@@ -57,3 +57,9 @@ mainClassName = defaultPackage + '.PlayerEnvironment'
artifacts { artifacts {
install(distTar) install(distTar)
} }
processResources {
filesMatching("**/log4j2.xml") {
expand(rootProject.properties)
}
}

View File

@@ -131,8 +131,8 @@ public class PlayerEnvironment
} }
} }
private static void init(HashMap<String, Object> ops, public static void init(Map<String, Object> ops,
ArrayList<String> params) List<String> params)
{ {
/* /*
* Add this just in case of a crash or something. It won't work if the * Add this just in case of a crash or something. It won't work if the
@@ -154,25 +154,28 @@ public class PlayerEnvironment
queue.addSongChangeListener(queue1 -> ForkJoinPool.commonPool().submit(() -> { queue.addSongChangeListener(queue1 -> ForkJoinPool.commonPool().submit(() -> {
ForkJoinTask<Void> command = PlayerManager.getPlayers() ForkJoinTask<Void> command = PlayerManager.getPlayers()
.stopSong(); .stopSong();
command.join(); if (command != null)
if (command.isCompletedAbnormally())
{ {
logger.error("Could not run command", command.join();
command.getException()); if (command.isCompletedAbnormally())
if (Interface.getInstance() != null)
{ {
JOptionPane logger.error("Could not run command",
.showMessageDialog(Interface.getInstance(), command.getException());
command.getException(), if (Interface.getInstance() != null)
command.getException().getMessage(), {
JOptionPane.ERROR_MESSAGE); JOptionPane
.showMessageDialog(Interface.getInstance(),
command.getException(),
command.getException().getMessage(),
JOptionPane.ERROR_MESSAGE);
}
} }
} }
else if (queue1.getCurrentSong() != null) if (queue1.getCurrentSong() != null)
{ {
command = command =
PlayerManager.getPlayers() PlayerManager.getPlayers()
.playSong(queue1.getCurrentSong()); .playSong(queue1.getCurrentSong());
command.join(); command.join();
if (command.isCompletedAbnormally()) if (command.isCompletedAbnormally())
{ {
@@ -249,7 +252,6 @@ public class PlayerEnvironment
else else
{ {
equals = args[i].length(); equals = args[i].length();
valStr = null;
} }
key = args[i].substring(2, equals); key = args[i].substring(2, equals);
} }
@@ -478,7 +480,7 @@ public class PlayerEnvironment
.map(Map.Entry::getKey) .map(Map.Entry::getKey)
.collect(Collectors.toList()); .collect(Collectors.toList());
Collections.reverse(songs); Collections.reverse(songs);
out.println("Playing " + songs.toString()); out.println("Playing " + songs);
Queue.getInstance().addAll(songs); Queue.getInstance().addAll(songs);
} }
} }