« get me outta code hell

interactive-bgm - Browser extension that adds background music based on the site you're browsing
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--README.md2
-rw-r--r--extension/background.js83
-rw-r--r--extension/interactive-bgm.js121
-rw-r--r--extension/manifest.json1
-rw-r--r--extension/popup/main.js58
-rwxr-xr-xnative-app/index.js154
-rw-r--r--package-lock.json21
-rw-r--r--package.json3
-rwxr-xr-xtrack1.wavbin29635016 -> 0 bytes
-rwxr-xr-xtrack2.wavbin29635016 -> 0 bytes
-rwxr-xr-xtrack3.wavbin29635016 -> 0 bytes
12 files changed, 348 insertions, 96 deletions
diff --git a/.gitignore b/.gitignore
index cf5c15b..435af89 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 /node_modules
 /native-app/log
+/native-app/music
diff --git a/README.md b/README.md
index 8328941..da245ff 100644
--- a/README.md
+++ b/README.md
@@ -7,5 +7,3 @@ Adds background music to your browsing.
 Run `npm install` to automatically install dependency modules. You'll also need to have [`mpv`](https://mpv.io/) installed and available at the command line.
 
 Edit `native-app/interactive_bgm.json` to contain the full path of `native-app/index.js` on your system. Then copy (or link) that file into `~/.mozilla/native-messaging-hosts/`. Open `about:debugging`, click "Load Temporary Add-on...", and pick `extension/manifest.json`. Music should automatically start playing after a few seconds.
-
-(The music files included here, while WIP, are ripped from Hollow Knight.)
diff --git a/extension/background.js b/extension/background.js
index eedbb73..c450c9d 100644
--- a/extension/background.js
+++ b/extension/background.js
@@ -2,44 +2,69 @@ console.log('Start');
 
 const port = browser.runtime.connectNative('interactive_bgm');
 
-browser.browserAction.onClicked.addListener(() => {
-    console.log('Hello??');
-    // port.postMessage('[{"track": "mantis", "volume": 100}]\n');
-});
-
-console.log('Hi', port);
-
 port.postMessage('[{"track": "mantis", "volume": 100}]\n');
 
 port.onMessage.addListener(msg => {
     console.log('Nyoom', msg);
 });
 
-/*
-setTimeout(() => {
-    port.disconnect();
-}, 4000);
-*/
-
 port.onDisconnect.addListener(() => {
     console.log('Disconnected');
 });
 
-browser.runtime.onMessage.addListener(({hostname}) => {
-    browser.storage.sync.get(['siteSettings', 'disableEverywhere'])
-        .then(({siteSettings, disableEverywhere}) => {
-            if (disableEverywhere) {
-                port.postMessage([]);
-                return;
-            }
-
-            const mode = siteSettings[hostname];
-
-            if (mode) {
-                console.log('BGM:', mode);
-                port.postMessage(mode.map(track => ({track, volume: 100})));
-            } else {
-                console.log('No BGM found for ' + hostname);
-            }
+const uploadCallbacks = {};
+const deleteCallbacks = {};
+
+port.onMessage.addListener(({type, trackName}) => {
+    if (type === 'uploadFinished') {
+        if (uploadCallbacks[trackName]) {
+            uploadCallbacks[trackName]();
+        }
+    } else if (type === 'deleteFinished') {
+        if (deleteCallbacks[trackName]) {
+            deleteCallbacks[trackName]();
+        }
+    }
+});
+
+const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
+
+browser.runtime.onMessage.addListener(async ({hostname, type, base64, trackName}, sender, sendResponse) => {
+    if (hostname) {
+        browser.storage.sync.get(['siteSettings', 'disableEverywhere'])
+            .then(({siteSettings, disableEverywhere}) => {
+                if (disableEverywhere) {
+                    port.postMessage([]);
+                    return;
+                }
+
+                const mode = siteSettings[hostname];
+
+                if (mode) {
+                    port.postMessage(mode.map(track => ({track, volume: 100})));
+                }
+            });
+    } else if (type === 'uploadTrack' && trackName && base64) {
+        port.postMessage({type: 'uploadTrack', trackName, base64});
+        return new Promise(resolve => {
+            uploadCallbacks[trackName] = resolve;
+        }).then(() => {
+            browser.notifications.create({
+                type: 'basic',
+                title: 'Save Complete',
+                message: `Successfully saved track "${trackName}".`
+            });
+        });
+    } else if (type === 'deleteTrack' && trackName) {
+        port.postMessage({type: 'deleteTrack', trackName});
+        return new Promise(resolve => {
+            deleteCallbacks[trackName] = resolve;
+        }).then(() => {
+            browser.notifications.create({
+                type: 'basic',
+                title: 'Delete Complete',
+                message: `Successfully deleted track "${trackName}".`
+            });
         });
+    }
 });
diff --git a/extension/interactive-bgm.js b/extension/interactive-bgm.js
index 032dc19..1d29d06 100644
--- a/extension/interactive-bgm.js
+++ b/extension/interactive-bgm.js
@@ -1,3 +1,124 @@
 window.addEventListener('focus', () => {
     browser.runtime.sendMessage({hostname: location.hostname});
 });
+
+let hasShownCreateTrack = false;
+
+browser.runtime.onMessage.addListener(({createTrack}) => {
+    if (createTrack) {
+        if (hasShownCreateTrack) {
+            return;
+        }
+
+        hasShownCreateTrack = true;
+
+        const container = document.createElement('div')
+        document.body.appendChild(container);
+
+        Object.assign(container.style, {
+            all: 'initial',
+            position: 'fixed',
+            left: '0',
+            top: '0',
+            width: '100%',
+            height: '100%',
+            padding: '10px',
+            boxSizing: 'border-box',
+            zIndex: '99999999999',
+            display: 'flex',
+            justifyContent: 'center',
+            alignItems: 'center',
+            lineHeight: '2em',
+            fontFamily: 'Helvetica, Arial, sans-serif',
+            backgroundColor: 'rgba(105, 105, 105, 0.5)'
+        });
+
+        const div = document.createElement('div');
+        container.appendChild(div);
+
+        Object.assign(div.style, {
+            width: '50%',
+            height: '50%',
+            padding: '10px',
+            boxSizing: 'border-box',
+            backgroundColor: '#EEEEEE',
+            border: '2px solid black',
+            borderRadius: '4px',
+            boxShadow: '0 0 8px rgba(0, 0, 0, 0.5)',
+            zIndex: '99999999999',
+            minHeight: '120px',
+            minWidth: '200px'
+        });
+
+        const h1 = document.createElement('div');
+        div.appendChild(h1);
+
+        h1.appendChild(document.createTextNode('Create Track'));
+
+        Object.assign(h1.style, {
+            textAlign: 'center',
+            fontWeight: '800',
+            marginBottom: '8px'
+        });
+
+        const form = document.createElement('form');
+        div.appendChild(form);
+
+        const nameLabel = document.createElement('nameLabel');
+        form.appendChild(nameLabel);
+
+        nameLabel.appendChild(document.createTextNode('Name: '));
+
+        const nameInput = document.createElement('input');
+        nameLabel.appendChild(nameInput);
+
+        nameInput.type = 'text';
+        nameInput.required = true;
+
+        form.appendChild(document.createElement('br'));
+
+        const fileLabel = document.createElement('label');
+        form.appendChild(fileLabel);
+
+        fileLabel.appendChild(document.createTextNode('File: '));
+
+        const fileInput = document.createElement('input');
+        fileLabel.appendChild(fileInput);
+
+        fileInput.type = 'file';
+        fileInput.required = true;
+
+        form.appendChild(document.createElement('br'));
+
+        const submitInput = document.createElement('input');
+        form.appendChild(submitInput);
+
+        submitInput.type = 'submit';
+        submitInput.value = 'Save';
+
+        form.addEventListener('submit', event => {
+            event.preventDefault();
+
+            const trackName = nameInput.value;
+
+            const reader = new FileReader();
+            reader.onload = () => {
+                browser.storage.sync.get('tracks').then(({tracks}) => {
+                    browser.storage.sync.set({tracks: tracks.concat([trackName])});
+                });
+
+                const base64 = reader.result.split(',')[1];
+                const mime = reader.result.split(',')[0].split(';')[0];
+
+                browser.runtime.sendMessage({type: 'uploadTrack', base64, trackName}).then(() => {
+                    document.body.removeChild(container);
+                });
+            };
+
+            reader.readAsDataURL(fileInput.files[0]);
+
+            submitInput.value = 'Saving...';
+            submitInput.disabled = true;
+        });
+    }
+});
diff --git a/extension/manifest.json b/extension/manifest.json
index 2562fc9..7dbf537 100644
--- a/extension/manifest.json
+++ b/extension/manifest.json
@@ -8,6 +8,7 @@
     "permissions": [
         "activeTab",
         "nativeMessaging",
+        "notifications",
         "storage",
         "tabs"
     ],
diff --git a/extension/popup/main.js b/extension/popup/main.js
index 6175e36..06aca18 100644
--- a/extension/popup/main.js
+++ b/extension/popup/main.js
@@ -9,7 +9,7 @@ function changeScreen(id) {
 }
 
 function loadTrackList(opts) {
-    const {hostname, siteSettings} = opts;
+    const {tab, hostname, siteSettings} = opts;
     const site = siteSettings[hostname] || [];
     return browser.storage.sync.get('tracks').then(({tracks = []}) => {
         const ul = document.getElementById('track-list');
@@ -67,9 +67,17 @@ function loadTrackList(opts) {
             deleteButton.addEventListener('click', () => {
                 if (confirm(`This will delete "${track}" from ALL sites - this cannot be undone. Are you sure?`)) {
                     changeScreen('loading-screen');
-                    browser.storage.sync.set({tracks: tracks.filter(t => t !== track)})
-                        .then(() => loadTrackList(opts))
-                        .then(() => changeScreen('main-screen'));
+                    tracks = tracks.filter(t => t !== track);
+                    for (const site of Object.values(siteSettings)) {
+                        while (site.includes(track)) {
+                            site.splice(site.indexOf(track, 1));
+                        }
+                    }
+                    Promise.all([
+                        browser.runtime.sendMessage({type: 'deleteTrack', trackName: track}),
+                        browser.storage.sync.set({tracks, siteSettings})
+                            .then(() => loadTrackList(opts))
+                    ]).then(() => changeScreen('main-screen'));
                 }
             });
         }
@@ -85,41 +93,9 @@ function loadTrackList(opts) {
         addButton.appendChild(document.createTextNode('Create Track'));
         addButton.title = `Creates a new track, which will be an option present in all sites.`;
 
-        let newTrackInput = null;
         addButton.addEventListener('click', () => {
-            if (newTrackInput) {
-                newTrackInput.focus();
-                return;
-            }
-
-            const li = document.createElement('li');
-            li.classList.add('track');
-            ul.insertBefore(li, actionLi);
-
-            newTrackInput = document.createElement('input');
-            li.appendChild(newTrackInput);
-
-            const saveButton = document.createElement('button');
-            li.appendChild(saveButton);
-
-            saveButton.appendChild(document.createTextNode('Save'));
-
-            saveButton.addEventListener('click', () => {
-                while (li.firstChild) {
-                    li.removeChild(li.firstChild);
-                }
-                li.appendChild(document.createTextNode('Saving...'));
-
-                const name = newTrackInput.value.trim();
-                if (name.length) {
-                    changeScreen('loading-screen');
-                    browser.storage.sync.set({tracks: tracks.concat([name])})
-                        .then(() => loadTrackList(opts))
-                        .then(() => changeScreen('main-screen'));
-                }
-            });
-
-            newTrackInput.focus();
+            browser.tabs.sendMessage(tab.id, {createTrack: true});
+            window.close();
         });
 
         const disableButton = document.createElement('button');
@@ -161,14 +137,14 @@ Promise.all([
                 .then(() => browser.runtime.sendMessage({hostname}));
         });
 
-        return hostname;
+        return {tab, hostname};
     })(),
     browser.storage.sync.get('siteSettings')
         .then(({siteSettings = {}}) => siteSettings)
 ])
-    .then(([hostname, siteSettings]) => {
+    .then(([{tab, hostname}, siteSettings]) => {
         if (hostname) {
-            return loadTrackList({hostname, siteSettings})
+            return loadTrackList({tab, hostname, siteSettings})
                 .then(() => changeScreen('main-screen'));
         } else {
             changeScreen('invalid-host-screen');
diff --git a/native-app/index.js b/native-app/index.js
index 500eebf..f6936e3 100755
--- a/native-app/index.js
+++ b/native-app/index.js
@@ -1,18 +1,31 @@
 #!/usr/bin/env node
 
+const { promisify } = require('util');
 const { spawn } = require('child_process');
 const EventEmitter = require('events');
 const FIFO = require('fifo-js');
 const http = require('http');
 const path = require('path');
 const fs = require('fs');
+const os = require('os');
+const sanitizeFilename = require('sanitize-filename');
+
+const readdir = promisify(fs.readdir);
 
 const basePath = path.resolve(__dirname, '..');
 const logFile = basePath + '/native-app/log';
 const log = msg => fs.appendFileSync(logFile, msg + '\n');
+// const log = () => {};
 
 log('Started ' + Date());
 
+const emptyStream = () => {
+    const stream = new EventEmitter();
+    stream.write = () => {};
+    stream.end = () => {};
+    return stream;
+};
+
 class TrackPlayer {
     constructor(file) {
         this.file = file;
@@ -31,10 +44,7 @@ class TrackPlayer {
             this.file,
         ]);
 
-        const stream = new EventEmitter();
-        stream.write = () => {};
-        stream.end = () => {};
-        this.process.stderr.pipe(stream);
+        this.process.stderr.pipe(emptyStream());
     }
 
     sendCommand(command) {
@@ -64,23 +74,39 @@ class TrackPlayer {
     }
 }
 
-const tracks = {
-    mantis: new TrackPlayer(basePath + '/track1.wav'),
-    bass: new TrackPlayer(basePath + '/track2.wav'),
-    main: new TrackPlayer(basePath + '/track3.wav')
-};
+const musicDir = basePath + '/native-app/music';
 
-for (const track of Object.values(tracks)) {
-    track.loadProcess();
-    track.pause();
-}
+let tracks;
+
+const loadTracks = () => new Promise(async resolve => {
+    tracks = {};
+
+    for (const trackName of await readdir(musicDir)) {
+        tracks[trackName] = new TrackPlayer(musicDir + '/' + trackName);
+    }
+
+    for (const track of Object.values(tracks)) {
+        track.loadProcess();
+        track.pause();
+    }
+
+    setTimeout(() => {
+        for (const track of Object.values(tracks)) {
+            track.seekToStart();
+            track.play();
+        }
+
+        resolve();
+    }, 250);
+});
 
-setTimeout(() => {
+const killTracks = () => {
     for (const track of Object.values(tracks)) {
-        track.seekToStart();
-        track.play();
+        track.process.kill();
     }
+};
 
+loadTracks().then(async () => {
     let targetMode = [
         {track: 'main', volume: 100}
     ];
@@ -96,22 +122,104 @@ setTimeout(() => {
         }
     }, 20);
 
-    process.stdin.on('data', data => {
-        const probablyJSON = data.toString().slice(data.indexOf('[')).trim();
+    const uploadData = {};
+
+    const handleCommand = text => {
+        log('COMMAND: ' + text);
+
+        const probablyJSON = text.slice(Math.min(...[
+            text.indexOf('['), text.indexOf('{')
+        ].filter(x => x >= 0))).trim();
+
+        let data;
 
         try {
-            targetMode = JSON.parse(probablyJSON);
+            data = JSON.parse(probablyJSON);
         } catch (error) {
+            return;
+        }
+
+        if (Array.isArray(data)) {
+            targetMode = data;
+        } else if (data.type === 'uploadTrack') {
+            const buffer = Buffer.from(data.base64, 'base64');
+            log(`Write file: ${data.trackName} -- ${buffer.length} bytes`);
+            fs.writeFileSync(musicDir + '/' + sanitizeFilename(data.trackName), buffer);
+            killTracks();
+            loadTracks();
+            writeMessage(JSON.stringify({type: 'uploadFinished', trackName: data.trackName}));
+        } else if (data.type === 'deleteTrack') {
+            try {
+                fs.unlinkSync(musicDir + '/' + sanitizeFilename(data.trackName));
+            } catch (error) {
+                log('Error deleting track: ' + error);
+            }
+
+            if (tracks[data.trackName]) {
+                tracks[data.trackName].process.kill();
+                delete tracks[data.trackName];
+            }
+
+            writeMessage(JSON.stringify({type: 'deleteFinished', trackName: data.trackName}));
+        }
+    };
+
+    let readBytesPromise = null;
+    let stdinBuffer = Buffer.from([]);
+
+    process.stdin.on('data', data => {
+        stdinBuffer = Buffer.concat([stdinBuffer, data]);
+        if (readBytesPromise) {
+            readBytesPromise();
         }
     });
-}, 250);
+
+    const readBytes = numBytes => {
+        if (stdinBuffer.length >= numBytes) {
+            const bytes = stdinBuffer.slice(0, numBytes);
+            stdinBuffer = stdinBuffer.slice(numBytes); // Splice
+            return Promise.resolve(bytes);
+        } else {
+            return new Promise(resolve => {
+                readBytesCount = numBytes;
+                readBytesPromise = resolve;
+            }).then(() => readBytes(numBytes));
+        }
+    };
+
+    const writeMessage = message => {
+        log(message);
+
+        const size = message.length;
+        const rawSize = Buffer.alloc(4);
+        if (os.endianness() === 'BE') {
+            rawSize.writeUInt32BE(size, 0);
+        } else {
+            rawSize.writeUInt32LE(size, 0);
+        }
+
+        process.stdout.write(rawSize);
+        process.stdout.write(Buffer.from(message, 'utf8'));
+    };
+
+    while (true) {
+        const rawSize = await readBytes(4);
+        let size;
+        if (os.endianness() === 'BE') {
+            size = rawSize.readUInt32BE();
+        } else {
+            size = rawSize.readUInt32LE();
+        }
+        const data = await readBytes(size);
+        const command = data.toString('utf8');
+        handleCommand(command);
+    }
+});
 
 log('Go!');
 
 process.on('SIGTERM', () => {
     log('Exiting');
-    for (const track of Object.values(tracks)) {
-        track.process.kill();
-    }
+    killTracks();
     log('Cleaned up');
 });
diff --git a/package-lock.json b/package-lock.json
index 0a342b5..7c49514 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,27 @@
       "requires": {
         "es6-error": "^3.0.1"
       }
+    },
+    "sanitize-filename": {
+      "version": "1.6.1",
+      "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.1.tgz",
+      "integrity": "sha1-YS2hyWRz+gLczaktzVtKsWSmdyo=",
+      "requires": {
+        "truncate-utf8-bytes": "^1.0.0"
+      }
+    },
+    "truncate-utf8-bytes": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/truncate-utf8-bytes/-/truncate-utf8-bytes-1.0.2.tgz",
+      "integrity": "sha1-QFkjkJWS1W94pYGENLC3hInKXys=",
+      "requires": {
+        "utf8-byte-length": "^1.0.1"
+      }
+    },
+    "utf8-byte-length": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz",
+      "integrity": "sha1-9F8VDExm7uloGGUFq5P8u4rWv2E="
     }
   }
 }
diff --git a/package.json b/package.json
index 3d8c562..bb88e13 100644
--- a/package.json
+++ b/package.json
@@ -3,6 +3,7 @@
   "version": "1.0.0",
   "license": "GPL-3.0",
   "dependencies": {
-    "fifo-js": "^2.1.0"
+    "fifo-js": "^2.1.0",
+    "sanitize-filename": "^1.6.1"
   }
 }
diff --git a/track1.wav b/track1.wav
deleted file mode 100755
index bdbba6e..0000000
--- a/track1.wav
+++ /dev/null
Binary files differdiff --git a/track2.wav b/track2.wav
deleted file mode 100755
index 76452bd..0000000
--- a/track2.wav
+++ /dev/null
Binary files differdiff --git a/track3.wav b/track3.wav
deleted file mode 100755
index 09fcef7..0000000
--- a/track3.wav
+++ /dev/null
Binary files differ