mirror of
https://github.com/gabehf/Koito.git
synced 2026-03-08 23:18:15 -07:00
Pre-release version v0.0.14 (#96)
* add dev branch container to workflow * correctly set the default range of ActivityGrid * fix: set name/short_name to koito (#61) * fix dev container push workflow * fix: race condition with using getComputedStyle primary color for dynamic activity grid darkening (#76) * Fix race condition with using getComputedStyle primary color for dynamic activity grid darkening Instead just use the color from the current theme directly. Tested works on initial load and theme changes. Fixes https://github.com/gabehf/Koito/issues/75 * Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name Split name out of the Theme struct to simplify custom theme saving/reading * fix: set first artist listed as primary by default (#81) * feat: add server-side configuration with default theme (#90) * docs: add example for usage of the main listenbrainz instance (#71) * docs: add example for usage of the main listenbrainz instance * Update scrobbler.md --------- Co-authored-by: Gabe Farrell <90876006+gabehf@users.noreply.github.com> * feat: add server-side cfg and default theme * fix: repair custom theme --------- Co-authored-by: m0d3rnX <jesper@posteo.de> * docs: add default theme cfg option to docs * feat: add ability to manually scrobble track (#91) * feat: add button to manually scrobble from ui * fix: ensure timestamp is in the past, log fix * test: add integration test * feat: add first listened to dates for media items (#92) * fix: ensure error checks for ErrNoRows * feat: add now playing endpoint and ui (#93) * wip * feat: add now playing * fix: set default theme when config is not set * feat: fetch images from subsonic server (#94) * fix: useQuery instead of useEffect for now playing * feat: custom artist separator regex (#95) * Fix race condition with using getComputedStyle primary color for dynamic activity grid darkening Instead just use the color from the current theme directly. Tested works on initial load and theme changes. Fixes https://github.com/gabehf/Koito/issues/75 * Rework theme provider to provide the actual Theme object throughtout the app, in addition to the name Split name out of the Theme struct to simplify custom theme saving/reading * feat: add server-side configuration with default theme (#90) * docs: add example for usage of the main listenbrainz instance (#71) * docs: add example for usage of the main listenbrainz instance * Update scrobbler.md --------- Co-authored-by: Gabe Farrell <90876006+gabehf@users.noreply.github.com> * feat: add server-side cfg and default theme * fix: repair custom theme --------- Co-authored-by: m0d3rnX <jesper@posteo.de> * fix: rebase errors --------- Co-authored-by: pet <128837728+againstpetra@users.noreply.github.com> Co-authored-by: mlandry <mike.landry@gmail.com> Co-authored-by: m0d3rnX <jesper@posteo.de>
This commit is contained in:
parent
bf0ec68cfe
commit
36f984a1a2
56 changed files with 1887 additions and 906 deletions
|
|
@ -1,327 +1,419 @@
|
|||
interface getItemsArgs {
|
||||
limit: number,
|
||||
period: string,
|
||||
page: number,
|
||||
artist_id?: number,
|
||||
album_id?: number,
|
||||
track_id?: number
|
||||
limit: number;
|
||||
period: string;
|
||||
page: number;
|
||||
artist_id?: number;
|
||||
album_id?: number;
|
||||
track_id?: number;
|
||||
}
|
||||
interface getActivityArgs {
|
||||
step: string
|
||||
range: number
|
||||
month: number
|
||||
year: number
|
||||
artist_id: number
|
||||
album_id: number
|
||||
track_id: number
|
||||
step: string;
|
||||
range: number;
|
||||
month: number;
|
||||
year: number;
|
||||
artist_id: number;
|
||||
album_id: number;
|
||||
track_id: number;
|
||||
}
|
||||
|
||||
function getLastListens(args: getItemsArgs): Promise<PaginatedResponse<Listen>> {
|
||||
return fetch(`/apis/web/v1/listens?period=${args.period}&limit=${args.limit}&artist_id=${args.artist_id}&album_id=${args.album_id}&track_id=${args.track_id}&page=${args.page}`).then(r => r.json() as Promise<PaginatedResponse<Listen>>)
|
||||
function getLastListens(
|
||||
args: getItemsArgs
|
||||
): Promise<PaginatedResponse<Listen>> {
|
||||
return fetch(
|
||||
`/apis/web/v1/listens?period=${args.period}&limit=${args.limit}&artist_id=${args.artist_id}&album_id=${args.album_id}&track_id=${args.track_id}&page=${args.page}`
|
||||
).then((r) => r.json() as Promise<PaginatedResponse<Listen>>);
|
||||
}
|
||||
|
||||
function getTopTracks(args: getItemsArgs): Promise<PaginatedResponse<Track>> {
|
||||
if (args.artist_id) {
|
||||
return fetch(`/apis/web/v1/top-tracks?period=${args.period}&limit=${args.limit}&artist_id=${args.artist_id}&page=${args.page}`).then(r => r.json() as Promise<PaginatedResponse<Track>>)
|
||||
} else if (args.album_id) {
|
||||
return fetch(`/apis/web/v1/top-tracks?period=${args.period}&limit=${args.limit}&album_id=${args.album_id}&page=${args.page}`).then(r => r.json() as Promise<PaginatedResponse<Track>>)
|
||||
} else {
|
||||
return fetch(`/apis/web/v1/top-tracks?period=${args.period}&limit=${args.limit}&page=${args.page}`).then(r => r.json() as Promise<PaginatedResponse<Track>>)
|
||||
}
|
||||
if (args.artist_id) {
|
||||
return fetch(
|
||||
`/apis/web/v1/top-tracks?period=${args.period}&limit=${args.limit}&artist_id=${args.artist_id}&page=${args.page}`
|
||||
).then((r) => r.json() as Promise<PaginatedResponse<Track>>);
|
||||
} else if (args.album_id) {
|
||||
return fetch(
|
||||
`/apis/web/v1/top-tracks?period=${args.period}&limit=${args.limit}&album_id=${args.album_id}&page=${args.page}`
|
||||
).then((r) => r.json() as Promise<PaginatedResponse<Track>>);
|
||||
} else {
|
||||
return fetch(
|
||||
`/apis/web/v1/top-tracks?period=${args.period}&limit=${args.limit}&page=${args.page}`
|
||||
).then((r) => r.json() as Promise<PaginatedResponse<Track>>);
|
||||
}
|
||||
}
|
||||
|
||||
function getTopAlbums(args: getItemsArgs): Promise<PaginatedResponse<Album>> {
|
||||
const baseUri = `/apis/web/v1/top-albums?period=${args.period}&limit=${args.limit}&page=${args.page}`
|
||||
if (args.artist_id) {
|
||||
return fetch(baseUri+`&artist_id=${args.artist_id}`).then(r => r.json() as Promise<PaginatedResponse<Album>>)
|
||||
} else {
|
||||
return fetch(baseUri).then(r => r.json() as Promise<PaginatedResponse<Album>>)
|
||||
}
|
||||
const baseUri = `/apis/web/v1/top-albums?period=${args.period}&limit=${args.limit}&page=${args.page}`;
|
||||
if (args.artist_id) {
|
||||
return fetch(baseUri + `&artist_id=${args.artist_id}`).then(
|
||||
(r) => r.json() as Promise<PaginatedResponse<Album>>
|
||||
);
|
||||
} else {
|
||||
return fetch(baseUri).then(
|
||||
(r) => r.json() as Promise<PaginatedResponse<Album>>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getTopArtists(args: getItemsArgs): Promise<PaginatedResponse<Artist>> {
|
||||
const baseUri = `/apis/web/v1/top-artists?period=${args.period}&limit=${args.limit}&page=${args.page}`
|
||||
return fetch(baseUri).then(r => r.json() as Promise<PaginatedResponse<Artist>>)
|
||||
const baseUri = `/apis/web/v1/top-artists?period=${args.period}&limit=${args.limit}&page=${args.page}`;
|
||||
return fetch(baseUri).then(
|
||||
(r) => r.json() as Promise<PaginatedResponse<Artist>>
|
||||
);
|
||||
}
|
||||
|
||||
function getActivity(args: getActivityArgs): Promise<ListenActivityItem[]> {
|
||||
return fetch(`/apis/web/v1/listen-activity?step=${args.step}&range=${args.range}&month=${args.month}&year=${args.year}&album_id=${args.album_id}&artist_id=${args.artist_id}&track_id=${args.track_id}`).then(r => r.json() as Promise<ListenActivityItem[]>)
|
||||
return fetch(
|
||||
`/apis/web/v1/listen-activity?step=${args.step}&range=${args.range}&month=${args.month}&year=${args.year}&album_id=${args.album_id}&artist_id=${args.artist_id}&track_id=${args.track_id}`
|
||||
).then((r) => r.json() as Promise<ListenActivityItem[]>);
|
||||
}
|
||||
|
||||
function getStats(period: string): Promise<Stats> {
|
||||
return fetch(`/apis/web/v1/stats?period=${period}`).then(r => r.json() as Promise<Stats>)
|
||||
return fetch(`/apis/web/v1/stats?period=${period}`).then(
|
||||
(r) => r.json() as Promise<Stats>
|
||||
);
|
||||
}
|
||||
|
||||
function search(q: string): Promise<SearchResponse> {
|
||||
q = encodeURIComponent(q)
|
||||
return fetch(`/apis/web/v1/search?q=${q}`).then(r => r.json() as Promise<SearchResponse>)
|
||||
q = encodeURIComponent(q);
|
||||
return fetch(`/apis/web/v1/search?q=${q}`).then(
|
||||
(r) => r.json() as Promise<SearchResponse>
|
||||
);
|
||||
}
|
||||
|
||||
function imageUrl(id: string, size: string) {
|
||||
if (!id) {
|
||||
id = 'default'
|
||||
}
|
||||
return `/images/${size}/${id}`
|
||||
if (!id) {
|
||||
id = "default";
|
||||
}
|
||||
return `/images/${size}/${id}`;
|
||||
}
|
||||
function replaceImage(form: FormData): Promise<Response> {
|
||||
return fetch(`/apis/web/v1/replace-image`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
})
|
||||
return fetch(`/apis/web/v1/replace-image`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
}
|
||||
|
||||
function mergeTracks(from: number, to: number): Promise<Response> {
|
||||
return fetch(`/apis/web/v1/merge/tracks?from_id=${from}&to_id=${to}`, {
|
||||
method: "POST",
|
||||
})
|
||||
return fetch(`/apis/web/v1/merge/tracks?from_id=${from}&to_id=${to}`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
function mergeAlbums(from: number, to: number, replaceImage: boolean): Promise<Response> {
|
||||
return fetch(`/apis/web/v1/merge/albums?from_id=${from}&to_id=${to}&replace_image=${replaceImage}`, {
|
||||
method: "POST",
|
||||
})
|
||||
function mergeAlbums(
|
||||
from: number,
|
||||
to: number,
|
||||
replaceImage: boolean
|
||||
): Promise<Response> {
|
||||
return fetch(
|
||||
`/apis/web/v1/merge/albums?from_id=${from}&to_id=${to}&replace_image=${replaceImage}`,
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
}
|
||||
function mergeArtists(from: number, to: number, replaceImage: boolean): Promise<Response> {
|
||||
return fetch(`/apis/web/v1/merge/artists?from_id=${from}&to_id=${to}&replace_image=${replaceImage}`, {
|
||||
method: "POST",
|
||||
})
|
||||
function mergeArtists(
|
||||
from: number,
|
||||
to: number,
|
||||
replaceImage: boolean
|
||||
): Promise<Response> {
|
||||
return fetch(
|
||||
`/apis/web/v1/merge/artists?from_id=${from}&to_id=${to}&replace_image=${replaceImage}`,
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
);
|
||||
}
|
||||
function login(username: string, password: string, remember: boolean): Promise<Response> {
|
||||
const form = new URLSearchParams
|
||||
form.append('username', username)
|
||||
form.append('password', password)
|
||||
form.append('remember_me', String(remember))
|
||||
return fetch(`/apis/web/v1/login`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
})
|
||||
function login(
|
||||
username: string,
|
||||
password: string,
|
||||
remember: boolean
|
||||
): Promise<Response> {
|
||||
const form = new URLSearchParams();
|
||||
form.append("username", username);
|
||||
form.append("password", password);
|
||||
form.append("remember_me", String(remember));
|
||||
return fetch(`/apis/web/v1/login`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
}
|
||||
function logout(): Promise<Response> {
|
||||
return fetch(`/apis/web/v1/logout`, {
|
||||
method: "POST",
|
||||
})
|
||||
return fetch(`/apis/web/v1/logout`, {
|
||||
method: "POST",
|
||||
});
|
||||
}
|
||||
|
||||
function getCfg(): Promise<Config> {
|
||||
return fetch(`/apis/web/v1/config`).then((r) => r.json() as Promise<Config>);
|
||||
}
|
||||
|
||||
function submitListen(id: string, ts: Date): Promise<Response> {
|
||||
const form = new URLSearchParams();
|
||||
form.append("track_id", id);
|
||||
const ms = new Date(ts).getTime();
|
||||
const unix = Math.floor(ms / 1000);
|
||||
form.append("unix", unix.toString());
|
||||
return fetch(`/apis/web/v1/listen`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
}
|
||||
|
||||
function getApiKeys(): Promise<ApiKey[]> {
|
||||
return fetch(`/apis/web/v1/user/apikeys`).then((r) => r.json() as Promise<ApiKey[]>)
|
||||
return fetch(`/apis/web/v1/user/apikeys`).then(
|
||||
(r) => r.json() as Promise<ApiKey[]>
|
||||
);
|
||||
}
|
||||
const createApiKey = async (label: string): Promise<ApiKey> => {
|
||||
const form = new URLSearchParams
|
||||
form.append('label', label)
|
||||
const r = await fetch(`/apis/web/v1/user/apikeys`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
if (!r.ok) {
|
||||
let errorMessage = `error: ${r.status}`;
|
||||
try {
|
||||
const errorData: ApiError = await r.json();
|
||||
if (errorData && typeof errorData.error === 'string') {
|
||||
errorMessage = errorData.error;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("unexpected api error:", e);
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
const form = new URLSearchParams();
|
||||
form.append("label", label);
|
||||
const r = await fetch(`/apis/web/v1/user/apikeys`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
if (!r.ok) {
|
||||
let errorMessage = `error: ${r.status}`;
|
||||
try {
|
||||
const errorData: ApiError = await r.json();
|
||||
if (errorData && typeof errorData.error === "string") {
|
||||
errorMessage = errorData.error;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("unexpected api error:", e);
|
||||
}
|
||||
const data: ApiKey = await r.json();
|
||||
return data;
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
const data: ApiKey = await r.json();
|
||||
return data;
|
||||
};
|
||||
function deleteApiKey(id: number): Promise<Response> {
|
||||
return fetch(`/apis/web/v1/user/apikeys?id=${id}`, {
|
||||
method: "DELETE"
|
||||
})
|
||||
return fetch(`/apis/web/v1/user/apikeys?id=${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
function updateApiKeyLabel(id: number, label: string): Promise<Response> {
|
||||
const form = new URLSearchParams
|
||||
form.append('id', String(id))
|
||||
form.append('label', label)
|
||||
return fetch(`/apis/web/v1/user/apikeys`, {
|
||||
method: "PATCH",
|
||||
body: form,
|
||||
})
|
||||
const form = new URLSearchParams();
|
||||
form.append("id", String(id));
|
||||
form.append("label", label);
|
||||
return fetch(`/apis/web/v1/user/apikeys`, {
|
||||
method: "PATCH",
|
||||
body: form,
|
||||
});
|
||||
}
|
||||
|
||||
function deleteItem(itemType: string, id: number): Promise<Response> {
|
||||
return fetch(`/apis/web/v1/${itemType}?id=${id}`, {
|
||||
method: "DELETE"
|
||||
})
|
||||
return fetch(`/apis/web/v1/${itemType}?id=${id}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
function updateUser(username: string, password: string) {
|
||||
const form = new URLSearchParams
|
||||
form.append('username', username)
|
||||
form.append('password', password)
|
||||
return fetch(`/apis/web/v1/user`, {
|
||||
method: "PATCH",
|
||||
body: form,
|
||||
})
|
||||
const form = new URLSearchParams();
|
||||
form.append("username", username);
|
||||
form.append("password", password);
|
||||
return fetch(`/apis/web/v1/user`, {
|
||||
method: "PATCH",
|
||||
body: form,
|
||||
});
|
||||
}
|
||||
function getAliases(type: string, id: number): Promise<Alias[]> {
|
||||
return fetch(`/apis/web/v1/aliases?${type}_id=${id}`).then(r => r.json() as Promise<Alias[]>)
|
||||
return fetch(`/apis/web/v1/aliases?${type}_id=${id}`).then(
|
||||
(r) => r.json() as Promise<Alias[]>
|
||||
);
|
||||
}
|
||||
function createAlias(type: string, id: number, alias: string): Promise<Response> {
|
||||
const form = new URLSearchParams
|
||||
form.append(`${type}_id`, String(id))
|
||||
form.append('alias', alias)
|
||||
return fetch(`/apis/web/v1/aliases`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
})
|
||||
function createAlias(
|
||||
type: string,
|
||||
id: number,
|
||||
alias: string
|
||||
): Promise<Response> {
|
||||
const form = new URLSearchParams();
|
||||
form.append(`${type}_id`, String(id));
|
||||
form.append("alias", alias);
|
||||
return fetch(`/apis/web/v1/aliases`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
}
|
||||
function deleteAlias(type: string, id: number, alias: string): Promise<Response> {
|
||||
const form = new URLSearchParams
|
||||
form.append(`${type}_id`, String(id))
|
||||
form.append('alias', alias)
|
||||
return fetch(`/apis/web/v1/aliases/delete`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
})
|
||||
function deleteAlias(
|
||||
type: string,
|
||||
id: number,
|
||||
alias: string
|
||||
): Promise<Response> {
|
||||
const form = new URLSearchParams();
|
||||
form.append(`${type}_id`, String(id));
|
||||
form.append("alias", alias);
|
||||
return fetch(`/apis/web/v1/aliases/delete`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
}
|
||||
function setPrimaryAlias(type: string, id: number, alias: string): Promise<Response> {
|
||||
const form = new URLSearchParams
|
||||
form.append(`${type}_id`, String(id))
|
||||
form.append('alias', alias)
|
||||
return fetch(`/apis/web/v1/aliases/primary`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
})
|
||||
function setPrimaryAlias(
|
||||
type: string,
|
||||
id: number,
|
||||
alias: string
|
||||
): Promise<Response> {
|
||||
const form = new URLSearchParams();
|
||||
form.append(`${type}_id`, String(id));
|
||||
form.append("alias", alias);
|
||||
return fetch(`/apis/web/v1/aliases/primary`, {
|
||||
method: "POST",
|
||||
body: form,
|
||||
});
|
||||
}
|
||||
function getAlbum(id: number): Promise<Album> {
|
||||
return fetch(`/apis/web/v1/album?id=${id}`).then(r => r.json() as Promise<Album>)
|
||||
return fetch(`/apis/web/v1/album?id=${id}`).then(
|
||||
(r) => r.json() as Promise<Album>
|
||||
);
|
||||
}
|
||||
|
||||
function deleteListen(listen: Listen): Promise<Response> {
|
||||
const ms = new Date(listen.time).getTime()
|
||||
const unix= Math.floor(ms / 1000);
|
||||
return fetch(`/apis/web/v1/listen?track_id=${listen.track.id}&unix=${unix}`, {
|
||||
method: "DELETE"
|
||||
})
|
||||
const ms = new Date(listen.time).getTime();
|
||||
const unix = Math.floor(ms / 1000);
|
||||
return fetch(`/apis/web/v1/listen?track_id=${listen.track.id}&unix=${unix}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
function getExport() {
|
||||
function getExport() {}
|
||||
|
||||
function getNowPlaying(): Promise<NowPlaying> {
|
||||
return fetch("/apis/web/v1/now-playing").then((r) => r.json());
|
||||
}
|
||||
|
||||
export {
|
||||
getLastListens,
|
||||
getTopTracks,
|
||||
getTopAlbums,
|
||||
getTopArtists,
|
||||
getActivity,
|
||||
getStats,
|
||||
search,
|
||||
replaceImage,
|
||||
mergeTracks,
|
||||
mergeAlbums,
|
||||
mergeArtists,
|
||||
imageUrl,
|
||||
login,
|
||||
logout,
|
||||
deleteItem,
|
||||
updateUser,
|
||||
getAliases,
|
||||
createAlias,
|
||||
deleteAlias,
|
||||
setPrimaryAlias,
|
||||
getApiKeys,
|
||||
createApiKey,
|
||||
deleteApiKey,
|
||||
updateApiKeyLabel,
|
||||
deleteListen,
|
||||
getAlbum,
|
||||
getExport,
|
||||
}
|
||||
getLastListens,
|
||||
getTopTracks,
|
||||
getTopAlbums,
|
||||
getTopArtists,
|
||||
getActivity,
|
||||
getStats,
|
||||
search,
|
||||
replaceImage,
|
||||
mergeTracks,
|
||||
mergeAlbums,
|
||||
mergeArtists,
|
||||
imageUrl,
|
||||
login,
|
||||
logout,
|
||||
getCfg,
|
||||
deleteItem,
|
||||
updateUser,
|
||||
getAliases,
|
||||
createAlias,
|
||||
deleteAlias,
|
||||
setPrimaryAlias,
|
||||
getApiKeys,
|
||||
createApiKey,
|
||||
deleteApiKey,
|
||||
updateApiKeyLabel,
|
||||
deleteListen,
|
||||
getAlbum,
|
||||
getExport,
|
||||
submitListen,
|
||||
getNowPlaying,
|
||||
};
|
||||
type Track = {
|
||||
id: number
|
||||
title: string
|
||||
artists: SimpleArtists[]
|
||||
listen_count: number
|
||||
image: string
|
||||
album_id: number
|
||||
musicbrainz_id: string
|
||||
time_listened: number
|
||||
}
|
||||
id: number;
|
||||
title: string;
|
||||
artists: SimpleArtists[];
|
||||
listen_count: number;
|
||||
image: string;
|
||||
album_id: number;
|
||||
musicbrainz_id: string;
|
||||
time_listened: number;
|
||||
first_listen: number;
|
||||
};
|
||||
type Artist = {
|
||||
id: number
|
||||
name: string
|
||||
image: string,
|
||||
aliases: string[]
|
||||
listen_count: number
|
||||
musicbrainz_id: string
|
||||
time_listened: number
|
||||
is_primary: boolean
|
||||
}
|
||||
id: number;
|
||||
name: string;
|
||||
image: string;
|
||||
aliases: string[];
|
||||
listen_count: number;
|
||||
musicbrainz_id: string;
|
||||
time_listened: number;
|
||||
first_listen: number;
|
||||
is_primary: boolean;
|
||||
};
|
||||
type Album = {
|
||||
id: number,
|
||||
title: string
|
||||
image: string
|
||||
listen_count: number
|
||||
is_various_artists: boolean
|
||||
artists: SimpleArtists[]
|
||||
musicbrainz_id: string
|
||||
time_listened: number
|
||||
}
|
||||
id: number;
|
||||
title: string;
|
||||
image: string;
|
||||
listen_count: number;
|
||||
is_various_artists: boolean;
|
||||
artists: SimpleArtists[];
|
||||
musicbrainz_id: string;
|
||||
time_listened: number;
|
||||
first_listen: number;
|
||||
};
|
||||
type Alias = {
|
||||
id: number
|
||||
alias: string
|
||||
source: string
|
||||
is_primary: boolean
|
||||
}
|
||||
id: number;
|
||||
alias: string;
|
||||
source: string;
|
||||
is_primary: boolean;
|
||||
};
|
||||
type Listen = {
|
||||
time: string,
|
||||
track: Track,
|
||||
}
|
||||
time: string;
|
||||
track: Track;
|
||||
};
|
||||
type PaginatedResponse<T> = {
|
||||
items: T[],
|
||||
total_record_count: number,
|
||||
has_next_page: boolean,
|
||||
current_page: number,
|
||||
items_per_page: number,
|
||||
}
|
||||
items: T[];
|
||||
total_record_count: number;
|
||||
has_next_page: boolean;
|
||||
current_page: number;
|
||||
items_per_page: number;
|
||||
};
|
||||
type ListenActivityItem = {
|
||||
start_time: Date,
|
||||
listens: number
|
||||
}
|
||||
start_time: Date;
|
||||
listens: number;
|
||||
};
|
||||
type SimpleArtists = {
|
||||
name: string
|
||||
id: number
|
||||
}
|
||||
name: string;
|
||||
id: number;
|
||||
};
|
||||
type Stats = {
|
||||
listen_count: number
|
||||
track_count: number
|
||||
album_count: number
|
||||
artist_count: number
|
||||
minutes_listened: number
|
||||
}
|
||||
listen_count: number;
|
||||
track_count: number;
|
||||
album_count: number;
|
||||
artist_count: number;
|
||||
minutes_listened: number;
|
||||
};
|
||||
type SearchResponse = {
|
||||
albums: Album[]
|
||||
artists: Artist[]
|
||||
tracks: Track[]
|
||||
}
|
||||
albums: Album[];
|
||||
artists: Artist[];
|
||||
tracks: Track[];
|
||||
};
|
||||
type User = {
|
||||
id: number
|
||||
username: string
|
||||
role: 'user' | 'admin'
|
||||
}
|
||||
id: number;
|
||||
username: string;
|
||||
role: "user" | "admin";
|
||||
};
|
||||
type ApiKey = {
|
||||
id: number
|
||||
key: string
|
||||
label: string
|
||||
created_at: Date
|
||||
}
|
||||
id: number;
|
||||
key: string;
|
||||
label: string;
|
||||
created_at: Date;
|
||||
};
|
||||
type ApiError = {
|
||||
error: string
|
||||
}
|
||||
error: string;
|
||||
};
|
||||
type Config = {
|
||||
default_theme: string;
|
||||
};
|
||||
type NowPlaying = {
|
||||
currently_playing: boolean;
|
||||
track: Track;
|
||||
};
|
||||
|
||||
export type {
|
||||
getItemsArgs,
|
||||
getActivityArgs,
|
||||
Track,
|
||||
Artist,
|
||||
Album,
|
||||
Listen,
|
||||
SearchResponse,
|
||||
PaginatedResponse,
|
||||
ListenActivityItem,
|
||||
User,
|
||||
Alias,
|
||||
ApiKey,
|
||||
ApiError
|
||||
}
|
||||
getItemsArgs,
|
||||
getActivityArgs,
|
||||
Track,
|
||||
Artist,
|
||||
Album,
|
||||
Listen,
|
||||
SearchResponse,
|
||||
PaginatedResponse,
|
||||
ListenActivityItem,
|
||||
User,
|
||||
Alias,
|
||||
ApiKey,
|
||||
ApiError,
|
||||
Config,
|
||||
NowPlaying,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue