1057 lines
25 KiB
JavaScript
Executable File
1057 lines
25 KiB
JavaScript
Executable File
import { compact } from 'lodash';
|
|
import {
|
|
isObject,
|
|
upgradeSpotifyPlaylistUri,
|
|
uriSource,
|
|
uriType,
|
|
getFromUri,
|
|
} from './helpers';
|
|
|
|
/**
|
|
* Convert a string to JSON, after we've checked whether it needs
|
|
* conversion or not.
|
|
*
|
|
* @param data String or Object
|
|
* @return Object
|
|
* */
|
|
const toJSON = (data) => {
|
|
// Parse it
|
|
try {
|
|
const json = JSON.parse(data);
|
|
return json;
|
|
|
|
// Could not parse string
|
|
} catch (e) {
|
|
// Check if it's JSON already
|
|
if (data.constructor === {}.constructor) {
|
|
return data;
|
|
}
|
|
console.error('Could not convert non-JSON', string);
|
|
}
|
|
return {};
|
|
};
|
|
|
|
/**
|
|
* Get a track's icon
|
|
* @param track object
|
|
* @return string
|
|
* */
|
|
const getTrackIcon = function (current_track = false, core = false) {
|
|
if (!core) return false;
|
|
if (!current_track) return false;
|
|
if (typeof (current_track.uri) === 'undefined') return false;
|
|
if (typeof (core.items[current_track.uri]) === 'undefined') return false;
|
|
const track = core.items[current_track.uri];
|
|
if (!track.images) return false;
|
|
return formatImages(track.images).small;
|
|
};
|
|
|
|
/**
|
|
* Digest an array of Mopidy image objects into a universal format. We also re-write
|
|
* image URLs to be absolute to the mopidy server (required for proxy setups).
|
|
*
|
|
* @param mopidy = obj (mopidy store object)
|
|
* @param images = array
|
|
* @return array
|
|
* */
|
|
const digestMopidyImages = function (mopidy, images) {
|
|
const digested = [];
|
|
|
|
for (let i = 0; i < images.length; i++) {
|
|
// Image object (ie from images.get)
|
|
if (typeof images[i] === 'object') {
|
|
// Accommodate backends that provide URIs vs URLs
|
|
let { url } = images[i];
|
|
if (!url && images[i].uri) {
|
|
url = images[i].uri;
|
|
}
|
|
|
|
// Amend our URL
|
|
images[i].url = url;
|
|
|
|
// Replace local images to point directly to our Mopidy server
|
|
if (url && url.startsWith('/images/')) {
|
|
url = `//${mopidy.host}:${mopidy.port}${url}`;
|
|
}
|
|
|
|
// String-based image
|
|
} else if (typeof images[i] === 'string') {
|
|
// Replace local images to point directly to our Mopidy server
|
|
if (images[i].startsWith('/images/')) {
|
|
images[i] = `//${mopidy.host}:${mopidy.port}${images[i]}`;
|
|
}
|
|
}
|
|
|
|
digested.push(images[i]);
|
|
}
|
|
|
|
return digested;
|
|
};
|
|
|
|
|
|
/**
|
|
* Format image URLs into a consistent size-based object
|
|
* We digest all our known image source formats into a universal small,medium,large,huge object
|
|
*
|
|
* @param $data mixed
|
|
* @return Object
|
|
* */
|
|
const formatImages = function (data) {
|
|
const sizes = {
|
|
formatted: true,
|
|
small: null,
|
|
medium: null,
|
|
large: null,
|
|
huge: null,
|
|
};
|
|
|
|
if (!data) {
|
|
return sizes;
|
|
}
|
|
|
|
// An array of images has been provided
|
|
if (Array.isArray(data)) {
|
|
if (data.length <= 0) {
|
|
return sizes;
|
|
}
|
|
|
|
for (let i = 0; i < data.length; i++) {
|
|
const image = data[i];
|
|
|
|
// Already-formatted
|
|
if (image.formatted) {
|
|
return image;
|
|
|
|
// Mopidy image object
|
|
} if (image.__model__ && image.__model__ == 'Image') {
|
|
if (image.width < 400) {
|
|
sizes.small = image.url;
|
|
} else if (image.width < 800) {
|
|
sizes.medium = image.url;
|
|
} else if (image.width < 1000) {
|
|
sizes.large = image.url;
|
|
} else {
|
|
sizes.huge = image.url;
|
|
}
|
|
|
|
// Mopidy image string
|
|
} else if (typeof (image) === 'string') {
|
|
sizes.small = image;
|
|
|
|
// spotify-styled images
|
|
} else if (image.width !== undefined) {
|
|
if (image.width < 400) {
|
|
sizes.small = image.url;
|
|
} else if (image.width < 800) {
|
|
sizes.medium = image.url;
|
|
} else if (image.width < 1000) {
|
|
sizes.large = image.url;
|
|
} else {
|
|
sizes.huge = image.url;
|
|
}
|
|
|
|
// lastfm-styled images
|
|
} else if (image.size !== undefined) {
|
|
switch (image.size) {
|
|
case 'mega':
|
|
case 'extralarge':
|
|
case 'large':
|
|
sizes.medium = image['#text'];
|
|
break;
|
|
case 'medium':
|
|
case 'small':
|
|
sizes.small = image['#text'];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// An object of images has been provided
|
|
// The Genius avatar object is an example of this
|
|
} else {
|
|
if (data.small) sizes.small = data.small.url;
|
|
if (data.medium) sizes.medium = data.medium.url;
|
|
if (data.large) sizes.large = data.large.url;
|
|
if (data.huge) sizes.huge = data.huge.url;
|
|
}
|
|
|
|
// Inherit images where we haven't been given the appropriate size
|
|
// Ie small duplicated to tiny, large duplicated to medium, etc
|
|
if (!sizes.small) {
|
|
if (sizes.medium) sizes.small = sizes.medium;
|
|
else if (sizes.large) sizes.small = sizes.large;
|
|
else if (sizes.huge) sizes.small = sizes.huge;
|
|
else sizes.small = null;
|
|
}
|
|
if (!sizes.medium) {
|
|
if (sizes.large) sizes.medium = sizes.large;
|
|
else if (sizes.huge) sizes.medium = sizes.huge;
|
|
else sizes.medium = sizes.small;
|
|
}
|
|
if (!sizes.large) sizes.large = sizes.medium;
|
|
if (!sizes.huge) sizes.huge = sizes.large;
|
|
|
|
return sizes;
|
|
};
|
|
|
|
|
|
/**
|
|
* Format a simple object
|
|
* This is a shell record containing only the bare essentials. Typically
|
|
* a tracks' artists/album
|
|
*
|
|
* @param data obj
|
|
* @return obj
|
|
* */
|
|
const formatSimpleObject = function (data) {
|
|
const simple_object = {};
|
|
const fields = [
|
|
'uri',
|
|
'name',
|
|
];
|
|
|
|
for (const field of fields) {
|
|
if (data.hasOwnProperty(field)) {
|
|
simple_object[field] = data[field];
|
|
}
|
|
}
|
|
|
|
return simple_object;
|
|
};
|
|
|
|
/**
|
|
* Format multiple items
|
|
*
|
|
* @param tracks Array
|
|
* @return Array
|
|
* */
|
|
const formatTracks = function (records = []) {
|
|
const formatted = [];
|
|
for (const record of records) {
|
|
formatted.push(formatTrack(record));
|
|
}
|
|
return formatted;
|
|
};
|
|
const formatAlbums = function (records = []) {
|
|
const formatted = [];
|
|
for (const record of records) {
|
|
formatted.push(formatAlbum(record));
|
|
}
|
|
return formatted;
|
|
};
|
|
const formatArtists = function (records = []) {
|
|
const formatted = [];
|
|
for (const record of records) {
|
|
formatted.push(formatArtist(record));
|
|
}
|
|
return formatted;
|
|
};
|
|
const formatPlaylists = function (records = []) {
|
|
const formatted = [];
|
|
for (const record of records) {
|
|
formatted.push(formatPlaylist(record));
|
|
}
|
|
return formatted;
|
|
};
|
|
const formatUsers = function (records = []) {
|
|
const formatted = [];
|
|
for (const record of records) {
|
|
formatted.push(formatUser(record));
|
|
}
|
|
return formatted;
|
|
};
|
|
const formatCategories = function (records = []) {
|
|
const formatted = [];
|
|
for (const record of records) {
|
|
formatted.push(formatCategory(record));
|
|
}
|
|
return formatted;
|
|
};
|
|
const formatSimpleObjects = function (records = []) {
|
|
const formatted = [];
|
|
for (const record of records) {
|
|
formatted.push(formatSimpleObject(record));
|
|
}
|
|
return formatted;
|
|
};
|
|
|
|
/**
|
|
* Prepare a URI for use in a URL
|
|
*
|
|
* Needs to have all special characters encoded to avoid being parsed incorrectly, especially
|
|
* '/' as this is a URL parameter delimiter
|
|
* @param {String} uri
|
|
*/
|
|
const encodeUri = (uri) => {
|
|
return encodeURIComponent(uri);
|
|
};
|
|
|
|
/**
|
|
* Rebuild a URI with some ugly-ass handling of encoding.
|
|
*
|
|
* Basically the ID part of a Mopidy URI needs to be encoded, but the rest of the URI can't be.
|
|
* This means we need to break down the URI (decoded) and then reconstruct with an encoded ID.
|
|
* This is all required because he URI is passed to us *from* a URL which has been encoded for
|
|
* obvious reasons.
|
|
*
|
|
* For example somebackend:track:https://youtube.com/1234 would cause issues with the
|
|
* https:// section. Our explode-by-':' approach would break this type of URI.
|
|
*
|
|
* @param {String} rawUri
|
|
*/
|
|
const decodeUri = (rawUri) => {
|
|
const uri = decodeURIComponent(rawUri);
|
|
const source = uriSource(uri);
|
|
const type = uriType(uri);
|
|
|
|
// Escape unreserved characters (RFC 3986)
|
|
// https://stackoverflow.com/questions/18251399/why-doesnt-encodeuricomponent-encode-single-quotes-apostrophes
|
|
let id = getFromUri(`${type}id`, uri);
|
|
id = encodeURIComponent(id).replace(/[!'()*]/g, escape);
|
|
|
|
// Reinstate slashes for the Mopidy-Local structure
|
|
id = id.replace(/%2F/g, '/');
|
|
|
|
// Ensure all ':' are uri encoded to lowercase
|
|
id = id.replace(/%3A/g, '%3a');
|
|
|
|
let decoded = `${source}:`;
|
|
switch (source) {
|
|
case 'dleyna':
|
|
// Doesn't contain URI type within it's URI schema *facepalm*
|
|
break;
|
|
default:
|
|
decoded += `${type}:`;
|
|
}
|
|
decoded += `${id}`;
|
|
|
|
return decoded;
|
|
};
|
|
|
|
/**
|
|
* Format our album objects into a universal format
|
|
*
|
|
* @param data obj
|
|
* @return obj
|
|
* */
|
|
const formatAlbum = function (data) {
|
|
const album = { type: 'album' };
|
|
const fields = [
|
|
'uri',
|
|
'in_library',
|
|
'provider',
|
|
'name',
|
|
'type',
|
|
'last_modified',
|
|
'added_at',
|
|
'release_date',
|
|
'listeners',
|
|
'play_count',
|
|
'wiki',
|
|
'wiki_publish_date',
|
|
'popularity',
|
|
'images',
|
|
'tracks',
|
|
'tracks_total',
|
|
'artists',
|
|
];
|
|
|
|
// Nested album object (eg in spotify library)
|
|
if (data && data.album && isObject(data.album)) {
|
|
if (data.added_at) {
|
|
data.album.added_at = data.added_at;
|
|
}
|
|
data = data.album;
|
|
}
|
|
|
|
// Loop fields and import from data
|
|
for (const field of fields) {
|
|
if (data.hasOwnProperty(field)) {
|
|
album[field] = data[field];
|
|
}
|
|
}
|
|
|
|
if (album.images && !album.images.formatted) {
|
|
album.images = formatImages(album.images);
|
|
}
|
|
if (album.tracks) {
|
|
if (album.tracks.items) {
|
|
album.tracks = formatTracks(album.tracks.items);
|
|
} else {
|
|
album.tracks = formatTracks(album.tracks);
|
|
}
|
|
album.tracks_total = album.tracks.length;
|
|
}
|
|
if (album.artists) {
|
|
album.artists = formatSimpleObjects(album.artists);
|
|
}
|
|
|
|
if (data.last_modified && album.added_at === undefined) {
|
|
album.added_at = data.last_modified;
|
|
} else if (data.added_at && album.last_modified === undefined) {
|
|
album.last_modified = data.added_at;
|
|
}
|
|
if (data.date && !album.date) {
|
|
album.release_date = data.date;
|
|
}
|
|
if (data.album_type) {
|
|
album.type = data.album_type;
|
|
}
|
|
if (album.provider === undefined && album.uri !== undefined) {
|
|
album.provider = uriSource(album.uri);
|
|
}
|
|
|
|
return album;
|
|
};
|
|
|
|
|
|
/**
|
|
* Format our artist objects into a universal format
|
|
*
|
|
* @param data obj
|
|
* @return obj
|
|
* */
|
|
const formatArtist = function (data) {
|
|
const artist = { type: 'artist' };
|
|
const fields = [
|
|
'uri',
|
|
'in_library',
|
|
'provider',
|
|
'mbid',
|
|
'name',
|
|
'type',
|
|
'popularity',
|
|
'followers',
|
|
'listeners',
|
|
'added_at',
|
|
'biography',
|
|
'biography_link',
|
|
'biography_publish_date',
|
|
'related_artists',
|
|
'albums_uris',
|
|
'tracks',
|
|
];
|
|
|
|
// Loop fields and import from data
|
|
for (const field of fields) {
|
|
if (data.hasOwnProperty(field)) {
|
|
artist[field] = data[field];
|
|
}
|
|
}
|
|
|
|
if (data.images && data.images.length > 0) {
|
|
artist.images = [formatImages(data.images)];
|
|
}
|
|
|
|
if (data.followers && data.followers.total !== undefined) {
|
|
artist.followers = data.followers.total;
|
|
}
|
|
|
|
if (data.bio) {
|
|
if (data.bio.content && !artist.biography) {
|
|
artist.biography = data.bio.content;
|
|
}
|
|
if (data.bio.links && data.bio.links.link && data.bio.links.link.href && !artist.biography_link) {
|
|
artist.biography_link = data.bio.links.link.href;
|
|
}
|
|
if (data.bio.published && !artist.biography_publish_date) {
|
|
artist.biography_publish_date = data.bio.published;
|
|
}
|
|
}
|
|
|
|
if (artist.provider === undefined && artist.uri !== undefined) {
|
|
artist.provider = uriSource(artist.uri);
|
|
}
|
|
|
|
return artist;
|
|
};
|
|
|
|
|
|
/**
|
|
* Format our playlist objects into a universal format
|
|
*
|
|
* @param data obj
|
|
* @return obj
|
|
* */
|
|
const formatPlaylist = function (data) {
|
|
const playlist = { type: 'playlist' };
|
|
const fields = [
|
|
'uri',
|
|
'in_library',
|
|
'snapshot_id',
|
|
'provider',
|
|
'type',
|
|
'collaborative',
|
|
'public',
|
|
'name',
|
|
'description',
|
|
'images',
|
|
'popularity',
|
|
'followers',
|
|
'last_modified',
|
|
'can_edit',
|
|
'user',
|
|
'tracks',
|
|
'tracks_total',
|
|
];
|
|
|
|
// Loop fields and import from data
|
|
for (const field of fields) {
|
|
if (data.hasOwnProperty(field)) {
|
|
playlist[field] = data[field];
|
|
}
|
|
}
|
|
|
|
if (playlist.images && !playlist.images.formatted) {
|
|
playlist.images = formatImages(playlist.images);
|
|
}
|
|
if (playlist.tracks) {
|
|
if (Array.isArray(playlist.tracks)) {
|
|
playlist.tracks = formatTracks(playlist.tracks);
|
|
} else if (playlist.tracks.items) {
|
|
playlist.tracks = formatTracks(playlist.tracks.items);
|
|
} else {
|
|
playlist.tracks = null;
|
|
}
|
|
}
|
|
|
|
if (playlist.tracks_total === undefined) {
|
|
playlist.tracks_total = playlist.tracks ? playlist.tracks.length : 0;
|
|
}
|
|
|
|
if (data.last_modified_date && playlist.last_modified === undefined) {
|
|
playlist.last_modified = data.last_modified_date;
|
|
}
|
|
|
|
if (data.followers && data.followers.total !== undefined) {
|
|
playlist.followers = data.followers.total;
|
|
}
|
|
|
|
if (data.tracks && data.tracks.total !== undefined) {
|
|
playlist.tracks_total = data.tracks.total;
|
|
}
|
|
|
|
if (playlist.last_modified && playlist.added_at === undefined) {
|
|
playlist.added_at = data.last_modified;
|
|
} else if (playlist.added_at && playlist.last_modified === undefined) {
|
|
playlist.last_modified = data.added_at;
|
|
}
|
|
|
|
if (data.owner && playlist.user === undefined) {
|
|
playlist.user = {
|
|
id: data.owner.id,
|
|
uri: data.owner.uri,
|
|
name: (data.owner.display_name || data.owner.id),
|
|
};
|
|
}
|
|
|
|
// Spotify upgraded their playlists URI to remove user component (Sept 2018)
|
|
playlist.uri = upgradeSpotifyPlaylistUri(playlist.uri);
|
|
|
|
if (playlist.provider === undefined && playlist.uri !== undefined) {
|
|
playlist.provider = uriSource(playlist.uri);
|
|
}
|
|
|
|
return playlist;
|
|
};
|
|
|
|
|
|
/**
|
|
* Format a user objects into a universal format
|
|
*
|
|
* @param data obj
|
|
* @return obj
|
|
* */
|
|
const formatUser = function (data) {
|
|
const user = { type: 'user' };
|
|
const fields = [
|
|
'id',
|
|
'in_library',
|
|
'uri',
|
|
'provider',
|
|
'name',
|
|
'images',
|
|
'followers',
|
|
'playlists_uris',
|
|
'playlists_total',
|
|
'playlists_more',
|
|
];
|
|
|
|
// Loop fields and import from data
|
|
for (const field of fields) {
|
|
if (data.hasOwnProperty(field)) {
|
|
user[field] = data[field];
|
|
}
|
|
}
|
|
|
|
if (!user.images && data.image) {
|
|
user.images = formatImages(data.image);
|
|
} else if (!user.images && data.avatar) {
|
|
user.images = formatImages(data.avatar);
|
|
} else if (user.images && !user.images.formatted) {
|
|
user.images = formatImages(user.images);
|
|
}
|
|
|
|
if (data.followers && data.followers.total !== undefined) {
|
|
user.followers = data.followers.total;
|
|
}
|
|
if (data.realname) {
|
|
user.name = data.realname;
|
|
}
|
|
if (data.display_name && !user.name) {
|
|
user.name = data.display_name;
|
|
}
|
|
if (data.id && !user.name) {
|
|
user.name = data.id;
|
|
}
|
|
if (user.provider === undefined && user.uri !== undefined) {
|
|
user.provider = uriSource(user.uri);
|
|
}
|
|
|
|
return user;
|
|
};
|
|
|
|
|
|
/**
|
|
* Format tracks into our universal format
|
|
*
|
|
* @param data obj
|
|
* @return obj
|
|
* */
|
|
const formatTrack = function (data) {
|
|
const track = { type: 'track' };
|
|
const fields = [
|
|
'uri',
|
|
'in_library',
|
|
'tlid',
|
|
'provider',
|
|
'name',
|
|
'images',
|
|
'release_date',
|
|
'disc_number',
|
|
'track_number',
|
|
'disc_track',
|
|
'duration',
|
|
'followers',
|
|
'popularity',
|
|
'userloved',
|
|
'last_modified',
|
|
'added_at',
|
|
'is_explicit',
|
|
'is_local',
|
|
'lyrics',
|
|
'lyrics_path',
|
|
'lyrics_results',
|
|
'artists', // Array of simple records
|
|
'album', // Array of simple records
|
|
];
|
|
|
|
// Nested track object (eg in spotify playlist)
|
|
if (data && data.track && isObject(data.track)) {
|
|
// Copy wrapper's details (if applicable)
|
|
if (data.added_by) {
|
|
data.track.added_by = data.added_by;
|
|
}
|
|
if (data.added_at) {
|
|
data.track.added_at = data.added_at;
|
|
}
|
|
if (data.tlid) {
|
|
data.track.tlid = data.tlid;
|
|
}
|
|
|
|
// And now flatten
|
|
data = data.track;
|
|
}
|
|
|
|
// Loop fields and import from data
|
|
for (const field of fields) {
|
|
if (data.hasOwnProperty(field)) {
|
|
track[field] = data[field];
|
|
}
|
|
}
|
|
|
|
if (data.followers && data.followers.total) {
|
|
track.followers = data.followers.total;
|
|
}
|
|
|
|
if (track.last_modified && track.added_at === undefined) {
|
|
track.added_at = track.last_modified;
|
|
} else if (track.added_at && track.last_modified === undefined) {
|
|
track.last_modified = track.added_at;
|
|
}
|
|
|
|
if (data.duration_ms !== undefined) {
|
|
track.duration = data.duration_ms;
|
|
} else if (data.length !== undefined) {
|
|
track.duration = data.length;
|
|
}
|
|
|
|
if (track.track_number === undefined && data.track_no !== undefined) {
|
|
track.track_number = data.track_no;
|
|
}
|
|
|
|
if (track.disc_number === undefined && data.disc_no !== undefined) {
|
|
track.disc_number = data.disc_no;
|
|
}
|
|
|
|
if (track.release_date === undefined && data.date !== undefined) {
|
|
track.release_date = data.date;
|
|
}
|
|
|
|
if (track.explicit === undefined && data.explicit !== undefined) {
|
|
track.is_explicit = data.explicit;
|
|
}
|
|
|
|
// Copy images from albums (if applicable)
|
|
// TOOD: Identify if we stil need this...
|
|
if (data.album && data.album.images) {
|
|
if (track.images === undefined || !track.images.formatted) {
|
|
track.images = formatImages(data.album.images);
|
|
}
|
|
}
|
|
|
|
if (track.provider === undefined && track.uri !== undefined) {
|
|
track.provider = uriSource(track.uri);
|
|
}
|
|
|
|
if (track.artists) {
|
|
track.artists = formatSimpleObjects(track.artists);
|
|
}
|
|
|
|
if (track.album) {
|
|
track.album = formatSimpleObject(track.album);
|
|
}
|
|
|
|
track.disc_track = track.disc_number + (track.track_number / 10); // Gives us a decimal d.t
|
|
|
|
return track;
|
|
};
|
|
|
|
/**
|
|
* Format a snapcast client object into a universal format
|
|
*
|
|
* @param data obj
|
|
* @return obj
|
|
* */
|
|
const formatClient = function (data) {
|
|
const client = { type: 'client' };
|
|
const fields = [
|
|
'id',
|
|
'connected',
|
|
'name',
|
|
'host_name',
|
|
'volume',
|
|
'mute',
|
|
'latency',
|
|
'power_on_command',
|
|
'power_off_command',
|
|
];
|
|
|
|
for (const field of fields) {
|
|
if (data.hasOwnProperty(field)) {
|
|
client[field] = data[field];
|
|
}
|
|
}
|
|
|
|
if (data.config) {
|
|
if (client.latency === undefined && data.config.latency !== undefined) {
|
|
client.latency = data.config.latency;
|
|
}
|
|
|
|
if (!client.name && data.config.name) {
|
|
client.name = data.config.name;
|
|
}
|
|
|
|
if (data.config.volume !== undefined) {
|
|
if (data.config.volume.percent !== undefined) {
|
|
client.volume = data.config.volume.percent;
|
|
}
|
|
if (data.config.volume.muted) {
|
|
client.mute = data.config.volume.muted;
|
|
}
|
|
}
|
|
} else {
|
|
if (client.latency === undefined && data.latency !== undefined) {
|
|
client.latency = data.latency;
|
|
}
|
|
if (typeof data.volume === 'object') {
|
|
if (data.volume.percent) {
|
|
client.volume = data.volume.percent;
|
|
}
|
|
if (data.volume.muted !== undefined) {
|
|
client.mute = data.volume.muted;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (client.name === undefined && data.host && data.host.name) {
|
|
client.name = data.host.name;
|
|
}
|
|
|
|
return client;
|
|
};
|
|
|
|
/**
|
|
* Spotify playlists category
|
|
*
|
|
* @param data obj
|
|
* @return obj
|
|
* */
|
|
const formatCategory = function (data) {
|
|
const category = { type: 'category' };
|
|
const fields = [
|
|
'id',
|
|
'uri',
|
|
'name',
|
|
'playlists_uris',
|
|
];
|
|
|
|
for (const field of fields) {
|
|
if (data.hasOwnProperty(field)) {
|
|
category[field] = data[field];
|
|
}
|
|
}
|
|
|
|
if (!category.uri && data.id) {
|
|
category.uri = `spotify:category:${data.id}`;
|
|
}
|
|
|
|
if (data.icons) {
|
|
category.images = formatImages(data.icons);
|
|
}
|
|
|
|
return category;
|
|
};
|
|
|
|
/**
|
|
* Format a snapcast client object into a universal format
|
|
*
|
|
* @param data obj
|
|
* @return obj
|
|
* */
|
|
const formatGroup = function (data) {
|
|
const group = { type: 'group' };
|
|
const fields = [
|
|
'id',
|
|
'name',
|
|
'mute',
|
|
'stream_id',
|
|
'clients_ids',
|
|
];
|
|
|
|
for (const field of fields) {
|
|
if (data.hasOwnProperty(field)) {
|
|
group[field] = data[field];
|
|
}
|
|
}
|
|
|
|
if (group.mute === undefined && data.muted !== undefined) {
|
|
group.mute = data.muted;
|
|
}
|
|
|
|
return group;
|
|
};
|
|
|
|
|
|
const collateLibrary = (uris, itemsIndex) => {
|
|
return compact(uris.map((uri) => itemsIndex[uri]));
|
|
};
|
|
|
|
/**
|
|
* Collate an object with external references into a fully self-contained object
|
|
* We merge *_uris references (ie tracks_uris) into the main object
|
|
*
|
|
* @param object Obj
|
|
* @param indexes Obj (the relevant core indexes)
|
|
* @return object Obj
|
|
* */
|
|
const collate = function (obj, indexes = {}) {
|
|
// First, let's reset this object
|
|
// This is important because by changing this object, we inadvertently
|
|
// change the source object (ie the indexed record), which undoes the
|
|
// efficiencies of a lean index object
|
|
obj = { ...obj };
|
|
|
|
// Setup empty arrays for the appropriate reference objects
|
|
// This helps create a consistent object structure
|
|
if (obj.artists_uris !== undefined) obj.artists = [];
|
|
if (obj.albums_uris !== undefined) obj.albums = [];
|
|
if (obj.tracks_uris !== undefined) obj.tracks = [];
|
|
if (obj.users_uris !== undefined) obj.users = [];
|
|
if (obj.playlists_uris !== undefined) obj.playlists = [];
|
|
if (obj.related_artists_uris !== undefined) obj.related_artists = [];
|
|
if (obj.clients_ids !== undefined) obj.clients = [];
|
|
if (obj.items_uris !== undefined) obj.items = [];
|
|
|
|
if (indexes.artists) {
|
|
if (obj.artists_uris) {
|
|
for (var uri of obj.artists_uris) {
|
|
if (indexes.artists[uri]) {
|
|
obj.artists.push(indexes.artists[uri]);
|
|
}
|
|
}
|
|
}
|
|
if (obj.related_artists_uris) {
|
|
for (var uri of obj.related_artists_uris) {
|
|
if (indexes.artists[uri]) {
|
|
obj.related_artists.push(indexes.artists[uri]);
|
|
}
|
|
}
|
|
}
|
|
if (obj.artist_uri) {
|
|
if (indexes.artists[obj.artist_uri]) {
|
|
obj.artist = indexes.artists[obj.artist_uri];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (indexes.albums) {
|
|
if (obj.albums_uris) {
|
|
for (var uri of obj.albums_uris) {
|
|
if (indexes.albums[uri]) {
|
|
obj.albums.push(indexes.albums[uri]);
|
|
}
|
|
}
|
|
}
|
|
if (obj.album_uri) {
|
|
if (indexes.albums[obj.album_uri]) {
|
|
obj.album = indexes.albums[obj.album_uri];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (indexes.tracks) {
|
|
if (obj.tracks_uris) {
|
|
for (var uri of obj.tracks_uris) {
|
|
if (indexes.tracks[uri]) {
|
|
obj.tracks.push(indexes.tracks[uri]);
|
|
}
|
|
}
|
|
}
|
|
if (obj.track_uri) {
|
|
if (indexes.tracks[obj.track_uri]) {
|
|
obj.track = indexes.tracks[obj.track_uri];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (indexes.users) {
|
|
if (obj.users_uris) {
|
|
for (var uri of obj.users_uris) {
|
|
if (indexes.users[uri]) {
|
|
obj.users.push(indexes.users[uri]);
|
|
}
|
|
}
|
|
}
|
|
if (obj.user_uri) {
|
|
if (indexes.users[obj.user_uri]) {
|
|
obj.user = indexes.users[obj.user_uri];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (indexes.playlists) {
|
|
if (obj.playlists_uris) {
|
|
for (var uri of obj.playlists_uris) {
|
|
if (indexes.playlists[uri]) {
|
|
obj.playlists.push(indexes.playlists[uri]);
|
|
}
|
|
}
|
|
}
|
|
if (obj.playlist_uri) {
|
|
if (indexes.playlists[obj.playlist_uri]) {
|
|
obj.playlist = indexes.playlists[obj.playlist_uri];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (indexes.clients) {
|
|
if (obj.clients_ids) {
|
|
for (const id of obj.clients_ids) {
|
|
if (indexes.clients[id]) {
|
|
obj.clients.push(indexes.clients[id]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (indexes.items) {
|
|
if (obj.items_uris) {
|
|
for (const uri of obj.items_uris) {
|
|
if (indexes.items[uri]) {
|
|
obj.items.push(indexes.items[uri]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return obj;
|
|
};
|
|
|
|
/**
|
|
* Add the array index as our sort_id.
|
|
* This allows us a source of truth for the original order of items when we have applied
|
|
* sort rules in the UI.
|
|
*
|
|
* @param {Array} array of objects to have sort_id added
|
|
*/
|
|
const injectSortId = (array) => {
|
|
return array.map((item, index) => ({ ...item, sort_id: index }));
|
|
};
|
|
|
|
export {
|
|
toJSON,
|
|
getTrackIcon,
|
|
digestMopidyImages,
|
|
formatImages,
|
|
formatSimpleObject,
|
|
formatSimpleObjects,
|
|
formatAlbum,
|
|
formatAlbums,
|
|
formatArtist,
|
|
formatArtists,
|
|
formatPlaylist,
|
|
formatPlaylists,
|
|
formatUser,
|
|
formatUsers,
|
|
formatTrack,
|
|
formatTracks,
|
|
formatClient,
|
|
formatGroup,
|
|
formatCategory,
|
|
formatCategories,
|
|
collate,
|
|
collateLibrary,
|
|
injectSortId,
|
|
encodeUri,
|
|
decodeUri,
|
|
};
|
|
|
|
export default {
|
|
toJSON,
|
|
getTrackIcon,
|
|
digestMopidyImages,
|
|
formatImages,
|
|
formatSimpleObject,
|
|
formatSimpleObjects,
|
|
formatAlbum,
|
|
formatAlbums,
|
|
formatArtist,
|
|
formatArtists,
|
|
formatPlaylist,
|
|
formatPlaylists,
|
|
formatUser,
|
|
formatUsers,
|
|
formatTrack,
|
|
formatTracks,
|
|
formatClient,
|
|
formatGroup,
|
|
formatCategory,
|
|
formatCategories,
|
|
collate,
|
|
collateLibrary,
|
|
injectSortId,
|
|
encodeUri,
|
|
decodeUri,
|
|
};
|