smart-playlist.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. import {getCrawlerByName} from './crawlers.js'
  2. import {filterTracks, isGroup, updatePlaylistFormat} from './playlist-utils.js'
  3. export default async function processSmartPlaylist(item, topItem = true) {
  4. // Object.assign is used so that we keep original properties, e.g. "name"
  5. // or "apply". (It's also used so we return copies of original objects.)
  6. if (topItem) {
  7. item = await updatePlaylistFormat(item)
  8. }
  9. const newItem = Object.assign({}, item)
  10. if ('source' in newItem) {
  11. const [ name, ...args ] = item.source
  12. const crawl = getCrawlerByName(name)
  13. if (crawl) {
  14. Object.assign(newItem, await crawl(...args))
  15. // If the passed smart playlist had a name, retain that instead of using
  16. // the name resulting from the crawler.
  17. if (item.name) {
  18. newItem.name = item.name
  19. }
  20. } else {
  21. console.error(`No crawler by name ${name} - skipped item:`, item)
  22. newItem.failed = true
  23. }
  24. delete newItem.source
  25. } else if ('items' in newItem) {
  26. // Pass topItem = false, since we don't want to use updatePlaylistFormat
  27. // on these items.
  28. newItem.items = await Promise.all(item.items.map(x => processSmartPlaylist(x, false)))
  29. }
  30. if ('filters' in newItem) filters: {
  31. if (!isGroup(newItem)) {
  32. console.warn('Filter on non-group (no effect):', newItem)
  33. break filters
  34. }
  35. newItem.filters = newItem.filters.filter(filter => {
  36. if ('tag' in filter === false) {
  37. console.warn('Filter is missing "tag" property (skipping this filter):', filter)
  38. return false
  39. }
  40. return true
  41. })
  42. Object.assign(newItem, filterTracks(newItem, track => {
  43. for (const filter of newItem.filters) {
  44. const { tag } = filter
  45. let value = track
  46. for (const key of tag.split('.')) {
  47. if (key in Object(value)) {
  48. value = value[key]
  49. } else {
  50. console.warn(`In tag "${tag}", key "${key}" not found.`)
  51. console.warn('...value until now:', value)
  52. console.warn('...track:', track)
  53. console.warn('...filter:', filter)
  54. return false
  55. }
  56. }
  57. if ('gt' in filter && value <= filter.gt) return false
  58. if ('lt' in filter && value >= filter.lt) return false
  59. if ('gte' in filter && value < filter.gte) return false
  60. if ('lte' in filter && value > filter.lte) return false
  61. if ('least' in filter && value < filter.least) return false
  62. if ('most' in filter && value > filter.most) return false
  63. if ('min' in filter && value < filter.min) return false
  64. if ('max' in filter && value > filter.max) return false
  65. for (const prop of ['includes', 'contains']) {
  66. if (prop in filter) {
  67. if (Array.isArray(value) || typeof value === 'string') {
  68. if (!value.includes(filter.includes)) return false
  69. } else {
  70. console.warn(
  71. `Value of tag "${tag}" is not an array or string, so passing ` +
  72. `"${prop}" does not make sense.`
  73. )
  74. console.warn('...value:', value)
  75. console.warn('...track:', track)
  76. console.warn('...filter:', filter)
  77. return false
  78. }
  79. }
  80. }
  81. if (filter.regex) {
  82. if (typeof value === 'string') {
  83. let re
  84. try {
  85. re = new RegExp(filter.regex)
  86. } catch (error) {
  87. console.warn('Invalid regular expression:', re)
  88. console.warn('...error message:', error.message)
  89. console.warn('...filter:', filter)
  90. return false
  91. }
  92. if (!re.test(value)) return false
  93. } else {
  94. console.warn(
  95. `Value of tag "${tag}" is not a string, so passing "regex" ` +
  96. 'does not make sense.'
  97. )
  98. console.warn('...value:', value)
  99. console.warn('...track:', track)
  100. console.warn('...filter:', filter)
  101. return false
  102. }
  103. }
  104. }
  105. return true
  106. }))
  107. delete newItem.filters
  108. }
  109. if (topItem) {
  110. // We pass true so that the playlist-format-updater knows that this
  111. // is going to be the source playlist, probably.
  112. return updatePlaylistFormat(newItem, true)
  113. } else {
  114. return newItem
  115. }
  116. }