@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 +5 -4
- package/dist/auth/LastfmOAuthProvider.js +9 -3
- package/dist/connectors/LastfmFriendsExtractor.js +2 -0
- package/dist/connectors/LastfmLovedTracksExtractor.js +2 -0
- package/dist/connectors/LastfmRecentTracksExtractor.js +2 -0
- package/dist/connectors/LastfmTransformer.js +6 -7
- package/package.json +5 -5
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
|
|
109
|
-
befriended date), sighted at read time by the
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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', '
|
|
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
|
|
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', '
|
|
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
|
+
"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.
|
|
26
|
-
"@chronicle.app/schema": "0.
|
|
27
|
-
"@chronicle.app/tsconfig": "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.
|
|
42
|
+
"@chronicle.app/auth": "0.5.0",
|
|
43
43
|
"axios": "^1.7.9",
|
|
44
44
|
"zod": "^3.24.1"
|
|
45
45
|
},
|