From c0d6ea0363473cbb43e4f1f50c92803ed14742cb Mon Sep 17 00:00:00 2001 From: Florrie Date: Tue, 15 Oct 2019 17:55:58 -0300 Subject: (o) to open through system; show non-music files --- README.md | 1 + backend.js | 35 +++++++++++++++++++------- crawlers.js | 8 +++--- package-lock.json | 13 ++++++++++ package.json | 1 + ui.js | 75 ++++++++++++++++++++++++++++++++++++++++++++----------- 6 files changed, 106 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 4b3cb33..784ce6a 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ You're also welcome to share any ideas, suggestions, and questions through there * Enter: play the selected track * Ctrl+Up, p: play previous track * Ctrl+Down, n: play next track +* o: open the selected item through the system * Shift+Up/Down or drag: select multiple items at once * Space: toggle pause * Escape: stop playing the current track diff --git a/backend.js b/backend.js index d68d448..f2a9d99 100644 --- a/backend.js +++ b/backend.js @@ -35,6 +35,11 @@ async function download(item, record) { return } + // You can't download things that aren't tracks! + if (!isTrack(item)) { + return + } + // Don't start downloading an item if we're already downloading it! if (record.downloading) { return @@ -110,6 +115,11 @@ class QueuePlayer extends EventEmitter { return } + // If the item isn't a track, it can't be queued. + if (!isTrack(item)) { + return + } + // You can't put the same track in the queue twice - we automatically // remove the old entry. (You can't for a variety of technical reasons, // but basically you either have the display all bork'd, or new tracks @@ -161,15 +171,15 @@ class QueuePlayer extends EventEmitter { } const { items } = this.queueGrouplike - const newItems = flattenGrouplike(grouplike).items + const newTracks = flattenGrouplike(grouplike).items.filter(isTrack) // Expressly do an initial pass and unqueue the items we want to queue - // otherwise they would mess with the math we do afterwords. - for (const item of newItems) { + for (const item of newTracks) { if (items.includes(item)) { /* if (!movePlayingTrack && item === this.playingTrack) { - // NB: if uncommenting this code, splice item from newItems and do + // NB: if uncommenting this code, splice item from newTracks and do // continue instead of return! return } @@ -207,17 +217,17 @@ class QueuePlayer extends EventEmitter { if (how === 'evenly') { let offset = 0 - for (const item of newItems) { + for (const item of newTracks) { const insertIndex = distributeStart + Math.floor(offset) items.splice(insertIndex, 0, item) offset++ - offset += distributeSize / newItems.length + offset += distributeSize / newTracks.length } } else if (how === 'randomly') { - const indexes = newItems.map(() => Math.floor(Math.random() * distributeSize)) + const indexes = newTracks.map(() => Math.floor(Math.random() * distributeSize)) indexes.sort() - for (let i = 0; i < newItems.length; i++) { - const item = newItems[i] + for (let i = 0; i < newTracks.length; i++) { + const item = newTracks[i] const insertIndex = distributeStart + indexes[i] + i items.splice(insertIndex, 0, item) } @@ -375,6 +385,11 @@ class QueuePlayer extends EventEmitter { return } + // If it's not a track, you can't play it. + if (!isTrack(item)) { + return + } + playTrack: { // No downloader argument? That's no good - stop here. // TODO: An error icon on this item, or something??? @@ -660,7 +675,9 @@ class Backend extends EventEmitter { items = [item] } - const seconds = items.reduce(durationFn, 0) + const tracks = items.filter(isTrack) + + const seconds = tracks.reduce(durationFn, 0) let { duration: string } = getTimeStringsFromSec(0, seconds) const approxSymbol = noticedMissingMetadata ? '+' : '' diff --git a/crawlers.js b/crawlers.js index f665c17..578a9f2 100644 --- a/crawlers.js +++ b/crawlers.js @@ -257,11 +257,12 @@ function crawlLocal(dirPath, extensions = [ return Promise.all(items.map(item => { const itemPath = path.join(dirPath, item) + const itemURL = url.pathToFileURL(itemPath).href return stat(itemPath).then(stats => { if (stats.isDirectory()) { return crawlLocal(itemPath, extensions, false) - .then(group => Object.assign({name: item}, group)) + .then(group => Object.assign({name: item, url: itemURL}, group)) } else if (stats.isFile()) { // Extname returns a string starting with a dot; we don't want the // dot, so we slice it off of the front. @@ -273,10 +274,9 @@ function crawlLocal(dirPath, extensions = [ // playlist, or want them in an auto-generated one. const basename = path.basename(item, path.extname(item)) - const track = {name: basename, downloaderArg: itemPath} - return track + return {name: basename, downloaderArg: itemPath, url: itemURL} } else { - return null + return {name: item, url: itemURL} } } }, statErr => null) diff --git a/package-lock.json b/package-lock.json index 6cb87cb..afed660 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,6 +45,11 @@ "es6-error": "^3.0.1" } }, + "is-wsl": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.1.1.tgz", + "integrity": "sha512-umZHcSrwlDHo2TGMXv0DZ8dIUGunZ2Iv68YZnrmCiBPkZ4aaOhtv7pXJKeki9k3qJ3RJr0cDyitcl5wEH3AYog==" + }, "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", @@ -68,6 +73,14 @@ "resolved": "https://registry.npmjs.org/node-natural-sort/-/node-natural-sort-0.8.6.tgz", "integrity": "sha1-AdxrrcR0OxYDNAjw2FiasubAlM8=" }, + "open": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/open/-/open-7.0.0.tgz", + "integrity": "sha512-K6EKzYqnwQzk+/dzJAQSBORub3xlBTxMz+ntpZpH/LyCa1o6KjXhuN+2npAaI9jaSmU3R1Q8NWf4KUWcyytGsQ==", + "requires": { + "is-wsl": "^2.1.0" + } + }, "sanitize-filename": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/sanitize-filename/-/sanitize-filename-1.6.1.tgz", diff --git a/package.json b/package.json index 37cc485..943a7c4 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "mkdirp": "^0.5.1", "node-fetch": "^2.1.2", "node-natural-sort": "^0.8.6", + "open": "^7.0.0", "sanitize-filename": "^1.6.1", "tempy": "^0.2.1", "wcwidth": "^1.0.1", diff --git a/ui.js b/ui.js index 674541d..f334a70 100644 --- a/ui.js +++ b/ui.js @@ -47,6 +47,11 @@ const { } } = require('./tui-lib') +const open = require('open') + +const isPlayable = item => isGroup(item) || isTrack(item) +const isOpenable = item => !!(item && item.url) + const input = {} const keyBindings = [ @@ -79,6 +84,7 @@ const keyBindings = [ ['isDownload', 'd'], ['isRemove', 'x'], ['isQueueAfterSelectedTrack', 'q'], + ['isOpenThroughSystem', 'o'], ['isShuffleQueue', 's'], ['isClearQueue', 'c'], ['isFocusMenubar', ';'], @@ -217,6 +223,7 @@ class AppElement extends FocusElement { this.queueTimeLabel = new Label('') this.paneRight.addChild(this.queueTimeLabel) + this.queueListingElement.on('open', item => this.openThroughSystem(item)) this.queueListingElement.on('queue', item => this.play(item)) this.queueListingElement.on('remove', item => this.unqueue(item)) this.queueListingElement.on('shuffle', () => this.shuffleQueue()) @@ -572,8 +579,9 @@ class AppElement extends FocusElement { this.tabber.addTab(grouplikeListing) this.tabber.selectTab(grouplikeListing) - grouplikeListing.on('download', item => this.SQP.download(item)) grouplikeListing.on('browse', item => grouplikeListing.loadGrouplike(item)) + grouplikeListing.on('download', item => this.SQP.download(item)) + grouplikeListing.on('open', item => this.openThroughSystem(item)) grouplikeListing.on('queue', (item, opts) => this.handleQueueOptions(item, opts)) const updateListingsFor = item => { @@ -789,6 +797,14 @@ class AppElement extends FocusElement { } } + openThroughSystem(item) { + if (!isOpenable(item)) { + return + } + + open(item.url) + } + set actOnAllPlayers(val) { if (val) { this.queuePlayersToActOn = this.backend.queuePlayers.slice() @@ -888,15 +904,19 @@ class AppElement extends FocusElement { {divider: true}, */ - canControlQueue && {element: this.whereControl}, + canControlQueue && isPlayable(item) && {element: this.whereControl}, canControlQueue && isGroup(item) && {element: this.orderControl}, - canControlQueue && {label: 'Play!', action: emitControls(true)}, - canControlQueue && {label: 'Queue!', action: emitControls(false)}, + canControlQueue && isPlayable(item) && {label: 'Play!', action: emitControls(true)}, + canControlQueue && isPlayable(item) && {label: 'Queue!', action: emitControls(false)}, + {divider: true}, + + canProcessMetadata && isGroup(item) && {label: 'Process metadata (new entries)', action: () => this.processMetadata(item, false)}, + canProcessMetadata && isGroup(item) && {label: 'Process metadata (reprocess)', action: () => this.processMetadata(item, true)}, + canProcessMetadata && isTrack(item) && {label: 'Process metadata', action: () => this.processMetadata(item, true)}, + canControlQueue && isPlayable(item) && {label: 'Remove from queue', action: () => this.unqueue(item)}, {divider: true}, - canProcessMetadata && {label: 'Process metadata (new entries)', action: () => this.processMetadata(item, false)}, - canProcessMetadata && {label: 'Process metadata (reprocess)', action: () => this.processMetadata(item, true)}, - canControlQueue && {label: 'Remove from queue', action: () => this.unqueue(item)}, + isOpenable(item) && {label: 'Open (System)', action: () => this.openThroughSystem(item)}, {divider: true}, item === this.markGrouplike && {label: 'Deselect', action: () => this.deselectAll()} @@ -1610,7 +1630,17 @@ class GrouplikeListingElement extends Form { } addEventListeners(itemElement) { - for (const evtName of ['download', 'remove', 'mark', 'paste', 'browse', 'queue', 'unqueue', 'menu']) { + for (const evtName of [ + 'browse', + 'download', + 'paste', + 'mark', + 'menu', + 'open', + 'queue', + 'remove', + 'unqueue' + ]) { itemElement.on(evtName, (...data) => this.emit(evtName, itemElement.item, ...data)) } @@ -2512,11 +2542,15 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { this.emit('download') } else if (input.isQueueAfterSelectedTrack(keyBuf)) { this.emit('queue', {where: 'next-selected'}) + } else if (input.isOpenThroughSystem(keyBuf)) { + this.emit('open') } else if (telc.isEnter(keyBuf)) { if (isGroup(this.item)) { this.emit('browse') - } else { + } else if (isTrack(this.item)) { this.emit('queue', {where: 'next', play: true}) + } else if (!this.isPlayable) { + this.emit('open') } } else if (input.isRemove(keyBuf)) { this.emit('remove') @@ -2559,11 +2593,12 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { } if (this.isGroup) { this.emit('browse') - return false - } else { + } else if (this.isTrack) { this.emit('queue', {where: 'next', play: true}) - return false + } else if (!this.isPlayable) { + this.emit('open') } + return false } else { this.parent.selectInput(this) } @@ -2584,10 +2619,16 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { } else { writable.write(ansi.setAttributes([ansi.C_BLUE, ansi.A_BRIGHT])) } - } else { + } else if (this.isTrack) { if (this.isMarked) { writable.write(ansi.setAttributes([ansi.C_WHITE + 10, ansi.C_BLACK, ansi.A_BRIGHT])) } + } else if (!this.isPlayable) { + if (this.isMarked) { + writable.write(ansi.setAttributes([ansi.C_WHITE + 10, ansi.C_BLACK, ansi.A_BRIGHT])) + } else { + writable.write(ansi.setAttributes([ansi.A_DIM])) + } } this.drawX += 3 @@ -2605,6 +2646,8 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { if (this.isGroup) { writable.write('G') + } else if (!this.isPlayable) { + writable.write('F') } else if (record.downloading) { writable.write(braille[Math.floor(Date.now() / 250) % 6]) } else if (this.app.SQP.playingTrack === this.item) { @@ -2627,6 +2670,10 @@ class InteractiveGrouplikeItemElement extends BasicGrouplikeItemElement { get isTrack() { return isTrack(this.item) } + + get isPlayable() { + return isPlayable(this.item) + } } class ListingJumpElement extends Form { @@ -2682,7 +2729,7 @@ class PathElement extends ListScrollForm { this.removeInput(this.inputs[0]) } - if (!isTrack(item) && !isGroup(item)) { + if (!item) { return } -- cgit 1.3.0-6-gf8a5