diff options
author | (quasar) nebula <qznebula@protonmail.com> | 2023-05-13 21:54:52 -0300 |
---|---|---|
committer | (quasar) nebula <qznebula@protonmail.com> | 2023-05-13 21:54:52 -0300 |
commit | 4ea5e57d2b6c12f42ff21c4b43056021553f07a0 (patch) | |
tree | 175c5c662639496fc859fa9a4852c363a6bab11d /playlist-utils.js | |
parent | 84c49e453336d6105655edd08e93bab071c0fc3b (diff) | |
parent | a36e372ba88b59e08fa938f76b261fdc2797bef2 (diff) |
Merge branch 'main' into socket-mtui
And also get most of it working wow.
Diffstat (limited to 'playlist-utils.js')
-rw-r--r-- | playlist-utils.js | 192 |
1 files changed, 64 insertions, 128 deletions
diff --git a/playlist-utils.js b/playlist-utils.js index 979c6d6..dd1d8c8 100644 --- a/playlist-utils.js +++ b/playlist-utils.js @@ -1,16 +1,12 @@ 'use strict' -const path = require('path') -const fs = require('fs') +import path from 'node:path' -const { promisify } = require('util') -const unlink = promisify(fs.unlink) +import {shuffleArray} from './general-util.js' -const { shuffleArray } = require('./general-util') +export const parentSymbol = Symbol('Parent group') -const parentSymbol = Symbol('Parent group') - -function updatePlaylistFormat(playlist) { +export function updatePlaylistFormat(playlist) { const defaultPlaylist = { options: [], items: [] @@ -43,7 +39,7 @@ function updatePlaylistFormat(playlist) { return updateGroupFormat(fullPlaylistObj) } -function updateGroupFormat(group) { +export function updateGroupFormat(group) { const defaultGroup = { name: '', items: [] @@ -61,7 +57,7 @@ function updateGroupFormat(group) { groupObj.items = groupObj.items.map(item => { // Check if it's a group; if not, it's probably a track. - if (typeof item[1] === 'array' || item.items) { + if (Array.isArray(item[1]) || item.items) { item = updateGroupFormat(item) } else { item = updateTrackFormat(item) @@ -85,7 +81,7 @@ function updateGroupFormat(group) { return groupObj } -function updateTrackFormat(track) { +export function updateTrackFormat(track) { const defaultTrack = { name: '', downloaderArg: '' @@ -106,7 +102,7 @@ function updateTrackFormat(track) { return Object.assign(defaultTrack, trackObj) } -function cloneGrouplike(grouplike) { +export function cloneGrouplike(grouplike) { const newGrouplike = { name: grouplike.name, items: grouplike.items.map(item => { @@ -128,7 +124,7 @@ function cloneGrouplike(grouplike) { return newGrouplike } -function filterTracks(grouplike, handleTrack) { +export 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 @@ -161,7 +157,7 @@ function filterTracks(grouplike, handleTrack) { }) } -function flattenGrouplike(grouplike) { +export function flattenGrouplike(grouplike) { // Flattens a group-like, taking all of the non-group items (tracks) at all // levels in the group tree and returns them as a new group containing those // tracks. @@ -169,7 +165,7 @@ function flattenGrouplike(grouplike) { return {items: getFlatTrackList(grouplike)} } -function getFlatTrackList(grouplike) { +export function getFlatTrackList(grouplike) { // Underlying function for flattenGrouplike. Can be used if you just want to // get an array and not a grouplike, too. @@ -182,7 +178,7 @@ function getFlatTrackList(grouplike) { }).reduce((a, b) => a.concat(b), []) } -function getFlatGroupList(grouplike) { +export function getFlatGroupList(grouplike) { // Analogue of getFlatTrackList for groups instead of tracks. Returns a flat // array of all the groups in each level of the provided grouplike. @@ -192,7 +188,7 @@ function getFlatGroupList(grouplike) { .reduce((a, b) => a.concat(b), []) } -function countTotalTracks(item) { +export function countTotalTracks(item) { // Returns the total number of tracks in a grouplike, including tracks in any // descendant groups. Basically the same as flattenGrouplike().items.length. @@ -206,7 +202,7 @@ function countTotalTracks(item) { } } -function shuffleOrderOfGroups(grouplike) { +export function shuffleOrderOfGroups(grouplike) { // OK, this is opinionated on how it should work, but I think it Makes Sense. // Also sorry functional-programming friends, I'm sure this is a horror. // (FYI, this is the same as how http-music used to work with shuffle-groups, @@ -224,12 +220,12 @@ function shuffleOrderOfGroups(grouplike) { return {items: shuffleArray(items)} } -function reverseOrderOfGroups(grouplike) { +export function reverseOrderOfGroups(grouplike) { const { items } = collapseGrouplike(grouplike) return {items: items.reverse()} } -function collectGrouplikeChildren(grouplike, filter = null) { +export function collectGrouplikeChildren(grouplike, filter = null) { // Collects all descendants of a grouplike into a single flat array. // Can be passed a filter function, which will decide whether or not to add // an item to the return array. However, note that all descendants will be @@ -252,7 +248,7 @@ function collectGrouplikeChildren(grouplike, filter = null) { return items } -function partiallyFlattenGrouplike(grouplike, resultDepth) { +export 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 @@ -273,7 +269,7 @@ function partiallyFlattenGrouplike(grouplike, resultDepth) { return {items} } -function collapseGrouplike(grouplike) { +export function collapseGrouplike(grouplike) { // Similar to partiallyFlattenGrouplike, but doesn't discard the individual // ordering of tracks; rather, it just collapses them all to one level. @@ -299,7 +295,7 @@ function collapseGrouplike(grouplike) { return {items: ret} } -function filterGrouplikeByProperty(grouplike, property, value) { +export function filterGrouplikeByProperty(grouplike, property, value) { // Returns a copy of the original grouplike, only keeping tracks with the // given property-value pair. (If the track's value for the given property // is an array, this will check if that array includes the given value.) @@ -329,13 +325,13 @@ function filterGrouplikeByProperty(grouplike, property, value) { }) } -function filterPlaylistByPathString(playlist, pathString) { +export function filterPlaylistByPathString(playlist, pathString) { // Calls filterGroupContentsByPath, taking an unparsed path string. return filterGrouplikeByPath(playlist, parsePathString(pathString)) } -function filterGrouplikeByPath(grouplike, pathParts) { +export function filterGrouplikeByPath(grouplike, pathParts) { // Finds a group by following the given group path and returns it. If the // function encounters an item in the group path that is not found, it logs // a warning message and returns the group found up to that point. If the @@ -386,13 +382,13 @@ function filterGrouplikeByPath(grouplike, pathParts) { } } -function removeGroupByPathString(playlist, pathString) { +export function removeGroupByPathString(playlist, pathString) { // Calls removeGroupByPath, taking a path string, rather than a parsed path. return removeGroupByPath(playlist, parsePathString(pathString)) } -function removeGroupByPath(playlist, pathParts) { +export function removeGroupByPath(playlist, pathParts) { // Removes the group at the given path from the given playlist. const groupToRemove = filterGrouplikeByPath(playlist, pathParts) @@ -433,7 +429,7 @@ function removeGroupByPath(playlist, pathParts) { } } -function getPlaylistTreeString(playlist, showTracks = false) { +export function getPlaylistTreeString(playlist, showTracks = false) { function recursive(group) { const groups = group.items.filter(x => isGroup(x)) const nonGroups = group.items.filter(x => !isGroup(x)) @@ -469,7 +465,7 @@ function getPlaylistTreeString(playlist, showTracks = false) { return recursive(playlist) } -function getItemPath(item) { +export function getItemPath(item) { if (item[parentSymbol]) { return [...getItemPath(item[parentSymbol]), item] } else { @@ -477,7 +473,7 @@ function getItemPath(item) { } } -function getItemPathString(item) { +export function getItemPathString(item) { // Gets the playlist path of an item by following its parent chain. // // Returns a string in format Foo/Bar/Baz, where Foo and Bar are group @@ -504,12 +500,12 @@ function getItemPathString(item) { } } -function parsePathString(pathString) { +export function parsePathString(pathString) { const pathParts = pathString.split('/').filter(item => item.length) return pathParts } -function getTrackIndexInParent(track) { +export function getTrackIndexInParent(track) { if (parentSymbol in track === false) { throw new Error( 'getTrackIndexInParent called with a track that has no parent!' @@ -520,6 +516,11 @@ function getTrackIndexInParent(track) { let i = 0, foundTrack = false; for (; i < parent.items.length; i++) { + // TODO: Port isSameTrack from http-music, if it makes sense - doing + // so involves porting the [oldSymbol] property on all tracks and groups, + // so may or may not be the right call. This function isn't used anywhere + // in mtui so it'll take a little extra investigation. + /* eslint-disable-next-line no-undef */ if (isSameTrack(track, parent.items[i])) { foundTrack = true break @@ -534,14 +535,14 @@ function getTrackIndexInParent(track) { } const nameWithoutTrackNumberSymbol = Symbol('Cached name without track number') -function getNameWithoutTrackNumber(track) { +export function getNameWithoutTrackNumber(track) { // A "part" is a series of numeric digits, separated from other parts by // whitespace, dashes, and dots, always preceding either the first non- // numeric/separator character or (if there are no such characters) the // first word (i.e. last whitespace). const getNumberOfParts = ({ name }) => { - name = name.replace(/^[\-\s.]+$/, '') - const match = name.match(/[^0-9\-\s.]/) + name = name.replace(/^[-\s.]+$/, '') + const match = name.match(/[^0-9-\s.]/) if (match) { if (match.index === 0) { return 0 @@ -553,12 +554,12 @@ function getNameWithoutTrackNumber(track) { } else { return 0 } - name = name.replace(/[\-\s.]+$/, '') - return name.split(/[\-\s.]+/g).length + name = name.replace(/[-\s.]+$/, '') + return name.split(/[-\s.]+/g).length } const removeParts = (name, numParts) => { - const regex = new RegExp(String.raw`[\-\s.]{0,}([0-9]+[\-\s.]+){${numParts},${numParts}}`) + const regex = new RegExp(String.raw`[-\s.]{0,}([0-9]+[-\s.]+){${numParts},${numParts}}`) return track.name.replace(regex, '') } @@ -606,24 +607,24 @@ function getNameWithoutTrackNumber(track) { } } -function isGroup(obj) { +export function isGroup(obj) { return !!(obj && obj.items) } -function isTrack(obj) { +export function isTrack(obj) { return !!(obj && obj.downloaderArg) } -function isPlayable(obj) { +export function isPlayable(obj) { return isGroup(obj) || isTrack(obj) } -function isOpenable(obj) { +export function isOpenable(obj) { return !!(obj && obj.url) } -function searchForItem(grouplike, value, preferredStartIndex = -1) { +export function searchForItem(grouplike, value, preferredStartIndex = -1) { if (value.length) { // We prioritize searching past the index that the user opened the jump // element from (oldFocusedIndex). This is so that it's more practical @@ -663,12 +664,12 @@ function searchForItem(grouplike, value, preferredStartIndex = -1) { return null } -function getCorrespondingFileForItem(item, extension) { +export function getCorrespondingFileForItem(item, extension) { if (!(item && item.url)) { return null } - const checkExtension = item => item.url && path.extname(item.url) === extension + const checkExtension = item => item.url && item.url.endsWith(extension) if (isPlayable(item)) { const parent = item[parentSymbol] @@ -688,7 +689,7 @@ function getCorrespondingFileForItem(item, extension) { return null } -function getCorrespondingPlayableForFile(item) { +export function getCorrespondingPlayableForFile(item) { if (!(item && item.url)) { return null } @@ -707,7 +708,7 @@ function getCorrespondingPlayableForFile(item) { return parent.items.find(item => isPlayable(item) && path.basename(item.url, path.extname(item.url)) === basename) } -function getPathScore(path1, path2) { +export function getPathScore(path1, path2) { // This function is basically only used in findTrackObject, but it's kinda // huge and I need to test that it works outside of that context, so I'm // sticking it on the global scope. Feel free to steal for whatever your @@ -836,7 +837,7 @@ function getPathScore(path1, path2) { return scores.reduce((a, b) => a < b ? a : b) } -function getNameScore(name1, name2) { +export function getNameScore(name1, name2) { // Pretty simple algorithm here: we're looking for the longest continuous // series of words which is shared between both names. The score is the // length of that series, so a higher score is better (and a zero score @@ -902,7 +903,7 @@ function getNameScore(name1, name2) { ) } -function findItemObject(referenceData, possibleChoices) { +export function findItemObject(referenceData, possibleChoices) { // Finds the item object in the provided choices which most closely resembles // the provided reference data. This is used for maintaining the identity of // item objects when reloading a playlist (see serialized-backend.js). It's @@ -977,88 +978,23 @@ function findItemObject(referenceData, possibleChoices) { return mostResembles.item } -module.exports = { - parentSymbol, - updatePlaylistFormat, updateGroupFormat, updateTrackFormat, - cloneGrouplike, - filterTracks, - flattenGrouplike, - getFlatTrackList, - getFlatGroupList, - countTotalTracks, - shuffleOrderOfGroups, - reverseOrderOfGroups, - partiallyFlattenGrouplike, collapseGrouplike, - filterGrouplikeByProperty, - filterPlaylistByPathString, filterGrouplikeByPath, - removeGroupByPathString, removeGroupByPath, - getPlaylistTreeString, - getItemPath, getItemPathString, - parsePathString, - getTrackIndexInParent, - getNameWithoutTrackNumber, - searchForItem, - getCorrespondingFileForItem, - getCorrespondingPlayableForFile, - getPathScore, - findItemObject, - isGroup, isTrack, - isOpenable, isPlayable -} - -if (require.main === module) { - console.log(getPathScore(['A', 'B', 'C'], ['A', 'B', 'C'])) - console.log(getPathScore(['A', 'B', 'C'], ['A', 'B', 'C', 'D'])) - console.log(getPathScore(['A', 'B', 'C', 'E'], ['A', 'B', 'C'])) - console.log(getPathScore(['W', 'X'], ['Y', 'Z'])) - console.log(getNameScore('C418 - Vlem', 'Vlem')) - console.log(getNameScore('glimmer', 'glimmer')) - console.log(getNameScore('C418 - Vlem', 'covet - glimmer')) - console.log(findItemObject( - // {name: 'T', downloaderArg: 'foo', path: ['A', 'B', 'C']}, - {name: 'B'}, - // getFlatTrackList( - getFlatGroupList( - updateGroupFormat({items: [ - {id: 1, name: 'T'}, - {id: 2, name: 'T'}, - {id: 3, name: 'T'}, - // {id: 4, name: 'T', downloaderArg: 'foo'}, - {id: 5, name: 'T'}, - {id: 6, name: 'Y', downloaderArg: 'foo'}, - {name: 'A', items: [ - {name: 'B', items: [ - {name: 'C', items: [ - {name: 'T'} - ]}, - {name: 'T'} - ]} - ]} - ]}) - ) - )) - - { - const group = updateGroupFormat({items: [ - {name: '- 1.01 Hello World 425', downloaderArg: 'x'}, - {name: '1.02 Aww Yeah 371', downloaderArg: 'x'}, - {name: ' 1.03 Here Goes 472', downloaderArg: 'x'} - ]}) +export function walkSharedStructure(modelGrouplike, ...additionalGrouplikesAndCallback) { + // Recursively traverse (aka "walk") a model grouplike and follow the same + // path through one or more additional grouplikes, running a callback with + // the item at that path from each of the grouplikes (model and additional). - for (let i = 0; i < group.items.length; i++) { - console.log(group.items[i].name, '->', getNameWithoutTrackNumber(group.items[i])) - } - } + const additionalGrouplikes = additionalGrouplikesAndFunction.slice(0, -1) + const callback = additionalGrouplikesAndCallback[additionalGrouplikesAndFunction.length - 1] - { - const group = updateGroupFormat({items: [ - {name: 'BAM #1', downloaderArg: 'x'}, - {name: 'BAM #2', downloaderArg: 'x'}, - {name: 'BAM #3.1 - no', downloaderArg: 'x'} - ]}) + const recursive = (model, ...additional) => { + for (let i = 0; i < model.items.length; i++) { + const modelItem = model.items[i] + const additionalItems = additional.map(a => a.items[i]) + callback(modelItem, ...additionalItems) - for (let i = 0; i < group.items.length; i++) { - console.log(group.items[i].name, '->', getNameWithoutTrackNumber(group.items[i])) + if (isGroup(modelItem)) { + recursive(modelItem, ...additionalItems) + } } } } |