Skip to content

Commit 68c46fe

Browse files
authored
Merge pull request #471 from FoxxMD/componentPagination
feat: Implement pagination fetching for component listen data
2 parents eddda41 + d110dcb commit 68c46fe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1979
-825
lines changed

package-lock.json

Lines changed: 241 additions & 155 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@awaitjs/express": "^0.6.3",
5353
"@curvenote/ansi-to-react": "^7.0.0",
5454
"@donedeal0/superdiff": "^1.1.1",
55+
"@ewanc26/tid": "^1.0.2",
5556
"@fortawesome/fontawesome-svg-core": "^6.4.2",
5657
"@fortawesome/free-solid-svg-icons": "^6.4.2",
5758
"@fortawesome/react-fontawesome": "^0.2.0",
@@ -120,6 +121,7 @@
120121
"ntfy": "^1.7.6",
121122
"p-event": "^4.2.0",
122123
"p-map": "^7.0.4",
124+
"p-retry": "^7.1.1",
123125
"passport": "^0.6.0",
124126
"passport-deezer": "^0.2.0",
125127
"patch-package": "^8.0.0",
@@ -174,6 +176,7 @@
174176
"@types/react": "^18.2.18",
175177
"@types/react-dom": "^18.2.7",
176178
"@types/react-window": "^1.8.5",
179+
"@types/sinon": "^21.0.0",
177180
"@types/spotify-web-api-node": "^5.0.7",
178181
"@types/superagent": "^8.1.9",
179182
"@types/xml2js": "^0.4.11",
@@ -185,8 +188,9 @@
185188
"git-cliff": "^2.12.0",
186189
"mocha": "^10.3.0",
187190
"mockdate": "^3.0.5",
188-
"msw": "^2.1.2",
191+
"msw": "^2.12.10",
189192
"nodemon": "^3.0.3",
193+
"sinon": "^21.0.2",
190194
"ts-essentials": "^9.1.2",
191195
"typescript": "5.5.4",
192196
"typescript-eslint": "^7.0.1",

src/backend/common/Cache.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { fileOrDirectoryIsWriteable } from '../utils.js';
1717
import { asCacheAuthProvider, asCacheMetadataProvider, asCacheScrobbleProvider, CacheAuthProvider, CacheConfig, CacheConfigOptions, CacheMetadataProvider, CacheProvider, CacheScrobbleProvider } from './infrastructure/Atomic.js';
1818
import { Typeson } from 'typeson';
1919
import { builtin } from 'typeson-registry';
20-
import { MaybeLogger } from './logging.js';
20+
import { loggerNoop } from './logging.js';
2121
import { ListenProgressPositional, ListenProgressTS } from '../sources/PlayerState/ListenProgress.js';
2222
const configDir = process.env.CONFIG_DIR || path.resolve(projectDir, `./config`);
2323

@@ -50,6 +50,7 @@ export class MSCache {
5050
cacheAuth: Cacheable;
5151
regexCache: ReturnType<typeof cacheFunctions>;
5252
cacheTransform: Cacheable;
53+
cacheClientScrobbles: Cacheable;
5354

5455
logger: Logger;
5556

@@ -96,6 +97,7 @@ export class MSCache {
9697

9798
this.regexCache = cacheFunctions(this.config.regex);
9899
this.cacheTransform = new Cacheable({primary: initMemoryCache({lruSize: 500})});
100+
this.cacheClientScrobbles = new Cacheable({primary: initMemoryCache({lruSize: 100, ttl: '5m'})});
99101
}
100102

101103
init = async () => {
@@ -213,7 +215,7 @@ export const flatCacheCreate = (opts: FlatCacheOptions) => {
213215
});
214216
}
215217

216-
export const flatCacheLoad = async (flatCache: FlatCache, logger: MaybeLogger): Promise<void> => {
218+
export const flatCacheLoad = async (flatCache: FlatCache, logger: Logger = loggerNoop): Promise<void> => {
217219

218220
const cachePath = path.join(flatCache.cacheDir, flatCache.cacheId);
219221
try {
@@ -224,7 +226,7 @@ export const flatCacheLoad = async (flatCache: FlatCache, logger: MaybeLogger):
224226

225227
const streamPromise = new Promise((resolve, reject) => {
226228
flatCache.loadFileStream(cachePath, (progress: number, total: number) => {
227-
logger.debug(`Loading ${progress}/${total} chunks...`);
229+
logger.trace(`Loading ${progress}/${total} chunks...`);
228230
}, () => {
229231
resolve(true);
230232
}, (err: Error) => {
@@ -260,7 +262,7 @@ export const flatCacheLoad = async (flatCache: FlatCache, logger: MaybeLogger):
260262
}
261263
}
262264

263-
export const initFileCache = async (opts: FlatCacheOptions = {}, logger: MaybeLogger = new MaybeLogger()): Promise<[Keyv | KeyvStoreAdapter | undefined, FlatCache | undefined]> => {
265+
export const initFileCache = async (opts: FlatCacheOptions = {}, logger: Logger = loggerNoop): Promise<[Keyv | KeyvStoreAdapter | undefined, FlatCache | undefined]> => {
264266
const flatCache = flatCacheCreate(opts);
265267
try {
266268
await flatCacheLoad(flatCache, logger);

src/backend/common/infrastructure/Atomic.ts

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Logger } from '@foxxmd/logging';
2-
import { Dayjs } from "dayjs";
2+
import { SearchAndReplaceRegExp } from "@foxxmd/regex-buddy-core";
3+
import { Dayjs, ManipulateType } from "dayjs";
34
import { Request, Response } from "express";
45
import { NextFunction, ParamsDictionary, Query } from "express-serve-static-core";
56
import { FixedSizeList } from 'fixed-size-list';
6-
import { isPlayObject, PlayMeta, PlayMetaLifecycleless, PlayObject, PlayObjectLifecycleless } from "../../../core/Atomic.js";
7+
import { isPlayObject, PlayMeta, PlayMetaLifecycleless, PlayObject, PlayObjectLifecycleless, UnixTimestamp } from "../../../core/Atomic.js";
78
import TupleMap from "../TupleMap.js";
89
import { MusicBrainzApi } from 'musicbrainz-api';
910

@@ -346,6 +347,10 @@ export interface Authenticatable {
346347
testAuth: () => Promise<any>
347348
}
348349

350+
export interface ScrobbleRangeFetchable {
351+
getScrobblesForTimeRange: TimeRangeListensFetcher
352+
}
353+
349354
export interface MdnsDeviceInfo {
350355
name: string
351356
type: string
@@ -406,6 +411,8 @@ export const MBID_VARIOUS_ARTISTS = "89ad4ac3-39f7-470e-963a-56509c546377";
406411

407412
export type MusicBrainzSingletonMap = Map<string,MusicBrainzApi>;
408413

414+
415+
409416
/* https://websocket.org/reference/close-codes/ */
410417
export const WEBSOCKET_CLOSE_CODE_REASONS = {
411418
1000: 'Closed gracefully',
@@ -428,4 +435,83 @@ export const WEBSOCKET_CLOSE_CODE = {
428435
InternalError: 1011
429436
}
430437

431-
export const WEBSOCKET_CLOSE_CODES_RETRY = [1006,1011,1001];
438+
export const WEBSOCKET_CLOSE_CODES_RETRY = [1006,1011,1001];export interface PaginatedLimit {
439+
/** per page max number of results to return */
440+
limit?: number
441+
}
442+
443+
export interface PaginatedTimeRangeOptions {
444+
/** Unix timestamp */
445+
from: UnixTimestamp
446+
/** Unix timestamp */
447+
to: UnixTimestamp
448+
/** maximum number of results to fetch before returning early */
449+
fetchMax?: number
450+
}
451+
452+
export type PaginatedTimeRangeCommonOptions = Partial<PaginatedTimeRangeOptions> & PaginatedLimit;
453+
454+
export type CursorType = number | string;
455+
456+
export interface PaginatedListensOptions<T extends CursorType> extends PaginatedLimit {
457+
cursor: T
458+
}
459+
460+
export interface PagelessListensTimeRangeOptions extends PaginatedTimeRangeCommonOptions {
461+
}
462+
463+
export interface PaginatedListensTimeRangeOptions<T extends CursorType = CursorType> extends Partial<PaginatedTimeRangeOptions>, PaginatedListensOptions<T> {
464+
}
465+
466+
export interface PaginatedResults<T extends CursorType = CursorType> {
467+
total?: number
468+
more?: boolean
469+
cursorNext?: T
470+
order?: 'desc' | 'asc'
471+
}
472+
473+
export interface PaginatedListens {
474+
getPaginatedListens(params: PaginatedListensOptions<CursorType>): Promise<{data: PlayObject[], meta: PaginatedListensOptions<CursorType> & PaginatedResults<CursorType>}>
475+
}
476+
477+
export const hasPaginagedListens = (obj: Object): obj is PaginatedListens => {
478+
return 'getPaginatedListens' in obj;
479+
}
480+
481+
export interface PaginatedTimeRangeListensResult<T extends CursorType> {
482+
data: PlayObject[];
483+
meta: PaginatedListensTimeRangeOptions<T> & PaginatedResults<T>;
484+
}
485+
export interface PaginatedTimeRangeListens<T extends CursorType = CursorType> {
486+
getPaginatedTimeRangeListens(params: PaginatedListensTimeRangeOptions<T>): Promise<PaginatedTimeRangeListensResult<T>>
487+
getPaginatedUnitOfTime(): ManipulateType;
488+
}
489+
490+
export const hasPaginatedTimeRangeListens = (obj: Object): obj is PaginatedTimeRangeListens => {
491+
return 'getPaginatedTimeRangeListens' in obj;
492+
}
493+
494+
export interface PagelessTimeRangeListensResult {
495+
data: PlayObject[]
496+
meta: PagelessListensTimeRangeOptions & PaginatedResults
497+
}
498+
export interface PagelessTimeRangeListens {
499+
getPagelessTimeRangeListens(params: PagelessListensTimeRangeOptions): Promise<PagelessTimeRangeListensResult>
500+
getPaginatedUnitOfTime(): ManipulateType;
501+
}
502+
503+
export const hasPagelessTimeRangeListens = (obj: Object): obj is PagelessTimeRangeListens => {
504+
return 'getPagelessTimeRangeListens' in obj;
505+
}
506+
507+
export type PaginatedTimeRangeSource = PaginatedTimeRangeListens | PagelessTimeRangeListens;
508+
export type PaginatedSource = PaginatedListens | PaginatedTimeRangeSource;
509+
510+
export type TimeRangeListensFetcher<T extends CursorType = CursorType> = (opts: PaginatedTimeRangeCommonOptions | PaginatedListensTimeRangeOptions<T>) => Promise<PlayObject[]>
511+
512+
export interface ScrobbleRangeResult {
513+
plays: PlayObject[]
514+
fetchedAt: Dayjs
515+
}
516+
517+
export const REFRESH_STALE_DEFAULT = 60;

src/backend/common/infrastructure/config/client/koito.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { UnixTimestamp } from "../../../../../core/Atomic.js"
12
import { RequestRetryOptions } from "../common.js"
23
import { CommonClientConfig, CommonClientData } from "./index.js"
34

@@ -15,6 +16,18 @@ export interface ListenObjectResponse {
1516
track: TrackResponse
1617
}
1718

19+
export interface GetListensOptions {
20+
limit?: number
21+
page?: number
22+
week?: number
23+
month?: number
24+
year?: number
25+
26+
// new in 0.1.0
27+
to?: UnixTimestamp
28+
from?: UnixTimestamp
29+
}
30+
1831
export interface TrackResponse {
1932
id: number
2033
title: string

src/backend/common/infrastructure/config/client/listenbrainz.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,11 @@ export interface ListenBrainzClientConfig extends CommonClientConfig {
3838
export interface ListenBrainzClientAIOConfig extends ListenBrainzClientConfig {
3939
type: 'listenbrainz'
4040
}
41+
42+
43+
/** https://github.com/metabrainz/listenbrainz-server/pull/2572
44+
* https://github.com/metabrainz/listenbrainz-server/blob/master/listenbrainz/webserver/views/api_tools.py#L48
45+
*/
46+
export const MAX_ITEMS_PER_GET_LZ = 1000;
47+
export const DEFAULT_ITEMS_PER_GET_LZ = 25;
48+
export const DEFAULT_MS_ITEMS_PER_GET_LZ = 100;

src/backend/common/logging.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,19 @@ export class MaybeLogger {
115115
}
116116
}
117117
}
118+
119+
const noopLog = (_: any, ...rest: any) => undefined;
120+
121+
export const loggerNoop: Logger = {
122+
trace: noopLog,
123+
debug: noopLog,
124+
log: noopLog,
125+
info: noopLog,
126+
verbose: noopLog,
127+
warn: noopLog,
128+
error: noopLog,
129+
fatal: noopLog,
130+
silent: noopLog,
131+
level: 'silent',
132+
child: (_: any, ...rest: any) => loggerNoop as Logger
133+
} as unknown as Logger;

0 commit comments

Comments
 (0)