Skip to content

Commit cb6b826

Browse files
authored
last.fm - add option to download more users (#4)
by request body param
1 parent dbd2b35 commit cb6b826

File tree

9 files changed

+156
-52
lines changed

9 files changed

+156
-52
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ supabase start
3535
**run server to allow you to invoke Edge Functions locally**
3636

3737
```bash
38-
supabase function serve --no-verify-jwt
38+
supabase functions serve --no-verify-jwt
3939
```
4040

4141
**deploy modified function**

supabase/functions/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
DEVELOPER_MODE=true
12
SENTRY_DSN=xxx
23
LASTFM_API_KEY=xxx
34
LASTFM_USERNAME=Insuit
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { SupabaseClient } from "https://esm.sh/@supabase/supabase-js@2";
2+
3+
interface SupabaseTableInterface {
4+
readonly supabase: SupabaseClient | undefined;
5+
getSupabase(): SupabaseClient | undefined;
6+
}
7+
8+
export class DbSupabaseTable implements SupabaseTableInterface {
9+
readonly supabase: SupabaseClient | undefined;
10+
11+
constructor(supabase: SupabaseClient) {
12+
this.supabase = supabase;
13+
}
14+
15+
getSupabase(): SupabaseClient {
16+
if (!this.supabase) {
17+
throw new Error("supabase is not initialized");
18+
}
19+
return this.supabase;
20+
}
21+
}
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
# test request without JWT
2-
GET http://localhost:54321/functions/v1/fetch-scrobbles
3-
Accept: application/json
1+
### test request without JWT
2+
POST http://localhost:54321/functions/v1/fetch-scrobbles
3+
Content-Type: application/json
4+
5+
{
6+
"lastFmUser": "Insuit"
7+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { DbSupabaseTable } from "../_shared/supabase-table.ts";
2+
3+
export interface Row {
4+
created_at: string;
5+
lastfm_user: string;
6+
}
7+
8+
const columnName: Row = {
9+
created_at: "created_at",
10+
lastfm_user: "lastfm_user",
11+
};
12+
13+
export class TableHooman extends DbSupabaseTable {
14+
tableName = "hooman";
15+
16+
async findOrCreateByLastFmUser(lastFmUser: string): Promise<string> {
17+
const { data, error } = await this.getSupabase()
18+
.from(this.tableName)
19+
.select("id")
20+
.eq(columnName.lastfm_user, lastFmUser)
21+
.limit(1)
22+
.maybeSingle<{ id: string } | null>();
23+
24+
if (error) {
25+
throw new Error("Error fetching hooman: " + JSON.stringify(error));
26+
}
27+
28+
if (data) {
29+
return data.id;
30+
}
31+
32+
console.log(`Creating new user: ${lastFmUser}`);
33+
34+
const { data: insertData, error: insertError } = await this.getSupabase()
35+
.from(this.tableName)
36+
.insert({
37+
[columnName.created_at]: new Date(),
38+
[columnName.lastfm_user]: lastFmUser,
39+
})
40+
.select("id")
41+
.maybeSingle<{ id: string }>();
42+
43+
if (insertError || !insertData) {
44+
throw new Error("Error creating hooman: " + JSON.stringify(insertError));
45+
}
46+
47+
return insertData.id;
48+
}
49+
}

supabase/functions/fetch-scrobbles/table.ts renamed to supabase/functions/fetch-scrobbles/db.listened.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,35 @@
1-
import { SupabaseClient } from "https://esm.sh/@supabase/supabase-js@2";
1+
import { DbSupabaseTable } from "../_shared/supabase-table.ts";
22

3-
const tableName = "listened";
4-
5-
export interface Row {
3+
export interface ListenedRow {
64
created_at: string;
75
listened_at: string;
86
artist_name: string;
97
track_name: string;
108
album_name: string | null;
119
lastfm_data: string | object;
10+
hooman_id: string | null;
1211
}
1312

14-
const columnName: Row = {
13+
const columnName: ListenedRow = {
1514
created_at: "created_at",
1615
listened_at: "listened_at",
1716
artist_name: "artist_name",
1817
track_name: "track_name",
1918
album_name: "album_name",
2019
lastfm_data: "lastfm_data",
20+
hooman_id: "hooman_id",
2121
};
2222

23-
export class TableListened {
24-
private readonly supabase: SupabaseClient | undefined;
25-
26-
constructor(supabase: SupabaseClient) {
27-
this.supabase = supabase;
28-
}
23+
export class TableListened extends DbSupabaseTable {
24+
tableName = "listened";
2925

3026
/**
3127
* Get the latest timestamp from the scrobbles table.
3228
* @returns The latest timestamp in Unix seconds or null.
3329
*/
3430
async getLastListenedDate(): Promise<number | null> {
35-
if (!this.supabase) {
36-
throw new Error("supabase is not initialized");
37-
}
38-
39-
const { data, error } = await this.supabase
40-
.from(tableName)
31+
const { data, error } = await this.getSupabase()
32+
.from(this.tableName)
4133
.select(columnName.listened_at)
4234
.order(columnName.listened_at, { ascending: false })
4335
.limit(1)
@@ -58,17 +50,17 @@ export class TableListened {
5850
* @param tracks - Array of new scrobbles to save.
5951
* @returns An object containing a message or error.
6052
*/
61-
async save(tracks: Row[]): Promise<{ message?: string; error?: unknown }> {
53+
async save(
54+
tracks: ListenedRow[],
55+
): Promise<{ message?: string; error?: unknown }> {
6256
if (tracks.length === 0) {
6357
console.log("No new scrobbles to save.");
6458
return { message: "No new scrobbles" };
6559
}
6660

67-
if (!this.supabase) {
68-
throw new Error("supabase is not initialized");
69-
}
70-
71-
const { error } = await this.supabase.from(tableName).insert(tracks);
61+
const { error } = await this.getSupabase()
62+
.from(this.tableName)
63+
.insert(tracks);
7264

7365
if (error) {
7466
console.error("Error inserting data:", error);

supabase/functions/fetch-scrobbles/index.ts

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,43 @@ import * as Sentry from "https://deno.land/x/sentry/index.mjs";
22
import { scrobbles } from "./scrobbles.ts";
33
import { env } from "./env.ts";
44

5-
Sentry.init({
6-
dsn: Deno.env.get("SENTRY_DSN")!,
7-
defaultIntegrations: false,
8-
tracesSampleRate: 1.0,
9-
// deno-lint-ignore ban-ts-comment
10-
// @ts-ignore
11-
profilesSampleRate: 1.0,
12-
});
5+
const isProduction = Deno.env.get("DEVELOPER_MODE") !== "true";
6+
7+
if (isProduction) {
8+
Sentry.init({
9+
dsn: Deno.env.get("SENTRY_DSN")!,
10+
defaultIntegrations: false,
11+
tracesSampleRate: 1.0,
12+
// deno-lint-ignore ban-ts-comment
13+
// @ts-ignore
14+
profilesSampleRate: 1.0,
15+
});
1316

14-
// Set region and execution_id as custom tags
15-
Sentry.setTag("region", Deno.env.get("SB_REGION"));
16-
Sentry.setTag("execution_id", Deno.env.get("SB_EXECUTION_ID"));
17-
Sentry.setTag("url", env.SUPABASE_URL);
17+
// Set region and execution_id as custom tags
18+
Sentry.setTag("region", Deno.env.get("SB_REGION"));
19+
Sentry.setTag("execution_id", Deno.env.get("SB_EXECUTION_ID"));
20+
Sentry.setTag("url", env.SUPABASE_URL);
21+
}
1822

19-
Deno.serve(async () => {
23+
Deno.serve(async (req) => {
2024
try {
21-
const result = await scrobbles(env);
25+
const body = await req.json();
26+
27+
const lastFmUser = typeof body.lastFmUser === "string"
28+
? body.lastFmUser
29+
: null;
30+
31+
const result = await scrobbles(env, lastFmUser);
32+
2233
return new Response(result, {
2334
status: 200,
2435
headers: { "Content-Type": "text/plain" },
2536
});
2637
} catch (e) {
27-
Sentry.captureException(e);
38+
if (isProduction) {
39+
Sentry.captureException(e);
40+
}
41+
console.error(e);
2842
return new Response("error occured, please check sentry/supabase logs", {
2943
status: 500,
3044
headers: { "Content-Type": "text/plain" },

supabase/functions/fetch-scrobbles/scrobbles.ts

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
22
import { getRecentTracks } from "./lastfm.ts";
3-
import { Row, TableListened } from "./table.ts";
43
import { Variables } from "./env.ts";
4+
import { TableHooman } from "./db.hooman.ts";
5+
import { ListenedRow, TableListened } from "./db.listened.ts";
56

6-
export async function scrobbles(env: Variables): Promise<string> {
7+
/**
8+
* Sync data from Last.fm to Supabase Database.
9+
*/
10+
export async function scrobbles(
11+
env: Variables,
12+
lastFmUser: string | null = null,
13+
): Promise<string> {
714
const size = 50;
815
const supabaseClient = createClient(env.SUPABASE_URL, env.SUPABASE_ANON_KEY);
9-
const table = new TableListened(supabaseClient);
10-
const startFrom: number | null = await table.getLastListenedDate();
16+
const listened = new TableListened(supabaseClient);
17+
const hooman = new TableHooman(supabaseClient);
18+
const startFrom: number | null = await listened.getLastListenedDate();
19+
1120
let totalPages = 1;
1221
let total = 0;
1322
let page = 1;
@@ -21,9 +30,14 @@ export async function scrobbles(env: Variables): Promise<string> {
2130
console.log("Database is empty, starting from the last page");
2231
}
2332

33+
const lastFmUserToUse = lastFmUser ? lastFmUser : env.LASTFM_USERNAME;
34+
console.log(`Last.fm user: ${lastFmUserToUse}`);
35+
36+
const hoomanId = await hooman.findOrCreateByLastFmUser(lastFmUserToUse);
37+
2438
const fmInitial = await getRecentTracks(
2539
env.LASTFM_API_KEY,
26-
env.LASTFM_USERNAME,
40+
lastFmUserToUse,
2741
1,
2842
size,
2943
startFrom,
@@ -37,8 +51,8 @@ export async function scrobbles(env: Variables): Promise<string> {
3751
page = totalPages;
3852

3953
if (total === 0) {
40-
console.log('Nothing new to save.');
41-
return 'ok';
54+
console.log("Nothing new to save.");
55+
return "ok";
4256
}
4357

4458
if (startFrom) {
@@ -64,7 +78,7 @@ export async function scrobbles(env: Variables): Promise<string> {
6478

6579
const fm = await getRecentTracks(
6680
env.LASTFM_API_KEY,
67-
env.LASTFM_USERNAME,
81+
lastFmUserToUse,
6882
page,
6983
size,
7084
startFrom,
@@ -82,7 +96,7 @@ export async function scrobbles(env: Variables): Promise<string> {
8296

8397
console.log(`Fetching page ${page}/${totalPages}`);
8498

85-
const toInsert: Row[] = tracks
99+
const toInsert: ListenedRow[] = tracks
86100
.filter((track) => !(track["@attr"] && track["@attr"].nowplaying))
87101
.map((track) => ({
88102
created_at: new Date().toISOString(),
@@ -91,9 +105,10 @@ export async function scrobbles(env: Variables): Promise<string> {
91105
track_name: track.name,
92106
album_name: track.album["#text"],
93107
lastfm_data: track,
108+
hooman_id: hoomanId,
94109
}));
95110

96-
const { error, message } = await table.save(toInsert);
111+
const { error, message } = await listened.save(toInsert);
97112
if (error) {
98113
throw new Error(error.toString());
99114
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
CREATE TABLE hooman
2+
(
3+
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
4+
created_at TIMESTAMPTZ NOT NULL,
5+
lastfm_user TEXT NOT NULL
6+
);
7+
8+
ALTER TABLE listened ADD COLUMN hooman_id UUID DEFAULT NULL;

0 commit comments

Comments
 (0)