« get me outta code hell

(o) to open through system; show non-music files - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2019-10-15 17:55:58 -0300
committerFlorrie <towerofnix@gmail.com>2019-10-15 17:55:58 -0300
commitc0d6ea0363473cbb43e4f1f50c92803ed14742cb (patch)
tree883b8602584f66e9ddeb53cfe32b2d96dbfd909f
parentb2b424c977099d5fc9d49a9bbba6d911e2bc7901 (diff)
(o) to open through system; show non-music files
-rw-r--r--README.md1
-rw-r--r--backend.js35
-rw-r--r--crawlers.js8
-rw-r--r--package-lock.json13
-rw-r--r--package.json1
-rw-r--r--ui.js75
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
     }