mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Fix issue where transcoded files would not play, provide support for custom clients to transcode (#194)
This commit is contained in:
@@ -16,5 +16,12 @@
|
|||||||
"version": "latest",
|
"version": "latest",
|
||||||
"moby": true
|
"moby": true
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ Support for Subsonic API clones (tested against Navidrome and Gonic).
|
|||||||
- Discovery of sonos devices using seed IP address
|
- Discovery of sonos devices using seed IP address
|
||||||
- Auto registration with sonos on start
|
- Auto registration with sonos on start
|
||||||
- Multiple registrations within a single household.
|
- Multiple registrations within a single household.
|
||||||
- Transcoding support for flacs using a specific player for the flac mimeType bonob/sonos
|
- Transcoding within subsonic clone
|
||||||
|
- Custom players by mime type, allowing custom transcoding rules for different file types
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|
||||||
@@ -163,7 +164,6 @@ BNB_URL | http://$(hostname):4534 | URL (including path) for bonob so that sonos
|
|||||||
BNB_SECRET | bonob | secret used for encrypting credentials
|
BNB_SECRET | bonob | secret used for encrypting credentials
|
||||||
BNB_AUTH_TIMEOUT | 1h | Timeout for the sonos auth token, described in the format [ms](https://github.com/vercel/ms), ie. '5s' == 5 seconds, '11h' == 11 hours. In the case of using Navidrome this should be less than the value for ND_SESSIONTIMEOUT
|
BNB_AUTH_TIMEOUT | 1h | Timeout for the sonos auth token, described in the format [ms](https://github.com/vercel/ms), ie. '5s' == 5 seconds, '11h' == 11 hours. In the case of using Navidrome this should be less than the value for ND_SESSIONTIMEOUT
|
||||||
BNB_LOG_LEVEL | info | Log level. One of ['debug', 'info', 'warn', 'error']
|
BNB_LOG_LEVEL | info | Log level. One of ['debug', 'info', 'warn', 'error']
|
||||||
BNB_DISABLE_PLAYLIST_ART | undefined | Disables playlist art generation, ie. when there are many playlists and art generation takes too long
|
|
||||||
BNB_SERVER_LOG_REQUESTS | false | Whether or not to log http requests
|
BNB_SERVER_LOG_REQUESTS | false | Whether or not to log http requests
|
||||||
BNB_SONOS_AUTO_REGISTER | false | Whether or not to try and auto-register on startup
|
BNB_SONOS_AUTO_REGISTER | false | Whether or not to try and auto-register on startup
|
||||||
BNB_SONOS_DEVICE_DISCOVERY | true | Enable/Disable sonos device discovery entirely. Setting this to 'false' will disable sonos device search, regardless of whether a seed host is specified.
|
BNB_SONOS_DEVICE_DISCOVERY | true | Enable/Disable sonos device discovery entirely. Setting this to 'false' will disable sonos device search, regardless of whether a seed host is specified.
|
||||||
@@ -171,7 +171,7 @@ BNB_SONOS_SEED_HOST | undefined | sonos device seed host for discovery, or ommit
|
|||||||
BNB_SONOS_SERVICE_NAME | bonob | service name for sonos
|
BNB_SONOS_SERVICE_NAME | bonob | service name for sonos
|
||||||
BNB_SONOS_SERVICE_ID | 246 | service id for sonos
|
BNB_SONOS_SERVICE_ID | 246 | service id for sonos
|
||||||
BNB_SUBSONIC_URL | http://$(hostname):4533 | URL for subsonic clone
|
BNB_SUBSONIC_URL | http://$(hostname):4533 | URL for subsonic clone
|
||||||
BNB_SUBSONIC_CUSTOM_CLIENTS | undefined | Comma delimeted mime types for custom subsonic clients when streaming. ie. "audio/flac,audio/ogg" would use client = 'bonob+audio/flac' for flacs, and 'bonob+audio/ogg' for oggs.
|
BNB_SUBSONIC_CUSTOM_CLIENTS | undefined | Comma delimeted mime types for custom subsonic clients when streaming. Must specify by the source mime type and the transcoded mime type. For example; <p>If you want to simply re-encode some flacs, then you could specify just "audio/flac". <p>However; if your subsonic server will transcode the track then you need to specify the resulting mime type, ie. "audio/flac>audio/mp3" <p>If you want to specify many something like; "audio/flac>audio/mp3,audio/ogg" would use client = 'bonob+audio/flac' for flacs, and 'bonob+audio/ogg' for oggs. <p>!!! Getting this configuration wrong will confuse SONOS as it will expect the wrong mime type for a track, as a result it will not play. Use with care...
|
||||||
BNB_SUBSONIC_ARTIST_IMAGE_CACHE | undefined | Path for caching of artist images that are sourced externally. ie. Navidrome provides spotify URLs. Remember to provide a volume-mapping for Docker, when enabling this cache.
|
BNB_SUBSONIC_ARTIST_IMAGE_CACHE | undefined | Path for caching of artist images that are sourced externally. ie. Navidrome provides spotify URLs. Remember to provide a volume-mapping for Docker, when enabling this cache.
|
||||||
BNB_SCROBBLE_TRACKS | true | Whether to scrobble the playing of a track if it has been played for >30s
|
BNB_SCROBBLE_TRACKS | true | Whether to scrobble the playing of a track if it has been played for >30s
|
||||||
BNB_REPORT_NOW_PLAYING | true | Whether to report a track as now playing
|
BNB_REPORT_NOW_PLAYING | true | Whether to report a track as now playing
|
||||||
|
|||||||
@@ -68,8 +68,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"clean": "rm -Rf build node_modules",
|
"clean": "rm -Rf build node_modules",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"dev": "BNB_LOG_LEVEL=debug BNB_DEBUG=true BNB_SCROBBLE_TRACKS=false BNB_REPORT_NOW_PLAYING=false BNB_SONOS_SEED_HOST=$BNB_DEV_SONOS_DEVICE_IP BNB_SONOS_SERVICE_NAME=z_bonobDev BNB_URL=\"http://${BNB_DEV_HOST_IP}:4534\" BNB_SUBSONIC_URL=\"${BNB_DEV_SUBSONIC_URL}\" nodemon -V ./src/app.ts",
|
"dev": "BNB_SUBSONIC_CUSTOM_CLIENTS1=audio/flac,audio/mpeg,audio/mp4\\>audio/flac BNB_LOG_LEVEL=debug BNB_DEBUG=true BNB_SCROBBLE_TRACKS=false BNB_REPORT_NOW_PLAYING=false BNB_SONOS_SEED_HOST=$BNB_DEV_SONOS_DEVICE_IP BNB_SONOS_SERVICE_NAME=z_bonobDev BNB_URL=\"http://${BNB_DEV_HOST_IP}:4534\" BNB_SUBSONIC_URL=\"${BNB_DEV_SUBSONIC_URL}\" nodemon -V ./src/app.ts",
|
||||||
"devr": "BNB_DISABLE_PLAYLIST_ART=true BNB_LOG_LEVEL=debug BNB_DEBUG=true BNB_SCROBBLE_TRACKS=false BNB_REPORT_NOW_PLAYING=false BNB_SONOS_SEED_HOST=$BNB_DEV_SONOS_DEVICE_IP BNB_SONOS_SERVICE_NAME=z_bonobDev BNB_SONOS_DEVICE_DISCOVERY=true BNB_SONOS_AUTO_REGISTER=true BNB_URL=\"http://${BNB_DEV_HOST_IP}:4534\" BNB_SUBSONIC_URL=\"${BNB_DEV_SUBSONIC_URL}\" nodemon -V ./src/app.ts",
|
"devr": "BNB_LOG_LEVEL=debug BNB_DEBUG=true BNB_SCROBBLE_TRACKS=false BNB_REPORT_NOW_PLAYING=false BNB_SONOS_SEED_HOST=$BNB_DEV_SONOS_DEVICE_IP BNB_SONOS_SERVICE_NAME=z_bonobDev BNB_SONOS_DEVICE_DISCOVERY=true BNB_SONOS_AUTO_REGISTER=true BNB_URL=\"http://${BNB_DEV_HOST_IP}:4534\" BNB_SUBSONIC_URL=\"${BNB_DEV_SUBSONIC_URL}\" nodemon -V ./src/app.ts",
|
||||||
"register-dev": "ts-node ./src/register.ts http://${BNB_DEV_HOST_IP}:4534",
|
"register-dev": "ts-node ./src/register.ts http://${BNB_DEV_HOST_IP}:4534",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"testw": "jest --watch",
|
"testw": "jest --watch",
|
||||||
|
|||||||
12
src/app.ts
12
src/app.ts
@@ -4,11 +4,11 @@ import server from "./server";
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
appendMimeTypeToClientFor,
|
|
||||||
axiosImageFetcher,
|
axiosImageFetcher,
|
||||||
cachingImageFetcher,
|
cachingImageFetcher,
|
||||||
DEFAULT,
|
|
||||||
Subsonic,
|
Subsonic,
|
||||||
|
TranscodingCustomPlayers,
|
||||||
|
NO_CUSTOM_PLAYERS
|
||||||
} from "./subsonic";
|
} from "./subsonic";
|
||||||
import { InMemoryAPITokens, sha256 } from "./api_tokens";
|
import { InMemoryAPITokens, sha256 } from "./api_tokens";
|
||||||
import { InMemoryLinkCodes } from "./link_codes";
|
import { InMemoryLinkCodes } from "./link_codes";
|
||||||
@@ -32,9 +32,9 @@ const bonob = bonobService(
|
|||||||
|
|
||||||
const sonosSystem = sonos(config.sonos.discovery);
|
const sonosSystem = sonos(config.sonos.discovery);
|
||||||
|
|
||||||
const streamUserAgent = config.subsonic.customClientsFor
|
const customPlayers = config.subsonic.customClientsFor
|
||||||
? appendMimeTypeToClientFor(config.subsonic.customClientsFor.split(","))
|
? TranscodingCustomPlayers.from(config.subsonic.customClientsFor)
|
||||||
: DEFAULT;
|
: NO_CUSTOM_PLAYERS;
|
||||||
|
|
||||||
const artistImageFetcher = config.subsonic.artistImageCache
|
const artistImageFetcher = config.subsonic.artistImageCache
|
||||||
? cachingImageFetcher(config.subsonic.artistImageCache, axiosImageFetcher)
|
? cachingImageFetcher(config.subsonic.artistImageCache, axiosImageFetcher)
|
||||||
@@ -42,7 +42,7 @@ const artistImageFetcher = config.subsonic.artistImageCache
|
|||||||
|
|
||||||
const subsonic = new Subsonic(
|
const subsonic = new Subsonic(
|
||||||
config.subsonic.url,
|
config.subsonic.url,
|
||||||
streamUserAgent,
|
customPlayers,
|
||||||
artistImageFetcher
|
artistImageFetcher
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -51,10 +51,15 @@ export type Rating = {
|
|||||||
stars: number;
|
stars: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Encoding = {
|
||||||
|
player: string,
|
||||||
|
mimeType: string
|
||||||
|
}
|
||||||
|
|
||||||
export type Track = {
|
export type Track = {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
mimeType: string;
|
encoding: Encoding,
|
||||||
duration: number;
|
duration: number;
|
||||||
number: number | undefined;
|
number: number | undefined;
|
||||||
genre: Genre | undefined;
|
genre: Genre | undefined;
|
||||||
|
|||||||
@@ -302,7 +302,7 @@ export const album = (bonobUrl: URLBuilder, album: AlbumSummary) => ({
|
|||||||
export const track = (bonobUrl: URLBuilder, track: Track) => ({
|
export const track = (bonobUrl: URLBuilder, track: Track) => ({
|
||||||
itemType: "track",
|
itemType: "track",
|
||||||
id: `track:${track.id}`,
|
id: `track:${track.id}`,
|
||||||
mimeType: sonosifyMimeType(track.mimeType),
|
mimeType: sonosifyMimeType(track.encoding.mimeType),
|
||||||
title: track.name,
|
title: track.name,
|
||||||
|
|
||||||
trackMetadata: {
|
trackMetadata: {
|
||||||
|
|||||||
164
src/subsonic.ts
164
src/subsonic.ts
@@ -20,7 +20,8 @@ import {
|
|||||||
AlbumQueryType,
|
AlbumQueryType,
|
||||||
Artist,
|
Artist,
|
||||||
AuthFailure,
|
AuthFailure,
|
||||||
PlaylistSummary
|
PlaylistSummary,
|
||||||
|
Encoding,
|
||||||
} from "./music_service";
|
} from "./music_service";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
@@ -163,7 +164,7 @@ export type song = {
|
|||||||
duration: number | undefined;
|
duration: number | undefined;
|
||||||
bitRate: number | undefined;
|
bitRate: number | undefined;
|
||||||
suffix: string | undefined;
|
suffix: string | undefined;
|
||||||
contentType: string | undefined;
|
contentType: string;
|
||||||
transcodedContentType: string | undefined;
|
transcodedContentType: string | undefined;
|
||||||
type: string | undefined;
|
type: string | undefined;
|
||||||
userRating: number | undefined;
|
userRating: number | undefined;
|
||||||
@@ -275,10 +276,16 @@ export const artistImageURN = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const asTrack = (album: Album, song: song): Track => ({
|
export const asTrack = (album: Album, song: song, customPlayers: CustomPlayers): Track => ({
|
||||||
id: song.id,
|
id: song.id,
|
||||||
name: song.title,
|
name: song.title,
|
||||||
mimeType: song.transcodedContentType ? song.transcodedContentType : song.contentType!,
|
encoding: pipe(
|
||||||
|
customPlayers.encodingFor({ mimeType: song.contentType }),
|
||||||
|
O.getOrElse(() => ({
|
||||||
|
player: DEFAULT_CLIENT_APPLICATION,
|
||||||
|
mimeType: song.transcodedContentType ? song.transcodedContentType : song.contentType
|
||||||
|
}))
|
||||||
|
),
|
||||||
duration: song.duration || 0,
|
duration: song.duration || 0,
|
||||||
number: song.track || 0,
|
number: song.track || 0,
|
||||||
genre: maybeAsGenre(song.genre),
|
genre: maybeAsGenre(song.genre),
|
||||||
@@ -311,11 +318,11 @@ const asAlbum = (album: album): Album => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// coverArtURN
|
// coverArtURN
|
||||||
const asPlayListSummary = (playlist: playlist): PlaylistSummary => ({
|
const asPlayListSummary = (playlist: playlist): PlaylistSummary => ({
|
||||||
id: playlist.id,
|
id: playlist.id,
|
||||||
name: playlist.name,
|
name: playlist.name,
|
||||||
coverArt: coverArtURN(playlist.coverArt)
|
coverArt: coverArtURN(playlist.coverArt),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const asGenre = (genreName: string) => ({
|
export const asGenre = (genreName: string) => ({
|
||||||
id: b64Encode(genreName),
|
id: b64Encode(genreName),
|
||||||
@@ -330,19 +337,53 @@ const maybeAsGenre = (genreName: string | undefined): Genre | undefined =>
|
|||||||
O.getOrElseW(() => undefined)
|
O.getOrElseW(() => undefined)
|
||||||
);
|
);
|
||||||
|
|
||||||
export type StreamClientApplication = (track: Track) => string;
|
export interface CustomPlayers {
|
||||||
|
encodingFor({ mimeType }: { mimeType: string }): O.Option<Encoding>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CustomClient = {
|
||||||
|
mimeType: string;
|
||||||
|
transcodedMimeType: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class TranscodingCustomPlayers implements CustomPlayers {
|
||||||
|
transcodings: Map<string, string>;
|
||||||
|
|
||||||
|
constructor(transcodings: Map<string, string>) {
|
||||||
|
this.transcodings = transcodings;
|
||||||
|
}
|
||||||
|
|
||||||
|
static from(config: string): TranscodingCustomPlayers {
|
||||||
|
const parts: [string, string][] = config
|
||||||
|
.split(",")
|
||||||
|
.map((it) => it.split(">"))
|
||||||
|
.map((pair) => {
|
||||||
|
if (pair.length == 1) return [pair[0]!, pair[0]!];
|
||||||
|
else if (pair.length == 2) return [pair[0]!, pair[1]!];
|
||||||
|
else throw new Error(`Invalid configuration item ${config}`);
|
||||||
|
});
|
||||||
|
return new TranscodingCustomPlayers(new Map(parts));
|
||||||
|
}
|
||||||
|
|
||||||
|
encodingFor = ({ mimeType }: { mimeType: string }): O.Option<Encoding> => pipe(
|
||||||
|
this.transcodings.get(mimeType),
|
||||||
|
O.fromNullable,
|
||||||
|
O.map(transcodedMimeType => ({
|
||||||
|
player:`${DEFAULT_CLIENT_APPLICATION}+${mimeType}`,
|
||||||
|
mimeType: transcodedMimeType
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NO_CUSTOM_PLAYERS: CustomPlayers = {
|
||||||
|
encodingFor(_) {
|
||||||
|
return O.none
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
const DEFAULT_CLIENT_APPLICATION = "bonob";
|
const DEFAULT_CLIENT_APPLICATION = "bonob";
|
||||||
const USER_AGENT = "bonob";
|
const USER_AGENT = "bonob";
|
||||||
|
|
||||||
export const DEFAULT: StreamClientApplication = (_: Track) =>
|
|
||||||
DEFAULT_CLIENT_APPLICATION;
|
|
||||||
|
|
||||||
export function appendMimeTypeToClientFor(mimeTypes: string[]) {
|
|
||||||
return (track: Track) =>
|
|
||||||
mimeTypes.includes(track.mimeType) ? `bonob+${track.mimeType}` : "bonob";
|
|
||||||
}
|
|
||||||
|
|
||||||
export const asURLSearchParams = (q: any) => {
|
export const asURLSearchParams = (q: any) => {
|
||||||
const urlSearchParams = new URLSearchParams();
|
const urlSearchParams = new URLSearchParams();
|
||||||
Object.keys(q).forEach((k) => {
|
Object.keys(q).forEach((k) => {
|
||||||
@@ -357,28 +398,28 @@ export type ImageFetcher = (url: string) => Promise<CoverArt | undefined>;
|
|||||||
|
|
||||||
export const cachingImageFetcher =
|
export const cachingImageFetcher =
|
||||||
(cacheDir: string, delegate: ImageFetcher) =>
|
(cacheDir: string, delegate: ImageFetcher) =>
|
||||||
async (url: string): Promise<CoverArt | undefined> => {
|
async (url: string): Promise<CoverArt | undefined> => {
|
||||||
const filename = path.join(cacheDir, `${Md5.hashStr(url)}.png`);
|
const filename = path.join(cacheDir, `${Md5.hashStr(url)}.png`);
|
||||||
return fse
|
return fse
|
||||||
.readFile(filename)
|
.readFile(filename)
|
||||||
.then((data) => ({ contentType: "image/png", data }))
|
.then((data) => ({ contentType: "image/png", data }))
|
||||||
.catch(() =>
|
.catch(() =>
|
||||||
delegate(url).then((image) => {
|
delegate(url).then((image) => {
|
||||||
if (image) {
|
if (image) {
|
||||||
return sharp(image.data)
|
return sharp(image.data)
|
||||||
.png()
|
.png()
|
||||||
.toBuffer()
|
.toBuffer()
|
||||||
.then((png) => {
|
.then((png) => {
|
||||||
return fse
|
return fse
|
||||||
.writeFile(filename, png)
|
.writeFile(filename, png)
|
||||||
.then(() => ({ contentType: "image/png", data: png }));
|
.then(() => ({ contentType: "image/png", data: png }));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const axiosImageFetcher = (url: string): Promise<CoverArt | undefined> =>
|
export const axiosImageFetcher = (url: string): Promise<CoverArt | undefined> =>
|
||||||
axios
|
axios
|
||||||
@@ -426,16 +467,16 @@ interface SubsonicMusicLibrary extends MusicLibrary {
|
|||||||
|
|
||||||
export class Subsonic implements MusicService {
|
export class Subsonic implements MusicService {
|
||||||
url: URLBuilder;
|
url: URLBuilder;
|
||||||
streamClientApplication: StreamClientApplication;
|
customPlayers: CustomPlayers;
|
||||||
externalImageFetcher: ImageFetcher;
|
externalImageFetcher: ImageFetcher;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
url: URLBuilder,
|
url: URLBuilder,
|
||||||
streamClientApplication: StreamClientApplication = DEFAULT,
|
customPlayers: CustomPlayers = NO_CUSTOM_PLAYERS,
|
||||||
externalImageFetcher: ImageFetcher = axiosImageFetcher
|
externalImageFetcher: ImageFetcher = axiosImageFetcher
|
||||||
) {
|
) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
this.streamClientApplication = streamClientApplication;
|
this.customPlayers = customPlayers;
|
||||||
this.externalImageFetcher = externalImageFetcher;
|
this.externalImageFetcher = externalImageFetcher;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -630,7 +671,7 @@ export class Subsonic implements MusicService {
|
|||||||
.then((it) => it.song)
|
.then((it) => it.song)
|
||||||
.then((song) =>
|
.then((song) =>
|
||||||
this.getAlbum(credentials, song.albumId!).then((album) =>
|
this.getAlbum(credentials, song.albumId!).then((album) =>
|
||||||
asTrack(album, song)
|
asTrack(album, song, this.customPlayers)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -733,7 +774,7 @@ export class Subsonic implements MusicService {
|
|||||||
})
|
})
|
||||||
.then((it) => it.album)
|
.then((it) => it.album)
|
||||||
.then((album) =>
|
.then((album) =>
|
||||||
(album.song || []).map((song) => asTrack(asAlbum(album), song))
|
(album.song || []).map((song) => asTrack(asAlbum(album), song, this.customPlayers))
|
||||||
),
|
),
|
||||||
track: (trackId: string) => subsonic.getTrack(credentials, trackId),
|
track: (trackId: string) => subsonic.getTrack(credentials, trackId),
|
||||||
rate: (trackId: string, rating: Rating) =>
|
rate: (trackId: string, rating: Rating) =>
|
||||||
@@ -784,7 +825,7 @@ export class Subsonic implements MusicService {
|
|||||||
`/rest/stream`,
|
`/rest/stream`,
|
||||||
{
|
{
|
||||||
id: trackId,
|
id: trackId,
|
||||||
c: this.streamClientApplication(track),
|
c: track.encoding.player,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
headers: pipe(
|
headers: pipe(
|
||||||
@@ -801,15 +842,15 @@ export class Subsonic implements MusicService {
|
|||||||
responseType: "stream",
|
responseType: "stream",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.then((res) => ({
|
.then((stream) => ({
|
||||||
status: res.status,
|
status: stream.status,
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": res.headers["content-type"],
|
"content-type": stream.headers["content-type"],
|
||||||
"content-length": res.headers["content-length"],
|
"content-length": stream.headers["content-length"],
|
||||||
"content-range": res.headers["content-range"],
|
"content-range": stream.headers["content-range"],
|
||||||
"accept-ranges": res.headers["accept-ranges"],
|
"accept-ranges": stream.headers["accept-ranges"],
|
||||||
},
|
},
|
||||||
stream: res.data,
|
stream: stream.data,
|
||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
coverArt: async (coverArtURN: BUrn, size?: number) =>
|
coverArt: async (coverArtURN: BUrn, size?: number) =>
|
||||||
@@ -872,9 +913,7 @@ export class Subsonic implements MusicService {
|
|||||||
subsonic
|
subsonic
|
||||||
.getJSON<GetPlaylistsResponse>(credentials, "/rest/getPlaylists")
|
.getJSON<GetPlaylistsResponse>(credentials, "/rest/getPlaylists")
|
||||||
.then((it) => it.playlists.playlist || [])
|
.then((it) => it.playlists.playlist || [])
|
||||||
.then((playlists) =>
|
.then((playlists) => playlists.map(asPlayListSummary)),
|
||||||
playlists.map(asPlayListSummary)
|
|
||||||
),
|
|
||||||
playlist: async (id: string) =>
|
playlist: async (id: string) =>
|
||||||
subsonic
|
subsonic
|
||||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/getPlaylist", {
|
.getJSON<GetPlaylistResponse>(credentials, "/rest/getPlaylist", {
|
||||||
@@ -898,7 +937,8 @@ export class Subsonic implements MusicService {
|
|||||||
artistId: entry.artistId,
|
artistId: entry.artistId,
|
||||||
coverArt: coverArtURN(entry.coverArt),
|
coverArt: coverArtURN(entry.coverArt),
|
||||||
},
|
},
|
||||||
entry
|
entry,
|
||||||
|
this.customPlayers
|
||||||
),
|
),
|
||||||
number: trackNumber++,
|
number: trackNumber++,
|
||||||
})),
|
})),
|
||||||
@@ -911,7 +951,11 @@ export class Subsonic implements MusicService {
|
|||||||
})
|
})
|
||||||
.then((it) => it.playlist)
|
.then((it) => it.playlist)
|
||||||
// todo: why is this line so similar to other playlist lines??
|
// todo: why is this line so similar to other playlist lines??
|
||||||
.then((it) => ({ id: it.id, name: it.name, coverArt: coverArtURN(it.coverArt) })),
|
.then((it) => ({
|
||||||
|
id: it.id,
|
||||||
|
name: it.name,
|
||||||
|
coverArt: coverArtURN(it.coverArt),
|
||||||
|
})),
|
||||||
deletePlaylist: async (id: string) =>
|
deletePlaylist: async (id: string) =>
|
||||||
subsonic
|
subsonic
|
||||||
.getJSON<GetPlaylistResponse>(credentials, "/rest/deletePlaylist", {
|
.getJSON<GetPlaylistResponse>(credentials, "/rest/deletePlaylist", {
|
||||||
@@ -945,7 +989,7 @@ export class Subsonic implements MusicService {
|
|||||||
songs.map((song) =>
|
songs.map((song) =>
|
||||||
subsonic
|
subsonic
|
||||||
.getAlbum(credentials, song.albumId!)
|
.getAlbum(credentials, song.albumId!)
|
||||||
.then((album) => asTrack(album, song))
|
.then((album) => asTrack(album, song, this.customPlayers))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -962,7 +1006,7 @@ export class Subsonic implements MusicService {
|
|||||||
songs.map((song) =>
|
songs.map((song) =>
|
||||||
subsonic
|
subsonic
|
||||||
.getAlbum(credentials, song.albumId!)
|
.getAlbum(credentials, song.albumId!)
|
||||||
.then((album) => asTrack(album, song))
|
.then((album) => asTrack(album, song, this.customPlayers))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -979,7 +1023,7 @@ export class Subsonic implements MusicService {
|
|||||||
TE.tryCatch(
|
TE.tryCatch(
|
||||||
() =>
|
() =>
|
||||||
axios.post(
|
axios.post(
|
||||||
this.url.append({ pathname: '/auth/login' }).href(),
|
this.url.append({ pathname: "/auth/login" }).href(),
|
||||||
_.pick(credentials, "username", "password")
|
_.pick(credentials, "username", "password")
|
||||||
),
|
),
|
||||||
() => new AuthFailure("Failed to get bearerToken")
|
() => new AuthFailure("Failed to get bearerToken")
|
||||||
|
|||||||
@@ -173,7 +173,10 @@ export function aTrack(fields: Partial<Track> = {}): Track {
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: `Track ${id}`,
|
name: `Track ${id}`,
|
||||||
mimeType: `audio/mp3-${id}`,
|
encoding: {
|
||||||
|
player: "bonob",
|
||||||
|
mimeType: `audio/mp3-${id}`
|
||||||
|
},
|
||||||
duration: randomInt(500),
|
duration: randomInt(500),
|
||||||
number: randomInt(100),
|
number: randomInt(100),
|
||||||
genre,
|
genre,
|
||||||
|
|||||||
@@ -352,7 +352,10 @@ describe("track", () => {
|
|||||||
const someTrack = aTrack({
|
const someTrack = aTrack({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
// audio/x-flac should be mapped to audio/flac
|
// audio/x-flac should be mapped to audio/flac
|
||||||
mimeType: "audio/x-flac",
|
encoding: {
|
||||||
|
player: "something",
|
||||||
|
mimeType: "audio/x-flac"
|
||||||
|
},
|
||||||
name: "great song",
|
name: "great song",
|
||||||
duration: randomInt(1000),
|
duration: randomInt(1000),
|
||||||
number: randomInt(100),
|
number: randomInt(100),
|
||||||
@@ -407,7 +410,10 @@ describe("track", () => {
|
|||||||
const someTrack = aTrack({
|
const someTrack = aTrack({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
// audio/x-flac should be mapped to audio/flac
|
// audio/x-flac should be mapped to audio/flac
|
||||||
mimeType: "audio/x-flac",
|
encoding: {
|
||||||
|
player: "something",
|
||||||
|
mimeType: "audio/x-flac"
|
||||||
|
},
|
||||||
name: "great song",
|
name: "great song",
|
||||||
duration: randomInt(1000),
|
duration: randomInt(1000),
|
||||||
number: randomInt(100),
|
number: randomInt(100),
|
||||||
@@ -2579,7 +2585,7 @@ describe("wsdl api", () => {
|
|||||||
id: `track:${track.id}`,
|
id: `track:${track.id}`,
|
||||||
itemType: "track",
|
itemType: "track",
|
||||||
title: track.name,
|
title: track.name,
|
||||||
mimeType: track.mimeType,
|
mimeType: track.encoding.mimeType,
|
||||||
trackMetadata: {
|
trackMetadata: {
|
||||||
artistId: `artist:${track.artist.id}`,
|
artistId: `artist:${track.artist.id}`,
|
||||||
artist: track.artist.name,
|
artist: track.artist.name,
|
||||||
@@ -2627,7 +2633,7 @@ describe("wsdl api", () => {
|
|||||||
id: `track:${track.id}`,
|
id: `track:${track.id}`,
|
||||||
itemType: "track",
|
itemType: "track",
|
||||||
title: track.name,
|
title: track.name,
|
||||||
mimeType: track.mimeType,
|
mimeType: track.encoding.mimeType,
|
||||||
trackMetadata: {
|
trackMetadata: {
|
||||||
artistId: `artist:${track.artist.id}`,
|
artistId: `artist:${track.artist.id}`,
|
||||||
artist: track.artist.name,
|
artist: track.artist.name,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user