diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6c6f2af..a4d5105 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -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 + sudo apt-get install -y libasound2-dev - name: Install uv uses: astral-sh/setup-uv@v6 diff --git a/cogs/lastfm.py b/cogs/lastfm.py index d4d51e5..6834e03 100644 --- a/cogs/lastfm.py +++ b/cogs/lastfm.py @@ -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 @@ -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: @@ -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 ) @@ -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: @@ -1665,15 +1699,35 @@ 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( @@ -1681,17 +1735,32 @@ async def server_topartists( 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 ) @@ -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: @@ -1734,15 +1805,33 @@ 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( @@ -1750,17 +1839,32 @@ async def server_toptracks( 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 ) @@ -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: @@ -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, ) @@ -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 @@ -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: @@ -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"

{name}

{data['score'] / len(data):.2f}%

", + image, + f"

{name}

{artist_data['score'] / contributors:.2f}%

", ) ) if topster: @@ -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: @@ -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"

{name}

{data['score'] / len(data):.2f}%

", + LastFmImage.from_url(album_data["image"]), + f"

{name}

{album_data['score'] / contributors:.2f}%

", ) ) if topster: diff --git a/cogs/roles.py b/cogs/roles.py index cdea96b..9ae9751 100644 --- a/cogs/roles.py +++ b/cogs/roles.py @@ -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: diff --git a/modules/lastfm.py b/modules/lastfm.py index 2637c8d..0e3d358 100644 --- a/modules/lastfm.py +++ b/modules/lastfm.py @@ -92,7 +92,7 @@ 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: @@ -100,7 +100,7 @@ def display(self): case Period.QUARTER: return "Quarterly" case Period.HALFYEAR: - return "6 month" + return "6 Months" case Period.YEAR: return "Yearly"