« get me outta code hell

Smart playlists - mtui - Music Text User Interface - user-friendly command line music player
about summary refs log tree commit diff
path: root/smart-playlist.js
diff options
context:
space:
mode:
authorFlorrie <towerofnix@gmail.com>2018-06-04 21:27:18 -0300
committerFlorrie <towerofnix@gmail.com>2018-06-04 21:27:20 -0300
commit6055638558a345904b41467839191a7143862d25 (patch)
tree9b192640b171f48282650903b398084f24481507 /smart-playlist.js
parent6d270d43d5f09108132557100065fab3c0d34afc (diff)
Smart playlists
Basically directly pulled from http-music. Want to make a nice UI for
this eventually ("opening playlist..." popup dialog), but not for now.
Diffstat (limited to 'smart-playlist.js')
-rw-r--r--smart-playlist.js131
1 files changed, 131 insertions, 0 deletions
diff --git a/smart-playlist.js b/smart-playlist.js
new file mode 100644
index 0000000..09badd9
--- /dev/null
+++ b/smart-playlist.js
@@ -0,0 +1,131 @@
+const { getCrawlerByName } = require('./crawlers')
+const { isGroup, filterTracks, sourceSymbol, updatePlaylistFormat } = require('./playlist-utils')
+
+async function processSmartPlaylist(item, topItem = true) {
+  // 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 (topItem) {
+    item = await updatePlaylistFormat(item)
+  }
+
+  const newItem = Object.assign({}, item)
+
+  if ('source' in newItem) {
+    const [ name, ...args ] = item.source
+
+    const crawl = getCrawlerByName(name)
+
+    if (crawl) {
+      Object.assign(newItem, await crawl(...args))
+    } else {
+      console.error(`No crawler by name ${name} - skipped item:`, item)
+      newItem.failed = true
+    }
+
+    delete newItem.source
+  } else if ('items' in newItem) {
+    // Pass topItem = false, since we don't want to use updatePlaylistFormat
+    // on these items.
+    newItem.items = await Promise.all(item.items.map(x => processSmartPlaylist(x, false)))
+  }
+
+  if ('filters' in newItem) filters: {
+    if (!isGroup(newItem)) {
+      console.warn('Filter on non-group (no effect):', newItem)
+      break filters
+    }
+
+    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
+    })
+
+    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
+  }
+
+  if (topItem) {
+    // We pass true so that the playlist-format-updater knows that this
+    // is going to be the source playlist, probably.
+    return updatePlaylistFormat(newItem, true)
+  } else {
+    return newItem
+  }
+}
+
+module.exports = processSmartPlaylist