From 213ef476d5aa27e3590754da52ff1e8c0b48cc38 Mon Sep 17 00:00:00 2001 From: Florrie Date: Sat, 23 Mar 2019 12:05:04 -0300 Subject: Upload/delete tracks --- .gitignore | 1 + README.md | 2 - extension/background.js | 83 +++++++++++++++-------- extension/interactive-bgm.js | 121 ++++++++++++++++++++++++++++++++++ extension/manifest.json | 1 + extension/popup/main.js | 58 +++++----------- native-app/index.js | 154 ++++++++++++++++++++++++++++++++++++------- package-lock.json | 21 ++++++ package.json | 3 +- track1.wav | Bin 29635016 -> 0 bytes track2.wav | Bin 29635016 -> 0 bytes track3.wav | Bin 29635016 -> 0 bytes 12 files changed, 348 insertions(+), 96 deletions(-) delete mode 100755 track1.wav delete mode 100755 track2.wav delete mode 100755 track3.wav 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 Binary files a/track1.wav and /dev/null differ diff --git a/track2.wav b/track2.wav deleted file mode 100755 index 76452bd..0000000 Binary files a/track2.wav and /dev/null differ diff --git a/track3.wav b/track3.wav deleted file mode 100755 index 09fcef7..0000000 Binary files a/track3.wav and /dev/null differ -- cgit 1.3.0-6-gf8a5