« get me outta code hell

Add --collapse-groups option - http-music - Command-line music player + utils (not a server!)
about summary refs log tree commit diff
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2017-09-26 20:59:59 -0300
committerFlorrie <towerofnix@gmail.com>2017-09-26 20:59:59 -0300
commit78dc49edf83ad4eec8fc66330543eb601c2b2689 (patch)
treeb66c9185df786439055a5ccf4727c680f54916d8
parent8d99a28e2466c43ec554904ef90d09109f2c1004 (diff)
Add --collapse-groups option
-rwxr-xr-xsrc/play.js15
-rw-r--r--src/playlist-utils.js126
-rw-r--r--todo.txt6
3 files changed, 136 insertions, 11 deletions
diff --git a/src/play.js b/src/play.js
index 8b051ac..6061d09 100755
--- a/src/play.js
+++ b/src/play.js
@@ -14,7 +14,7 @@ const processSmartPlaylist = require('./smart-playlist')
 
 const {
   filterPlaylistByPathString, removeGroupByPathString, getPlaylistTreeString,
-  updatePlaylistFormat
+  updatePlaylistFormat, collapseGrouplike
 } = require('./playlist-utils')
 
 const readFile = promisify(fs.readFile)
@@ -300,6 +300,19 @@ async function main(args) {
     'r': util => util.alias('-remove'),
     'x': util => util.alias('-remove'),
 
+    '-collapse-groups': function() {
+      // --collapse-groups  (alias: --collapse)
+      // Collapses groups in the active playlist so that there is only one
+      // level of sub-groups. Handy for shuffling the order groups play in;
+      // try `--collapse-groups --sort shuffle-groups`.
+
+      requiresOpenPlaylist()
+
+      activePlaylist = updatePlaylistFormat(collapseGrouplike(activePlaylist))
+    },
+
+    '-collapse': util => util.alias('-collapse-groups'),
+
     '-list-groups': function(util) {
       // --list-groups  (alias: -l, --list)
       // Lists all groups in the playlist.
diff --git a/src/playlist-utils.js b/src/playlist-utils.js
index 5c8f206..6452559 100644
--- a/src/playlist-utils.js
+++ b/src/playlist-utils.js
@@ -58,9 +58,10 @@ function updateGroupFormat(group) {
   groupObj = Object.assign(defaultGroup, groupObj)
 
   groupObj.items = groupObj.items.map(item => {
-    // Theoretically this wouldn't work on downloader-args where the value
-    // isn't a string..
-    if (typeof item[1] === 'string' || item.downloaderArg) {
+    // Check if it's a group; if not, it's probably a track.
+    if (typeof item[1] === 'array' || item.items) {
+      item = updateGroupFormat(item)
+    } else {
       item = updateTrackFormat(item)
 
       // TODO: Should this also apply to groups? Is recursion good? Probably
@@ -72,8 +73,6 @@ function updateGroupFormat(group) {
       if (groupObj.apply) {
         Object.assign(item, groupObj.apply)
       }
-    } else {
-      item = updateGroupFormat(item)
     }
 
     item[parentSymbol] = groupObj
@@ -131,9 +130,7 @@ function flattenGrouplike(grouplike) {
   return {
     items: grouplike.items.map(item => {
       if (isGroup(item)) {
-        const flat = flattenGrouplike(item).items
-
-        return flat
+        return flattenGrouplike(item).items
       } else {
         return [item]
       }
@@ -141,6 +138,53 @@ function flattenGrouplike(grouplike) {
   }
 }
 
+function partiallyFlattenGrouplike(grouplike, resultDepth) {
+  // Flattens a grouplike so that it is never more than a given number of
+  // groups deep, INCLUDING the "top" group -- e.g. a resultDepth of 2
+  // means that there can be one level of groups remaining in the resulting
+  // grouplike, plus the top group.
+
+  if (resultDepth <= 1) {
+    return flattenGrouplike(grouplike)
+  }
+
+  const items = grouplike.items.map(item => {
+    if (isGroup(item)) {
+      return {items: partiallyFlattenGrouplike(item, resultDepth - 1).items}
+    } else {
+      return item
+    }
+  })
+
+  return {items}
+}
+
+function collapseGrouplike(grouplike) {
+  // Similar to partiallyFlattenGrouplike, but doesn't discard the individual
+  // ordering of tracks; rather, it just collapses them all to one level.
+
+  // Gather the groups. The result is an array of groups.
+  // Collapsing [Kar/Baz/Foo, Kar/Baz/Lar] results in [Foo, Lar].
+  // Aha! Just collect the top levels.
+  // Only trouble is what to do with groups that contain both groups and
+  // tracks. Maybe give them their own separate group (e.g. Baz).
+
+  const subgroups = grouplike.items.filter(x => isGroup(x))
+  const nonGroups = grouplike.items.filter(x => !isGroup(x))
+
+  // Get each group's own collapsed groups, and store them all in one big
+  // array.
+  const ret = subgroups.map(group => {
+    return collapseGrouplike(group).items
+  }).reduce((a, b) => a.concat(b), [])
+
+  if (nonGroups.length) {
+    ret.unshift({name: grouplike.name, items: nonGroups})
+  }
+
+  return {items: ret}
+}
+
 function filterPlaylistByPathString(playlist, pathString) {
   // Calls filterGroupContentsByPath, taking an unparsed path string.
 
@@ -308,13 +352,13 @@ function parsePathString(pathString) {
 }
 
 function isGroup(obj) {
-  return obj && obj.items
+  return !!(obj && obj.items)
 
   // return Array.isArray(array[1])
 }
 
 function isTrack(obj) {
-  return obj && obj.downloaderArg
+  return !!(obj && obj.downloaderArg)
 
   // return typeof array[1] === 'string'
 }
@@ -358,6 +402,7 @@ module.exports = {
   parentSymbol,
   updatePlaylistFormat, updateTrackFormat,
   flattenGrouplike,
+  partiallyFlattenGrouplike, collapseGrouplike,
   filterPlaylistByPathString, filterGrouplikeByPath,
   removeGroupByPathString, removeGroupByPath,
   getPlaylistTreeString,
@@ -503,4 +548,65 @@ if (require.main === module) {
     console.log('- (//foo/////bar//) should return [foo, bar]')
     assertArray(parsePathString('//foo/////bar//'), ['foo', 'bar'])
   }
+
+  console.log('partiallyFlattenGrouplike')
+
+  test: {
+    console.log('- ([[a1, [aa1]], out], 2) should return [[a1, aa1], out]')
+
+    const playlist = updatePlaylistFormat({name: 'top', items: [
+      {name: 'a', items: [
+        {name: 'a1'},
+        {name: 'aa', items: [
+          {name: 'aa1'}
+        ]}
+      ]},
+      {name: 'out'}
+    ]})
+
+    const result = partiallyFlattenGrouplike(playlist, 2)
+
+    console.log('  -> ' + JSON.stringify(result, null))
+
+    // TODO: A nicer way to compare playlists, haha.
+    assert(result.items.length, 2)
+    assert(result.items[0].items.length, 2)
+    assert(result.items[0].items[0].name, 'a1')
+    assert(result.items[0].items[1].name, 'aa1')
+    assert(result.items[1].name, 'out')
+  }
+
+  console.log('collapseGrouplike')
+
+  test: {
+    console.log('- (top: [a: [a1, aa: [aa1], a2], out])')
+
+    const playlist = updatePlaylistFormat({name: 'top', items: [
+      {name: 'a', items: [
+        {name: 'a1'},
+        {name: 'aa', items: [
+          {name: 'aa1'}
+        ]},
+        {name: 'a2'}
+      ]},
+      {name: 'out'}
+    ]})
+
+    const result = collapseGrouplike(playlist)
+
+    console.log('  -> ' + JSON.stringify(result, null))
+
+    // output should be [top: [out], a: [a1, a2], aa: [aa1]]
+    assert(result.items.length, 3)
+    assert(result.items[0].name, 'top')
+    assert(result.items[0].items.length, 1)
+    assert(result.items[0].items[0].name, 'out')
+    assert(result.items[1].name, 'a')
+    assert(result.items[1].items.length, 2)
+    assert(result.items[1].items[0].name, 'a1')
+    assert(result.items[1].items[1].name, 'a2')
+    assert(result.items[2].name, 'aa')
+    assert(result.items[2].items.length, 1)
+    assert(result.items[2].items[0].name, 'aa1')
+  }
 }
diff --git a/todo.txt b/todo.txt
index a3e576c..32f000b 100644
--- a/todo.txt
+++ b/todo.txt
@@ -380,3 +380,9 @@ TODO: Let track objects have an option for command line arguments to pass to
       converter program that isn't installed, it simply won't run the
       convertion options on the tracks in the playlist.
       (Done!)
+
+TODO: Should the '@ ...' part display the path to the track in the SOURCE
+      playlist, rather than in the active one? I'm split on this; showing the
+      active path is handy for debugging or writing your own playlist, but
+      showing the source path is usually more practically useful, so you know
+      where the album came from (e.g. displaying /C418/BAM instead of /BAM).