@ansango/lastfm-api 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +61 -0
- package/README.md +66 -22
- package/dist/client.d.ts +2 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -1
- package/dist/client.js.map +1 -1
- package/dist/entrypoints/insights.d.ts +4 -0
- package/dist/entrypoints/insights.d.ts.map +1 -0
- package/dist/entrypoints/insights.js +3 -0
- package/dist/entrypoints/insights.js.map +1 -0
- package/dist/entrypoints/insights.schemas.d.ts +2 -0
- package/dist/entrypoints/insights.schemas.d.ts.map +1 -0
- package/dist/entrypoints/insights.schemas.js +3 -0
- package/dist/entrypoints/insights.schemas.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/services/album.d.ts +2 -4
- package/dist/services/album.d.ts.map +1 -1
- package/dist/services/album.js.map +1 -1
- package/dist/services/artist.d.ts +2 -4
- package/dist/services/artist.d.ts.map +1 -1
- package/dist/services/artist.js.map +1 -1
- package/dist/services/auth.d.ts +1 -38
- package/dist/services/auth.d.ts.map +1 -1
- package/dist/services/auth.js +1 -19
- package/dist/services/auth.js.map +1 -1
- package/dist/services/auth.schemas.d.ts +0 -13
- package/dist/services/auth.schemas.d.ts.map +1 -1
- package/dist/services/auth.schemas.js +0 -7
- package/dist/services/auth.schemas.js.map +1 -1
- package/dist/services/index.d.ts +2 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/index.js +2 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/insights/binges.d.ts +8 -0
- package/dist/services/insights/binges.d.ts.map +1 -0
- package/dist/services/insights/binges.js +53 -0
- package/dist/services/insights/binges.js.map +1 -0
- package/dist/services/insights/compare.d.ts +8 -0
- package/dist/services/insights/compare.d.ts.map +1 -0
- package/dist/services/insights/compare.js +39 -0
- package/dist/services/insights/compare.js.map +1 -0
- package/dist/services/insights/discoveries.d.ts +8 -0
- package/dist/services/insights/discoveries.d.ts.map +1 -0
- package/dist/services/insights/discoveries.js +53 -0
- package/dist/services/insights/discoveries.js.map +1 -0
- package/dist/services/insights/hours.d.ts +8 -0
- package/dist/services/insights/hours.d.ts.map +1 -0
- package/dist/services/insights/hours.js +43 -0
- package/dist/services/insights/hours.js.map +1 -0
- package/dist/services/insights/lib/binges.d.ts +31 -0
- package/dist/services/insights/lib/binges.d.ts.map +1 -0
- package/dist/services/insights/lib/binges.js +61 -0
- package/dist/services/insights/lib/binges.js.map +1 -0
- package/dist/services/insights/lib/compare.d.ts +31 -0
- package/dist/services/insights/lib/compare.d.ts.map +1 -0
- package/dist/services/insights/lib/compare.js +68 -0
- package/dist/services/insights/lib/compare.js.map +1 -0
- package/dist/services/insights/lib/discoveries.d.ts +17 -0
- package/dist/services/insights/lib/discoveries.d.ts.map +1 -0
- package/dist/services/insights/lib/discoveries.js +24 -0
- package/dist/services/insights/lib/discoveries.js.map +1 -0
- package/dist/services/insights/lib/diversity.d.ts +28 -0
- package/dist/services/insights/lib/diversity.d.ts.map +1 -0
- package/dist/services/insights/lib/diversity.js +39 -0
- package/dist/services/insights/lib/diversity.js.map +1 -0
- package/dist/services/insights/lib/hours.d.ts +39 -0
- package/dist/services/insights/lib/hours.d.ts.map +1 -0
- package/dist/services/insights/lib/hours.js +66 -0
- package/dist/services/insights/lib/hours.js.map +1 -0
- package/dist/services/insights/lib/mood.d.ts +22 -0
- package/dist/services/insights/lib/mood.d.ts.map +1 -0
- package/dist/services/insights/lib/mood.js +187 -0
- package/dist/services/insights/lib/mood.js.map +1 -0
- package/dist/services/insights/lib/periods.d.ts +17 -0
- package/dist/services/insights/lib/periods.d.ts.map +1 -0
- package/dist/services/insights/lib/periods.js +66 -0
- package/dist/services/insights/lib/periods.js.map +1 -0
- package/dist/services/insights/lib/personality.d.ts +30 -0
- package/dist/services/insights/lib/personality.d.ts.map +1 -0
- package/dist/services/insights/lib/personality.js +131 -0
- package/dist/services/insights/lib/personality.js.map +1 -0
- package/dist/services/insights/lib/trends.d.ts +30 -0
- package/dist/services/insights/lib/trends.d.ts.map +1 -0
- package/dist/services/insights/lib/trends.js +67 -0
- package/dist/services/insights/lib/trends.js.map +1 -0
- package/dist/services/insights/mood.d.ts +8 -0
- package/dist/services/insights/mood.d.ts.map +1 -0
- package/dist/services/insights/mood.js +53 -0
- package/dist/services/insights/mood.js.map +1 -0
- package/dist/services/insights/now-playing.d.ts +20 -0
- package/dist/services/insights/now-playing.d.ts.map +1 -0
- package/dist/services/insights/now-playing.js +128 -0
- package/dist/services/insights/now-playing.js.map +1 -0
- package/dist/services/insights/personality.d.ts +8 -0
- package/dist/services/insights/personality.d.ts.map +1 -0
- package/dist/services/insights/personality.js +42 -0
- package/dist/services/insights/personality.js.map +1 -0
- package/dist/services/insights/summary.d.ts +8 -0
- package/dist/services/insights/summary.d.ts.map +1 -0
- package/dist/services/insights/summary.js +117 -0
- package/dist/services/insights/summary.js.map +1 -0
- package/dist/services/insights/trends.d.ts +8 -0
- package/dist/services/insights/trends.d.ts.map +1 -0
- package/dist/services/insights/trends.js +96 -0
- package/dist/services/insights/trends.js.map +1 -0
- package/dist/services/insights.d.ts +96 -0
- package/dist/services/insights.d.ts.map +1 -0
- package/dist/services/insights.js +26 -0
- package/dist/services/insights.js.map +1 -0
- package/dist/services/insights.schemas.d.ts +710 -0
- package/dist/services/insights.schemas.d.ts.map +1 -0
- package/dist/services/insights.schemas.js +307 -0
- package/dist/services/insights.schemas.js.map +1 -0
- package/dist/services/track.d.ts +6 -12
- package/dist/services/track.d.ts.map +1 -1
- package/dist/services/track.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/package.json +12 -4
package/dist/services/index.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export * from './chart.js';
|
|
|
8
8
|
export * from './chart.schemas.js';
|
|
9
9
|
export * from './geo.js';
|
|
10
10
|
export * from './geo.schemas.js';
|
|
11
|
+
export * from './insights.js';
|
|
12
|
+
export * from './insights.schemas.js';
|
|
11
13
|
export * from './library.js';
|
|
12
14
|
export * from './library.schemas.js';
|
|
13
15
|
export * from './schemas/index.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,YAAY,CAAA;AAC1B,cAAc,oBAAoB,CAAA;AAClC,cAAc,UAAU,CAAA;AACxB,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,UAAU,CAAA;AACxB,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,oBAAoB,CAAA;AAClC,cAAc,WAAW,CAAA;AAEzB,cAAc,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,YAAY,CAAA;AAC1B,cAAc,oBAAoB,CAAA;AAClC,cAAc,UAAU,CAAA;AACxB,cAAc,kBAAkB,CAAA;AAChC,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,UAAU,CAAA;AACxB,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,oBAAoB,CAAA;AAClC,cAAc,WAAW,CAAA;AAEzB,cAAc,mBAAmB,CAAA"}
|
package/dist/services/index.js
CHANGED
|
@@ -8,6 +8,8 @@ export * from './chart.js';
|
|
|
8
8
|
export * from './chart.schemas.js';
|
|
9
9
|
export * from './geo.js';
|
|
10
10
|
export * from './geo.schemas.js';
|
|
11
|
+
export * from './insights.js';
|
|
12
|
+
export * from './insights.schemas.js';
|
|
11
13
|
export * from './library.js';
|
|
12
14
|
export * from './library.schemas.js';
|
|
13
15
|
export * from './schemas/index.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,YAAY,CAAA;AAC1B,cAAc,oBAAoB,CAAA;AAClC,cAAc,UAAU,CAAA;AACxB,cAAc,kBAAkB,CAAA;AAChC,cAAc,cAAc,CAAA;AAC5B,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,UAAU,CAAA;AACxB,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,oBAAoB,CAAA;AAClC,cAAc,WAAW,CAAA;AACzB,+BAA+B;AAC/B,cAAc,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/services/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA;AAC3B,cAAc,qBAAqB,CAAA;AACnC,cAAc,WAAW,CAAA;AACzB,cAAc,mBAAmB,CAAA;AACjC,cAAc,YAAY,CAAA;AAC1B,cAAc,oBAAoB,CAAA;AAClC,cAAc,UAAU,CAAA;AACxB,cAAc,kBAAkB,CAAA;AAChC,cAAc,eAAe,CAAA;AAC7B,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,sBAAsB,CAAA;AACpC,cAAc,oBAAoB,CAAA;AAClC,cAAc,UAAU,CAAA;AACxB,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,oBAAoB,CAAA;AAClC,cAAc,WAAW,CAAA;AACzB,+BAA+B;AAC/B,cAAc,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LastFmConfig } from '../../config.js';
|
|
2
|
+
import type { InsightsBingesRequest, InsightsBingesResponse } from '../insights.schemas.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detects binge listening streaks (consecutive plays of the same artist or track)
|
|
5
|
+
* across a user's recent scrobbles.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getBinges(config: LastFmConfig, params: InsightsBingesRequest, init?: RequestInit): Promise<InsightsBingesResponse>;
|
|
8
|
+
//# sourceMappingURL=binges.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binges.d.ts","sourceRoot":"","sources":["../../../src/services/insights/binges.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,KAAK,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAI3F;;;GAGG;AACH,wBAAsB,SAAS,CAC9B,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,qBAAqB,EAC7B,IAAI,CAAC,EAAE,WAAW,GAChB,OAAO,CAAC,sBAAsB,CAAC,CAwDjC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createUserService } from '../user.js';
|
|
2
|
+
import { findBinges } from './lib/binges.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detects binge listening streaks (consecutive plays of the same artist or track)
|
|
5
|
+
* across a user's recent scrobbles.
|
|
6
|
+
*/
|
|
7
|
+
export async function getBinges(config, params, init) {
|
|
8
|
+
const userService = createUserService(config);
|
|
9
|
+
const to = params.to ?? Math.floor(Date.now() / 1000);
|
|
10
|
+
const from = params.from ?? (params.sinceDays ? to - params.sinceDays * 86400 : to - 7 * 86400);
|
|
11
|
+
const maxPages = params.maxPages ?? 50;
|
|
12
|
+
const scrobbles = [];
|
|
13
|
+
let page = 1;
|
|
14
|
+
while (page <= maxPages) {
|
|
15
|
+
const res = await userService.getRecentTracks({
|
|
16
|
+
user: params.user,
|
|
17
|
+
from: String(from),
|
|
18
|
+
to: String(to),
|
|
19
|
+
limit: 200,
|
|
20
|
+
page,
|
|
21
|
+
}, init);
|
|
22
|
+
const tracks = res.recenttracks?.track ?? [];
|
|
23
|
+
for (const t of tracks) {
|
|
24
|
+
const rawArtist = t.artist;
|
|
25
|
+
const artist = typeof rawArtist === 'object' && rawArtist !== null
|
|
26
|
+
? String(rawArtist.name ?? rawArtist['#text'] ?? '')
|
|
27
|
+
: String(rawArtist ?? '');
|
|
28
|
+
const track = String(t.name ?? '');
|
|
29
|
+
const uts = t.date?.uts ? Number(t.date.uts) : undefined;
|
|
30
|
+
if (artist.length > 0 && track.length > 0 && typeof uts === 'number' && !Number.isNaN(uts)) {
|
|
31
|
+
scrobbles.push({ artist, track, uts });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (tracks.length < 200)
|
|
35
|
+
break;
|
|
36
|
+
page++;
|
|
37
|
+
}
|
|
38
|
+
// Recenttracks returns descending order; sort ascending for consecutive streak detection
|
|
39
|
+
scrobbles.sort((a, b) => a.uts - b.uts);
|
|
40
|
+
const trackKey = params.trackKey === true || params.trackKey === 'track' ? 'track' : 'artist';
|
|
41
|
+
const binges = findBinges(scrobbles, {
|
|
42
|
+
minLength: params.minLength,
|
|
43
|
+
maxGapSeconds: params.maxGapSeconds,
|
|
44
|
+
trackKey,
|
|
45
|
+
maxResults: params.maxResults,
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
user: params.user,
|
|
49
|
+
totalScrobbles: scrobbles.length,
|
|
50
|
+
binges,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=binges.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binges.js","sourceRoot":"","sources":["../../../src/services/insights/binges.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LastFmConfig } from '../../config.js';
|
|
2
|
+
import type { InsightsCompareRequest, InsightsCompareResponse } from '../insights.schemas.js';
|
|
3
|
+
/**
|
|
4
|
+
* Compares two Last.fm users' listening affinity using Jaccard similarity
|
|
5
|
+
* over mutual top artist rosters.
|
|
6
|
+
*/
|
|
7
|
+
export declare function compareUsers(config: LastFmConfig, params: InsightsCompareRequest, init?: RequestInit): Promise<InsightsCompareResponse>;
|
|
8
|
+
//# sourceMappingURL=compare.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.d.ts","sourceRoot":"","sources":["../../../src/services/insights/compare.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AAK7F;;;GAGG;AACH,wBAAsB,YAAY,CACjC,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,sBAAsB,EAC9B,IAAI,CAAC,EAAE,WAAW,GAChB,OAAO,CAAC,uBAAuB,CAAC,CAmClC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createUserService } from '../user.js';
|
|
2
|
+
import { compareArtists } from './lib/compare.js';
|
|
3
|
+
import { resolvePeriod } from './lib/periods.js';
|
|
4
|
+
/**
|
|
5
|
+
* Compares two Last.fm users' listening affinity using Jaccard similarity
|
|
6
|
+
* over mutual top artist rosters.
|
|
7
|
+
*/
|
|
8
|
+
export async function compareUsers(config, params, init) {
|
|
9
|
+
const userService = createUserService(config);
|
|
10
|
+
const resolved = resolvePeriod(params.period ?? 'overall');
|
|
11
|
+
const limit = params.limit ?? 100;
|
|
12
|
+
const [topArtistsA, topArtistsB] = await Promise.all([
|
|
13
|
+
userService.getTopArtists({ user: params.userA, period: resolved.lastfm, limit }, init),
|
|
14
|
+
userService.getTopArtists({ user: params.userB, period: resolved.lastfm, limit }, init),
|
|
15
|
+
]);
|
|
16
|
+
const rosterA = (topArtistsA.topartists?.artist ?? []).map((a) => ({
|
|
17
|
+
name: a.name,
|
|
18
|
+
playcount: Number(a.playcount ?? 0),
|
|
19
|
+
}));
|
|
20
|
+
const rosterB = (topArtistsB.topartists?.artist ?? []).map((b) => ({
|
|
21
|
+
name: b.name,
|
|
22
|
+
playcount: Number(b.playcount ?? 0),
|
|
23
|
+
}));
|
|
24
|
+
const comparison = compareArtists(rosterA, rosterB);
|
|
25
|
+
return {
|
|
26
|
+
userA: params.userA,
|
|
27
|
+
userB: params.userB,
|
|
28
|
+
period: resolved.lastfm,
|
|
29
|
+
compatibilityScore: comparison.compatibilityScore,
|
|
30
|
+
jaccard: comparison.jaccard,
|
|
31
|
+
sharedCount: comparison.intersection.length,
|
|
32
|
+
userACount: comparison.aCount,
|
|
33
|
+
userBCount: comparison.bCount,
|
|
34
|
+
sharedArtists: [...comparison.sharedArtists],
|
|
35
|
+
onlyUserA: [...comparison.onlyA],
|
|
36
|
+
onlyUserB: [...comparison.onlyB],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=compare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.js","sourceRoot":"","sources":["../../../src/services/insights/compare.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,cAAc,EAAyB,MAAM,kBAAkB,CAAA;AACxE,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAEhD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CACjC,MAAoB,EACpB,MAA8B,EAC9B,IAAkB;IAElB,MAAM,WAAW,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAA;IAC7C,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,MAAM,IAAI,SAAS,CAAC,CAAA;IAC1D,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,GAAG,CAAA;IAEjC,MAAM,CAAC,WAAW,EAAE,WAAW,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QACpD,WAAW,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC;QACvF,WAAW,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC;KACvF,CAAC,CAAA;IAEF,MAAM,OAAO,GAAuB,CAAC,WAAW,CAAC,UAAU,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACtF,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC;KACnC,CAAC,CAAC,CAAA;IAEH,MAAM,OAAO,GAAuB,CAAC,WAAW,CAAC,UAAU,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACtF,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC;KACnC,CAAC,CAAC,CAAA;IAEH,MAAM,UAAU,GAAG,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;IAEnD,OAAO;QACN,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,kBAAkB,EAAE,UAAU,CAAC,kBAAkB;QACjD,OAAO,EAAE,UAAU,CAAC,OAAO;QAC3B,WAAW,EAAE,UAAU,CAAC,YAAY,CAAC,MAAM;QAC3C,UAAU,EAAE,UAAU,CAAC,MAAM;QAC7B,UAAU,EAAE,UAAU,CAAC,MAAM;QAC7B,aAAa,EAAE,CAAC,GAAG,UAAU,CAAC,aAAa,CAAC;QAC5C,SAAS,EAAE,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC;QAChC,SAAS,EAAE,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC;KAChC,CAAA;AACF,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LastFmConfig } from '../../config.js';
|
|
2
|
+
import type { InsightsDiscoveriesRequest, InsightsDiscoveriesResponse } from '../insights.schemas.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detects newly discovered artists in a recent time window by comparing
|
|
5
|
+
* against the user's historical baseline roster.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getDiscoveries(config: LastFmConfig, params: InsightsDiscoveriesRequest, init?: RequestInit): Promise<InsightsDiscoveriesResponse>;
|
|
8
|
+
//# sourceMappingURL=discoveries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discoveries.d.ts","sourceRoot":"","sources":["../../../src/services/insights/discoveries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,KAAK,EAAE,0BAA0B,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAA;AAIrG;;;GAGG;AACH,wBAAsB,cAAc,CACnC,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,0BAA0B,EAClC,IAAI,CAAC,EAAE,WAAW,GAChB,OAAO,CAAC,2BAA2B,CAAC,CA0DtC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { createUserService } from '../user.js';
|
|
2
|
+
import { findNewArtists } from './lib/discoveries.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detects newly discovered artists in a recent time window by comparing
|
|
5
|
+
* against the user's historical baseline roster.
|
|
6
|
+
*/
|
|
7
|
+
export async function getDiscoveries(config, params, init) {
|
|
8
|
+
const userService = createUserService(config);
|
|
9
|
+
const windowDays = params.windowDays ?? 7;
|
|
10
|
+
const to = params.to ?? Math.floor(Date.now() / 1000);
|
|
11
|
+
const from = params.from ?? to - windowDays * 86400;
|
|
12
|
+
const baselineLimit = params.baselineLimit ?? 200;
|
|
13
|
+
const maxPages = params.maxPages ?? 20;
|
|
14
|
+
const maxResults = params.maxResults ?? 20;
|
|
15
|
+
// 1. Fetch baseline overall top artists
|
|
16
|
+
const baselineRes = await userService.getTopArtists({ user: params.user, period: 'overall', limit: baselineLimit }, init);
|
|
17
|
+
const baselineSet = new Set((baselineRes.topartists?.artist ?? []).map((a) => a.name));
|
|
18
|
+
// 2. Fetch window recent tracks
|
|
19
|
+
const windowScrobbles = [];
|
|
20
|
+
let page = 1;
|
|
21
|
+
while (page <= maxPages) {
|
|
22
|
+
const recentRes = await userService.getRecentTracks({
|
|
23
|
+
user: params.user,
|
|
24
|
+
from: String(from),
|
|
25
|
+
to: String(to),
|
|
26
|
+
limit: 200,
|
|
27
|
+
page,
|
|
28
|
+
}, init);
|
|
29
|
+
const tracks = recentRes.recenttracks?.track ?? [];
|
|
30
|
+
for (const t of tracks) {
|
|
31
|
+
const rawArtist = t.artist;
|
|
32
|
+
const artistName = typeof rawArtist === 'object' && rawArtist !== null
|
|
33
|
+
? String(rawArtist.name ?? rawArtist['#text'] ?? '')
|
|
34
|
+
: String(rawArtist ?? '');
|
|
35
|
+
const uts = t.date?.uts ? Number(t.date.uts) : undefined;
|
|
36
|
+
if (artistName.length > 0 && typeof uts === 'number' && !Number.isNaN(uts)) {
|
|
37
|
+
windowScrobbles.push({ name: artistName, firstSeen: uts });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (tracks.length < 200)
|
|
41
|
+
break;
|
|
42
|
+
page++;
|
|
43
|
+
}
|
|
44
|
+
const discoveries = findNewArtists(windowScrobbles, baselineSet, { maxResults });
|
|
45
|
+
return {
|
|
46
|
+
user: params.user,
|
|
47
|
+
windowDays,
|
|
48
|
+
baselineSize: baselineSet.size,
|
|
49
|
+
totalDiscovered: discoveries.length,
|
|
50
|
+
discoveries,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=discoveries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discoveries.js","sourceRoot":"","sources":["../../../src/services/insights/discoveries.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LastFmConfig } from '../../config.js';
|
|
2
|
+
import type { InsightsHoursRequest, InsightsHoursResponse } from '../insights.schemas.js';
|
|
3
|
+
/**
|
|
4
|
+
* Buckets a user's recent scrobbles by hour-of-day (0..23) and weekday (0..6),
|
|
5
|
+
* calculating diurnal distribution and peak listening times.
|
|
6
|
+
*/
|
|
7
|
+
export declare function getHoursHistogram(config: LastFmConfig, params: InsightsHoursRequest, init?: RequestInit): Promise<InsightsHoursResponse>;
|
|
8
|
+
//# sourceMappingURL=hours.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hours.d.ts","sourceRoot":"","sources":["../../../src/services/insights/hours.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAIzF;;;GAGG;AACH,wBAAsB,iBAAiB,CACtC,MAAM,EAAE,YAAY,EACpB,MAAM,EAAE,oBAAoB,EAC5B,IAAI,CAAC,EAAE,WAAW,GAChB,OAAO,CAAC,qBAAqB,CAAC,CA0ChC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createUserService } from '../user.js';
|
|
2
|
+
import { buildHourHistogram } from './lib/hours.js';
|
|
3
|
+
/**
|
|
4
|
+
* Buckets a user's recent scrobbles by hour-of-day (0..23) and weekday (0..6),
|
|
5
|
+
* calculating diurnal distribution and peak listening times.
|
|
6
|
+
*/
|
|
7
|
+
export async function getHoursHistogram(config, params, init) {
|
|
8
|
+
const userService = createUserService(config);
|
|
9
|
+
const to = params.to ?? Math.floor(Date.now() / 1000);
|
|
10
|
+
const from = params.from ?? (params.sinceDays ? to - params.sinceDays * 86400 : to - 30 * 86400);
|
|
11
|
+
const maxPages = params.maxPages ?? 50;
|
|
12
|
+
const timestamps = [];
|
|
13
|
+
let page = 1;
|
|
14
|
+
while (page <= maxPages) {
|
|
15
|
+
const res = await userService.getRecentTracks({
|
|
16
|
+
user: params.user,
|
|
17
|
+
from: String(from),
|
|
18
|
+
to: String(to),
|
|
19
|
+
limit: 200,
|
|
20
|
+
page,
|
|
21
|
+
}, init);
|
|
22
|
+
const tracks = res.recenttracks?.track ?? [];
|
|
23
|
+
for (const t of tracks) {
|
|
24
|
+
const uts = t.date?.uts;
|
|
25
|
+
if (uts) {
|
|
26
|
+
const num = Number(uts);
|
|
27
|
+
if (!Number.isNaN(num))
|
|
28
|
+
timestamps.push(num);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
if (tracks.length < 200)
|
|
32
|
+
break;
|
|
33
|
+
page++;
|
|
34
|
+
}
|
|
35
|
+
const histogram = buildHourHistogram(timestamps);
|
|
36
|
+
return {
|
|
37
|
+
user: params.user,
|
|
38
|
+
from,
|
|
39
|
+
to,
|
|
40
|
+
...histogram,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=hours.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hours.js","sourceRoot":"","sources":["../../../src/services/insights/hours.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAA;AAEnD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACtC,MAAoB,EACpB,MAA4B,EAC5B,IAAkB;IAElB,MAAM,WAAW,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAA;IAC7C,MAAM,EAAE,GAAG,MAAM,CAAC,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;IACrD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,SAAS,GAAG,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,GAAG,KAAK,CAAC,CAAA;IAChG,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAA;IAEtC,MAAM,UAAU,GAAa,EAAE,CAAA;IAC/B,IAAI,IAAI,GAAG,CAAC,CAAA;IAEZ,OAAO,IAAI,IAAI,QAAQ,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,MAAM,WAAW,CAAC,eAAe,CAC5C;YACC,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC;YAClB,EAAE,EAAE,MAAM,CAAC,EAAE,CAAC;YACd,KAAK,EAAE,GAAG;YACV,IAAI;SACJ,EACD,IAAI,CACJ,CAAA;QAED,MAAM,MAAM,GAAG,GAAG,CAAC,YAAY,EAAE,KAAK,IAAI,EAAE,CAAA;QAC5C,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;YACxB,MAAM,GAAG,GAAG,CAAC,CAAC,IAAI,EAAE,GAAG,CAAA;YACvB,IAAI,GAAG,EAAE,CAAC;gBACT,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAA;gBACvB,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC;oBAAE,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;YAC7C,CAAC;QACF,CAAC;QAED,IAAI,MAAM,CAAC,MAAM,GAAG,GAAG;YAAE,MAAK;QAC9B,IAAI,EAAE,CAAA;IACP,CAAC;IAED,MAAM,SAAS,GAAG,kBAAkB,CAAC,UAAU,CAAC,CAAA;IAEhD,OAAO;QACN,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,IAAI;QACJ,EAAE;QACF,GAAG,SAAS;KACZ,CAAA;AACF,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* binges.ts — detect binge listening runs from a list of scrobbles.
|
|
3
|
+
*
|
|
4
|
+
* Groups consecutive plays of the same artist or track where the gap
|
|
5
|
+
* between adjacent scrobbles does not exceed maxGapSeconds (default 1h).
|
|
6
|
+
*/
|
|
7
|
+
export interface ScrobbleItem {
|
|
8
|
+
readonly artist: string;
|
|
9
|
+
readonly track: string;
|
|
10
|
+
readonly uts: number;
|
|
11
|
+
}
|
|
12
|
+
export type BingesTrackKey = 'artist' | 'track';
|
|
13
|
+
export interface BingeItem {
|
|
14
|
+
readonly artist: string;
|
|
15
|
+
readonly track?: string;
|
|
16
|
+
readonly length: number;
|
|
17
|
+
readonly startUts: number;
|
|
18
|
+
readonly endUts: number;
|
|
19
|
+
readonly durationSeconds: number;
|
|
20
|
+
}
|
|
21
|
+
export interface FindBingesOptions {
|
|
22
|
+
readonly minLength?: number;
|
|
23
|
+
readonly maxGapSeconds?: number;
|
|
24
|
+
readonly trackKey?: BingesTrackKey;
|
|
25
|
+
readonly maxResults?: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Finds consecutive listening runs in a chronologically ordered array of scrobbles.
|
|
29
|
+
*/
|
|
30
|
+
export declare function findBinges(scrobbles: readonly ScrobbleItem[], options?: FindBingesOptions): BingeItem[];
|
|
31
|
+
//# sourceMappingURL=binges.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binges.d.ts","sourceRoot":"","sources":["../../../../src/services/insights/lib/binges.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,OAAO,CAAA;AAE/C,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CAChC;AAED,MAAM,WAAW,iBAAiB;IACjC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAA;IAClC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC5B;AAMD;;GAEG;AACH,wBAAgB,UAAU,CAAC,SAAS,EAAE,SAAS,YAAY,EAAE,EAAE,OAAO,GAAE,iBAAsB,GAAG,SAAS,EAAE,CAyD3G"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
function keyOf(s, k) {
|
|
2
|
+
return k === 'artist' ? s.artist : `${s.artist}::${s.track}`;
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* Finds consecutive listening runs in a chronologically ordered array of scrobbles.
|
|
6
|
+
*/
|
|
7
|
+
export function findBinges(scrobbles, options = {}) {
|
|
8
|
+
const minLength = options.minLength ?? 2;
|
|
9
|
+
const maxGap = options.maxGapSeconds ?? 3600;
|
|
10
|
+
const trackKey = options.trackKey ?? 'artist';
|
|
11
|
+
const out = [];
|
|
12
|
+
let runKey = null;
|
|
13
|
+
let runArtist = null;
|
|
14
|
+
let runTrack = null;
|
|
15
|
+
let runStart = 0;
|
|
16
|
+
let runEnd = 0;
|
|
17
|
+
let runLen = 0;
|
|
18
|
+
const flush = () => {
|
|
19
|
+
if (runLen >= minLength && runKey !== null && runArtist !== null) {
|
|
20
|
+
out.push({
|
|
21
|
+
artist: runArtist,
|
|
22
|
+
...(trackKey === 'track' && runTrack !== null ? { track: runTrack } : {}),
|
|
23
|
+
length: runLen,
|
|
24
|
+
startUts: runStart,
|
|
25
|
+
endUts: runEnd,
|
|
26
|
+
durationSeconds: runEnd - runStart,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
runKey = null;
|
|
30
|
+
runLen = 0;
|
|
31
|
+
};
|
|
32
|
+
for (const s of scrobbles) {
|
|
33
|
+
const k = keyOf(s, trackKey);
|
|
34
|
+
if (runKey === null) {
|
|
35
|
+
runKey = k;
|
|
36
|
+
runArtist = s.artist;
|
|
37
|
+
runTrack = s.track;
|
|
38
|
+
runStart = s.uts;
|
|
39
|
+
runEnd = s.uts;
|
|
40
|
+
runLen = 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (k !== runKey || s.uts - runEnd > maxGap) {
|
|
44
|
+
flush();
|
|
45
|
+
runKey = k;
|
|
46
|
+
runArtist = s.artist;
|
|
47
|
+
runTrack = s.track;
|
|
48
|
+
runStart = s.uts;
|
|
49
|
+
runEnd = s.uts;
|
|
50
|
+
runLen = 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
runEnd = s.uts;
|
|
54
|
+
runLen++;
|
|
55
|
+
}
|
|
56
|
+
flush();
|
|
57
|
+
// Sort primarily by run length descending, secondarily by most recent
|
|
58
|
+
out.sort((a, b) => b.length - a.length || b.endUts - a.endUts);
|
|
59
|
+
return options.maxResults !== undefined ? out.slice(0, options.maxResults) : out;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=binges.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binges.js","sourceRoot":"","sources":["../../../../src/services/insights/lib/binges.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compare.ts — compare two ranked artist lists and compute Jaccard similarity.
|
|
3
|
+
*/
|
|
4
|
+
export interface NamedArtistEntry {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly playcount: number;
|
|
7
|
+
}
|
|
8
|
+
export interface CompareArtistsResult {
|
|
9
|
+
readonly aCount: number;
|
|
10
|
+
readonly bCount: number;
|
|
11
|
+
readonly intersection: readonly string[];
|
|
12
|
+
readonly sharedArtists: ReadonlyArray<{
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly userAPlaycount: number;
|
|
15
|
+
readonly userBPlaycount: number;
|
|
16
|
+
readonly weight: number;
|
|
17
|
+
}>;
|
|
18
|
+
readonly onlyA: readonly string[];
|
|
19
|
+
readonly onlyB: readonly string[];
|
|
20
|
+
readonly jaccard: number;
|
|
21
|
+
readonly compatibilityScore: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Computes Jaccard similarity: |A ∩ B| / |A ∪ B|.
|
|
25
|
+
*/
|
|
26
|
+
export declare function jaccard<T>(a: ReadonlySet<T>, b: ReadonlySet<T>): number;
|
|
27
|
+
/**
|
|
28
|
+
* Compares two artist rosters, calculating mutual artist overlap and affinity metrics.
|
|
29
|
+
*/
|
|
30
|
+
export declare function compareArtists(a: readonly NamedArtistEntry[], b: readonly NamedArtistEntry[]): CompareArtistsResult;
|
|
31
|
+
//# sourceMappingURL=compare.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.d.ts","sourceRoot":"","sources":["../../../../src/services/insights/lib/compare.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;IACxC,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;QACrC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;QACrB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;QAC/B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;QAC/B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;KACvB,CAAC,CAAA;IACF,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAA;IACjC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAA;CACnC;AAED;;GAEG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,MAAM,CASvE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,SAAS,gBAAgB,EAAE,EAAE,CAAC,EAAE,SAAS,gBAAgB,EAAE,GAAG,oBAAoB,CA+CnH"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Computes Jaccard similarity: |A ∩ B| / |A ∪ B|.
|
|
3
|
+
*/
|
|
4
|
+
export function jaccard(a, b) {
|
|
5
|
+
if (a.size === 0 && b.size === 0)
|
|
6
|
+
return 0;
|
|
7
|
+
let inter = 0;
|
|
8
|
+
for (const x of a) {
|
|
9
|
+
if (b.has(x))
|
|
10
|
+
inter++;
|
|
11
|
+
}
|
|
12
|
+
const union = a.size + b.size - inter;
|
|
13
|
+
if (union === 0)
|
|
14
|
+
return 0;
|
|
15
|
+
return inter / union;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Compares two artist rosters, calculating mutual artist overlap and affinity metrics.
|
|
19
|
+
*/
|
|
20
|
+
export function compareArtists(a, b) {
|
|
21
|
+
const mapA = new Map();
|
|
22
|
+
const mapB = new Map();
|
|
23
|
+
for (const x of a)
|
|
24
|
+
mapA.set(x.name, x.playcount);
|
|
25
|
+
for (const x of b)
|
|
26
|
+
mapB.set(x.name, x.playcount);
|
|
27
|
+
const intersection = [];
|
|
28
|
+
const onlyA = [];
|
|
29
|
+
const onlyB = [];
|
|
30
|
+
for (const [name] of mapA) {
|
|
31
|
+
if (mapB.has(name))
|
|
32
|
+
intersection.push(name);
|
|
33
|
+
else
|
|
34
|
+
onlyA.push(name);
|
|
35
|
+
}
|
|
36
|
+
for (const [name] of mapB) {
|
|
37
|
+
if (!mapA.has(name))
|
|
38
|
+
onlyB.push(name);
|
|
39
|
+
}
|
|
40
|
+
const sharedArtists = intersection
|
|
41
|
+
.map((name) => {
|
|
42
|
+
const userAPlaycount = mapA.get(name) ?? 0;
|
|
43
|
+
const userBPlaycount = mapB.get(name) ?? 0;
|
|
44
|
+
const weight = Math.min(userAPlaycount, userBPlaycount);
|
|
45
|
+
return {
|
|
46
|
+
name,
|
|
47
|
+
userAPlaycount,
|
|
48
|
+
userBPlaycount,
|
|
49
|
+
weight,
|
|
50
|
+
};
|
|
51
|
+
})
|
|
52
|
+
.sort((x, y) => y.weight - x.weight);
|
|
53
|
+
const setA = new Set(mapA.keys());
|
|
54
|
+
const setB = new Set(mapB.keys());
|
|
55
|
+
const j = jaccard(setA, setB);
|
|
56
|
+
const compatibilityScore = Math.round(j * 100);
|
|
57
|
+
return {
|
|
58
|
+
aCount: a.length,
|
|
59
|
+
bCount: b.length,
|
|
60
|
+
intersection: intersection.sort(),
|
|
61
|
+
sharedArtists,
|
|
62
|
+
onlyA: onlyA.sort(),
|
|
63
|
+
onlyB: onlyB.sort(),
|
|
64
|
+
jaccard: Math.round(j * 1000) / 1000,
|
|
65
|
+
compatibilityScore,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=compare.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compare.js","sourceRoot":"","sources":["../../../../src/services/insights/lib/compare.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* discoveries.ts — detect newly discovered artists inside a time window
|
|
3
|
+
* relative to an overall baseline roster.
|
|
4
|
+
*/
|
|
5
|
+
export interface ArtistTimestampItem {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
/** UNIX seconds; the earliest occurrence of this artist in the window. */
|
|
8
|
+
readonly firstSeen: number;
|
|
9
|
+
}
|
|
10
|
+
export interface FindNewArtistsOptions {
|
|
11
|
+
readonly maxResults?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Filters and sorts artists first scrobbled in the window that do not exist in the baseline set.
|
|
15
|
+
*/
|
|
16
|
+
export declare function findNewArtists(window: readonly ArtistTimestampItem[], baseline: ReadonlySet<string>, options?: FindNewArtistsOptions): ArtistTimestampItem[];
|
|
17
|
+
//# sourceMappingURL=discoveries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discoveries.d.ts","sourceRoot":"","sources":["../../../../src/services/insights/lib/discoveries.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,WAAW,mBAAmB;IACnC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC7B,MAAM,EAAE,SAAS,mBAAmB,EAAE,EACtC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,EAC7B,OAAO,GAAE,qBAA0B,GACjC,mBAAmB,EAAE,CAqBvB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filters and sorts artists first scrobbled in the window that do not exist in the baseline set.
|
|
3
|
+
*/
|
|
4
|
+
export function findNewArtists(window, baseline, options = {}) {
|
|
5
|
+
// Dedupe by name; keep earliest firstSeen
|
|
6
|
+
const earliest = new Map();
|
|
7
|
+
for (const a of window) {
|
|
8
|
+
const prev = earliest.get(a.name);
|
|
9
|
+
if (prev === undefined || a.firstSeen < prev) {
|
|
10
|
+
earliest.set(a.name, a.firstSeen);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const newbies = [];
|
|
14
|
+
for (const [name, firstSeen] of earliest) {
|
|
15
|
+
if (!baseline.has(name)) {
|
|
16
|
+
newbies.push({ name, firstSeen });
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Sort chronologically by first seen
|
|
20
|
+
newbies.sort((a, b) => a.firstSeen - b.firstSeen);
|
|
21
|
+
const max = options.maxResults ?? newbies.length;
|
|
22
|
+
return newbies.slice(0, max);
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=discoveries.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"discoveries.js","sourceRoot":"","sources":["../../../../src/services/insights/lib/discoveries.ts"],"names":[],"mappings":"AAcA;;GAEG;AACH,MAAM,UAAU,cAAc,CAC7B,MAAsC,EACtC,QAA6B,EAC7B,UAAiC,EAAE;IAEnC,0CAA0C;IAC1C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAA;IAC1C,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAA;QACjC,IAAI,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,SAAS,GAAG,IAAI,EAAE,CAAC;YAC9C,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,CAAA;QAClC,CAAC;IACF,CAAC;IAED,MAAM,OAAO,GAA0B,EAAE,CAAA;IACzC,KAAK,MAAM,CAAC,IAAI,EAAE,SAAS,CAAC,IAAI,QAAQ,EAAE,CAAC;QAC1C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACzB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAA;QAClC,CAAC;IACF,CAAC;IAED,qCAAqC;IACrC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC,CAAA;IACjD,MAAM,GAAG,GAAG,OAAO,CAAC,UAAU,IAAI,OAAO,CAAC,MAAM,CAAA;IAChD,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;AAC7B,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diversity.ts — Shannon entropy + top-N concentration over a count map.
|
|
3
|
+
*
|
|
4
|
+
* Pure: takes a {name: count} map and returns normalized metrics.
|
|
5
|
+
*
|
|
6
|
+
* Shannon entropy H = -Σ pᵢ ln(pᵢ) where pᵢ = countᵢ / total.
|
|
7
|
+
* Normalized H / ln(uniqueCount) puts it on [0, 1].
|
|
8
|
+
*/
|
|
9
|
+
export type CountMap = Readonly<Record<string, number>>;
|
|
10
|
+
export interface Diversity {
|
|
11
|
+
readonly total: number;
|
|
12
|
+
readonly uniqueCount: number;
|
|
13
|
+
/** Raw Shannon entropy in nats. */
|
|
14
|
+
readonly shannon: number;
|
|
15
|
+
/** Shannon normalized to [0, 1] by dividing by ln(uniqueCount). 1 = perfectly even. */
|
|
16
|
+
readonly normalized: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Computes the Shannon diversity index over a map of item counts.
|
|
20
|
+
*/
|
|
21
|
+
export declare function computeDiversity(counts: CountMap): Diversity;
|
|
22
|
+
/**
|
|
23
|
+
* Fraction of total plays consumed by the top-N entries.
|
|
24
|
+
* - N larger than uniqueCount returns 1.
|
|
25
|
+
* - Empty map returns 0.
|
|
26
|
+
*/
|
|
27
|
+
export declare function topNShare(counts: CountMap, n: number): number;
|
|
28
|
+
//# sourceMappingURL=diversity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diversity.d.ts","sourceRoot":"","sources":["../../../../src/services/insights/lib/diversity.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;AAEvD,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,mCAAmC;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,uFAAuF;IACvF,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC3B;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,QAAQ,GAAG,SAAS,CAiB5D;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAS7D"}
|