@chronicle.app/lastfm 0.3.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
@@ -105,10 +105,11 @@ The plugin generates the following Chronicle schema types:
105
105
 
106
106
  - `ListenAction`: a scrobble (recent track), timestamped at the play time
107
107
  - `LikeAction`: a loved/favorited track, timestamped when loved
108
- - `FollowAction`: the listener following a friend; no timestamp (Last.fm gives no
109
- befriended date), sighted at read time by the snapshot `friends` extractor
110
- - `CreateAction`: a friend's Last.fm account registration, keyed on the account
111
- handle and timestamped at their `registered` date (the account's
108
+ - `FollowAction`: the listener following a friend, keyed by the friend's account;
109
+ no timestamp (Last.fm gives no befriended date), sighted at read time by the
110
+ snapshot `friends` extractor
111
+ - `CreateAction`: a friend's Last.fm account registration, keyed by the account
112
+ it made and timestamped at their `registered` date (the account's
112
113
  coming-into-being on `result`)
113
114
  - `Agent`: the listener and their friends, keyed on the Last.fm `handle` (the
114
115
  canonical username), with real name, profile URL, and avatar `image` when
@@ -1,6 +1,7 @@
1
1
  import axios from 'axios';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { OAuthProvider } from '@chronicle.app/auth';
4
+ import { AuthRequired } from '@chronicle.app/etl';
4
5
  export class LastfmOAuthProvider extends OAuthProvider {
5
6
  static providerId = 'lastfm';
6
7
  static authorizationUrl = 'https://www.last.fm/api/auth';
@@ -43,10 +44,12 @@ export class LastfmOAuthProvider extends OAuthProvider {
43
44
  });
44
45
  const sessionData = sessionResponse.data;
45
46
  if (sessionData.error) {
46
- throw new Error(`Last.fm API error: ${sessionData.message || sessionData.error}`);
47
+ throw new AuthRequired(`Last.fm refused the session: ${sessionData.message || sessionData.error}`, {
48
+ source: 'lastfm',
49
+ });
47
50
  }
48
51
  if (!sessionData.session || !sessionData.session.key) {
49
- throw new Error('No session key received from Last.fm');
52
+ throw new AuthRequired('Last.fm sent no session key', { source: 'lastfm' });
50
53
  }
51
54
  // Last.fm returns a session key instead of access/refresh tokens
52
55
  const tokens = {
@@ -62,7 +65,10 @@ export class LastfmOAuthProvider extends OAuthProvider {
62
65
  catch (error) {
63
66
  if (axios.isAxiosError(error)) {
64
67
  const message = error.response?.data?.message || error.message;
65
- throw new Error(`Failed to get Last.fm session: ${message}`);
68
+ throw new AuthRequired(`Couldn't get a Last.fm session: ${message}`, {
69
+ source: 'lastfm',
70
+ cause: error,
71
+ });
66
72
  }
67
73
  throw error;
68
74
  }
@@ -42,6 +42,7 @@ export class LastfmFriendsExtractor extends Extractor {
42
42
  '1. Run: chronicle auth login lastfm --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET\n' +
43
43
  '2. Or use: --api-key YOUR_API_KEY for traditional auth\n' +
44
44
  '3. Or use: --access-token TOKEN for OAuth session key',
45
+ hint: 'Run `chronicle auth login lastfm` to sign in.',
45
46
  });
46
47
  // Get username from config or stored credentials
47
48
  const { username } = await resolveCredentials('lastfm', {
@@ -49,6 +50,7 @@ export class LastfmFriendsExtractor extends Extractor {
49
50
  }, {
50
51
  overrides: { username: config.username },
51
52
  errorMessage: 'Last.fm username is required. Either provide --username or authenticate with: chronicle auth login lastfm',
53
+ hint: 'Run `chronicle auth login lastfm` to sign in.',
52
54
  });
53
55
  this.proxy = new LastfmProxy(authToken, username);
54
56
  this.username = username;
@@ -37,6 +37,7 @@ export class LastfmLovedTracksExtractor extends Extractor {
37
37
  '1. Run: chronicle auth login lastfm --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET\n' +
38
38
  '2. Or use: --api-key YOUR_API_KEY for traditional auth\n' +
39
39
  '3. Or use: --access-token TOKEN for OAuth session key',
40
+ hint: 'Run `chronicle auth login lastfm` to sign in.',
40
41
  });
41
42
  // Get username from config or stored credentials
42
43
  const { username } = await resolveCredentials('lastfm', {
@@ -44,6 +45,7 @@ export class LastfmLovedTracksExtractor extends Extractor {
44
45
  }, {
45
46
  overrides: { username: config.username },
46
47
  errorMessage: 'Last.fm username is required. Either provide --username or authenticate with: chronicle auth login lastfm',
48
+ hint: 'Run `chronicle auth login lastfm` to sign in.',
47
49
  });
48
50
  this.proxy = new LastfmProxy(authToken, username);
49
51
  this.username = username;
@@ -38,6 +38,7 @@ export class LastfmRecentTracksExtractor extends Extractor {
38
38
  '1. Run: chronicle auth login lastfm --client-id YOUR_CLIENT_ID --client-secret YOUR_CLIENT_SECRET\n' +
39
39
  '2. Or use: --api-key YOUR_API_KEY for traditional auth\n' +
40
40
  '3. Or use: --access-token TOKEN for OAuth session key',
41
+ hint: 'Run `chronicle auth login lastfm` to sign in.',
41
42
  });
42
43
  // Get username from config or stored credentials
43
44
  const { username } = await resolveCredentials('lastfm', {
@@ -45,6 +46,7 @@ export class LastfmRecentTracksExtractor extends Extractor {
45
46
  }, {
46
47
  overrides: { username: config.username },
47
48
  errorMessage: 'Last.fm username is required. Either provide --username or authenticate with: chronicle auth login lastfm',
49
+ hint: 'Run `chronicle auth login lastfm` to sign in.',
48
50
  });
49
51
  this.proxy = new LastfmProxy(authToken, username);
50
52
  this.username = username;
@@ -32,8 +32,8 @@ export default class LastfmTransformer extends ChronicleTransformer {
32
32
  }
33
33
  // A Last.fm friend is a user the listener follows. Last.fm gives no
34
34
  // befriended date, so the FollowAction carries no timestamp — it's sighted at
35
- // read time by the snapshot extractor — and keys on the followed user's handle
36
- // so re-reads supersede rather than duplicate.
35
+ // read time by the snapshot extractor — and keys on the followed user, by
36
+ // handle, so re-reads supersede rather than duplicate.
37
37
  buildFriendAction(record) {
38
38
  const friend = record.data;
39
39
  const friendAgent = this.buildLastfmAgent(friend.name, {
@@ -44,9 +44,8 @@ export default class LastfmTransformer extends ChronicleTransformer {
44
44
  const actions = [
45
45
  {
46
46
  '@type': 'FollowAction',
47
- '@key': ['@type', 'source', 'sourceId'],
47
+ '@key': ['@type', 'source', 'object.handle'],
48
48
  source: 'lastfm',
49
- sourceId: friend.name,
50
49
  agent: this.resolveUser(record),
51
50
  object: friendAgent,
52
51
  },
@@ -59,7 +58,8 @@ export default class LastfmTransformer extends ChronicleTransformer {
59
58
  }
60
59
  // When a Last.fm account was created (its `registered` date). This is a real
61
60
  // source event with a real timestamp, so it's a keyed action — not synthetic —
62
- // keyed on the account handle so it's referenceable and re-reads supersede. The
61
+ // keyed on the account it made, by handle, so it's referenceable and re-reads
62
+ // supersede. The
63
63
  // account's coming-into-being is origin-grade evidence carried on `result`; a
64
64
  // bare handle reference is used so the friend's *current* avatar/name aren't
65
65
  // back-dated to the registration instant — those stay on the FollowAction.
@@ -77,9 +77,8 @@ export default class LastfmTransformer extends ChronicleTransformer {
77
77
  };
78
78
  return {
79
79
  '@type': 'CreateAction',
80
- '@key': ['@type', 'source', 'sourceId'],
80
+ '@key': ['@type', 'source', 'result.handle'],
81
81
  source: 'lastfm',
82
- sourceId: handle,
83
82
  timestamp: at,
84
83
  agent: ref,
85
84
  result: ref,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chronicle.app/lastfm",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -22,9 +22,9 @@
22
22
  "type": "module",
23
23
  "description": "Chronicle plugin for Last.fm listening history and loved tracks",
24
24
  "devDependencies": {
25
- "@chronicle.app/etl": "0.3.0",
26
- "@chronicle.app/schema": "0.3.0",
27
- "@chronicle.app/tsconfig": "0.3.0",
25
+ "@chronicle.app/etl": "0.5.0",
26
+ "@chronicle.app/schema": "0.5.0",
27
+ "@chronicle.app/tsconfig": "0.5.0",
28
28
  "@types/node": "^22.10.1",
29
29
  "typescript": "^5.7.2"
30
30
  },
@@ -39,7 +39,7 @@
39
39
  "registry": "https://registry.npmjs.org/"
40
40
  },
41
41
  "dependencies": {
42
- "@chronicle.app/auth": "0.3.0",
42
+ "@chronicle.app/auth": "0.5.0",
43
43
  "axios": "^1.7.9",
44
44
  "zod": "^3.24.1"
45
45
  },