Ability to heart and star tracks whilst playing

Ability to heart and star tracks whilst playing
This commit is contained in:
Simon J
2021-10-07 15:57:09 +11:00
committed by GitHub
parent a02b8c1ecd
commit 8f3d2bddf7
28 changed files with 1739 additions and 861 deletions

View File

@@ -12,7 +12,7 @@ export type KEY =
| "playlists"
| "genres"
| "random"
| "starred"
| "topRated"
| "recentlyAdded"
| "recentlyPlayed"
| "mostPlayed"
@@ -37,7 +37,14 @@ export type KEY =
| "invalidLinkCode"
| "loginSuccessful"
| "loginFailed"
| "noSonosDevices";
| "noSonosDevices"
| "favourites"
| "LOVE"
| "LOVE_SUCCESS"
| "STAR"
| "UNSTAR"
| "STAR_SUCCESS"
| "UNSTAR_SUCCESS";
const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
"en-US": {
@@ -48,7 +55,7 @@ const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
playlists: "Playlists",
genres: "Genres",
random: "Random",
starred: "Starred",
topRated: "Top Rated",
recentlyAdded: "Recently added",
recentlyPlayed: "Recently played",
mostPlayed: "Most played",
@@ -73,6 +80,13 @@ const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
loginSuccessful: "Login successful!",
loginFailed: "Login failed!",
noSonosDevices: "No sonos devices",
favourites: "Favourites",
STAR: "Star track",
UNSTAR: "Un-star track",
STAR_SUCCESS: "Track starred successfully",
UNSTAR_SUCCESS: "Track un-starred successfully",
LOVE: "Love",
LOVE_SUCCESS: "Track loved"
},
"nl-NL": {
AppLinkMessage: "Sonos koppelen aan $BNB_SONOS_SERVICE_NAME",
@@ -82,7 +96,7 @@ const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
playlists: "Afspeellijsten",
genres: "Genres",
random: "Willekeurig",
starred: "Favorieten",
topRated: "Best beoordeeld",
recentlyAdded: "Onlangs toegevoegd",
recentlyPlayed: "Onlangs afgespeeld",
mostPlayed: "Meest afgespeeld",
@@ -107,6 +121,13 @@ const translations: Record<SUPPORTED_LANG, Record<KEY, string>> = {
loginSuccessful: "Inloggen gelukt!",
loginFailed: "Inloggen mislukt!",
noSonosDevices: "Geen Sonos-apparaten",
favourites: "Favorieten",
STAR: "Ster spoor",
UNSTAR: "Track zonder ster",
STAR_SUCCESS: "Track succesvol gemarkeerd",
UNSTAR_SUCCESS: "Succes zonder ster bijhouden",
LOVE: "Liefde ",
LOVE_SUCCESS: "Volg geliefd"
},
};

View File

@@ -166,7 +166,7 @@ export type ICON =
| "playlists"
| "genres"
| "random"
| "starred"
| "topRated"
| "recentlyAdded"
| "recentlyPlayed"
| "mostPlayed"
@@ -225,7 +225,10 @@ export type ICON =
| "skywalker"
| "leia"
| "r2d2"
| "yoda";
| "yoda"
| "heart"
| "star"
| "solidStar";
const iconFrom = (name: string) =>
new SvgIcon(
@@ -241,7 +244,7 @@ export const ICONS: Record<ICON, SvgIcon> = {
playlists: iconFrom("navidrome-playlists.svg"),
genres: iconFrom("Theatre-Mask-111172.svg"),
random: iconFrom("navidrome-random.svg"),
starred: iconFrom("navidrome-topRated.svg"),
topRated: iconFrom("navidrome-topRated.svg"),
recentlyAdded: iconFrom("navidrome-recentlyAdded.svg"),
recentlyPlayed: iconFrom("navidrome-recentlyPlayed.svg"),
mostPlayed: iconFrom("navidrome-mostPlayed.svg"),
@@ -300,6 +303,9 @@ export const ICONS: Record<ICON, SvgIcon> = {
leia: iconFrom("Princess-Leia-68568.svg"),
r2d2: iconFrom("R2-D2-39423.svg"),
yoda: iconFrom("Yoda-68107.svg"),
heart: iconFrom("Heart-85038.svg"),
star: iconFrom("Star-16101.svg"),
solidStar: iconFrom("Star-43879.svg")
};
export const STAR_WARS = [ICONS.c3po, ICONS.chewy, ICONS.darth, ICONS.skywalker, ICONS.leia, ICONS.r2d2, ICONS.yoda];

View File

@@ -54,8 +54,8 @@ export type AlbumSummary = {
genre: Genre | undefined;
coverArt: string | undefined;
artistName: string;
artistId: string;
artistName: string | undefined;
artistId: string | undefined;
};
export type Album = AlbumSummary & {};
@@ -65,6 +65,11 @@ export type Genre = {
id: string;
}
export type Rating = {
love: boolean;
stars: number;
}
export type Track = {
id: string;
name: string;
@@ -75,6 +80,7 @@ export type Track = {
coverArt: string | undefined;
album: AlbumSummary;
artist: ArtistSummary;
rating: Rating;
};
export type Paging = {
@@ -177,6 +183,7 @@ export interface MusicLibrary {
trackId: string;
range: string | undefined;
}): Promise<TrackStream>;
rate(trackId: string, rating: Rating): Promise<boolean>;
coverArt(id: string, size?: number): Promise<CoverArt | undefined>;
nowPlaying(id: string): Promise<boolean>
scrobble(id: string): Promise<boolean>

View File

@@ -4,6 +4,7 @@ import * as Eta from "eta";
import path from "path";
import sharp from "sharp";
import { v4 as uuid } from "uuid";
import dayjs from "dayjs";
import { PassThrough, Transform, TransformCallback } from "stream";
@@ -17,6 +18,8 @@ import {
CREATE_REGISTRATION_ROUTE,
REMOVE_REGISTRATION_ROUTE,
sonosifyMimeType,
ratingFromInt,
ratingAsInt,
} from "./smapi";
import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
import { MusicService, isSuccess } from "./music_service";
@@ -107,6 +110,8 @@ function server(
const accessTokens = serverOpts.accessTokens();
const clock = serverOpts.clock;
const startUpTime = dayjs();
const app = express();
const i8n = makeI8N(service.name);
@@ -253,6 +258,28 @@ function server(
});
app.get(PRESENTATION_MAP_ROUTE, (_, res) => {
const LastModified = startUpTime.format("HH:mm:ss D MMM YYYY");
const nowPlayingRatingsMatch = (value: number) => {
const rating = ratingFromInt(value);
const nextLove = { ...rating, love: !rating.love };
const nextStar = { ...rating, stars: (rating.stars === 5 ? 0 : rating.stars + 1) }
const loveRatingIcon = bonobUrl.append({pathname: rating.love ? '/love-selected.svg' : '/love-unselected.svg'}).href();
const starsRatingIcon = bonobUrl.append({pathname: `/star${rating.stars}.svg`}).href();
return `<Match propname="rating" value="${value}">
<Ratings>
<Rating Id="${ratingAsInt(nextLove)}" AutoSkip="NEVER" OnSuccessStringId="LOVE_SUCCESS" StringId="LOVE">
<Icon Controller="universal" LastModified="${LastModified}" Uri="${loveRatingIcon}" />
</Rating>
<Rating Id="${-ratingAsInt(nextStar)}" AutoSkip="NEVER" OnSuccessStringId="STAR_SUCCESS" StringId="STAR">
<Icon Controller="universal" LastModified="${LastModified}" Uri="${starsRatingIcon}" />
</Rating>
</Ratings>
</Match>`
}
res.type("application/xml").send(`<?xml version="1.0" encoding="utf-8" ?>
<Presentation>
<PresentationMap type="ArtWorkSizeMap">
@@ -285,6 +312,20 @@ function server(
</SearchCategories>
</Match>
</PresentationMap>
<PresentationMap type="NowPlayingRatings" trackEnabled="true" programEnabled="false">
${nowPlayingRatingsMatch(100)}
${nowPlayingRatingsMatch(101)}
${nowPlayingRatingsMatch(110)}
${nowPlayingRatingsMatch(111)}
${nowPlayingRatingsMatch(120)}
${nowPlayingRatingsMatch(121)}
${nowPlayingRatingsMatch(130)}
${nowPlayingRatingsMatch(131)}
${nowPlayingRatingsMatch(140)}
${nowPlayingRatingsMatch(141)}
${nowPlayingRatingsMatch(150)}
${nowPlayingRatingsMatch(151)}
</PresentationMap>
</Presentation>`);
});

View File

@@ -14,6 +14,7 @@ import {
Genre,
MusicService,
Playlist,
Rating,
slice2,
Track,
} from "./music_service";
@@ -80,6 +81,12 @@ export type GetDeviceAuthTokenResult = {
};
};
export const ratingAsInt = (rating: Rating): number => rating.stars * 10 + (rating.love ? 1 : 0) + 100;
export const ratingFromInt = (value: number): Rating => {
const x = value - 100;
return { love: (x % 10 == 1), stars: Math.floor(x / 10) }
};
export type MediaCollection = {
id: string;
itemType: "collection";
@@ -300,6 +307,9 @@ export const track = (bonobUrl: URLBuilder, track: Track) => ({
genreId: track.album.genre?.id,
trackNumber: track.number,
},
dynamic: {
property: [{ name: "rating", value: `${ratingAsInt(track.rating)}` }],
}
});
export const artist = (bonobUrl: URLBuilder, artist: ArtistSummary) => ({
@@ -403,7 +413,7 @@ function bindSmapiSoapServiceToExpress(
getMediaURIResult: bonobUrl
.append({
pathname: `/stream/${type}/${typeId}`,
searchParams: { "bat": accessToken }
searchParams: { bat: accessToken },
})
.href(),
})),
@@ -416,7 +426,10 @@ function bindSmapiSoapServiceToExpress(
.then(splitId(id))
.then(async ({ musicLibrary, accessToken, typeId }) =>
musicLibrary.track(typeId!).then((it) => ({
getMediaMetadataResult: track(urlWithToken(accessToken), it),
getMediaMetadataResult: track(
urlWithToken(accessToken),
it
),
}))
),
search: async (
@@ -503,9 +516,10 @@ function bindSmapiSoapServiceToExpress(
case "track":
return musicLibrary.track(typeId).then((it) => ({
getExtendedMetadataResult: {
mediaMetadata: {
...track(urlWithToken(accessToken), it),
},
mediaMetadata: track(
urlWithToken(accessToken),
it
),
},
}));
case "album":
@@ -580,6 +594,24 @@ function bindSmapiSoapServiceToExpress(
albumArtURI: iconArtURI(bonobUrl, "albums").href(),
itemType: "albumList",
},
{
id: "randomAlbums",
title: lang("random"),
albumArtURI: iconArtURI(bonobUrl, "random").href(),
itemType: "albumList",
},
{
id: "favouriteAlbums",
title: lang("favourites"),
albumArtURI: iconArtURI(bonobUrl, "heart").href(),
itemType: "albumList",
},
// {
// id: "topRatedAlbums",
// title: lang("topRated"),
// albumArtURI: iconArtURI(bonobUrl, "star").href(),
// itemType: "albumList",
// },
{
id: "playlists",
title: lang("playlists"),
@@ -597,18 +629,6 @@ function bindSmapiSoapServiceToExpress(
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
itemType: "container",
},
{
id: "randomAlbums",
title: lang("random"),
albumArtURI: iconArtURI(bonobUrl, "random").href(),
itemType: "albumList",
},
{
id: "starredAlbums",
title: lang("starred"),
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
itemType: "albumList",
},
{
id: "recentlyAdded",
title: lang("recentlyAdded"),
@@ -689,7 +709,7 @@ function bindSmapiSoapServiceToExpress(
type: "random",
...paging,
});
case "starredAlbums":
case "favouriteAlbums":
return albums({
type: "starred",
...paging,
@@ -872,6 +892,18 @@ function bindSmapiSoapServiceToExpress(
}
})
.then((_) => ({ removeFromContainerResult: { updateId: "" } })),
rateItem: async (
{ id, rating }: { id: string; rating: number },
_,
soapyHeaders: SoapyHeaders
) =>
auth(musicService, accessTokens, soapyHeaders?.credentials)
.then(splitId(id))
.then(({ musicLibrary, typeId }) =>
musicLibrary.rate(typeId, ratingFromInt(Math.abs(rating)))
)
.then((_) => ({ rateItemResult: { shouldSkip: false } })),
setPlayedSeconds: async (
{ id, seconds }: { id: string; seconds: string },
_,

View File

@@ -24,25 +24,25 @@ export const SONOS_LANG: LANG[] = [
"zh-CN",
];
export const PRESENTATION_AND_STRINGS_VERSION = "21";
export const PRESENTATION_AND_STRINGS_VERSION =
process.env["BNB_DEBUG"] === "true"
? `${Math.round(new Date().getTime() / 1000)}`
: "23";
// NOTE: manifest requires https for the URL,
// otherwise you will get an error trying to register
// NOTE: manifest requires https for the URL, otherwise you will get an error trying to register
export type Capability =
| "search"
| "trFavorites"
| "alFavorites"
| "ucPlaylists"
| "extendedMD"
| "trFavorites" // Favorites: Adding/Removing Tracks (deprecated)
| "alFavorites" // Favorites: Adding/Removing Albums (deprecated)
| "ucPlaylists" // User Content Playlists
| "extendedMD" // Extended Metadata (More Menu, Info & Options)
| "contextHeaders"
| "authorizationHeader"
| "logging"
| "logging" // Playback duration logging at track end (deprecated)
| "manifest";
export const BONOB_CAPABILITIES: Capability[] = [
"search",
// "trFavorites",
// "alFavorites",
"ucPlaylists",
"extendedMD",
"logging",
@@ -247,9 +247,7 @@ export type Discovery = {
seedHost?: string;
};
export default (
sonosDiscovery: Discovery = { enabled: true }
): Sonos =>
export default (sonosDiscovery: Discovery = { enabled: true }): Sonos =>
sonosDiscovery.enabled
? autoDiscoverySonos(sonosDiscovery.seedHost)
: SONOS_DISABLED;

View File

@@ -19,8 +19,8 @@ import {
Genre,
Track,
CoverArt,
Rating,
} from "./music_service";
import X2JS from "x2js";
import sharp from "sharp";
import _ from "underscore";
import fse from "fs-extra";
@@ -60,36 +60,36 @@ export const isDodgyImage = (url: string) => url.endsWith(DODGY_IMAGE_NAME);
export const validate = (url: string | undefined) =>
url && !isDodgyImage(url) ? url : undefined;
export type SubconicEnvelope = {
export type SubsonicEnvelope = {
"subsonic-response": SubsonicResponse;
};
export type SubsonicResponse = {
_status: string;
status: string;
};
export type album = {
_id: string;
_name: string;
_genre: string | undefined;
_year: string | undefined;
_coverArt: string | undefined;
_artist: string;
_artistId: string;
id: string;
name: string;
artist: string | undefined;
artistId: string | undefined;
coverArt: string | undefined;
genre: string | undefined;
year: string | undefined;
};
export type artistSummary = {
_id: string;
_name: string;
_albumCount: string;
_artistImageUrl: string | undefined;
id: string;
name: string;
albumCount: number;
artistImageUrl: string | undefined;
};
export type GetArtistsResponse = SubsonicResponse & {
artists: {
index: {
artist: artistSummary[];
_name: string;
name: string;
}[];
};
};
@@ -101,9 +101,9 @@ export type GetAlbumListResponse = SubsonicResponse & {
};
export type genre = {
_songCount: string;
_albumCount: string;
__text: string;
songCount: number;
albumCount: number;
value: string;
};
export type GetGenresResponse = SubsonicResponse & {
@@ -114,8 +114,8 @@ export type GetGenresResponse = SubsonicResponse & {
export type SubsonicError = SubsonicResponse & {
error: {
_code: string;
_message: string;
code: string;
message: string;
};
};
@@ -145,22 +145,25 @@ export type GetArtistResponse = SubsonicResponse & {
};
export type song = {
_id: string;
_parent: string;
_title: string;
_album: string;
_artist: string;
_track: string | undefined;
_genre: string;
_coverArt: string | undefined;
_created: "2004-11-08T23:36:11";
_duration: string | undefined;
_bitRate: "128";
_suffix: "mp3";
_contentType: string;
_albumId: string;
_artistId: string;
_type: "music";
id: string;
parent: string | undefined;
title: string;
album: string | undefined;
artist: string | undefined;
track: number | undefined;
year: string | undefined;
genre: string | undefined;
coverArt: string | undefined;
created: string | undefined;
duration: number | undefined;
bitRate: number | undefined;
suffix: string | undefined;
contentType: string | undefined;
albumId: string | undefined;
artistId: string | undefined;
type: string | undefined;
userRating: number | undefined;
starred: string | undefined;
};
export type GetAlbumResponse = {
@@ -170,31 +173,15 @@ export type GetAlbumResponse = {
};
export type playlist = {
_id: string;
_name: string;
};
export type entry = {
_id: string;
_parent: string;
_title: string;
_album: string;
_artist: string;
_track: string;
_year: string;
_genre: string;
_coverArt: string;
_contentType: string;
_duration: string;
_albumId: string;
_artistId: string;
id: string;
name: string;
};
export type GetPlaylistResponse = {
playlist: {
_id: string;
_name: string;
entry: entry[];
id: string;
name: string;
entry: song[];
};
};
@@ -214,6 +201,12 @@ export type GetSongResponse = {
song: song;
};
export type GetStarredResponse = {
starred2: {
song: song[];
};
};
export type Search3Response = SubsonicResponse & {
searchResult3: {
artist: artistSummary[];
@@ -253,29 +246,33 @@ export const MAX_ALBUM_LIST = 500;
const maybeAsCoverArt = (coverArt: string | undefined) =>
coverArt ? `coverArt:${coverArt}` : undefined;
const asTrack = (album: Album, song: song) => ({
id: song._id,
name: song._title,
mimeType: song._contentType,
duration: parseInt(song._duration || "0"),
number: parseInt(song._track || "0"),
genre: maybeAsGenre(song._genre),
coverArt: maybeAsCoverArt(song._coverArt),
export const asTrack = (album: Album, song: song): Track => ({
id: song.id,
name: song.title,
mimeType: song.contentType!,
duration: song.duration || 0,
number: song.track || 0,
genre: maybeAsGenre(song.genre),
coverArt: maybeAsCoverArt(song.coverArt),
album,
artist: {
id: song._artistId,
name: song._artist,
id: `${song.artistId!}`,
name: song.artist!,
},
rating: {
love: song.starred != undefined,
stars: (song.userRating && song.userRating <= 5 && song.userRating >= 0 ? song.userRating : 0),
},
});
const asAlbum = (album: album) => ({
id: album._id,
name: album._name,
year: album._year,
genre: maybeAsGenre(album._genre),
artistId: album._artistId,
artistName: album._artist,
coverArt: maybeAsCoverArt(album._coverArt),
id: album.id,
name: album.name,
year: album.year,
genre: maybeAsGenre(album.genre),
artistId: album.artistId,
artistName: album.artist,
coverArt: maybeAsCoverArt(album.coverArt),
});
export const asGenre = (genreName: string) => ({
@@ -318,7 +315,7 @@ export type ImageFetcher = (url: string) => Promise<CoverArt | undefined>;
export const cachingImageFetcher =
(cacheDir: string, delegate: ImageFetcher) =>
(url: string): Promise<CoverArt | undefined> => {
async (url: string): Promise<CoverArt | undefined> => {
const filename = path.join(cacheDir, `${Md5.hashStr(url)}.png`);
return fse
.readFile(filename)
@@ -402,31 +399,11 @@ export class Subsonic implements MusicService {
path: string,
q: {} = {}
): Promise<T> =>
this.get({ username, password }, path, q)
.then(
(response) =>
new X2JS({
arrayAccessFormPaths: [
"subsonic-response.album.song",
"subsonic-response.albumList2.album",
"subsonic-response.artist.album",
"subsonic-response.artists.index",
"subsonic-response.artists.index.artist",
"subsonic-response.artistInfo2.similarArtist",
"subsonic-response.genres.genre",
"subsonic-response.playlist.entry",
"subsonic-response.playlists.playlist",
"subsonic-response.searchResult3.album",
"subsonic-response.searchResult3.artist",
"subsonic-response.searchResult3.song",
"subsonic-response.similarSongs2.song",
"subsonic-response.topSongs.song",
],
}).xml2js(response.data) as SubconicEnvelope
)
this.get({ username, password }, path, { f: "json", ...q })
.then((response) => response.data as SubsonicEnvelope)
.then((json) => json["subsonic-response"])
.then((json) => {
if (isError(json)) throw `Subsonic error:${json.error._message}`;
if (isError(json)) throw `Subsonic error:${json.error.message}`;
else return json as unknown as T;
});
@@ -451,9 +428,9 @@ export class Subsonic implements MusicService {
.then((it) => (it.artists.index || []).flatMap((it) => it.artist || []))
.then((artists) =>
artists.map((artist) => ({
id: artist._id,
name: artist._name,
albumCount: Number.parseInt(artist._albumCount),
id: `${artist.id}`,
name: artist.name,
albumCount: artist.albumCount,
}))
);
@@ -469,9 +446,9 @@ export class Subsonic implements MusicService {
large: validate(it.artistInfo2.largeImageUrl),
},
similarArtist: (it.artistInfo2.similarArtist || []).map((artist) => ({
id: artist._id,
name: artist._name,
inLibrary: artist._id != "-1",
id: `${artist.id}`,
name: artist.name,
inLibrary: artist.id != "-1",
})),
}));
@@ -479,13 +456,13 @@ export class Subsonic implements MusicService {
this.getJSON<GetAlbumResponse>(credentials, "/rest/getAlbum", { id })
.then((it) => it.album)
.then((album) => ({
id: album._id,
name: album._name,
year: album._year,
genre: maybeAsGenre(album._genre),
artistId: album._artistId,
artistName: album._artist,
coverArt: maybeAsCoverArt(album._coverArt),
id: album.id,
name: album.name,
year: album.year,
genre: maybeAsGenre(album.genre),
artistId: album.artistId,
artistName: album.artist,
coverArt: maybeAsCoverArt(album.coverArt),
}));
getArtist = (
@@ -497,8 +474,8 @@ export class Subsonic implements MusicService {
})
.then((it) => it.artist)
.then((it) => ({
id: it._id,
name: it._name,
id: it.id,
name: it.name,
albums: this.toAlbumSummary(it.album || []),
}));
@@ -526,20 +503,25 @@ export class Subsonic implements MusicService {
})
.then((it) => it.song)
.then((song) =>
this.getAlbum(credentials, song._albumId).then((album) =>
this.getAlbum(credentials, song.albumId!).then((album) =>
asTrack(album, song)
)
);
getStarred = (credentials: Credentials) =>
this.getJSON<GetStarredResponse>(credentials, "/rest/getStarred2").then(
(it) => new Set(it.starred2.song.map((it) => it.id))
);
toAlbumSummary = (albumList: album[]): AlbumSummary[] =>
albumList.map((album) => ({
id: album._id,
name: album._name,
year: album._year,
genre: maybeAsGenre(album._genre),
artistId: album._artistId,
artistName: album._artist,
coverArt: maybeAsCoverArt(album._coverArt),
id: album.id,
name: album.name,
year: album.year,
genre: maybeAsGenre(album.genre),
artistId: album.artistId,
artistName: album.artist,
coverArt: maybeAsCoverArt(album.coverArt),
}));
search3 = (credentials: Credentials, q: any) =>
@@ -596,8 +578,8 @@ export class Subsonic implements MusicService {
.then((it) =>
pipe(
it.genres.genre || [],
A.filter((it) => Number.parseInt(it._albumCount) > 0),
A.map((it) => it.__text),
A.filter((it) => it.albumCount > 0),
A.map((it) => it.value),
A.sort(ordString),
A.map((it) => ({ id: b64Encode(it), name: it }))
)
@@ -612,6 +594,40 @@ export class Subsonic implements MusicService {
(album.song || []).map((song) => asTrack(asAlbum(album), song))
),
track: (trackId: string) => subsonic.getTrack(credentials, trackId),
rate: (trackId: string, rating: Rating) =>
Promise.resolve(true)
.then(() => {
if (rating.stars >= 0 && rating.stars <= 5) {
return subsonic.getTrack(credentials, trackId);
} else {
throw `Invalid rating.stars value of ${rating.stars}`;
}
})
.then((track) => {
const thingsToUpdate = [];
if (track.rating.love != rating.love) {
thingsToUpdate.push(
subsonic.getJSON(
credentials,
`/rest/${rating.love ? "star" : "unstar"}`,
{
id: trackId,
}
)
);
}
if (track.rating.stars != rating.stars) {
thingsToUpdate.push(
subsonic.getJSON(credentials, `/rest/setRating`, {
id: trackId,
rating: rating.stars,
})
);
}
return Promise.all(thingsToUpdate);
})
.then(() => true)
.catch(() => false),
stream: async ({
trackId,
range,
@@ -713,7 +729,7 @@ export class Subsonic implements MusicService {
},
scrobble: async (id: string) =>
subsonic
.get(credentials, `/rest/scrobble`, {
.getJSON(credentials, `/rest/scrobble`, {
id,
submission: true,
})
@@ -721,7 +737,7 @@ export class Subsonic implements MusicService {
.catch(() => false),
nowPlaying: async (id: string) =>
subsonic
.get(credentials, `/rest/scrobble`, {
.getJSON(credentials, `/rest/scrobble`, {
id,
submission: false,
})
@@ -732,8 +748,8 @@ export class Subsonic implements MusicService {
.search3(credentials, { query, artistCount: 20 })
.then(({ artists }) =>
artists.map((artist) => ({
id: artist._id,
name: artist._name,
id: artist.id,
name: artist.name,
}))
),
searchAlbums: async (query: string) =>
@@ -745,7 +761,7 @@ export class Subsonic implements MusicService {
.search3(credentials, { query, songCount: 20 })
.then(({ songs }) =>
Promise.all(
songs.map((it) => subsonic.getTrack(credentials, it._id))
songs.map((it) => subsonic.getTrack(credentials, it.id))
)
),
playlists: async () =>
@@ -753,7 +769,7 @@ export class Subsonic implements MusicService {
.getJSON<GetPlaylistsResponse>(credentials, "/rest/getPlaylists")
.then((it) => it.playlists.playlist || [])
.then((playlists) =>
playlists.map((it) => ({ id: it._id, name: it._name }))
playlists.map((it) => ({ id: it.id, name: it.name }))
),
playlist: async (id: string) =>
subsonic
@@ -764,29 +780,22 @@ export class Subsonic implements MusicService {
.then((playlist) => {
let trackNumber = 1;
return {
id: playlist._id,
name: playlist._name,
id: playlist.id,
name: playlist.name,
entries: (playlist.entry || []).map((entry) => ({
id: entry._id,
name: entry._title,
mimeType: entry._contentType,
duration: parseInt(entry._duration || "0"),
...asTrack(
{
id: entry.albumId!,
name: entry.album!,
year: entry.year,
genre: maybeAsGenre(entry.genre),
artistName: entry.artist,
artistId: entry.artistId,
coverArt: maybeAsCoverArt(entry.coverArt),
},
entry
),
number: trackNumber++,
genre: maybeAsGenre(entry._genre),
coverArt: maybeAsCoverArt(entry._coverArt),
album: {
id: entry._albumId,
name: entry._album,
year: entry._year,
genre: maybeAsGenre(entry._genre),
artistName: entry._artist,
artistId: entry._artistId,
coverArt: maybeAsCoverArt(entry._coverArt),
},
artist: {
id: entry._artistId,
name: entry._artist,
},
})),
};
}),
@@ -796,7 +805,7 @@ export class Subsonic implements MusicService {
name,
})
.then((it) => it.playlist)
.then((it) => ({ id: it._id, name: it._name })),
.then((it) => ({ id: it.id, name: it.name })),
deletePlaylist: async (id: string) =>
subsonic
.getJSON<GetPlaylistResponse>(credentials, "/rest/deletePlaylist", {
@@ -829,7 +838,7 @@ export class Subsonic implements MusicService {
Promise.all(
songs.map((song) =>
subsonic
.getAlbum(credentials, song._albumId)
.getAlbum(credentials, song.albumId!)
.then((album) => asTrack(album, song))
)
)
@@ -846,7 +855,7 @@ export class Subsonic implements MusicService {
Promise.all(
songs.map((song) =>
subsonic
.getAlbum(credentials, song._albumId)
.getAlbum(credentials, song.albumId!)
.then((album) => asTrack(album, song))
)
)