@chronicle.app/shazam 0.4.0 → 0.5.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/README.md CHANGED
@@ -40,7 +40,7 @@ A run reads only the library, offline. With `--link-artists`, it also looks up e
40
40
 
41
41
  **Where you were, when the device knew.** A Shazam made without a location fix records 0, 0 or -180, -180, which is left out.
42
42
 
43
- **The agent is your iCloud account,** whose library this is, from `@chronicle.app/icloud`. It's omitted when no account can be read. An explicit `account` object (or null) describes a copied library without a host account lookup.
43
+ **The agent is your iCloud account,** whose library this is, from `@chronicle.app/apple`. It's omitted when no account can be read. An explicit `account` object (or null) describes a copied library without a host account lookup.
44
44
 
45
45
  **The archived fields are decoded in the plugin.** Core Data keeps a song's genres and Apple Music response as NSKeyedArchiver archives in binary property lists. `archive.ts` reads them. A field that doesn't decode is logged and left out; the Shazam is kept.
46
46
 
package/dist/archive.d.ts CHANGED
@@ -1,20 +1,3 @@
1
- /**
2
- * Core Data keeps a transformable attribute, such as a track's genres, as an
3
- * NSKeyedArchiver archive in a binary property list (`bplist00`). This reads
4
- * the archives shazamd writes: arrays, dictionaries, strings, numbers, dates,
5
- * and data. Other classes come back as `null`.
6
- */
7
- /** A reference to an entry in an archive's `$objects`. */
8
- declare class Uid {
9
- readonly index: number;
10
- constructor(index: number);
11
- }
12
- type PlistValue = null | boolean | number | string | Date | Uint8Array | Uid | PlistValue[] | {
13
- [key: string]: PlistValue;
14
- };
15
- /** Parse a binary property list. */
16
- export declare function parseBinaryPlist(bytes: Uint8Array): PlistValue;
17
1
  /** Decode an NSKeyedArchiver archive to plain values. */
18
2
  export declare function unarchive(bytes: Uint8Array): unknown;
19
- export {};
20
3
  //# sourceMappingURL=archive.d.ts.map
package/dist/archive.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { parseBinaryPlist, Uid } from '@chronicle.app/apple';
1
2
  import { APPLE_EPOCH_OFFSET_SECONDS } from '@chronicle.app/etl-sqlite';
2
3
  /**
3
4
  * Core Data keeps a transformable attribute, such as a track's genres, as an
@@ -5,84 +6,8 @@ import { APPLE_EPOCH_OFFSET_SECONDS } from '@chronicle.app/etl-sqlite';
5
6
  * the archives shazamd writes: arrays, dictionaries, strings, numbers, dates,
6
7
  * and data. Other classes come back as `null`.
7
8
  */
8
- /** A reference to an entry in an archive's `$objects`. */
9
- class Uid {
10
- index;
11
- constructor(index) {
12
- this.index = index;
13
- }
14
- }
15
9
  // Nested containers deeper than this are a malformed (or cyclic) file.
16
10
  const MAX_DEPTH = 64;
17
- /** Parse a binary property list. */
18
- export function parseBinaryPlist(bytes) {
19
- const buf = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength);
20
- if (buf.length < 40 || buf.toString('latin1', 0, 8) !== 'bplist00') {
21
- throw new Error('Not a binary property list');
22
- }
23
- const trailer = buf.length - 32;
24
- const offsetSize = buf[trailer + 6];
25
- const refSize = buf[trailer + 7];
26
- const count = Number(buf.readBigUInt64BE(trailer + 8));
27
- const top = Number(buf.readBigUInt64BE(trailer + 16));
28
- const offsetTable = Number(buf.readBigUInt64BE(trailer + 24));
29
- const uint = (at, size) => {
30
- let n = 0;
31
- for (let i = 0; i < size; i++)
32
- n = n * 256 + buf[at + i];
33
- return n;
34
- };
35
- const read = (index, depth) => {
36
- if (index >= count || depth > MAX_DEPTH)
37
- throw new Error('Malformed binary property list');
38
- const at = uint(offsetTable + index * offsetSize, offsetSize);
39
- // A marker byte holds the object's type in its high four bits and its
40
- // size or count in its low four.
41
- const type = Math.floor(buf[at] / 16);
42
- const info = buf[at] % 16;
43
- // A count of 15 or more follows the marker as an integer object.
44
- let length = info;
45
- let start = at + 1;
46
- if (info === 0xf) {
47
- const size = 2 ** (buf[at + 1] % 16);
48
- length = uint(at + 2, size);
49
- start = at + 2 + size;
50
- }
51
- const refs = (offset, n) => Array.from({ length: n }, (_, i) => read(uint(start + (offset + i) * refSize, refSize), depth + 1));
52
- switch (type) {
53
- case 0x0:
54
- return info === 0x9 ? true : info === 0x8 ? false : null;
55
- case 0x1:
56
- return info === 3 ? Number(buf.readBigInt64BE(at + 1)) : uint(at + 1, 2 ** info);
57
- case 0x2:
58
- return info === 2 ? buf.readFloatBE(at + 1) : buf.readDoubleBE(at + 1);
59
- case 0x3:
60
- return new Date((buf.readDoubleBE(at + 1) + APPLE_EPOCH_OFFSET_SECONDS) * 1000);
61
- case 0x4:
62
- return buf.subarray(start, start + length);
63
- case 0x5:
64
- return buf.toString('latin1', start, start + length);
65
- case 0x6:
66
- // UTF-16 big-endian; Node decodes only little-endian.
67
- return Buffer.from(buf.subarray(start, start + length * 2))
68
- .swap16()
69
- .toString('utf16le');
70
- case 0x8:
71
- return new Uid(uint(at + 1, info + 1));
72
- case 0xa:
73
- case 0xc:
74
- return refs(0, length);
75
- case 0xd: {
76
- const keys = refs(0, length);
77
- const values = refs(length, length);
78
- return Object.fromEntries(keys.map((key, i) => [String(key), values[i]]));
79
- }
80
- default:
81
- throw new Error(`Unsupported binary property list type 0x${type.toString(16)}`);
82
- }
83
- };
84
- return read(top, 0);
85
- }
86
11
  /** Decode an NSKeyedArchiver archive to plain values. */
87
12
  export function unarchive(bytes) {
88
13
  const archive = parseBinaryPlist(bytes);
@@ -1,4 +1,4 @@
1
- import { Record } from '@chronicle.app/etl';
1
+ import { Record, type Survey, type SurveyOptions } from '@chronicle.app/etl';
2
2
  import { SqliteExtractor } from '@chronicle.app/etl-sqlite';
3
3
  import { type AppleMusicArtist } from '../appleMusic.js';
4
4
  import ShazamTransformer from './ShazamTransformer.js';
@@ -49,6 +49,11 @@ export declare class ShazamExtractor extends SqliteExtractor<typeof ShazamExtrac
49
49
  static default: boolean;
50
50
  static defaultTransformer: typeof ShazamTransformer;
51
51
  static newestFirst: boolean;
52
+ static databases: {
53
+ tables: {
54
+ ZSHTRACKMO: string[];
55
+ };
56
+ }[];
52
57
  static schema: any;
53
58
  private lookup?;
54
59
  setup(): Promise<void>;
@@ -62,5 +67,10 @@ export declare class ShazamExtractor extends SqliteExtractor<typeof ShazamExtrac
62
67
  private decode;
63
68
  private parseSong;
64
69
  determineCount(): Promise<number | null>;
70
+ /**
71
+ * The Shazams a run reads and when, of how many songs, by how many artists:
72
+ * by Shazam's artist line, as a run without linkArtists keys them.
73
+ */
74
+ survey(options?: SurveyOptions): Promise<Survey | null>;
65
75
  }
66
76
  //# sourceMappingURL=ShazamExtractor.d.ts.map
@@ -1,5 +1,5 @@
1
- import { APPLE_EPOCH_OFFSET_SECONDS, SqliteExtractor, getRow, iterateRows, timeRangeConditions, unixToSafariTimestamp, } from '@chronicle.app/etl-sqlite';
2
- import { getICloudAccount } from '@chronicle.app/icloud';
1
+ import { APPLE_EPOCH_OFFSET_SECONDS, SqliteExtractor, allRows, countsByMonth, getRow, iterateRows, monthSql, timeRangeConditions, unixToSafariTimestamp, } from '@chronicle.app/etl-sqlite';
2
+ import { getICloudAccount } from '@chronicle.app/apple';
3
3
  import { z } from 'zod';
4
4
  import { AppleMusicLookup, LOOKUP_BATCH, countryOf } from '../appleMusic.js';
5
5
  import { unarchive } from '../archive.js';
@@ -21,6 +21,36 @@ export class ShazamExtractor extends SqliteExtractor {
21
21
  static default = true;
22
22
  static defaultTransformer = ShazamTransformer;
23
23
  static newestFirst = true;
24
+ // What extract() and survey() read, which a ShazamLibrary.sqlite must have.
25
+ static databases = [
26
+ {
27
+ tables: {
28
+ ZSHTRACKMO: [
29
+ 'Z_PK',
30
+ 'ZRECOGNITIONID',
31
+ 'ZSYNCID',
32
+ 'ZDATE',
33
+ 'ZMODIFIEDDATE',
34
+ 'ZTITLE',
35
+ 'ZSUBTITLE',
36
+ 'ZSHAZAMKEY',
37
+ 'ZSHAZAMURL',
38
+ 'ZPROVIDERNAME',
39
+ 'ZLATITUDE',
40
+ 'ZLONGITUDE',
41
+ 'ZARTWORKURL',
42
+ 'ZAPPLEMUSICID',
43
+ 'ZAPPLEMUSICURL',
44
+ 'ZISRC',
45
+ 'ZALBUMNAME',
46
+ 'ZRELEASEDATE',
47
+ 'ZEXPLICIT',
48
+ 'ZGENRES',
49
+ 'ZRAWSONGRESPONSE',
50
+ ],
51
+ },
52
+ },
53
+ ];
24
54
  static schema = SqliteExtractor.schema.omit({ input: true }).extend({
25
55
  input: z.string().describe('Path to ShazamLibrary.sqlite').default(DEFAULT_DB),
26
56
  account: z
@@ -177,6 +207,32 @@ export class ShazamExtractor extends SqliteExtractor {
177
207
  return null;
178
208
  }
179
209
  }
210
+ /**
211
+ * The Shazams a run reads and when, of how many songs, by how many artists:
212
+ * by Shazam's artist line, as a run without linkArtists keys them.
213
+ */
214
+ async survey(options = {}) {
215
+ if (!this.db)
216
+ return null;
217
+ const { sql, values } = this.query(`COUNT(*) AS n, MIN(ZDATE) AS first, MAX(ZDATE) AS last,
218
+ COUNT(DISTINCT ZSHAZAMKEY) AS songs, COUNT(DISTINCT ZSUBTITLE) AS artists`);
219
+ const row = getRow(this.db.prepare(sql), ...values);
220
+ const months = this.query(`${monthSql(`ZDATE + ${APPLE_EPOCH_OFFSET_SECONDS}`)} AS month, COUNT(*) AS n`);
221
+ const byMonth = options.byMonth &&
222
+ countsByMonth(allRows(this.db.prepare(`${months.sql} GROUP BY month`), ...months.values));
223
+ return {
224
+ listens: {
225
+ count: row.n,
226
+ ...(row.first !== null && { earliest: new Date(appleDate(row.first)) }),
227
+ ...(row.last !== null && { latest: new Date(appleDate(row.last)) }),
228
+ ...(byMonth && { byMonth }),
229
+ measures: [
230
+ { key: 'songs', label: 'songs', value: row.songs },
231
+ { key: 'artists', label: 'artists', value: row.artists },
232
+ ],
233
+ },
234
+ };
235
+ }
180
236
  }
181
237
  function appleDate(seconds) {
182
238
  return new Date((seconds + APPLE_EPOCH_OFFSET_SECONDS) * 1000).toISOString();
@@ -1,5 +1,5 @@
1
1
  import { ChronicleTransformer, ISRC_SOURCE, createImageObject, normalizeIsrc, } from '@chronicle.app/etl';
2
- import { buildICloudPersonSchema } from '@chronicle.app/icloud';
2
+ import { buildICloudPersonSchema } from '@chronicle.app/apple';
3
3
  const SOURCE = 'shazam';
4
4
  /** Apple Music's catalog: the namespace of its song, album, and artist IDs. */
5
5
  const APPLE_MUSIC = 'apple-music';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chronicle.app/shazam",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -24,9 +24,9 @@
24
24
  "type": "module",
25
25
  "description": "Chronicle plugin for the songs you identified with Shazam, from the library macOS keeps for Music Recognition",
26
26
  "devDependencies": {
27
- "@chronicle.app/etl": "0.4.0",
28
- "@chronicle.app/schema": "0.4.0",
29
- "@chronicle.app/tsconfig": "0.4.0",
27
+ "@chronicle.app/etl": "0.5.0",
28
+ "@chronicle.app/schema": "0.5.0",
29
+ "@chronicle.app/tsconfig": "0.5.0",
30
30
  "@types/node": "^22.10.1",
31
31
  "typescript": "^5.7.2"
32
32
  },
@@ -41,8 +41,8 @@
41
41
  "registry": "https://registry.npmjs.org/"
42
42
  },
43
43
  "dependencies": {
44
- "@chronicle.app/etl-sqlite": "0.4.0",
45
- "@chronicle.app/icloud": "0.4.0",
44
+ "@chronicle.app/etl-sqlite": "0.5.0",
45
+ "@chronicle.app/apple": "0.5.0",
46
46
  "zod": "^3.24.1"
47
47
  },
48
48
  "peerDependencies": {
@@ -68,7 +68,41 @@
68
68
  "recordTypes": [
69
69
  "listens"
70
70
  ],
71
- "default": true
71
+ "default": true,
72
+ "locations": [
73
+ {
74
+ "path": "~/Library/Application Support/com.apple.shazamd/ShazamLibrary.sqlite"
75
+ }
76
+ ],
77
+ "databases": [
78
+ {
79
+ "tables": {
80
+ "ZSHTRACKMO": [
81
+ "Z_PK",
82
+ "ZRECOGNITIONID",
83
+ "ZSYNCID",
84
+ "ZDATE",
85
+ "ZMODIFIEDDATE",
86
+ "ZTITLE",
87
+ "ZSUBTITLE",
88
+ "ZSHAZAMKEY",
89
+ "ZSHAZAMURL",
90
+ "ZPROVIDERNAME",
91
+ "ZLATITUDE",
92
+ "ZLONGITUDE",
93
+ "ZARTWORKURL",
94
+ "ZAPPLEMUSICID",
95
+ "ZAPPLEMUSICURL",
96
+ "ZISRC",
97
+ "ZALBUMNAME",
98
+ "ZRELEASEDATE",
99
+ "ZEXPLICIT",
100
+ "ZGENRES",
101
+ "ZRAWSONGRESPONSE"
102
+ ]
103
+ }
104
+ }
105
+ ]
72
106
  }
73
107
  },
74
108
  "platforms": [