Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/backend/common/infrastructure/Atomic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export type SourceType =
| 'maloja'
| 'musikcube'
| 'mpd'
| 'navidrome'
| 'vlc'
| 'icecast'
| 'azuracast'
Expand Down Expand Up @@ -56,6 +57,7 @@ export const sourceTypes: SourceType[] = [
'maloja',
'musikcube',
'mpd',
'navidrome',
'vlc',
'icecast',
'azuracast',
Expand All @@ -66,7 +68,7 @@ export const isSourceType = (data: string): data is SourceType => {
return sourceTypes.includes(data as SourceType);
}

export const lowGranularitySources: SourceType[] = ['subsonic', 'ytmusic'];
export const lowGranularitySources: SourceType[] = ['subsonic', 'navidrome', 'ytmusic'];

export type ClientType =
'maloja'
Expand Down
31 changes: 31 additions & 0 deletions src/backend/common/infrastructure/config/source/navidrome.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { PollingOptions } from "../common.js";
import { CommonSourceConfig, CommonSourceData } from "./index.js";

export interface NavidromeData extends CommonSourceData, PollingOptions {
/**
* URL of the subsonic media server to query
*
* @examples ["http://airsonic.local"]
* */
url: string
/**
* Username to login to the server with
*
* @example ["MyUser"]
* */
user: string

/**
* Password for the user to login to the server with
*
* @examples ["MyPassword"]
* */
password: string
}
export interface NavidromeSourceConfig extends CommonSourceConfig {
data: NavidromeData
}

export interface NavidromeSourceAIOConfig extends NavidromeSourceConfig {
type: 'navidrome'
}
3 changes: 3 additions & 0 deletions src/backend/common/infrastructure/config/source/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { YTMusicSourceAIOConfig, YTMusicSourceConfig } from "./ytmusic.js";
import { IcecastSourceAIOConfig, IcecastSourceConfig } from "./icecast.js";
import { KoitoSourceAIOConfig, KoitoSourceConfig } from "./koito.js";
import { MalojaSourceAIOConfig, MalojaSourceConfig } from "./maloja.js";
import { NavidromeSourceAIOConfig, NavidromeSourceConfig } from "./navidrome.js";


export type SourceConfig =
Expand Down Expand Up @@ -50,6 +51,7 @@ export type SourceConfig =
| MusikcubeSourceConfig
| MusicCastSourceConfig
| MPDSourceConfig
| NavidromeSourceConfig
| VLCSourceConfig
| IcecastSourceConfig
| AzuracastSourceConfig
Expand Down Expand Up @@ -80,6 +82,7 @@ export type SourceAIOConfig =
| MusikcubeSourceAIOConfig
| MusicCastSourceAIOConfig
| MPDSourceAIOConfig
| NavidromeSourceAIOConfig
| VLCSourceAIOConfig
| IcecastSourceAIOConfig
| AzuracastSourceAIOConfig
Expand Down
92 changes: 92 additions & 0 deletions src/backend/common/vendor/navidrome/NavidromeApiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import dayjs from "dayjs";
import { PlayObject, URLData } from "../../../../core/Atomic.js";
import { AbstractApiOptions, DEFAULT_RETRY_MULTIPLIER } from "../../infrastructure/Atomic.js";
import { KoitoData, ListenObjectResponse, ListensResponse } from "../../infrastructure/config/client/koito.js";
import AbstractApiClient from "../AbstractApiClient.js";
import { getBaseFromUrl, isPortReachableConnect, joinedUrl, normalizeWebAddress } from "../../../utils/NetworkUtils.js";
import request, { Request, Response } from 'superagent';
import { UpstreamError } from "../../errors/UpstreamError.js";
import { playToListenPayload } from "../ListenbrainzApiClient.js";
import { SubmitPayload } from '../listenbrainz/interfaces.js';
import { ListenType } from '../listenbrainz/interfaces.js';
import { parseRegexSingleOrFail } from "../../../utils.js";
import { NavidromeData } from "../../infrastructure/config/source/navidrome.js";
import { isSuperAgentResponseError } from "../../errors/ErrorUtils.js";

export class NavidromeApiClient extends AbstractApiClient {

declare config: NavidromeData;
url: URLData;

token?: string;
subsonicToken?: string;
subsonicSalt?: String
userId?: string

constructor(name: any, config: NavidromeData, options: AbstractApiOptions) {
super('Navidrome', name, config, options);

const {
url
} = this.config;

this.url = normalizeWebAddress(url);
this.logger.verbose(`Config URL: '${url ?? '(None Given)'}' => Normalized: '${this.url.url}'`)
}

callApi = async <T = Response>(req: Request): Promise<T> => {

let resp: Response;
try {
req.set('x-nd-authorization', `Bearer ${this.token}`);
req.set('x-nd-client-unique-id', '2297e6c3-4f63-45ef-b60b-5a959e23e666')
resp = await req;
return resp as T;
} catch (e) {
if(isSuperAgentResponseError(e)) {
resp = e.response;
}
throw e;
} finally {
if(resp.headers !== undefined && resp.headers['x-nd-authorization'] !== undefined) {
this.token = resp.headers['x-nd-authorization'];
}
}
}

testConnection = async () => {
try {
await isPortReachableConnect(this.url.port, { host: this.url.url.hostname });
} catch (e) {
throw new Error(`Navidrome server is not reachable at ${this.url.url.hostname}:${this.url.port}`, { cause: e });
}
}

testAuth = async () => {
try {
const resp = await request.post(`${joinedUrl(this.url.url, '/auth/login')}`)
.type('json')
.send({
username: this.config.user,
password: this.config.password
});
this.token = resp.body.token;
this.subsonicToken = resp.body.subsonicToken;
this.subsonicSalt = resp.body.subsonicSalt;
this.userId = resp.body.id

return true;
} catch (e) {
throw new Error('Could not validate Navidrome user/password', { cause: e });
}
}

getRecentlyPlayed = async (maxTracks: number): Promise<any> => {
try {
return [];
} catch (e) {
this.logger.error(`Error encountered while getting User listens | Error => ${e.message}`);
return [];
}
}
}
Loading