Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ jobs:
- uses: actions/checkout@v4

- name: Install system dependencies
run: sudo apt install -y libasound2-dev
run: |
sudo apt-get update
Comment thread
joinemm marked this conversation as resolved.
sudo apt-get install -y libasound2-dev

- name: Install uv
uses: astral-sh/setup-uv@v6
Expand Down
196 changes: 168 additions & 28 deletions cogs/lastfm.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,20 @@ async def convert(self, _ctx: MisoContext, argument: str):
return argument


class ServerRankingArgument(commands.Converter):
ALIASES = {
"score": "score",
"scrobble": "scrobble",
}

@classmethod
async def convert(cls, _ctx, argument: str):
key = argument.lower()
if key not in cls.ALIASES:
raise commands.BadArgument("Valid options are: `score`, `scrobble`.")
return cls.ALIASES[key]


class StrOrNp:
def extract(self, data: dict):
raise NotImplementedError
Expand Down Expand Up @@ -1426,6 +1440,9 @@ async def chart_factory(

albums = []
for image, label in chart_nodes:
if image is None:
image = LastFmImage(LastFmImage.MISSING_IMAGE_HASH)

if size.width < 5:
image_url = image.as_full()
elif 9 > size.width >= 5:
Expand Down Expand Up @@ -1622,13 +1639,28 @@ async def server_recent(self, ctx: MisoContext):

await RowPaginator(content, rows).run(ctx)

@server.command(name="topartists", aliases=["ta"], usage="[timeframe]")
@server.command(
name="topartists", aliases=["ta"], usage="[timeframe] ['score' | 'scrobble']"
)
async def server_topartists(
self,
ctx: MisoContext,
timeframe: Annotated[Period, PeriodArgument] = Period.OVERALL,
*args: Union[
Annotated[Period, PeriodArgument],
Annotated[str, ServerRankingArgument],
],
):
"""Combined top artists of server members"""
timeframe = Period.OVERALL
mode = "score"
contributors = 0

for arg in args:
if isinstance(arg, Period):
timeframe = arg
else:
mode = arg

data = await self.task_for_each_server_member(
ctx.guild, self.api.user_get_top_artists, limit=100, period=timeframe
)
Expand All @@ -1647,6 +1679,8 @@ async def server_topartists(
if len(artists) == 0:
continue

contributors += 1

lowest_playcount = int(artists[-1]["playcount"])
highest_playcount = int(artists[0]["playcount"])
for artist in artists:
Expand All @@ -1665,33 +1699,68 @@ async def server_topartists(
except KeyError:
artist_map[name] = {"score": score, "playcount": playcount}

if mode == "score":
sort_key = "score"
else:
sort_key = "playcount"

top_artists = sorted(
artist_map.items(), key=lambda x: x[1]["score"], reverse=True
artist_map.items(), key=lambda x: x[1][sort_key], reverse=True
)[:100]

if not top_artists:
return await ctx.send("Nobody on this server has listened to anything!")

rows = []
for i, (artist, artist_data) in enumerate(top_artists, start=1):
rows.append(
f"`#{i:2}` **{artist_data['score'] / len(data):.2f}%** /"
f" **{artist_data['playcount']}** plays • **{artist}**"
if mode == "score":
rows.append(
f"`#{i:2}` **{artist_data['score'] / contributors:.2f}%** /"
f" **{artist_data['playcount']}** plays • **{artist}**"
)
else:
rows.append(
f"`#{i:2}` **{artist_data['playcount']}** plays • **{artist}**"
)

if mode == "score":
footer = f"Score calculated from top 100 artists of {contributors} members"
else:
footer = (
f"Ranked by scrobbles from top 100 artists of {contributors} members"
)

await self.paginated_user_stat_embed(
ctx,
rows,
f"Top 100 Artists ({timeframe.display()})",
image=await self.api.get_artist_image(top_artists[0][0]),
footer=f"Score calculated from top 100 artists of {len(data)} members",
footer=footer,
server_target=True,
)

@server.command(name="toptracks", aliases=["tt"], usage="[timeframe]")
@server.command(
name="toptracks", aliases=["tt"], usage="[timeframe] ['score' | 'scrobble']"
)
async def server_toptracks(
self,
ctx: MisoContext,
timeframe: Annotated[Period, PeriodArgument] = Period.OVERALL,
*args: Union[
Annotated[Period, PeriodArgument],
Annotated[str, ServerRankingArgument],
],
):
"""Combined top tracks of server members"""
timeframe = Period.OVERALL
mode = "score"
contributors = 0

for arg in args:
if isinstance(arg, Period):
timeframe = arg
else:
mode = arg

data = await self.task_for_each_server_member(
ctx.guild, self.api.user_get_top_tracks, limit=100, period=timeframe
)
Expand All @@ -1710,6 +1779,8 @@ async def server_toptracks(
if len(tracks) == 0:
continue

contributors += 1

lowest_playcount = int(tracks[-1]["playcount"])
highest_playcount = int(tracks[0]["playcount"])
for track in tracks:
Expand All @@ -1734,33 +1805,66 @@ async def server_toptracks(
"url": track["url"],
}

if mode == "score":
sort_key = "score"
else:
sort_key = "playcount"

top_tracks = sorted(
track_map.items(), key=lambda x: x[1]["score"], reverse=True
track_map.items(), key=lambda x: x[1][sort_key], reverse=True
)[:100]

if not top_tracks:
return await ctx.send("Nobody on this server has listened to anything!")

rows = []
for i, (track, track_data) in enumerate(top_tracks, start=1):
rows.append(
f"`#{i:2}` **{track_data['score'] / len(data):.2f}%** /"
f" **{track_data['playcount']}** plays • {track}"
if mode == "score":
rows.append(
f"`#{i:2}` **{track_data['score'] / contributors:.2f}%** /"
f" **{track_data['playcount']}** plays • {track}"
)
else:
rows.append(f"`#{i:2}` **{track_data['playcount']}** plays • {track}")

if mode == "score":
footer = f"Score calculated from top 100 tracks of {contributors} members"
else:
footer = (
f"Ranked by scrobbles from top 100 tracks of {contributors} members"
)

await self.paginated_user_stat_embed(
ctx,
rows,
f"Top 100 Tracks ({timeframe.display()})",
image=await self.api.scrape_track_image(top_tracks[0][1]["url"]),
footer=f"Score calculated from top 100 tracks of {len(data)} members",
footer=footer,
server_target=True,
)

@server.command(name="topalbums", aliases=["talb"], usage="[timeframe]")
@server.command(
name="topalbums", aliases=["talb"], usage="[timeframe] ['score' | 'scrobble']"
)
async def server_topalbums(
self,
ctx: MisoContext,
timeframe: Annotated[Period, PeriodArgument] = Period.OVERALL,
*args: Union[
Annotated[Period, PeriodArgument],
Annotated[str, ServerRankingArgument],
],
):
"""Combined top albums of server members"""
timeframe = Period.OVERALL
mode = "score"
contributors = 0

for arg in args:
if isinstance(arg, Period):
timeframe = arg
else:
mode = arg

data = await self.task_for_each_server_member(
ctx.guild, self.api.user_get_top_albums, limit=100, period=timeframe
)
Expand All @@ -1779,6 +1883,8 @@ async def server_topalbums(
if len(albums) == 0:
continue

contributors += 1

lowest_playcount = int(albums[-1]["playcount"])
highest_playcount = int(albums[0]["playcount"])
for album in albums:
Expand All @@ -1803,23 +1909,41 @@ async def server_topalbums(
"image": album["image"][0]["#text"],
}

if mode == "score":
sort_key = "score"
else:
sort_key = "playcount"

top_albums = sorted(
album_map.items(), key=lambda x: x[1]["score"], reverse=True
album_map.items(), key=lambda x: x[1][sort_key], reverse=True
)[:100]

if not top_albums:
return await ctx.send("Nobody on this server has listened to anything!")

rows = []
for i, (album, album_data) in enumerate(top_albums, start=1):
rows.append(
f"`#{i:2}` **{album_data['score'] / len(data):.2f}%** /"
f" **{album_data['playcount']}** plays • {album}"
if mode == "score":
rows.append(
f"`#{i:2}` **{album_data['score'] / contributors:.2f}%** /"
f" **{album_data['playcount']}** plays • {album}"
)
else:
rows.append(f"`#{i:2}` **{album_data['playcount']}** plays • {album}")

if mode == "score":
footer = f"Score calculated from top 100 albums of {contributors} members"
else:
footer = (
f"Ranked by scrobbles from top 100 albums of {contributors} members"
)

await self.paginated_user_stat_embed(
ctx,
rows,
f"Top 100 Tracks ({timeframe.display()})",
f"Top 100 Albums ({timeframe.display()})",
image=LastFmImage.from_url(top_albums[0][1]["image"]),
footer=f"Score calculated from top 100 albums of {len(data)} members",
footer=footer,
server_target=True,
)

Expand Down Expand Up @@ -1851,6 +1975,8 @@ async def server_chart(
"""
timeframe = Period.WEEK
size = ChartSize(3, 3)
contributors = 0

for arg in args:
if isinstance(arg, Period):
timeframe = arg
Expand Down Expand Up @@ -1887,6 +2013,8 @@ async def server_chart(
if len(artists) == 0:
continue

contributors += 1

lowest_playcount = int(artists[-1]["playcount"])
highest_playcount = int(artists[0]["playcount"])
for artist in artists:
Expand All @@ -1909,11 +2037,18 @@ async def server_chart(
artist_map.items(), key=lambda x: x[1]["score"], reverse=True
)[: size.count]

for i, (name, data) in enumerate(top_artists):
if not top_artists:
return await ctx.send("Nobody on this server has listened to anything!")

for i, (name, artist_data) in enumerate(top_artists):
image = await self.api.get_artist_image(name)
if image is None:
image = LastFmImage(LastFmImage.MISSING_IMAGE_HASH)

chart_nodes.append(
(
await self.api.get_artist_image(name),
f"<p class='label'>{name}<p><p class='playcount'>{data['score'] / len(data):.2f}%<p>",
image,
f"<p class='label'>{name}<p><p class='playcount'>{artist_data['score'] / contributors:.2f}%<p>",
)
)
if topster:
Expand Down Expand Up @@ -1989,6 +2124,8 @@ async def server_chart(
if len(albums) == 0:
continue

contributors += 1

lowest_playcount = int(albums[-1]["playcount"])
highest_playcount = int(albums[0]["playcount"])
for album in albums:
Expand All @@ -2014,11 +2151,14 @@ async def server_chart(
album_map.items(), key=lambda x: x[1]["score"], reverse=True
)[: size.count]

for i, (name, data) in enumerate(top_albums):
if not top_albums:
return await ctx.send("Nobody on this server has listened to anything!")

for i, (name, album_data) in enumerate(top_albums):
chart_nodes.append(
(
LastFmImage.from_url(data["image"]),
f"<p class='label'>{name}</p><p class='playcount'>{data['score'] / len(data):.2f}%</p>",
LastFmImage.from_url(album_data["image"]),
f"<p class='label'>{name}</p><p class='playcount'>{album_data['score'] / contributors:.2f}%</p>",
)
)
if topster:
Expand Down
5 changes: 3 additions & 2 deletions cogs/roles.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,8 +279,9 @@ async def colorme(self, ctx: commands.Context, hex_color: str):

# clean up any roles that are left with 0 users
unused_roles = filter(
lambda r: r.id in [x[1] for x in existing_roles or []]
and len(r.members) == 0,
lambda r: (
r.id in [x[1] for x in existing_roles or []] and len(r.members) == 0
),
ctx.guild.roles,
)
for role in unused_roles:
Expand Down
4 changes: 2 additions & 2 deletions modules/lastfm.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,15 +92,15 @@ def web_format(self):
def display(self):
match self:
case Period.OVERALL:
return "Alltime"
return "All Time"
case Period.WEEK:
return "Weekly"
case Period.MONTH:
return "Monthly"
case Period.QUARTER:
return "Quarterly"
case Period.HALFYEAR:
return "6 month"
return "6 Months"
case Period.YEAR:
return "Yearly"

Expand Down
Loading