@ansango/lastfm-api 3.3.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.
Files changed (103) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +62 -3
  3. package/dist/client.d.ts +2 -1
  4. package/dist/client.d.ts.map +1 -1
  5. package/dist/client.js +3 -1
  6. package/dist/client.js.map +1 -1
  7. package/dist/entrypoints/insights.d.ts +4 -0
  8. package/dist/entrypoints/insights.d.ts.map +1 -0
  9. package/dist/entrypoints/insights.js +3 -0
  10. package/dist/entrypoints/insights.js.map +1 -0
  11. package/dist/entrypoints/insights.schemas.d.ts +2 -0
  12. package/dist/entrypoints/insights.schemas.d.ts.map +1 -0
  13. package/dist/entrypoints/insights.schemas.js +3 -0
  14. package/dist/entrypoints/insights.schemas.js.map +1 -0
  15. package/dist/index.d.ts +4 -2
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +2 -1
  18. package/dist/index.js.map +1 -1
  19. package/dist/services/index.d.ts +2 -0
  20. package/dist/services/index.d.ts.map +1 -1
  21. package/dist/services/index.js +2 -0
  22. package/dist/services/index.js.map +1 -1
  23. package/dist/services/insights/binges.d.ts +8 -0
  24. package/dist/services/insights/binges.d.ts.map +1 -0
  25. package/dist/services/insights/binges.js +53 -0
  26. package/dist/services/insights/binges.js.map +1 -0
  27. package/dist/services/insights/compare.d.ts +8 -0
  28. package/dist/services/insights/compare.d.ts.map +1 -0
  29. package/dist/services/insights/compare.js +39 -0
  30. package/dist/services/insights/compare.js.map +1 -0
  31. package/dist/services/insights/discoveries.d.ts +8 -0
  32. package/dist/services/insights/discoveries.d.ts.map +1 -0
  33. package/dist/services/insights/discoveries.js +53 -0
  34. package/dist/services/insights/discoveries.js.map +1 -0
  35. package/dist/services/insights/hours.d.ts +8 -0
  36. package/dist/services/insights/hours.d.ts.map +1 -0
  37. package/dist/services/insights/hours.js +43 -0
  38. package/dist/services/insights/hours.js.map +1 -0
  39. package/dist/services/insights/lib/binges.d.ts +31 -0
  40. package/dist/services/insights/lib/binges.d.ts.map +1 -0
  41. package/dist/services/insights/lib/binges.js +61 -0
  42. package/dist/services/insights/lib/binges.js.map +1 -0
  43. package/dist/services/insights/lib/compare.d.ts +31 -0
  44. package/dist/services/insights/lib/compare.d.ts.map +1 -0
  45. package/dist/services/insights/lib/compare.js +68 -0
  46. package/dist/services/insights/lib/compare.js.map +1 -0
  47. package/dist/services/insights/lib/discoveries.d.ts +17 -0
  48. package/dist/services/insights/lib/discoveries.d.ts.map +1 -0
  49. package/dist/services/insights/lib/discoveries.js +24 -0
  50. package/dist/services/insights/lib/discoveries.js.map +1 -0
  51. package/dist/services/insights/lib/diversity.d.ts +28 -0
  52. package/dist/services/insights/lib/diversity.d.ts.map +1 -0
  53. package/dist/services/insights/lib/diversity.js +39 -0
  54. package/dist/services/insights/lib/diversity.js.map +1 -0
  55. package/dist/services/insights/lib/hours.d.ts +39 -0
  56. package/dist/services/insights/lib/hours.d.ts.map +1 -0
  57. package/dist/services/insights/lib/hours.js +66 -0
  58. package/dist/services/insights/lib/hours.js.map +1 -0
  59. package/dist/services/insights/lib/mood.d.ts +22 -0
  60. package/dist/services/insights/lib/mood.d.ts.map +1 -0
  61. package/dist/services/insights/lib/mood.js +187 -0
  62. package/dist/services/insights/lib/mood.js.map +1 -0
  63. package/dist/services/insights/lib/periods.d.ts +17 -0
  64. package/dist/services/insights/lib/periods.d.ts.map +1 -0
  65. package/dist/services/insights/lib/periods.js +66 -0
  66. package/dist/services/insights/lib/periods.js.map +1 -0
  67. package/dist/services/insights/lib/personality.d.ts +30 -0
  68. package/dist/services/insights/lib/personality.d.ts.map +1 -0
  69. package/dist/services/insights/lib/personality.js +131 -0
  70. package/dist/services/insights/lib/personality.js.map +1 -0
  71. package/dist/services/insights/lib/trends.d.ts +30 -0
  72. package/dist/services/insights/lib/trends.d.ts.map +1 -0
  73. package/dist/services/insights/lib/trends.js +67 -0
  74. package/dist/services/insights/lib/trends.js.map +1 -0
  75. package/dist/services/insights/mood.d.ts +8 -0
  76. package/dist/services/insights/mood.d.ts.map +1 -0
  77. package/dist/services/insights/mood.js +53 -0
  78. package/dist/services/insights/mood.js.map +1 -0
  79. package/dist/services/insights/now-playing.d.ts +20 -0
  80. package/dist/services/insights/now-playing.d.ts.map +1 -0
  81. package/dist/services/insights/now-playing.js +128 -0
  82. package/dist/services/insights/now-playing.js.map +1 -0
  83. package/dist/services/insights/personality.d.ts +8 -0
  84. package/dist/services/insights/personality.d.ts.map +1 -0
  85. package/dist/services/insights/personality.js +42 -0
  86. package/dist/services/insights/personality.js.map +1 -0
  87. package/dist/services/insights/summary.d.ts +8 -0
  88. package/dist/services/insights/summary.d.ts.map +1 -0
  89. package/dist/services/insights/summary.js +117 -0
  90. package/dist/services/insights/summary.js.map +1 -0
  91. package/dist/services/insights/trends.d.ts +8 -0
  92. package/dist/services/insights/trends.d.ts.map +1 -0
  93. package/dist/services/insights/trends.js +96 -0
  94. package/dist/services/insights/trends.js.map +1 -0
  95. package/dist/services/insights.d.ts +96 -0
  96. package/dist/services/insights.d.ts.map +1 -0
  97. package/dist/services/insights.js +26 -0
  98. package/dist/services/insights.js.map +1 -0
  99. package/dist/services/insights.schemas.d.ts +710 -0
  100. package/dist/services/insights.schemas.d.ts.map +1 -0
  101. package/dist/services/insights.schemas.js +307 -0
  102. package/dist/services/insights.schemas.js.map +1 -0
  103. package/package.json +12 -4
@@ -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":"AAwBA;;GAEG;AACH,MAAM,UAAU,OAAO,CAAI,CAAiB,EAAE,CAAiB;IAC9D,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IAC1C,IAAI,KAAK,GAAG,CAAC,CAAA;IACb,KAAK,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC;QACnB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,KAAK,EAAE,CAAA;IACtB,CAAC;IACD,MAAM,KAAK,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,IAAI,GAAG,KAAK,CAAA;IACrC,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IACzB,OAAO,KAAK,GAAG,KAAK,CAAA;AACrB,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,CAA8B,EAAE,CAA8B;IAC5F,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAA;IACtC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAA;IACtC,KAAK,MAAM,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,CAAA;IAChD,KAAK,MAAM,CAAC,IAAI,CAAC;QAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,SAAS,CAAC,CAAA;IAEhD,MAAM,YAAY,GAAa,EAAE,CAAA;IACjC,MAAM,KAAK,GAAa,EAAE,CAAA;IAC1B,MAAM,KAAK,GAAa,EAAE,CAAA;IAE1B,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;QAC3B,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;;YACtC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACtB,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC;QAC3B,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACtC,CAAC;IAED,MAAM,aAAa,GAAG,YAAY;SAChC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;QACb,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC1C,MAAM,cAAc,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,cAAc,CAAC,CAAA;QACvD,OAAO;YACN,IAAI;YACJ,cAAc;YACd,cAAc;YACd,MAAM;SACN,CAAA;IACF,CAAC,CAAC;SACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAA;IAErC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IACjC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAA;IACjC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAA;IAC7B,MAAM,kBAAkB,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,CAAA;IAE9C,OAAO;QACN,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,YAAY,EAAE,YAAY,CAAC,IAAI,EAAE;QACjC,aAAa;QACb,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE;QACnB,KAAK,EAAE,KAAK,CAAC,IAAI,EAAE;QACnB,OAAO,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,IAAI;QACpC,kBAAkB;KAClB,CAAA;AACF,CAAC"}
@@ -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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Computes the Shannon diversity index over a map of item counts.
3
+ */
4
+ export function computeDiversity(counts) {
5
+ const entries = Object.entries(counts).filter(([, c]) => c > 0);
6
+ const total = entries.reduce((acc, [, c]) => acc + c, 0);
7
+ const uniqueCount = entries.length;
8
+ if (total === 0 || uniqueCount === 0) {
9
+ return { total: 0, uniqueCount: 0, shannon: 0, normalized: 0 };
10
+ }
11
+ if (uniqueCount === 1) {
12
+ return { total, uniqueCount: 1, shannon: 0, normalized: 0 };
13
+ }
14
+ let h = 0;
15
+ for (const [, c] of entries) {
16
+ const p = c / total;
17
+ h -= p * Math.log(p);
18
+ }
19
+ const normalized = h / Math.log(uniqueCount);
20
+ return { total, uniqueCount, shannon: h, normalized };
21
+ }
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 function topNShare(counts, n) {
28
+ const sorted = Object.values(counts)
29
+ .filter((c) => c > 0)
30
+ .sort((a, b) => b - a);
31
+ if (sorted.length === 0)
32
+ return 0;
33
+ const total = sorted.reduce((acc, c) => acc + c, 0);
34
+ if (total === 0)
35
+ return 0;
36
+ const top = sorted.slice(0, Math.max(0, n)).reduce((acc, c) => acc + c, 0);
37
+ return top / total;
38
+ }
39
+ //# sourceMappingURL=diversity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diversity.js","sourceRoot":"","sources":["../../../../src/services/insights/lib/diversity.ts"],"names":[],"mappings":"AAmBA;;GAEG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAgB;IAChD,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IAC/D,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IACxD,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAA;IAClC,IAAI,KAAK,KAAK,CAAC,IAAI,WAAW,KAAK,CAAC,EAAE,CAAC;QACtC,OAAO,EAAE,KAAK,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAA;IAC/D,CAAC;IACD,IAAI,WAAW,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAA;IAC5D,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,CAAA;IACT,KAAK,MAAM,CAAC,EAAE,CAAC,CAAC,IAAI,OAAO,EAAE,CAAC;QAC7B,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;QACnB,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;IACrB,CAAC;IACD,MAAM,UAAU,GAAG,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IAC5C,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC,EAAE,UAAU,EAAE,CAAA;AACtD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,MAAgB,EAAE,CAAS;IACpD,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC;SAClC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC;SACpB,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACvB,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IACjC,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IACnD,IAAI,KAAK,KAAK,CAAC;QAAE,OAAO,CAAC,CAAA;IACzB,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC,CAAC,CAAA;IAC1E,OAAO,GAAG,GAAG,KAAK,CAAA;AACnB,CAAC"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * hours.ts — bucket timestamps by hour-of-day (0..23) and weekday (0..6).
3
+ *
4
+ * Weekday indexing follows ISO convention: Mon=0 .. Sun=6.
5
+ * All bucketing is in UTC for deterministic behavior.
6
+ */
7
+ export declare const WEEKDAY_LABELS_EN: readonly ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
8
+ export declare const WEEKDAY_LABELS_ES: readonly ["lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"];
9
+ export interface HourBucket {
10
+ /** Hour of the day in 24h format, 0–23 (UTC). */
11
+ readonly hour: number;
12
+ /** Day of the week, 0 = Monday … 6 = Sunday. */
13
+ readonly weekday: number;
14
+ }
15
+ /**
16
+ * Convert a UNIX-seconds timestamp to (hour, weekday).
17
+ */
18
+ export declare function bucketTimestamp(unixSeconds: number): HourBucket;
19
+ export interface HourHistogram {
20
+ readonly total: number;
21
+ /** Counts per hour, index = hour (0..23). Length always 24. */
22
+ readonly byHour: number[];
23
+ /** Counts per weekday, index = Mon(0)..Sun(6). Length always 7. */
24
+ readonly byWeekday: number[];
25
+ /** Hour with the highest count, or null when total === 0. */
26
+ readonly peakHour: number | null;
27
+ readonly peakHourCount: number;
28
+ readonly peakWeekday: number | null;
29
+ readonly peakWeekdayCount: number;
30
+ readonly peakWeekdayLabel: string | null;
31
+ /** Time-of-day shares [0..1] */
32
+ readonly nightShare: number;
33
+ readonly morningShare: number;
34
+ readonly afternoonShare: number;
35
+ readonly eveningShare: number;
36
+ readonly weekendShare: number;
37
+ }
38
+ export declare function buildHourHistogram(timestamps: readonly number[]): HourHistogram;
39
+ //# sourceMappingURL=hours.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hours.d.ts","sourceRoot":"","sources":["../../../../src/services/insights/lib/hours.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,4DAA6D,CAAA;AAC3F,eAAO,MAAM,iBAAiB,qFAAsF,CAAA;AAEpH,MAAM,WAAW,UAAU;IAC1B,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACxB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,GAAG,UAAU,CAO/D;AAED,MAAM,WAAW,aAAa;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,CAAA;IACzB,mEAAmE;IACnE,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,CAAA;IAC5B,6DAA6D;IAC7D,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;IACnC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAA;IACjC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAA;IACxC,gCAAgC;IAChC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAC3B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAA;CAC7B;AAgBD,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,GAAG,aAAa,CAmC/E"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * hours.ts — bucket timestamps by hour-of-day (0..23) and weekday (0..6).
3
+ *
4
+ * Weekday indexing follows ISO convention: Mon=0 .. Sun=6.
5
+ * All bucketing is in UTC for deterministic behavior.
6
+ */
7
+ export const WEEKDAY_LABELS_EN = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
8
+ export const WEEKDAY_LABELS_ES = ['lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado', 'domingo'];
9
+ /**
10
+ * Convert a UNIX-seconds timestamp to (hour, weekday).
11
+ */
12
+ export function bucketTimestamp(unixSeconds) {
13
+ const date = new Date(unixSeconds * 1000);
14
+ const hour = date.getUTCHours();
15
+ // Map JS getUTCDay() (Sun=0..Sat=6) to Mon=0..Sun=6.
16
+ const jsDow = date.getUTCDay();
17
+ const weekday = (jsDow + 6) % 7;
18
+ return { hour, weekday };
19
+ }
20
+ function indexOfMax(arr) {
21
+ if (arr.length === 0)
22
+ return null;
23
+ let maxIdx = 0;
24
+ let maxVal = arr[0] ?? 0;
25
+ for (let i = 1; i < arr.length; i++) {
26
+ const val = arr[i] ?? 0;
27
+ if (val > maxVal) {
28
+ maxVal = val;
29
+ maxIdx = i;
30
+ }
31
+ }
32
+ return maxVal > 0 ? maxIdx : null;
33
+ }
34
+ export function buildHourHistogram(timestamps) {
35
+ const byHour = new Array(24).fill(0);
36
+ const byWeekday = new Array(7).fill(0);
37
+ for (const ts of timestamps) {
38
+ const b = bucketTimestamp(ts);
39
+ byHour[b.hour] = (byHour[b.hour] ?? 0) + 1;
40
+ byWeekday[b.weekday] = (byWeekday[b.weekday] ?? 0) + 1;
41
+ }
42
+ const total = timestamps.length;
43
+ const peakHourIdx = indexOfMax(byHour);
44
+ const peakWeekdayIdx = indexOfMax(byWeekday);
45
+ const nightCount = byHour.slice(0, 6).reduce((a, b) => a + b, 0);
46
+ const morningCount = byHour.slice(6, 12).reduce((a, b) => a + b, 0);
47
+ const afternoonCount = byHour.slice(12, 18).reduce((a, b) => a + b, 0);
48
+ const eveningCount = byHour.slice(18, 24).reduce((a, b) => a + b, 0);
49
+ const weekendCount = (byWeekday[5] ?? 0) + (byWeekday[6] ?? 0);
50
+ return {
51
+ total,
52
+ byHour,
53
+ byWeekday,
54
+ peakHour: peakHourIdx,
55
+ peakHourCount: peakHourIdx !== null ? (byHour[peakHourIdx] ?? 0) : 0,
56
+ peakWeekday: peakWeekdayIdx,
57
+ peakWeekdayCount: peakWeekdayIdx !== null ? (byWeekday[peakWeekdayIdx] ?? 0) : 0,
58
+ peakWeekdayLabel: peakWeekdayIdx !== null ? (WEEKDAY_LABELS_EN[peakWeekdayIdx] ?? null) : null,
59
+ nightShare: total > 0 ? nightCount / total : 0,
60
+ morningShare: total > 0 ? morningCount / total : 0,
61
+ afternoonShare: total > 0 ? afternoonCount / total : 0,
62
+ eveningShare: total > 0 ? eveningCount / total : 0,
63
+ weekendShare: total > 0 ? weekendCount / total : 0,
64
+ };
65
+ }
66
+ //# sourceMappingURL=hours.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hours.js","sourceRoot":"","sources":["../../../../src/services/insights/lib/hours.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * mood.ts — derive a 2D mood profile from Last.fm tags.
3
+ *
4
+ * Axes:
5
+ * energy ∈ [-1, +1] -1 = ambient/sleep, +1 = hardcore/thrash
6
+ * valence ∈ [-1, +1] -1 = doom/sad, +1 = euphoric/uplifting
7
+ */
8
+ export interface MoodAxes {
9
+ readonly energy: number;
10
+ readonly valence: number;
11
+ }
12
+ export interface MoodProfile {
13
+ readonly axes: MoodAxes;
14
+ readonly label: string;
15
+ readonly categories: readonly string[];
16
+ readonly confidence: number;
17
+ }
18
+ export declare function axesFromTags(tags: readonly string[]): MoodAxes;
19
+ export declare function categoriesFromTags(tags: readonly string[]): readonly string[];
20
+ export declare function labelFromAxes({ energy, valence }: MoodAxes): string;
21
+ export declare function classifyMood(tags: readonly string[]): MoodProfile;
22
+ //# sourceMappingURL=mood.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mood.d.ts","sourceRoot":"","sources":["../../../../src/services/insights/lib/mood.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAuHH,MAAM,WAAW,QAAQ;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,UAAU,EAAE,SAAS,MAAM,EAAE,CAAA;IACtC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC3B;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,QAAQ,CAU9D;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,MAAM,EAAE,CAY7E;AAED,wBAAgB,aAAa,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,QAAQ,GAAG,MAAM,CAiBnE;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,WAAW,CAoBjE"}
@@ -0,0 +1,187 @@
1
+ /**
2
+ * mood.ts — derive a 2D mood profile from Last.fm tags.
3
+ *
4
+ * Axes:
5
+ * energy ∈ [-1, +1] -1 = ambient/sleep, +1 = hardcore/thrash
6
+ * valence ∈ [-1, +1] -1 = doom/sad, +1 = euphoric/uplifting
7
+ */
8
+ const ENERGY = {
9
+ // high energy
10
+ hardcore: 1,
11
+ punk: 0.8,
12
+ 'punk rock': 0.8,
13
+ 'hard rock': 0.7,
14
+ 'thrash metal': 1,
15
+ 'speed metal': 1,
16
+ grindcore: 1,
17
+ drum: 0.7,
18
+ 'drum and bass': 0.8,
19
+ dnb: 0.8,
20
+ techno: 0.6,
21
+ gabber: 1,
22
+ 'happy hardcore': 1,
23
+ rave: 0.9,
24
+ thrash: 0.9,
25
+ metal: 0.6,
26
+ 'heavy metal': 0.7,
27
+ 'death metal': 0.8,
28
+ 'black metal': 0.6,
29
+ rock: 0.5,
30
+ 'indie rock': 0.4,
31
+ pop: 0.3,
32
+ dance: 0.6,
33
+ hip: 0.5,
34
+ rap: 0.4,
35
+ hiphop: 0.5,
36
+ // low energy
37
+ ambient: -1,
38
+ drone: -0.9,
39
+ sleep: -1,
40
+ 'new age': -0.8,
41
+ meditation: -0.9,
42
+ downtempo: -0.6,
43
+ chillout: -0.6,
44
+ chill: -0.6,
45
+ lounge: -0.5,
46
+ 'trip-hop': -0.4,
47
+ 'slow core': -0.5,
48
+ slowcore: -0.5,
49
+ ballad: -0.4,
50
+ jazz: -0.2,
51
+ 'smooth jazz': -0.4,
52
+ 'bossa nova': -0.3,
53
+ };
54
+ const VALENCE = {
55
+ // high valence (euphoric / happy)
56
+ euphoric: 1,
57
+ happy: 1,
58
+ uplifting: 0.9,
59
+ summer: 0.5,
60
+ sunshine: 0.6,
61
+ 'feel good': 0.8,
62
+ feelgood: 0.8,
63
+ dance: 0.6,
64
+ party: 0.7,
65
+ pop: 0.4,
66
+ 'power pop': 0.6,
67
+ 'bubblegum pop': 0.5,
68
+ 'k-pop': 0.4,
69
+ // low valence (sad / dark)
70
+ doom: -1,
71
+ sad: -1,
72
+ melancholic: -0.9,
73
+ melancholy: -0.9,
74
+ depressive: -1,
75
+ black: -0.7,
76
+ 'black metal': -0.7,
77
+ 'dark ambient': -0.8,
78
+ dark: -0.6,
79
+ funeral: -1,
80
+ 'funeral doom': -1,
81
+ 'doom metal': -0.9,
82
+ goth: -0.7,
83
+ gothic: -0.7,
84
+ 'gothic rock': -0.6,
85
+ 'post-punk': -0.4,
86
+ coldwave: -0.6,
87
+ // neutral-ish
88
+ indie: 0,
89
+ 'indie rock': 0,
90
+ rock: 0,
91
+ electronic: 0,
92
+ experimental: 0,
93
+ };
94
+ const CATEGORIES = {
95
+ rock: ['rock', 'indie rock', 'post-rock', 'garage rock', 'hard rock'],
96
+ electronic: ['electronic', 'techno', 'house', 'ambient', 'idm', 'dnb', 'drum and bass', 'dubstep', 'synth'],
97
+ 'hip-hop': ['hip-hop', 'hip hop', 'rap', 'trap'],
98
+ jazz: ['jazz', 'bebop', 'bossa nova', 'smooth jazz', 'nu jazz', 'free jazz'],
99
+ folk: ['folk', 'singer-songwriter', 'acoustic', 'indie folk', 'folk rock'],
100
+ classical: ['classical', 'baroque', 'romantic', 'modern classical', 'opera', 'symphony'],
101
+ ambient: ['ambient', 'drone', 'new age', 'meditation', 'dark ambient'],
102
+ experimental: ['experimental', 'noise', 'avant-garde', 'free improvisation'],
103
+ metal: ['metal', 'thrash metal', 'doom metal', 'death metal', 'black metal', 'heavy metal', 'sludge'],
104
+ pop: ['pop', 'synth-pop', 'electropop', 'k-pop', 'indie pop', 'dream pop'],
105
+ world: ['world', 'afrobeat', 'latin', 'flamenco', 'brazilian', 'reggae'],
106
+ punk: ['punk', 'post-punk', 'hardcore', 'anarcho-punk'],
107
+ country: ['country', 'bluegrass', 'outlaw country', 'country rock'],
108
+ soul: ['soul', 'r&b', 'funk', 'motown', 'neo-soul'],
109
+ };
110
+ function normalize(tag) {
111
+ return tag.toLowerCase().trim();
112
+ }
113
+ function lookup(map, tag) {
114
+ return map[tag] ?? 0;
115
+ }
116
+ function round2(n) {
117
+ return Math.round(n * 100) / 100;
118
+ }
119
+ export function axesFromTags(tags) {
120
+ if (tags.length === 0)
121
+ return { energy: 0, valence: 0 };
122
+ let energy = 0;
123
+ let valence = 0;
124
+ for (const t of tags) {
125
+ energy += lookup(ENERGY, normalize(t));
126
+ valence += lookup(VALENCE, normalize(t));
127
+ }
128
+ const avg = (n) => Math.max(-1, Math.min(1, n / tags.length));
129
+ return { energy: round2(avg(energy)), valence: round2(avg(valence)) };
130
+ }
131
+ export function categoriesFromTags(tags) {
132
+ if (tags.length === 0)
133
+ return [];
134
+ const counts = new Map();
135
+ for (const tag of tags) {
136
+ const norm = normalize(tag);
137
+ for (const [cat, members] of Object.entries(CATEGORIES)) {
138
+ if (members.includes(norm)) {
139
+ counts.set(cat, (counts.get(cat) ?? 0) + 1);
140
+ }
141
+ }
142
+ }
143
+ return [...counts.entries()].sort((a, b) => b[1] - a[1]).map(([cat]) => cat);
144
+ }
145
+ export function labelFromAxes({ energy, valence }) {
146
+ if (energy === 0 && valence === 0)
147
+ return 'neutral / undefined';
148
+ const eHigh = energy > 0.2;
149
+ const eLow = energy < -0.2;
150
+ const vHigh = valence > 0.2;
151
+ const vLow = valence < -0.2;
152
+ if (eHigh && vHigh)
153
+ return 'euphoric & energetic';
154
+ if (eHigh && vLow)
155
+ return 'intense & dark';
156
+ if (eLow && vHigh)
157
+ return 'chill & uplifting';
158
+ if (eLow && vLow)
159
+ return 'melancholic & calm';
160
+ if (eHigh && !vHigh && !vLow)
161
+ return 'high energy';
162
+ if (eLow && !vHigh && !vLow)
163
+ return 'low energy / ambient';
164
+ if (vHigh && !eHigh && !eLow)
165
+ return 'positive / bright';
166
+ if (vLow && !eHigh && !eLow)
167
+ return 'somber / melancholic';
168
+ return 'neutral / undefined';
169
+ }
170
+ export function classifyMood(tags) {
171
+ const axes = axesFromTags(tags);
172
+ const categories = categoriesFromTags(tags);
173
+ const recognized = tags.filter((t) => {
174
+ const norm = normalize(t);
175
+ return (ENERGY[norm] !== undefined ||
176
+ VALENCE[norm] !== undefined ||
177
+ Object.values(CATEGORIES).some((arr) => arr.includes(norm)));
178
+ }).length;
179
+ const confidence = tags.length === 0 ? 0 : round2(recognized / tags.length);
180
+ return {
181
+ axes,
182
+ label: labelFromAxes(axes),
183
+ categories,
184
+ confidence,
185
+ };
186
+ }
187
+ //# sourceMappingURL=mood.js.map