lastfm.ts 1.0.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 ADDED
@@ -0,0 +1,107 @@
1
+ # Last.fm Client (Typed)
2
+
3
+ A robust, fully typed TypeScript client for the Last.fm API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install lastfm-client
9
+ ```
10
+
11
+ ## Getting Started
12
+
13
+ Initialize the client with your Last.fm API credentials.
14
+
15
+ ```typescript
16
+ import { LastFmClient } from "lastfm-client";
17
+
18
+ const client = new LastFmClient({
19
+ apiKey: "YOUR_API_KEY",
20
+ apiSecret: "YOUR_API_SECRET",
21
+ userAgent: "MyApp/1.0.0 (contact@example.com)", // Optional but recommended
22
+ });
23
+ ```
24
+
25
+ ## Features
26
+
27
+ ### Authentication
28
+
29
+ To scrobble or update "Now Playing", you need a user session.
30
+
31
+ 1. **Get Auth URL**: Redirect the user to this URL to approve your app.
32
+
33
+ ```typescript
34
+ const authUrl = client.getAuthUrl("http://localhost:3000/callback");
35
+ console.log("Please visit:", authUrl);
36
+ ```
37
+
38
+ 2. **Get Session**: Once the user returns with a `token`, exchange it for a session key.
39
+ ```typescript
40
+ const session = await client.getSession("TOKEN_FROM_CALLBACK");
41
+ const sessionKey = session.session.key;
42
+ ```
43
+
44
+ ### User Methods
45
+
46
+ #### Get User Info
47
+
48
+ ```typescript
49
+ const user = await client.getUser("rj");
50
+ console.log(`User: ${user.user.realname}, Playcount: ${user.user.playcount}`);
51
+ ```
52
+
53
+ #### Get Recent Tracks & Now Playing
54
+
55
+ ```typescript
56
+ // Get recent tracks
57
+ const recent = await client.getRecentTracks("rj", 10);
58
+
59
+ // Check if user is currently listening to something
60
+ const nowPlaying = await client.getNowPlaying("rj");
61
+ if (nowPlaying) {
62
+ console.log(`Listening to: ${nowPlaying.name} by ${nowPlaying.artist["#text"]}`);
63
+ }
64
+ ```
65
+
66
+ ### Track Methods
67
+
68
+ #### Scrobbling (Record a listen)
69
+
70
+ Requires `sessionKey`.
71
+
72
+ ```typescript
73
+ await client.scrobble(
74
+ sessionKey,
75
+ "Daft Punk", // Artist
76
+ "One More Time", // Track
77
+ Math.floor(Date.now() / 1000), // Timestamp (in seconds)
78
+ "Discovery", // Album (Optional)
79
+ );
80
+ ```
81
+
82
+ #### Update Now Playing
83
+
84
+ Requires `sessionKey`.
85
+
86
+ ```typescript
87
+ await client.updateNowPlaying(sessionKey, "Daft Punk", "Harder, Better, Faster, Stronger");
88
+ ```
89
+
90
+ #### Search & Similar
91
+
92
+ ```typescript
93
+ const searchResults = await client.searchTrack("Believe");
94
+ const similarTracks = await client.getSimilarTracks("Cher", "Believe");
95
+ ```
96
+
97
+ ### Other Methods
98
+
99
+ The client also supports methods for **Albums**, **Artists**, and **Tags**:
100
+
101
+ - `getAlbumInfo`, `searchAlbum`
102
+ - `getArtistInfo`, `getSimilarArtists`, `searchArtist`
103
+ - `getTagInfo`
104
+
105
+ ## License
106
+
107
+ ISC
@@ -0,0 +1,26 @@
1
+ import { AlbumInfoResponse, AlbumSearchResponse, ArtistInfoResponse, ArtistSearchResponse, ArtistSimilarResponse, AuthSessionResponse, LastFmClientOptions, LastFmTrack, RecentTracksResponse, TagInfoResponse, TrackInfoResponse, TrackSearchResponse, TrackSimilarResponse, UserInfoResponse } from "./types";
2
+ export declare class LastFmClient {
3
+ private apiKey;
4
+ private apiSecret;
5
+ private userAgent;
6
+ private baseUrl;
7
+ constructor(options: LastFmClientOptions);
8
+ private sign;
9
+ private request;
10
+ getAuthUrl(callbackUrl?: string): string;
11
+ getSession(token: string): Promise<AuthSessionResponse>;
12
+ getUser(username: string): Promise<UserInfoResponse>;
13
+ getRecentTracks(username: string, limit?: number, page?: number): Promise<RecentTracksResponse>;
14
+ getNowPlaying(username: string): Promise<LastFmTrack | null>;
15
+ getTrackInfo(artist: string, track: string, username?: string): Promise<TrackInfoResponse>;
16
+ updateNowPlaying(sessionKey: string, artist: string, track: string, album?: string, duration?: number): Promise<void>;
17
+ scrobble(sessionKey: string, artist: string, track: string, timestamp: number, album?: string): Promise<void>;
18
+ getAlbumInfo(artist: string, album: string, username?: string): Promise<AlbumInfoResponse>;
19
+ searchAlbum(album: string, limit?: number, page?: number): Promise<AlbumSearchResponse>;
20
+ getArtistInfo(artist: string, username?: string): Promise<ArtistInfoResponse>;
21
+ getSimilarArtists(artist: string, limit?: number): Promise<ArtistSimilarResponse>;
22
+ searchArtist(artist: string, limit?: number, page?: number): Promise<ArtistSearchResponse>;
23
+ getSimilarTracks(artist: string, track: string, limit?: number): Promise<TrackSimilarResponse>;
24
+ searchTrack(track: string, limit?: number, page?: number): Promise<TrackSearchResponse>;
25
+ getTagInfo(tag: string, lang?: string): Promise<TagInfoResponse>;
26
+ }
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.LastFmClient = void 0;
7
+ const crypto_1 = __importDefault(require("crypto"));
8
+ class LastFmClient {
9
+ constructor(options) {
10
+ this.baseUrl = "https://ws.audioscrobbler.com/2.0/";
11
+ this.apiKey = options.apiKey;
12
+ this.apiSecret = options.apiSecret;
13
+ this.userAgent = options.userAgent || "LastFmClient/1.0.0";
14
+ }
15
+ sign(params) {
16
+ const str = Object.keys(params)
17
+ .sort()
18
+ .filter((k) => params[k] !== undefined)
19
+ .map((k) => `${k}${params[k]}`)
20
+ .join("") + this.apiSecret;
21
+ return crypto_1.default.createHash("md5").update(str).digest("hex");
22
+ }
23
+ async request(method, params, signed = false) {
24
+ const searchParams = new URLSearchParams();
25
+ const allParams = {
26
+ api_key: this.apiKey,
27
+ format: "json",
28
+ ...params,
29
+ };
30
+ if (signed) {
31
+ allParams.api_sig = this.sign(allParams);
32
+ }
33
+ for (const [key, value] of Object.entries(allParams)) {
34
+ if (value !== undefined)
35
+ searchParams.append(key, String(value));
36
+ }
37
+ if (method === "GET") {
38
+ const url = `${this.baseUrl}?${searchParams.toString()}`;
39
+ const res = await fetch(url, {
40
+ headers: { "User-Agent": this.userAgent },
41
+ });
42
+ if (!res.ok) {
43
+ throw new Error(`Last.fm API Error: ${res.status} ${res.statusText}`);
44
+ }
45
+ return (await res.json());
46
+ }
47
+ else {
48
+ const res = await fetch(this.baseUrl, {
49
+ method: "POST",
50
+ body: searchParams,
51
+ headers: { "User-Agent": this.userAgent },
52
+ });
53
+ if (!res.ok) {
54
+ throw new Error(`Last.fm API Error: ${res.status} ${res.statusText}`);
55
+ }
56
+ return (await res.json());
57
+ }
58
+ }
59
+ getAuthUrl(callbackUrl) {
60
+ let url = `https://www.last.fm/api/auth/?api_key=${this.apiKey}`;
61
+ if (callbackUrl) {
62
+ url += `&cb=${encodeURIComponent(callbackUrl)}`;
63
+ }
64
+ return url;
65
+ }
66
+ async getSession(token) {
67
+ return this.request("GET", { method: "auth.getSession", token }, true);
68
+ }
69
+ async getUser(username) {
70
+ return this.request("GET", {
71
+ method: "user.getInfo",
72
+ user: username,
73
+ });
74
+ }
75
+ // Track methods
76
+ async getRecentTracks(username, limit = 50, page = 1) {
77
+ return this.request("GET", {
78
+ method: "user.getRecentTracks",
79
+ user: username,
80
+ limit,
81
+ page,
82
+ });
83
+ }
84
+ async getNowPlaying(username) {
85
+ const recentTracks = await this.getRecentTracks(username, 2);
86
+ const track = recentTracks.recenttracks.track[0];
87
+ if (track && track["@attr"]?.nowplaying === "true") {
88
+ return track;
89
+ }
90
+ return null;
91
+ }
92
+ async getTrackInfo(artist, track, username) {
93
+ return this.request("GET", {
94
+ method: "track.getInfo",
95
+ artist,
96
+ track,
97
+ username,
98
+ });
99
+ }
100
+ async updateNowPlaying(sessionKey, artist, track, album, duration) {
101
+ await this.request("POST", {
102
+ method: "track.updateNowPlaying",
103
+ sk: sessionKey,
104
+ artist,
105
+ track,
106
+ album,
107
+ duration,
108
+ }, true);
109
+ }
110
+ async scrobble(sessionKey, artist, track, timestamp, album) {
111
+ await this.request("POST", {
112
+ method: "track.scrobble",
113
+ sk: sessionKey,
114
+ artist,
115
+ track,
116
+ timestamp,
117
+ album,
118
+ }, true);
119
+ }
120
+ // Album Methods
121
+ async getAlbumInfo(artist, album, username) {
122
+ return this.request("GET", {
123
+ method: "album.getInfo",
124
+ artist,
125
+ album,
126
+ username,
127
+ });
128
+ }
129
+ async searchAlbum(album, limit, page) {
130
+ return this.request("GET", {
131
+ method: "album.search",
132
+ album,
133
+ limit,
134
+ page,
135
+ });
136
+ }
137
+ // Artist Methods
138
+ async getArtistInfo(artist, username) {
139
+ return this.request("GET", {
140
+ method: "artist.getInfo",
141
+ artist,
142
+ username,
143
+ });
144
+ }
145
+ async getSimilarArtists(artist, limit) {
146
+ return this.request("GET", {
147
+ method: "artist.getSimilar",
148
+ artist,
149
+ limit,
150
+ });
151
+ }
152
+ async searchArtist(artist, limit, page) {
153
+ return this.request("GET", {
154
+ method: "artist.search",
155
+ artist,
156
+ limit,
157
+ page,
158
+ });
159
+ }
160
+ // Track Methods
161
+ async getSimilarTracks(artist, track, limit) {
162
+ return this.request("GET", {
163
+ method: "track.getSimilar",
164
+ artist,
165
+ track,
166
+ limit,
167
+ });
168
+ }
169
+ async searchTrack(track, limit, page) {
170
+ return this.request("GET", {
171
+ method: "track.search",
172
+ track,
173
+ limit,
174
+ page,
175
+ });
176
+ }
177
+ // Tag Methods
178
+ async getTagInfo(tag, lang) {
179
+ return this.request("GET", {
180
+ method: "tag.getInfo",
181
+ tag,
182
+ lang,
183
+ });
184
+ }
185
+ }
186
+ exports.LastFmClient = LastFmClient;
@@ -0,0 +1,2 @@
1
+ export * from "./LastFmClient";
2
+ export * from "./types";
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./LastFmClient"), exports);
18
+ __exportStar(require("./types"), exports);
@@ -0,0 +1,195 @@
1
+ export type LastFmImage = {
2
+ size: "small" | "medium" | "large" | "extralarge";
3
+ "#text": string;
4
+ };
5
+ export type LastFmArtistRef = {
6
+ mbid: string;
7
+ name: string;
8
+ "#text": string;
9
+ url?: string;
10
+ };
11
+ export type LastFmAlbumRef = {
12
+ "#text": string;
13
+ mbid: string;
14
+ };
15
+ export type LastFmTrack = {
16
+ name: string;
17
+ mbid: string;
18
+ url: string;
19
+ artist: LastFmArtistRef;
20
+ album: LastFmAlbumRef;
21
+ playcount: number | string;
22
+ duration?: string;
23
+ image?: LastFmImage[];
24
+ date?: {
25
+ uts: string;
26
+ "#text": string;
27
+ };
28
+ "@attr"?: {
29
+ nowplaying: "true" | "false";
30
+ };
31
+ };
32
+ export type LastFmUser = {
33
+ user: {
34
+ id: string;
35
+ name: string;
36
+ realname: string;
37
+ gender: string;
38
+ country: string;
39
+ url: string;
40
+ playcount: string;
41
+ playlists: string;
42
+ bootstrap: string;
43
+ image: LastFmImage[];
44
+ registered: {
45
+ unixtime: string;
46
+ "#text": number;
47
+ };
48
+ };
49
+ };
50
+ export type TrackInfoResponse = {
51
+ track: LastFmTrack;
52
+ };
53
+ export type RecentTracksResponse = {
54
+ recenttracks: {
55
+ track: LastFmTrack[];
56
+ "@attr": {
57
+ user: string;
58
+ page: string;
59
+ perPage: string;
60
+ totalPages: string;
61
+ total: string;
62
+ };
63
+ };
64
+ };
65
+ export type AuthSessionResponse = {
66
+ session: {
67
+ name: string;
68
+ key: string;
69
+ subscriber: number;
70
+ };
71
+ };
72
+ export type UserInfoResponse = LastFmUser;
73
+ export interface LastFmClientOptions {
74
+ apiKey: string;
75
+ apiSecret: string;
76
+ userAgent?: string;
77
+ }
78
+ export type AlbumInfoResponse = {
79
+ album: {
80
+ name: string;
81
+ artist: string;
82
+ mbid?: string;
83
+ url: string;
84
+ image?: LastFmImage[];
85
+ listeners?: string;
86
+ playcount?: string;
87
+ tracks?: {
88
+ track: LastFmTrack[];
89
+ };
90
+ tags?: {
91
+ tag: LastFmTag[];
92
+ };
93
+ wiki?: {
94
+ published: string;
95
+ summary: string;
96
+ content: string;
97
+ };
98
+ };
99
+ };
100
+ export type AlbumSearchResponse = {
101
+ results: {
102
+ albummatches: {
103
+ album: Array<{
104
+ name: string;
105
+ artist: string;
106
+ url: string;
107
+ image: LastFmImage[];
108
+ streamable: string;
109
+ mbid: string;
110
+ }>;
111
+ };
112
+ };
113
+ };
114
+ export type ArtistInfoResponse = {
115
+ artist: {
116
+ name: string;
117
+ mbid?: string;
118
+ url: string;
119
+ image?: LastFmImage[];
120
+ streamable?: string;
121
+ ontour?: string;
122
+ stats?: {
123
+ listeners: string;
124
+ playcount: string;
125
+ };
126
+ similar?: {
127
+ artist: LastFmArtistRef[];
128
+ };
129
+ tags?: {
130
+ tag: LastFmTag[];
131
+ };
132
+ bio?: {
133
+ published: string;
134
+ summary: string;
135
+ content: string;
136
+ };
137
+ };
138
+ };
139
+ export type ArtistSimilarResponse = {
140
+ similarartists: {
141
+ artist: LastFmArtistRef[];
142
+ };
143
+ };
144
+ export type ArtistSearchResponse = {
145
+ results: {
146
+ artistmatches: {
147
+ artist: Array<{
148
+ name: string;
149
+ listeners: string;
150
+ mbid: string;
151
+ url: string;
152
+ streamable: string;
153
+ image: LastFmImage[];
154
+ }>;
155
+ };
156
+ };
157
+ };
158
+ export type TrackSimilarResponse = {
159
+ similartracks: {
160
+ track: LastFmTrack[];
161
+ };
162
+ };
163
+ export type TrackSearchResponse = {
164
+ results: {
165
+ trackmatches: {
166
+ track: Array<{
167
+ name: string;
168
+ artist: string;
169
+ url: string;
170
+ streamable: string;
171
+ listeners: string;
172
+ image: LastFmImage[];
173
+ mbid: string;
174
+ }>;
175
+ };
176
+ };
177
+ };
178
+ export type LastFmTag = {
179
+ name: string;
180
+ url: string;
181
+ reach?: string;
182
+ taggings?: string;
183
+ streamable?: string;
184
+ wiki?: {
185
+ published: string;
186
+ summary: string;
187
+ content: string;
188
+ };
189
+ };
190
+ export type TagInfoResponse = {
191
+ tag: LastFmTag & {
192
+ total?: number;
193
+ reach?: number;
194
+ };
195
+ };
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "lastfm.ts",
3
+ "version": "1.0.0",
4
+ "description": "A typed Last.fm API client",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "prepublishOnly": "npm run build",
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "lastfm",
14
+ "scrobbler",
15
+ "typescript"
16
+ ],
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "author": "Rafael",
21
+ "license": "ISC",
22
+ "devDependencies": {
23
+ "@types/node": "^20.0.0",
24
+ "typescript": "^5.9.3"
25
+ }
26
+ }