Last active 1732922313

Revision 09c5d39c2bfeeb8b6a5b68bb336634724b4b013f

lastfm-weekly-report.ts Raw
1const LASTFM_API_KEY = process.env.FEED_LASTFM_API_KEY || "";
2const LASTFM_USERNAME = process.env.FEED_LASTFM_USERNAME || "";
3const LASTFM_API = "https://ws.audioscrobbler.com/2.0/";
4
5function formatItem({
6 artist,
7 image,
8 name,
9 playcount,
10 url,
11}: {
12 artist: { "#text": string };
13 image?: { "#text": string }[];
14 name: string;
15 playcount: string;
16 url: string;
17}) {
18 return {
19 name: artist ? `${name} by ${artist?.["#text"]}` : name,
20 image: image ? image?.at(-1)?.["#text"] : undefined,
21 playcount,
22 url,
23 };
24}
25
26function epochToIso(epoch: string) {
27 const date = new Date(Number.parseInt(epoch) * 1000);
28 return date?.toISOString();
29}
30
31function isoToYearAndWeek(iso: string) {
32 const date = new Date(iso);
33 const year = date.getFullYear();
34 const firstOfYear = new Date(year, 0, 1);
35 const daysSinceFirstOfYear = Math.floor(
36 (+date - +firstOfYear) / (24 * 60 * 60 * 1000),
37 );
38 const week = Math.ceil((date.getDay() + 1 + daysSinceFirstOfYear) / 7);
39 return {
40 week,
41 year,
42 };
43}
44
45function listToHTML(
46 arr: {
47 name: string;
48 playcount: string;
49 url: string;
50 }[],
51) {
52 return arr
53 .map(
54 ({ name, playcount, url }) =>
55 `<li><a href="${url}">${name}</a> (${playcount} plays)</li>`,
56 )
57 .join("");
58}
59
60async function lastFm(method = "") {
61 const data = await fetch(
62 `${LASTFM_API}?method=${method}&api_key=${LASTFM_API_KEY}&format=json&user=${LASTFM_USERNAME}`,
63 );
64 return await data.json();
65}
66
67async function getWeeklyChartList(limit = 5) {
68 const data = await lastFm("user.getWeeklyChartList");
69 return data?.weeklychartlist?.chart?.slice(limit * -1);
70}
71
72async function getWeeklyTrackChart(to: string, from: string, limit = 5) {
73 const data = await lastFm(`user.getWeeklyTrackChart&from=${from}&to=${to}`);
74 return data?.weeklytrackchart?.track?.slice(0, limit)?.map(formatItem);
75}
76
77async function getWeeklyAlbumChart(to: string, from: string, limit = 5) {
78 const data = await lastFm(`user.getWeeklyAlbumChart&from=${from}&to=${to}`);
79 return data?.weeklyalbumchart?.album?.slice(0, limit)?.map(formatItem);
80}
81
82async function getWeeklyArtistChart(to: string, from: string, limit = 5) {
83 const data = await lastFm(`user.getWeeklyArtistChart&from=${from}&to=${to}`);
84 return data?.weeklyartistchart?.artist?.slice(0, limit)?.map(formatItem);
85}
86
87async function buildReport() {
88 const output = [];
89 const weeks = await getWeeklyChartList();
90 for (const week of weeks) {
91 output.push({
92 to: week.to,
93 from: week.from,
94 tracks: await getWeeklyTrackChart(week.to, week.from),
95 albums: await getWeeklyAlbumChart(week.to, week.from),
96 artists: await getWeeklyArtistChart(week.to, week.from),
97 });
98 }
99 return output;
100}
101
102export default async function buildFeedItems() {
103 const data = await buildReport();
104 return data
105 .map(({ to, from, tracks, albums, artists }) => {
106 const trackList = listToHTML(tracks);
107 const albumList = listToHTML(albums);
108 const artistList = listToHTML(artists);
109 const toIso = epochToIso(to);
110 const fromIso = epochToIso(from);
111 const { week, year } = isoToYearAndWeek(fromIso);
112
113 if (!trackList && !albumList && !artistList) {
114 return;
115 }
116
117 return {
118 content_html: [
119 artistList ? `<h2>Top Artists</h2><ol>${artistList}</ol>` : "",
120 albumList ? `<h2>Top Albums</h2><ol>${albumList}</ol>` : "",
121 trackList ? `<h2>Top Tracks</h2><ol>${trackList}</ol>` : "",
122 ].join(""),
123 date_published: toIso,
124 id: `last.fm-${from}-${to}`,
125 title: "weekly last.fm report",
126 url: `https://www.last.fm/user/ZicklePop/listening-report/year/${year}/week/${week}`,
127 tags: ["last.fm"],
128 };
129 })
130 .filter((o) => !!o);
131}
132