lastfm-weekly-report.ts
· 3.5 KiB · TypeScript
Ham
const LASTFM_API_KEY = process.env.FEED_LASTFM_API_KEY || "";
const LASTFM_USERNAME = process.env.FEED_LASTFM_USERNAME || "";
const LASTFM_API = "https://ws.audioscrobbler.com/2.0/";
function formatItem({
artist,
image,
name,
playcount,
url,
}: {
artist: { "#text": string };
image?: { "#text": string }[];
name: string;
playcount: string;
url: string;
}) {
return {
name: artist ? `${name} by ${artist?.["#text"]}` : name,
image: image ? image?.at(-1)?.["#text"] : undefined,
playcount,
url,
};
}
function epochToIso(epoch: string) {
const date = new Date(Number.parseInt(epoch) * 1000);
return date?.toISOString();
}
function isoToYearAndWeek(iso: string) {
const date = new Date(iso);
const year = date.getFullYear();
const firstOfYear = new Date(year, 0, 1);
const daysSinceFirstOfYear = Math.floor(
(+date - +firstOfYear) / (24 * 60 * 60 * 1000),
);
const week = Math.ceil((date.getDay() + 1 + daysSinceFirstOfYear) / 7);
return {
week,
year,
};
}
function listToHTML(
arr: {
name: string;
playcount: string;
url: string;
}[],
) {
return arr
.map(
({ name, playcount, url }) =>
`<li><a href="${url}">${name}</a> (${playcount} plays)</li>`,
)
.join("");
}
async function lastFm(method = "") {
const data = await fetch(
`${LASTFM_API}?method=${method}&api_key=${LASTFM_API_KEY}&format=json&user=${LASTFM_USERNAME}`,
);
return await data.json();
}
async function getWeeklyChartList(limit = 5) {
const data = await lastFm("user.getWeeklyChartList");
return data?.weeklychartlist?.chart?.slice(limit * -1);
}
async function getWeeklyTrackChart(to: string, from: string, limit = 5) {
const data = await lastFm(`user.getWeeklyTrackChart&from=${from}&to=${to}`);
return data?.weeklytrackchart?.track?.slice(0, limit)?.map(formatItem);
}
async function getWeeklyAlbumChart(to: string, from: string, limit = 5) {
const data = await lastFm(`user.getWeeklyAlbumChart&from=${from}&to=${to}`);
return data?.weeklyalbumchart?.album?.slice(0, limit)?.map(formatItem);
}
async function getWeeklyArtistChart(to: string, from: string, limit = 5) {
const data = await lastFm(`user.getWeeklyArtistChart&from=${from}&to=${to}`);
return data?.weeklyartistchart?.artist?.slice(0, limit)?.map(formatItem);
}
async function buildReport() {
const output = [];
const weeks = await getWeeklyChartList();
for (const week of weeks) {
output.push({
to: week.to,
from: week.from,
tracks: await getWeeklyTrackChart(week.to, week.from),
albums: await getWeeklyAlbumChart(week.to, week.from),
artists: await getWeeklyArtistChart(week.to, week.from),
});
}
return output;
}
export default async function buildFeedItems() {
const data = await buildReport();
return data
.map(({ to, from, tracks, albums, artists }) => {
const trackList = listToHTML(tracks);
const albumList = listToHTML(albums);
const artistList = listToHTML(artists);
const toIso = epochToIso(to);
const fromIso = epochToIso(from);
const { week, year } = isoToYearAndWeek(fromIso);
if (!trackList && !albumList && !artistList) {
return;
}
return {
content_html: [
artistList ? `<h2>Top Artists</h2><ol>${artistList}</ol>` : "",
albumList ? `<h2>Top Albums</h2><ol>${albumList}</ol>` : "",
trackList ? `<h2>Top Tracks</h2><ol>${trackList}</ol>` : "",
].join(""),
date_published: toIso,
id: `last.fm-${from}-${to}`,
title: "weekly last.fm report",
url: `https://www.last.fm/user/ZicklePop/listening-report/year/${year}/week/${week}`,
tags: ["last.fm"],
};
})
.filter((o) => !!o);
}
| 1 | const LASTFM_API_KEY = process.env.FEED_LASTFM_API_KEY || ""; |
| 2 | const LASTFM_USERNAME = process.env.FEED_LASTFM_USERNAME || ""; |
| 3 | const LASTFM_API = "https://ws.audioscrobbler.com/2.0/"; |
| 4 | |
| 5 | function 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 | |
| 26 | function epochToIso(epoch: string) { |
| 27 | const date = new Date(Number.parseInt(epoch) * 1000); |
| 28 | return date?.toISOString(); |
| 29 | } |
| 30 | |
| 31 | function 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 | |
| 45 | function 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 | |
| 60 | async 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 | |
| 67 | async function getWeeklyChartList(limit = 5) { |
| 68 | const data = await lastFm("user.getWeeklyChartList"); |
| 69 | return data?.weeklychartlist?.chart?.slice(limit * -1); |
| 70 | } |
| 71 | |
| 72 | async 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 | |
| 77 | async 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 | |
| 82 | async 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 | |
| 87 | async 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 | |
| 102 | export 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 |