Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/commands/stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { AppDataSource } from '../db';
import { SongRequest } from '../db/entities/SongRequest';
import { Command } from '../interfaces/Command';
import { ENV } from '../utils/ENV';

export default {
name: 'stats',
description: 'Bot requests stats',
async execute(client, message) {
if (!ENV.USE_DB) {
return message.reply('Song requests recording is not enabled');
}

const repository = AppDataSource.getRepository(SongRequest);
const averageRequestsPerGuild = await repository
.createQueryBuilder('song_request')
.select('song_request.guildId', 'guildId')
.addSelect('COUNT(*) / 30.0', 'average_requests_per_day')
.where('song_request.requestedAt >= :date', {
date: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // 30 days ago
})
.groupBy('song_request.guildId')
.getRawMany();

const avg = await Promise.all(
averageRequestsPerGuild.map(async (guild) => {
const cached = client.guilds.cache.get(guild.guildId);
if (cached) {
return `${cached.name}: ${guild.average_requests_per_day}`;
}

const guildName = await client.guilds
.fetch(guild.guildId)
.then((guild) => guild.name);
return `${guildName}: ${guild.average_requests_per_day}`;
})
);

if (!avg.length) {
return message.reply('No stats available');
}

message.reply(`Average requests per day:\n${avg.join('\n')}`);
},
} as Command;
24 changes: 24 additions & 0 deletions src/external/lastfm/findTrackByQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { lastFmFetch } from './lastFmAxiosClient';

export interface LastFmTrack {
name: string;
artist: string;
}

export async function findTrackByQuery(
query: string
): Promise<LastFmTrack | null> {
try {
const res = await lastFmFetch('track.search', {
params: {
track: query,
limit: 1,
},
});

return res?.results?.trackmatches?.track?.[0] || null;
} catch (error) {
console.error('Error occurred while fetching track by query', error);
return null;
}
}
31 changes: 31 additions & 0 deletions src/external/lastfm/getSimilarLastFmTracks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { LastFmTrack, findTrackByQuery } from './findTrackByQuery';
import { lastFmFetch } from './lastFmAxiosClient';

export async function getSimilarLastFmTracks(
query: string
): Promise<LastFmTrack[]> {
try {
const track = await findTrackByQuery(query);
if (!track) {
throw new Error('Track not found');
}

const res = await lastFmFetch('track.getSimilar', {
params: {
track: track.name,
artist: track.artist,
limit: 20,
},
});

const tracks = res?.similartracks?.track || [];

return tracks.map((track: any) => ({
name: track.name,
artist: track.artist.name,
}));
} catch (error) {
console.error('Error occurred while fetching track by query', error);
throw error;
}
}
68 changes: 68 additions & 0 deletions src/external/lastfm/lastFmAxiosClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import retry from 'async-retry';
import axios, { AxiosRequestConfig } from 'axios';
import { ENV } from '../../utils/ENV';

const lastFmClient = axios.create({
baseURL: 'https://ws.audioscrobbler.com/2.0/?',
params: {
api_key: ENV.LASTFM_API_KEY,
format: 'json',
},
});

lastFmClient.interceptors.response.use(
(response) => {
const error = response.data.error;
if (error) {
throw new Error(`Last.fm error: ${error.message}`);
}

return response;
},
(error) => {
const url = error.config.url;
const params = error.config.params;
const apiErrorMessage = error?.response?.data?.error?.message;
const errorMessage = apiErrorMessage || error.message || 'Unknown error';

return Promise.reject(
`Error occurred at endpoint: ${url} with params: ${JSON.stringify(
params
)}. Error message: ${errorMessage}`
);
}
);

export async function lastFmFetch(method: string, config?: AxiosRequestConfig) {
if (!process.env.LASTFM_API_KEY) {
throw new Error('LASTFM_API_KEY is not set');
}

return await retry(
async (bail) => {
try {
const response = await lastFmClient({
params: {
method,
...config?.params,
},
});

return response.data;
} catch (error: any) {
if (axios.isAxiosError(error)) {
error.message = `Last.fm error: ${error.message}`;
if (!error.response || error.response.status >= 500) {
throw error;
} else {
bail(error);
}
}
throw error;
}
},
{
retries: 0,
}
);
}
61 changes: 6 additions & 55 deletions src/external/spotify/getSimilarTracks.ts
Original file line number Diff line number Diff line change
@@ -1,65 +1,16 @@
import { TrackDetails, getTrackDetails } from './getTrackDetails';
import { getSpotifyTrackTitle } from './utils/getSpotifyTrackTitle';
import { getTrackFeatures } from './getTrackFeatures';
import { spotifyFetch } from './spotifyAxiosClient';
import { removeTrackDuplicates } from '../../utils/removeArrayDuplicates';
import { getBannedArtists } from '../../db/methods/getBannedArtists';
import { LastFmTrack } from '../lastfm/findTrackByQuery';
import { getSimilarLastFmTracks } from '../lastfm/getSimilarLastFmTracks';
import { TrackDetails } from './getTrackDetails';

export interface SpotifyTrack extends TrackDetails {
title: string;
}

export async function getSimilarTracks(id: string): Promise<SpotifyTrack[]> {
export async function getSimilarTracks(id: string): Promise<LastFmTrack[]> {
try {
const trackFeatures = await getTrackFeatures(id);
if (!trackFeatures) {
throw new Error(`Unable to get track features for ${id}`);
}
const tracks = await getSimilarLastFmTracks(id);

const trackDetails = await getTrackDetails(id);
if (!trackDetails) {
throw new Error(`Unable to get track details for ${id}`);
}

const requestParams: Record<string, string | number> = {
seed_tracks: id,
target_danceability: trackFeatures.danceability,
target_energy: trackFeatures.energy,
target_valence: trackFeatures.valence,
target_tempo: trackFeatures.tempo,
target_loudness: trackFeatures.loudness,
min_popularity: trackDetails.popularity - 10,
limit: 60,
};

const data = await spotifyFetch('/recommendations', {
params: requestParams,
});

const bannedArtists = await getBannedArtists();
const tracks = (data?.tracks || [])
.filter((t: any) => {
const artists = (t?.artists || []).map((a: any) => a.id);

return !bannedArtists.find((b) => artists.includes(b.spotifyId));
})
.map((t: any) => ({
id: t.id,
title: getSpotifyTrackTitle(t),
popularity: t.popularity,
}));
// .sort((a: SpotifyTrack, b: SpotifyTrack) => b.popularity - a.popularity);

const uniqueTracks: SpotifyTrack[] = removeTrackDuplicates([
{
id: id,
title: getSpotifyTrackTitle(trackDetails),
popularity: trackDetails.popularity,
},
...tracks,
]);

return uniqueTracks.slice(0, 30);
return tracks;
} catch (error) {
throw error;
}
Expand Down
4 changes: 2 additions & 2 deletions src/interfaces/RadioSession.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface RadioSessionTrack {
spotifyId: string;
interface RadioSessionTrack {
artist: string;
title: string;
}

Expand Down
19 changes: 4 additions & 15 deletions src/test/spotify_test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
import { AppDataSource } from '../db';
import { getSimilarTracks } from '../external/spotify/getSimilarTracks';
import { SPOTIFY_TRACK_REGEX } from '../utils/helpers';

async function main() {
try {
await AppDataSource.initialize();
const query = process.argv[2];
if (!query) throw new Error('Please provide query');

const url = process.argv[2];
if (!url) throw new Error('Please provide a Spotify track URL');

const id = url.match(SPOTIFY_TRACK_REGEX)?.[1];
if (!id) throw new Error('Invalid Spotify track URL');

const tracks = await getSimilarTracks(id);
const tracks = await getSimilarTracks(query);

for (const track of tracks) {
console.log(
`- [${track.popularity || 'xx'}] https://open.spotify.com/track/${
track.id
} : ${track.title}`
);
console.log(`- ${track.artist} - ${track.name}`);
}
} catch (error) {
if (error instanceof Error) {
Expand Down
1 change: 1 addition & 0 deletions src/utils/ENV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ export const ENV = {
SPOTIFY_CLIENT_ID: process.env.SPOTIFY_CLIENT_ID as string,
SPOTIFY_CLIENT_SECRET: process.env.SPOTIFY_CLIENT_SECRET as string,
ADMINS: process.env.ADMINS?.split(',') || [],
LASTFM_API_KEY: process.env.LASTFM_API_KEY as string,
};