diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/open-file.js | 4 | ||||
-rwxr-xr-x | src/play.js | 25 | ||||
-rw-r--r-- | src/playlist-utils.js | 32 | ||||
-rw-r--r-- | src/smart-playlist.js | 111 |
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) { |