« get me outta code hell

Make completely new filter system - http-music - Command-line music player + utils (not a server!)
about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2018-02-18 23:44:07 -0400
committerFlorrie <towerofnix@gmail.com>2018-02-18 23:44:08 -0400
commitb2ac9246886f72bef8b96cf218ed2d803397dafa (patch)
treedf7c287fc517f9837de15fda35bce6faae2b8e22 /src
parent86e42f7a7ec5cf27e2186d111017fe7943acd079 (diff)
Make completely new filter system
See the man page for how it works now.
Diffstat (limited to 'src')
-rw-r--r--src/open-file.js4
-rwxr-xr-xsrc/play.js25
-rw-r--r--src/playlist-utils.js32
-rw-r--r--src/smart-playlist.js111
4 files changed, 144 insertions, 28 deletions
diff --git a/src/open-file.js b/src/open-file.js
index 357bdae..f8af595 100644
--- a/src/open-file.js
+++ b/src/open-file.js
@@ -6,8 +6,8 @@ const {
   downloadPlaylistFromOptionValue
 } = require('./general-util')
 
-function crawl(input) {
-  return downloadPlaylistFromOptionValue(input)
+async function crawl(input) {
+  return JSON.parse(await downloadPlaylistFromOptionValue(input))
 }
 
 async function main(args, shouldReturn = false) {
diff --git a/src/play.js b/src/play.js
index 158c887..462cd88 100755
--- a/src/play.js
+++ b/src/play.js
@@ -335,16 +335,25 @@ async function main(args) {
     'r': util => util.alias('-remove'),
     'x': util => util.alias('-remove'),
 
-    '-filter': function(util) {
-      // --filter <property> <value>  (alias: -f)
-      // Filters the playlist so that only tracks with the given property-
-      // value pair are kept.
+    '-filter': async function(util) {
+      // --filter <filterJSON>
+      // Filters the playlist so that only tracks that match the given filter
+      // are kept. FilterJSON should be a JSON object as described in the
+      // man page section "filters".
 
-      const property = util.nextArg()
-      const value = util.nextArg()
+      const filterJSON = util.nextArg()
 
-      const p = filterGrouplikeByProperty(activePlaylist, property, value)
-      activePlaylist = updatePlaylistFormat(p)
+      let filterObj
+      try {
+        filterObj = JSON.parse(filterJSON)
+      } catch (error) {
+        console.error('Invalid JSON for filter:', filterJSON)
+        return
+      }
+
+      activePlaylist.filters = [filterObj]
+      activePlaylist = await processSmartPlaylist(activePlaylist)
+      activePlaylist = updatePlaylistFormat(activePlaylist)
     },
 
     'f': util => util.alias('-filter'),
diff --git a/src/playlist-utils.js b/src/playlist-utils.js
index dad828c..f817037 100644
--- a/src/playlist-utils.js
+++ b/src/playlist-utils.js
@@ -112,22 +112,37 @@ function updateTrackFormat(track) {
   return Object.assign(defaultTrack, trackObj)
 }
 
-function mapGrouplikeItems(grouplike, handleTrack) {
-  if (typeof handleTrack === 'undefined') {
+function filterTracks(grouplike, handleTrack) {
+  // Recursively filters every track in the passed grouplike. The track-handler
+  // function passed should either return true (to keep a track) or false (to
+  // remove the track). After tracks are filtered, groups which contain no
+  // items are removed.
+
+  if (typeof handleTrack !== 'function') {
     throw new Error("Missing track handler function")
   }
 
-  return {
-    items: grouplike.items.map(item => {
+  return Object.assign({}, grouplike, {
+    items: grouplike.items.filter(item => {
       if (isTrack(item)) {
         return handleTrack(item)
-      } else if (isGroup(item)) {
-        return mapGrouplikeItems(item, handleTrack, handleGroup)
       } else {
-        throw new Error('Non-track/group item')
+        return true
+      }
+    }).map(item => {
+      if (isGroup(item)) {
+        return filterTracks(item, handleTrack)
+      } else {
+        return item
+      }
+    }).filter(item => {
+      if (isGroup(item)) {
+        return item.items.length > 0
+      } else {
+        return true
       }
     })
-  }
+  })
 }
 
 function flattenGrouplike(grouplike) {
@@ -524,6 +539,7 @@ async function safeUnlink(file, playlist) {
 module.exports = {
   parentSymbol, oldSymbol, sourceSymbol,
   updatePlaylistFormat, updateTrackFormat,
+  filterTracks,
   flattenGrouplike,
   partiallyFlattenGrouplike, collapseGrouplike,
   filterGrouplikeByProperty,
diff --git a/src/smart-playlist.js b/src/smart-playlist.js
index 4d20e80..cbe5182 100644
--- a/src/smart-playlist.js
+++ b/src/smart-playlist.js
@@ -2,6 +2,7 @@
 
 const fs = require('fs')
 const { getCrawlerByName } = require('./crawlers')
+const { isGroup, filterTracks, sourceSymbol } = require('./playlist-utils')
 
 const { promisify } = require('util')
 const readFile = promisify(fs.readFile)
@@ -10,26 +11,116 @@ async function processSmartPlaylist(item) {
   // Object.assign is used so that we keep original properties, e.g. "name"
   // or "apply". (It's also used so we return copies of original objects.)
 
-  if ('source' in item) {
+  const newItem = Object.assign({}, item)
+
+  if ('source' in newItem) {
     const [ name, ...args ] = item.source
 
     const crawlModule = getCrawlerByName(name)
 
-    if (crawlModule === null) {
+    if (crawlModule) {
+      const { crawl } = crawlModule
+      Object.assign(newItem, await crawl(...args))
+    } else {
       console.error(`No crawler by name ${name} - skipped item:`, item)
-      return Object.assign({}, item, {failed: true})
+      newItem.failed = true
     }
 
-    const { crawl } = crawlModule
+    delete newItem.source
+  } else if ('items' in newItem) {
+    newItem.items = await Promise.all(item.items.map(processSmartPlaylist))
+  }
+
+  if ('filters' in newItem) filters: {
+    if (!isGroup(newItem)) {
+      console.warn('Filter on non-group (no effect):', newItem)
+      break filters
+    }
 
-    return Object.assign({}, item, await crawl(...args))
-  } else if ('items' in item) {
-    return Object.assign({}, item, {
-      items: await Promise.all(item.items.map(processSmartPlaylist))
+    newItem.filters = newItem.filters.filter(filter => {
+      if ('tag' in filter === false) {
+        console.warn('Filter is missing "tag" property (skipping this filter):', filter)
+        return false
+      }
+
+      return true
     })
-  } else {
-    return Object.assign({}, item)
+
+    Object.assign(newItem, filterTracks(newItem, track => {
+      for (const filter of newItem.filters) {
+        const { tag } = filter
+
+        let value = track
+        for (const key of tag.split('.')) {
+          if (key in Object(value)) {
+            value = value[key]
+          } else {
+            console.warn(`In tag "${tag}", key "${key}" not found.`)
+            console.warn('...value until now:', value)
+            console.warn('...track:', track)
+            console.warn('...filter:', filter)
+            return false
+          }
+        }
+
+        if ('gt' in filter && value <= filter.gt) return false
+        if ('lt' in filter && value >= filter.lt) return false
+        if ('gte' in filter && value < filter.gte) return false
+        if ('lte' in filter && value > filter.lte) return false
+        if ('least' in filter && value < filter.least) return false
+        if ('most' in filter && value > filter.most) return false
+        if ('min' in filter && value < filter.min) return false
+        if ('max' in filter && value > filter.max) return false
+
+        for (const prop of ['includes', 'contains']) {
+          if (prop in filter) {
+            if (Array.isArray(value) || typeof value === 'string') {
+              if (!value.includes(filter.includes)) return false
+            } else {
+              console.warn(
+                `Value of tag "${tag}" is not an array or string, so passing ` +
+                `"${prop}" does not make sense.`
+              )
+              console.warn('...value:', value)
+              console.warn('...track:', track)
+              console.warn('...filter:', filter)
+              return false
+            }
+          }
+        }
+
+        if (filter.regex) {
+          if (typeof value === 'string') {
+            let re
+            try {
+              re = new RegExp(filter.regex)
+            } catch (error) {
+              console.warn('Invalid regular expression:', re)
+              console.warn('...error message:', error.message)
+              console.warn('...filter:', filter)
+              return false
+            }
+            if (!re.test(value)) return false
+          } else {
+            console.warn(
+              `Value of tag "${tag}" is not a string, so passing "regex" ` +
+              'does not make sense.'
+            )
+            console.warn('...value:', value)
+            console.warn('...track:', track)
+            console.warn('...filter:', filter)
+            return false
+          }
+        }
+      }
+
+      return true
+    }))
+
+    delete newItem.filters
   }
+
+  return newItem
 }
 
 async function main(opts) {