Upload
This commit is contained in:
parent
21794dea70
commit
f235980e7f
31 changed files with 4014 additions and 0 deletions
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
9
.idea/Ava-Cogs.iml
generated
Normal file
9
.idea/Ava-Cogs.iml
generated
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
14
.idea/discord.xml
generated
Normal file
14
.idea/discord.xml
generated
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
<option name="applicationTheme" value="default" />
|
||||
<option name="iconsTheme" value="default" />
|
||||
<option name="button1Title" value="" />
|
||||
<option name="button1Url" value="" />
|
||||
<option name="button2Title" value="" />
|
||||
<option name="button2Url" value="" />
|
||||
<option name="customApplicationId" value="" />
|
||||
</component>
|
||||
</project>
|
6
.idea/misc.xml
generated
Normal file
6
.idea/misc.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/Ava-Cogs.iml" filepath="$PROJECT_DIR$/.idea/Ava-Cogs.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
9
lastfm/__init__.py
Normal file
9
lastfm/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
from .lastfm import LastFM
|
||||
|
||||
__red_end_user_data_statement__ = "This cog stores a user's last.fm username, their amount of VC scrobbles, and a last.fm session key to scrobble for them. It also stores the user's crowns. This is all data that can be cleared."
|
||||
|
||||
|
||||
async def setup(bot):
|
||||
cog = LastFM(bot)
|
||||
await bot.add_cog(cog)
|
||||
await cog.initialize()
|
22
lastfm/abc.py
Normal file
22
lastfm/abc.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from abc import ABC
|
||||
|
||||
from redbot.core import Config, commands
|
||||
from redbot.core.bot import Red
|
||||
|
||||
|
||||
class CompositeMetaClass(type(commands.Cog), type(ABC)):
|
||||
"""
|
||||
This allows the metaclass used for proper type detection to coexist with discord.py's
|
||||
metaclass.
|
||||
"""
|
||||
|
||||
|
||||
class MixinMeta(ABC):
|
||||
"""
|
||||
Base class for well behaved type hint detection with composite class.
|
||||
Basically, to keep developers sane when not all attributes are defined in each mixin.
|
||||
"""
|
||||
|
||||
def __init__(self, *_args):
|
||||
self.config: Config
|
||||
self.bot: Red
|
421
lastfm/charts.py
Normal file
421
lastfm/charts.py
Normal file
|
@ -0,0 +1,421 @@
|
|||
import asyncio
|
||||
from io import BytesIO
|
||||
|
||||
import discord
|
||||
from PIL import Image, ImageDraw, ImageFile, ImageFont
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils import AsyncIter
|
||||
from redbot.core.utils.chat_formatting import escape
|
||||
|
||||
from .abc import MixinMeta
|
||||
from .exceptions import *
|
||||
from .fmmixin import FMMixin
|
||||
|
||||
NO_IMAGE_PLACEHOLDER = (
|
||||
"https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png"
|
||||
)
|
||||
ImageFile.LOAD_TRUNCATED_IMAGES = True
|
||||
|
||||
command_fm = FMMixin.command_fm
|
||||
command_fm_server = FMMixin.command_fm_server
|
||||
|
||||
|
||||
class ChartMixin(MixinMeta):
|
||||
"""Chart Commands"""
|
||||
|
||||
async def get_img(self, url):
|
||||
async with self.session.get(url or NO_IMAGE_PLACEHOLDER) as resp:
|
||||
if resp.status == 200:
|
||||
img = await resp.read()
|
||||
return img
|
||||
async with self.session.get(NO_IMAGE_PLACEHOLDER) as resp:
|
||||
img = await resp.read()
|
||||
return img
|
||||
|
||||
@command_fm.command(
|
||||
name="chart", usage="[album | artist | recent | track] [timeframe] [width]x[height]"
|
||||
)
|
||||
@commands.max_concurrency(1, commands.BucketType.user)
|
||||
async def command_chart(self, ctx, *args):
|
||||
"""
|
||||
Visual chart of your top albums, tracks or artists.
|
||||
|
||||
Defaults to top albums, weekly, 3x3.
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in(conf)
|
||||
arguments = self.parse_chart_arguments(args)
|
||||
if arguments["width"] + arguments["height"] > 31: # TODO: Figure out a reasonable value.
|
||||
return await ctx.send(
|
||||
"Size is too big! Chart `width` + `height` total must not exceed `31`"
|
||||
)
|
||||
msg = await ctx.send("Gathering images and data, this may take some time.")
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"user": conf["lastfm_username"],
|
||||
"method": arguments["method"],
|
||||
"period": arguments["period"],
|
||||
"limit": arguments["amount"],
|
||||
},
|
||||
)
|
||||
chart = []
|
||||
chart_type = "ERROR"
|
||||
async with ctx.typing():
|
||||
if arguments["method"] == "user.gettopalbums":
|
||||
chart_type = "top album"
|
||||
albums = data["topalbums"]["album"]
|
||||
async for album in AsyncIter(albums[: arguments["width"] * arguments["height"]]):
|
||||
name = album["name"]
|
||||
artist = album["artist"]["name"]
|
||||
plays = album["playcount"]
|
||||
if album["image"][3]["#text"] in self.chart_data:
|
||||
chart_img = self.chart_data[album["image"][3]["#text"]]
|
||||
else:
|
||||
chart_img = await self.get_img(album["image"][3]["#text"])
|
||||
self.chart_data[album["image"][3]["#text"]] = chart_img
|
||||
chart.append(
|
||||
(
|
||||
f"{plays} {self.format_plays(plays)}\n{name} - {artist}",
|
||||
chart_img,
|
||||
)
|
||||
)
|
||||
img = await self.bot.loop.run_in_executor(
|
||||
None,
|
||||
charts,
|
||||
chart,
|
||||
arguments["width"],
|
||||
arguments["height"],
|
||||
self.data_loc,
|
||||
)
|
||||
|
||||
elif arguments["method"] == "user.gettopartists":
|
||||
chart_type = "top artist"
|
||||
artists = data["topartists"]["artist"]
|
||||
if self.login_token:
|
||||
scraped_images = await self.scrape_artists_for_chart(
|
||||
ctx, conf["lastfm_username"], arguments["period"], arguments["amount"]
|
||||
)
|
||||
else:
|
||||
scraped_images = [NO_IMAGE_PLACEHOLDER] * arguments["amount"]
|
||||
iterator = AsyncIter(artists[: arguments["width"] * arguments["height"]])
|
||||
async for i, artist in iterator.enumerate():
|
||||
name = artist["name"]
|
||||
plays = artist["playcount"]
|
||||
if i < len(scraped_images) and scraped_images[i] in self.chart_data:
|
||||
chart_img = self.chart_data[scraped_images[i]]
|
||||
else:
|
||||
chart_img = await self.get_img(scraped_images[i])
|
||||
self.chart_data[scraped_images[i]] = chart_img
|
||||
chart.append(
|
||||
(
|
||||
f"{plays} {self.format_plays(plays)}\n{name}",
|
||||
chart_img,
|
||||
)
|
||||
)
|
||||
img = await self.bot.loop.run_in_executor(
|
||||
None,
|
||||
charts,
|
||||
chart,
|
||||
arguments["width"],
|
||||
arguments["height"],
|
||||
self.data_loc,
|
||||
)
|
||||
|
||||
elif arguments["method"] == "user.getrecenttracks":
|
||||
chart_type = "recent tracks"
|
||||
tracks = data["recenttracks"]["track"]
|
||||
if isinstance(tracks, dict):
|
||||
tracks = [tracks]
|
||||
async for track in AsyncIter(tracks[: arguments["width"] * arguments["height"]]):
|
||||
name = track["name"]
|
||||
artist = track["artist"]["#text"]
|
||||
if track["image"][3]["#text"] in self.chart_data:
|
||||
chart_img = self.chart_data[track["image"][3]["#text"]]
|
||||
else:
|
||||
chart_img = await self.get_img(track["image"][3]["#text"])
|
||||
self.chart_data[track["image"][3]["#text"]] = chart_img
|
||||
chart.append(
|
||||
(
|
||||
f"{name} - {artist}",
|
||||
chart_img,
|
||||
)
|
||||
)
|
||||
img = await self.bot.loop.run_in_executor(
|
||||
None,
|
||||
track_chart,
|
||||
chart,
|
||||
arguments["width"],
|
||||
arguments["height"],
|
||||
self.data_loc,
|
||||
)
|
||||
elif arguments["method"] == "user.gettoptracks":
|
||||
chart_type = "top tracks"
|
||||
tracks = data["toptracks"]["track"]
|
||||
scraped_images = await self.scrape_artists_for_chart(
|
||||
ctx, conf["lastfm_username"], arguments["period"], arguments["amount"]
|
||||
)
|
||||
if isinstance(tracks, dict):
|
||||
tracks = [tracks]
|
||||
async for track in AsyncIter(tracks[: arguments["width"] * arguments["height"]]):
|
||||
name = track["name"]
|
||||
artist = track["artist"]["name"]
|
||||
plays = track["playcount"]
|
||||
if name in self.chart_data:
|
||||
chart_img = self.chart_data[name]
|
||||
else:
|
||||
chart_img = await self.get_img(await self.scrape_artist_image(artist, ctx))
|
||||
self.chart_data[name] = chart_img
|
||||
chart.append(
|
||||
(
|
||||
f"{plays} {self.format_plays(plays)}\n{name} - {artist}",
|
||||
chart_img,
|
||||
)
|
||||
)
|
||||
img = await self.bot.loop.run_in_executor(
|
||||
None,
|
||||
charts,
|
||||
chart,
|
||||
arguments["width"],
|
||||
arguments["height"],
|
||||
self.data_loc,
|
||||
)
|
||||
await msg.delete()
|
||||
u = conf["lastfm_username"]
|
||||
try:
|
||||
await ctx.send(
|
||||
f"`{u} - {self.humanized_period(arguments['period'])} - {arguments['width']}x{arguments['height']} {chart_type} chart`",
|
||||
file=img,
|
||||
)
|
||||
except discord.HTTPException:
|
||||
await ctx.send("File is to big to send, try lowering the size.")
|
||||
|
||||
@command_fm_server.command(
|
||||
name="chart", usage="[album | artist | tracks] [timeframe] [width]x[height]"
|
||||
)
|
||||
@commands.max_concurrency(1, commands.BucketType.user)
|
||||
async def server_chart(self, ctx, *args):
|
||||
"""Visual chart of the servers albums, artists or tracks."""
|
||||
arguments = self.parse_chart_arguments(args)
|
||||
if arguments["width"] + arguments["height"] > 31: # TODO: Figure out a reasonable value.
|
||||
return await ctx.send(
|
||||
"Size is too big! Chart `width` + `height` total must not exceed `31`"
|
||||
)
|
||||
if arguments["method"] not in [
|
||||
"user.gettopalbums",
|
||||
"user.gettopartists",
|
||||
"user.gettoptracks",
|
||||
]:
|
||||
return await ctx.send("Only albums, artists and tracks are supported.")
|
||||
chart_total = arguments["width"] * arguments["height"]
|
||||
msg = await ctx.send("Gathering images and data, this may take some time.")
|
||||
tasks = []
|
||||
userlist = await self.config.all_users()
|
||||
guildusers = [x.id for x in ctx.guild.members]
|
||||
userslist = [user for user in userlist if user in guildusers]
|
||||
datatype = {
|
||||
"user.gettopalbums": "album",
|
||||
"user.gettopartists": "artist",
|
||||
"user.gettoptracks": "track",
|
||||
}
|
||||
for user in userslist:
|
||||
lastfm_username = userlist[user]["lastfm_username"]
|
||||
if lastfm_username is None:
|
||||
continue
|
||||
member = ctx.guild.get_member(user)
|
||||
if member is None:
|
||||
continue
|
||||
|
||||
tasks.append(
|
||||
self.get_server_top(
|
||||
ctx,
|
||||
lastfm_username,
|
||||
datatype.get(arguments["method"]),
|
||||
arguments["period"],
|
||||
arguments["amount"],
|
||||
)
|
||||
)
|
||||
chart = []
|
||||
chart_type = "ERROR"
|
||||
if not tasks:
|
||||
return await ctx.send("No users have set their last.fm username yet.")
|
||||
content_map = {}
|
||||
async with ctx.typing():
|
||||
data = await asyncio.gather(*tasks)
|
||||
if arguments["method"] == "user.gettopalbums":
|
||||
chart_type = "top album"
|
||||
for user_data in data:
|
||||
if user_data is None:
|
||||
continue
|
||||
for album in user_data:
|
||||
album_name = album["name"]
|
||||
artist = album["artist"]["name"]
|
||||
name = f"{album_name} — {artist}"
|
||||
plays = int(album["playcount"])
|
||||
if name in content_map:
|
||||
content_map[name]["plays"] += plays
|
||||
else:
|
||||
content_map[name] = {
|
||||
"plays": plays,
|
||||
"link": album["image"][3]["#text"],
|
||||
}
|
||||
elif arguments["method"] == "user.gettopartists":
|
||||
chart_type = "top artist"
|
||||
for user_data in data:
|
||||
if user_data is None:
|
||||
continue
|
||||
for artist in user_data:
|
||||
name = artist["name"]
|
||||
plays = int(artist["playcount"])
|
||||
if name in content_map:
|
||||
content_map[name]["plays"] += plays
|
||||
else:
|
||||
content_map[name] = {"plays": plays}
|
||||
elif arguments["method"] == "user.gettoptracks":
|
||||
chart_type = "top tracks"
|
||||
for user in data:
|
||||
if user is None:
|
||||
continue
|
||||
for user_data in user:
|
||||
name = f'{escape(user_data["artist"]["name"])} — *{escape(user_data["name"])}*'
|
||||
plays = int(user_data["playcount"])
|
||||
if name in content_map:
|
||||
content_map[name]["plays"] += plays
|
||||
else:
|
||||
content_map[name] = {
|
||||
"plays": plays,
|
||||
"link": user_data["artist"]["name"],
|
||||
}
|
||||
cached_images = {}
|
||||
for i, (name, content_data) in enumerate(
|
||||
sorted(content_map.items(), key=lambda x: x[1]["plays"], reverse=True), start=1
|
||||
):
|
||||
if arguments["method"] == "user.gettopartists":
|
||||
image = await self.get_img(await self.scrape_artist_image(name, ctx))
|
||||
elif arguments["method"] == "user.gettoptracks":
|
||||
if content_data["link"] in cached_images:
|
||||
image = cached_images[content_data["link"]]
|
||||
else:
|
||||
image = await self.get_img(
|
||||
await self.scrape_artist_image(content_data["link"], ctx)
|
||||
)
|
||||
cached_images[content_data["link"]] = image
|
||||
else:
|
||||
image = await self.get_img(content_data["link"])
|
||||
chart.append(
|
||||
(
|
||||
f"{content_data['plays']} {self.format_plays(content_data['plays'])}\n{name}",
|
||||
image,
|
||||
)
|
||||
)
|
||||
if i >= chart_total:
|
||||
break
|
||||
img = await self.bot.loop.run_in_executor(
|
||||
None,
|
||||
charts,
|
||||
chart,
|
||||
arguments["width"],
|
||||
arguments["height"],
|
||||
self.data_loc,
|
||||
)
|
||||
await msg.delete()
|
||||
try:
|
||||
await ctx.send(
|
||||
f"`{ctx.guild} - {self.humanized_period(arguments['period'])} - {arguments['width']}x{arguments['height']} {chart_type} chart`",
|
||||
file=img,
|
||||
)
|
||||
except discord.HTTPException:
|
||||
await ctx.send("File is to big to send, try lowering the size.")
|
||||
|
||||
|
||||
def charts(data, w, h, loc):
|
||||
fnt_file = f"{loc}/fonts/Arial Unicode.ttf"
|
||||
fnt = ImageFont.truetype(fnt_file, 18, encoding="utf-8")
|
||||
imgs = []
|
||||
for item in data:
|
||||
img = BytesIO(item[1])
|
||||
image = Image.open(img).convert("RGBA")
|
||||
draw = ImageDraw.Draw(image)
|
||||
texts = item[0].split("\n")
|
||||
if len(texts[1]) > 30:
|
||||
height = 223
|
||||
text = f"{texts[0]}\n{texts[1][:30]}\n{texts[1][30:]}"
|
||||
else:
|
||||
height = 247
|
||||
text = item[0]
|
||||
draw.text(
|
||||
(5, height),
|
||||
text,
|
||||
fill=(255, 255, 255, 255),
|
||||
font=fnt,
|
||||
stroke_width=1,
|
||||
stroke_fill=(0, 0, 0),
|
||||
)
|
||||
_file = BytesIO()
|
||||
image.save(_file, "png")
|
||||
_file.name = f"{item[0]}.png"
|
||||
_file.seek(0)
|
||||
imgs.append(_file)
|
||||
return create_graph(imgs, w, h)
|
||||
|
||||
|
||||
def track_chart(data, w, h, loc):
|
||||
fnt_file = f"{loc}/fonts/Arial Unicode.ttf"
|
||||
fnt = ImageFont.truetype(fnt_file, 18, encoding="utf-8")
|
||||
imgs = []
|
||||
for item in data:
|
||||
img = BytesIO(item[1])
|
||||
image = Image.open(img).convert("RGBA")
|
||||
draw = ImageDraw.Draw(image)
|
||||
if len(item[0]) > 30:
|
||||
height = 243
|
||||
text = f"{item[0][:30]}\n{item[0][30:]}"
|
||||
else:
|
||||
height = 267
|
||||
text = item[0]
|
||||
draw.text(
|
||||
(5, height),
|
||||
text,
|
||||
fill=(255, 255, 255, 255),
|
||||
font=fnt,
|
||||
stroke_width=1,
|
||||
stroke_fill=(0, 0, 0),
|
||||
)
|
||||
_file = BytesIO()
|
||||
image.save(_file, "png")
|
||||
_file.name = f"{item[0]}.png"
|
||||
_file.seek(0)
|
||||
imgs.append(_file)
|
||||
return create_graph(imgs, w, h)
|
||||
|
||||
|
||||
def chunks(l, n):
|
||||
"""Yield successive n-sized chunks from l."""
|
||||
for i in range(0, len(l), n):
|
||||
yield l[i : i + n]
|
||||
|
||||
|
||||
def create_graph(data, w, h):
|
||||
dimensions = (300 * w, 300 * h)
|
||||
final = Image.new("RGBA", dimensions)
|
||||
images = chunks(data, w)
|
||||
y = 0
|
||||
for chunked in images:
|
||||
x = 0
|
||||
for img in chunked:
|
||||
new = Image.open(img)
|
||||
w, h = new.size
|
||||
final.paste(new, (x, y, x + w, y + h))
|
||||
x += 300
|
||||
y += 300
|
||||
w, h = final.size
|
||||
if w > 2100 and h > 2100:
|
||||
final = final.resize(
|
||||
(2100, 2100), resample=Image.ANTIALIAS
|
||||
) # Resize cause a 6x6k image is blocking when being sent
|
||||
file = BytesIO()
|
||||
final.save(file, "webp")
|
||||
file.name = f"chart.webp"
|
||||
file.seek(0)
|
||||
image = discord.File(file)
|
||||
return image
|
295
lastfm/compare.py
Normal file
295
lastfm/compare.py
Normal file
|
@ -0,0 +1,295 @@
|
|||
import contextlib
|
||||
from io import BytesIO
|
||||
|
||||
import discord
|
||||
import tabulate
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
from redbot.core.utils import AsyncIter
|
||||
from redbot.core.utils.chat_formatting import humanize_number
|
||||
|
||||
from .abc import MixinMeta
|
||||
from .exceptions import *
|
||||
from .fmmixin import FMMixin
|
||||
|
||||
command_fm = FMMixin.command_fm
|
||||
command_fm_server = FMMixin.command_fm_server
|
||||
|
||||
|
||||
class CompareMixin(MixinMeta):
|
||||
"""Commands for comparing two users"""
|
||||
|
||||
def make_table_into_image(self, text):
|
||||
|
||||
color = (150, 123, 182)
|
||||
|
||||
lines = 0
|
||||
keep_going = True
|
||||
width = 0
|
||||
for line in text:
|
||||
if keep_going:
|
||||
width += 6.6
|
||||
if line == "\n":
|
||||
lines += 16.5
|
||||
keep_going = False
|
||||
|
||||
img = Image.new("RGBA", (int(width), int(lines)), color=(255, 0, 0, 0))
|
||||
|
||||
d = ImageDraw.Draw(img)
|
||||
fnt_file = f"{self.data_loc}/fonts/NotoSansMono-Regular.ttf"
|
||||
font = ImageFont.truetype(fnt_file, 11, encoding="utf-8")
|
||||
d.text((0, 0), text, fill=color, font=font)
|
||||
|
||||
final = BytesIO()
|
||||
img.save(final, "webp")
|
||||
final.seek(0)
|
||||
return discord.File(final, "result.webp")
|
||||
|
||||
@command_fm.group(name="compare")
|
||||
async def command_compare(self, ctx):
|
||||
"""Compare two users music tastes"""
|
||||
|
||||
@command_compare.command(name="artists", aliases=["artist"])
|
||||
async def compare_artists(self, ctx, user: discord.Member, period: str = "1month"):
|
||||
"""
|
||||
Compare your top artists with someone else.
|
||||
|
||||
`[period]` can be one of: overall, 7day, 1month, 3month, 6month, 12month
|
||||
The default is 1 month.
|
||||
"""
|
||||
if user == ctx.author:
|
||||
await ctx.send("You need to compare with someone else.")
|
||||
return
|
||||
|
||||
period, displayperiod = self.get_period(period)
|
||||
if not period:
|
||||
await ctx.send(
|
||||
"Invalid period. Valid periods are: overall, 7day, 1month, 3month, 6month, 12month"
|
||||
)
|
||||
return
|
||||
|
||||
author_conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in(author_conf)
|
||||
user_conf = await self.config.user(user).all()
|
||||
self.check_if_logged_in(user_conf, False)
|
||||
async with ctx.typing():
|
||||
author_data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"user": author_conf["lastfm_username"],
|
||||
"method": "user.gettopartists",
|
||||
"period": period,
|
||||
"limit": "10",
|
||||
},
|
||||
)
|
||||
author_artists = author_data["topartists"]["artist"]
|
||||
if not author_artists:
|
||||
if period == "overall":
|
||||
await ctx.send("You haven't listened to any artists yet.")
|
||||
else:
|
||||
await ctx.send(
|
||||
"You haven't listened to any artists in the last {}s.".format(period)
|
||||
)
|
||||
return
|
||||
|
||||
g = await ctx.send("Gathering data... This might take a while.")
|
||||
|
||||
author_plays = []
|
||||
artist_names = []
|
||||
for artist in author_artists:
|
||||
if artist["playcount"] == 1:
|
||||
author_plays.append(f"{artist['playcount']} Play")
|
||||
else:
|
||||
author_plays.append(f"{humanize_number(artist['playcount'])} Plays")
|
||||
artist_names.append(artist["name"])
|
||||
|
||||
user_plays = []
|
||||
async for artist in AsyncIter(author_artists):
|
||||
plays = await self.get_playcount(
|
||||
ctx, user_conf["lastfm_username"], artist["name"], period
|
||||
)
|
||||
if plays == 1:
|
||||
user_plays.append(f"{plays} Play")
|
||||
else:
|
||||
user_plays.append(f"{humanize_number(plays)} Plays")
|
||||
|
||||
data = {"Artist": artist_names, ctx.author: author_plays, user: user_plays}
|
||||
table = tabulate.tabulate(data, headers="keys", tablefmt="fancy_grid")
|
||||
color = await ctx.embed_colour()
|
||||
img = await self.bot.loop.run_in_executor(None, self.make_table_into_image, table)
|
||||
embed = discord.Embed(color=color, title=f"{ctx.author} vs {user} ({displayperiod})")
|
||||
embed.set_image(url="attachment://result.webp")
|
||||
|
||||
with contextlib.suppress(discord.NotFound):
|
||||
await g.delete()
|
||||
|
||||
await ctx.send(file=img, embed=embed)
|
||||
|
||||
@command_compare.command(name="tracks", aliases=["track"])
|
||||
async def compare_tracks(self, ctx, user: discord.Member, period: str = "1month"):
|
||||
"""
|
||||
Compare your top tracks with someone else.
|
||||
|
||||
`[period]` can be one of: overall, 7day, 1month, 3month, 6month, 12month
|
||||
The default is 1 month.
|
||||
"""
|
||||
if user == ctx.author:
|
||||
await ctx.send("You need to compare with someone else.")
|
||||
return
|
||||
|
||||
period, displayperiod = self.get_period(period)
|
||||
if not period:
|
||||
await ctx.send(
|
||||
"Invalid period. Valid periods are: overall, 7day, 1month, 3month, 6month, 12month"
|
||||
)
|
||||
return
|
||||
|
||||
author_conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in(author_conf)
|
||||
user_conf = await self.config.user(user).all()
|
||||
self.check_if_logged_in(user_conf, False)
|
||||
async with ctx.typing():
|
||||
author_data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"user": author_conf["lastfm_username"],
|
||||
"method": "user.gettoptracks",
|
||||
"period": period,
|
||||
"limit": "10",
|
||||
},
|
||||
)
|
||||
author_tracks = author_data["toptracks"]["track"]
|
||||
if not author_tracks:
|
||||
if period == "overall":
|
||||
await ctx.send("You haven't listened to any tracks yet.")
|
||||
else:
|
||||
await ctx.send("You haven't listened to any tracks in that time period.")
|
||||
return
|
||||
|
||||
g = await ctx.send("Gathering data... This might take a while.")
|
||||
|
||||
author_plays = []
|
||||
artist_names = []
|
||||
track_names = []
|
||||
for track in author_tracks:
|
||||
if track["playcount"] == 1:
|
||||
author_plays.append(f"{track['playcount']} Play")
|
||||
else:
|
||||
author_plays.append(f"{humanize_number(track['playcount'])} Plays")
|
||||
artist_names.append(track["artist"]["name"])
|
||||
track_names.append(track["name"])
|
||||
|
||||
user_plays = []
|
||||
async for track in AsyncIter(author_tracks):
|
||||
plays = await self.get_playcount_track(
|
||||
ctx,
|
||||
user_conf["lastfm_username"],
|
||||
track["artist"]["name"],
|
||||
track["name"],
|
||||
period,
|
||||
)
|
||||
if plays == 1:
|
||||
user_plays.append(f"{plays} Play")
|
||||
else:
|
||||
user_plays.append(f"{humanize_number(plays)} Plays")
|
||||
|
||||
data = {
|
||||
"Artist": artist_names,
|
||||
"Track": track_names,
|
||||
ctx.author: author_plays,
|
||||
user: user_plays,
|
||||
}
|
||||
table = tabulate.tabulate(data, headers="keys", tablefmt="fancy_grid")
|
||||
color = await ctx.embed_colour()
|
||||
img = await self.bot.loop.run_in_executor(None, self.make_table_into_image, table)
|
||||
embed = discord.Embed(color=color, title=f"{ctx.author} vs {user} ({displayperiod})")
|
||||
embed.set_image(url="attachment://result.webp")
|
||||
|
||||
with contextlib.suppress(discord.NotFound):
|
||||
await g.delete()
|
||||
|
||||
await ctx.send(file=img, embed=embed)
|
||||
|
||||
@command_compare.command(name="albums", aliases=["album"])
|
||||
async def compare_albums(self, ctx, user: discord.Member, period: str = "1month"):
|
||||
"""
|
||||
Compare your top albums with someone else.
|
||||
|
||||
`[period]` can be one of: overall, 7day, 1month, 3month, 6month, 12month
|
||||
The default is 1 month.
|
||||
"""
|
||||
if user == ctx.author:
|
||||
await ctx.send("You need to compare with someone else.")
|
||||
return
|
||||
|
||||
period, displayperiod = self.get_period(period)
|
||||
if not period:
|
||||
await ctx.send(
|
||||
"Invalid period. Valid periods are: overall, 7day, 1month, 3month, 6month, 12month"
|
||||
)
|
||||
return
|
||||
|
||||
author_conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in(author_conf)
|
||||
user_conf = await self.config.user(user).all()
|
||||
self.check_if_logged_in(user_conf, False)
|
||||
async with ctx.typing():
|
||||
author_data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"user": author_conf["lastfm_username"],
|
||||
"method": "user.gettopalbums",
|
||||
"period": period,
|
||||
"limit": "10",
|
||||
},
|
||||
)
|
||||
author_albums = author_data["topalbums"]["album"]
|
||||
if not author_albums:
|
||||
if period == "overall":
|
||||
await ctx.send("You haven't listened to any albums yet.")
|
||||
else:
|
||||
await ctx.send("You haven't listened to any albums in that time period.")
|
||||
return
|
||||
|
||||
g = await ctx.send("Gathering data... This might take a while.")
|
||||
|
||||
author_plays = []
|
||||
artist_names = []
|
||||
album_names = []
|
||||
for album in author_albums:
|
||||
if album["playcount"] == 1:
|
||||
author_plays.append(f"{album['playcount']} Play")
|
||||
else:
|
||||
author_plays.append(f"{humanize_number(album['playcount'])} Plays")
|
||||
artist_names.append(album["artist"]["name"])
|
||||
album_names.append(album["name"])
|
||||
|
||||
user_plays = []
|
||||
async for album in AsyncIter(author_albums):
|
||||
plays = await self.get_playcount_album(
|
||||
ctx,
|
||||
user_conf["lastfm_username"],
|
||||
album["artist"]["name"],
|
||||
album["name"],
|
||||
period,
|
||||
)
|
||||
if plays == 1:
|
||||
user_plays.append(f"{plays} Play")
|
||||
else:
|
||||
user_plays.append(f"{humanize_number(plays)} Plays")
|
||||
|
||||
data = {
|
||||
"Artist": artist_names,
|
||||
"Album": album_names,
|
||||
ctx.author: author_plays,
|
||||
user: user_plays,
|
||||
}
|
||||
table = tabulate.tabulate(data, headers="keys", tablefmt="fancy_grid")
|
||||
color = await ctx.embed_colour()
|
||||
img = await self.bot.loop.run_in_executor(None, self.make_table_into_image, table)
|
||||
embed = discord.Embed(color=color, title=f"{ctx.author} vs {user} ({displayperiod})")
|
||||
embed.set_image(url="attachment://result.webp")
|
||||
|
||||
with contextlib.suppress(discord.NotFound):
|
||||
await g.delete()
|
||||
|
||||
await ctx.send(file=img, embed=embed)
|
BIN
lastfm/data/fonts/Arial Unicode.ttf
Normal file
BIN
lastfm/data/fonts/Arial Unicode.ttf
Normal file
Binary file not shown.
BIN
lastfm/data/fonts/NotoSansMono-Regular.ttf
Normal file
BIN
lastfm/data/fonts/NotoSansMono-Regular.ttf
Normal file
Binary file not shown.
22
lastfm/exceptions.py
Normal file
22
lastfm/exceptions.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
class LastFMError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotLoggedInError(LastFMError):
|
||||
pass
|
||||
|
||||
|
||||
class NeedToReauthorizeError(LastFMError):
|
||||
pass
|
||||
|
||||
|
||||
class SilentDeAuthorizedError(LastFMError):
|
||||
pass
|
||||
|
||||
|
||||
class NoScrobblesError(LastFMError):
|
||||
pass
|
||||
|
||||
|
||||
class NotScrobblingError(LastFMError):
|
||||
pass
|
24
lastfm/fmmixin.py
Normal file
24
lastfm/fmmixin.py
Normal file
|
@ -0,0 +1,24 @@
|
|||
from redbot.core import commands
|
||||
|
||||
from .utils.tokencheck import tokencheck
|
||||
|
||||
|
||||
|
||||
|
||||
class FMMixin:
|
||||
"""This is mostly here to easily mess with things..."""
|
||||
|
||||
|
||||
@commands.check(tokencheck)
|
||||
@commands.group(name="fm")
|
||||
async def command_fm(self, ctx: commands.Context):
|
||||
"""
|
||||
LastFM commands
|
||||
"""
|
||||
|
||||
|
||||
@command_fm.group(name="server")
|
||||
async def command_fm_server(self, ctx: commands.Context):
|
||||
"""
|
||||
LastFM Server Commands
|
||||
"""
|
27
lastfm/info.json
Normal file
27
lastfm/info.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"author": [
|
||||
"flare(flare#0001)",
|
||||
"joinem",
|
||||
"fixator10",
|
||||
"ryan"
|
||||
],
|
||||
"install_msg": "Thanks for installing!\n\nOnce you've loaded the cog run the `[p]lastfmset` command for instructions on how to set your API key!",
|
||||
"name": "LastFm",
|
||||
"disabled": false,
|
||||
"short": "Port of Miso Bot's LastFM Cog",
|
||||
"description": "Port of Miso Bot's LastFM Cog,",
|
||||
"tags": [
|
||||
"LastFM",
|
||||
"scrobbler",
|
||||
"music"
|
||||
],
|
||||
"hidden": false,
|
||||
"requirements": [
|
||||
"humanize",
|
||||
"bs4",
|
||||
"tabulate",
|
||||
"wordcloud",
|
||||
"arrow"
|
||||
],
|
||||
"min_bot_version": "3.5.0"
|
||||
}
|
387
lastfm/lastfm.py
Normal file
387
lastfm/lastfm.py
Normal file
|
@ -0,0 +1,387 @@
|
|||
import asyncio
|
||||
import urllib.parse
|
||||
from operator import itemgetter
|
||||
|
||||
import aiohttp
|
||||
import discord
|
||||
from redbot.core import Config, commands
|
||||
from redbot.core.data_manager import bundled_data_path
|
||||
from redbot.core.utils.chat_formatting import escape, pagify
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
|
||||
|
||||
from .abc import *
|
||||
from .charts import ChartMixin
|
||||
from .compare import CompareMixin
|
||||
from .exceptions import *
|
||||
from .fmmixin import FMMixin
|
||||
from .love import LoveMixin
|
||||
from .nowplaying import NowPlayingMixin
|
||||
from .profile import ProfileMixin
|
||||
from .recent import RecentMixin
|
||||
from .scrobbler import ScrobblerMixin
|
||||
from .tags import TagsMixin
|
||||
from .top import TopMixin
|
||||
from .utils.base import UtilsMixin
|
||||
from .utils.tokencheck import *
|
||||
from .whoknows import WhoKnowsMixin
|
||||
from .wordcloud import WordCloudMixin
|
||||
|
||||
command_fm = FMMixin.command_fm
|
||||
|
||||
|
||||
class LastFM(
|
||||
ChartMixin,
|
||||
CompareMixin,
|
||||
FMMixin,
|
||||
LoveMixin,
|
||||
NowPlayingMixin,
|
||||
ProfileMixin,
|
||||
RecentMixin,
|
||||
ScrobblerMixin,
|
||||
TagsMixin,
|
||||
TopMixin,
|
||||
UtilsMixin,
|
||||
WhoKnowsMixin,
|
||||
WordCloudMixin,
|
||||
commands.Cog,
|
||||
metaclass=CompositeMetaClass,
|
||||
):
|
||||
"""
|
||||
Interacts with the last.fm API.
|
||||
"""
|
||||
|
||||
__version__ = "1.7.2"
|
||||
|
||||
# noinspection PyMissingConstructor
|
||||
def __init__(self, bot, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.bot = bot
|
||||
self.config = Config.get_conf(self, identifier=95932766180343808, force_registration=True)
|
||||
defaults = {"lastfm_username": None, "session_key": None, "scrobbles": 0, "scrobble": True}
|
||||
self.config.register_global(version=1)
|
||||
self.config.register_user(**defaults)
|
||||
self.config.register_guild(crowns={})
|
||||
self.session = aiohttp.ClientSession(
|
||||
headers={
|
||||
"User-Agent": "Mozilla/5.0 (X11; Arch Linux; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0"
|
||||
},
|
||||
)
|
||||
self.token = None
|
||||
self.wc = None
|
||||
self.login_token = None
|
||||
self.wordcloud_create()
|
||||
self.data_loc = bundled_data_path(self)
|
||||
self.chart_data = {}
|
||||
self.chart_data_loop = self.bot.loop.create_task(self.chart_clear_loop())
|
||||
|
||||
def format_help_for_context(self, ctx):
|
||||
pre_processed = super().format_help_for_context(ctx)
|
||||
return f"{pre_processed}\n\nCog Version: {self.__version__}"
|
||||
|
||||
async def red_delete_data_for_user(self, *, requester, user_id):
|
||||
await self.config.user_from_id(user_id).clear()
|
||||
|
||||
async def chart_clear_loop(self):
|
||||
await self.bot.wait_until_ready()
|
||||
while True:
|
||||
self.chart_data = {}
|
||||
await asyncio.sleep(1800)
|
||||
|
||||
async def initialize(self):
|
||||
token = await self.bot.get_shared_api_tokens("lastfm")
|
||||
self.token = token.get("appid")
|
||||
self.secret = token.get("secret")
|
||||
self.login_token = token.get("logintoken")
|
||||
await self.migrate_config()
|
||||
|
||||
async def migrate_config(self):
|
||||
if await self.config.version() == 1:
|
||||
a = {}
|
||||
conf = await self.config.all_guilds()
|
||||
for guild in conf:
|
||||
a[guild] = {"crowns": {}}
|
||||
for artist in conf[guild]["crowns"]:
|
||||
a[guild]["crowns"][artist.lower()] = conf[guild]["crowns"][artist]
|
||||
group = self.config._get_base_group(self.config.GUILD)
|
||||
async with group.all() as new_data:
|
||||
for guild in a:
|
||||
new_data[guild] = a[guild]
|
||||
await self.config.version.set(2)
|
||||
|
||||
@commands.Cog.listener(name="on_red_api_tokens_update")
|
||||
async def listener_update_class_tokens(self, service_name, api_tokens):
|
||||
if service_name == "lastfm":
|
||||
self.token = api_tokens.get("appid")
|
||||
self.secret = api_tokens.get("secret")
|
||||
self.login_token = api_tokens.get("logintoken")
|
||||
|
||||
def cog_unload(self):
|
||||
self.bot.loop.create_task(self.session.close())
|
||||
if self.chart_data_loop:
|
||||
self.chart_data_loop.cancel()
|
||||
|
||||
@commands.is_owner()
|
||||
@commands.command(name="lastfmset", aliases=["fmset"])
|
||||
async def command_lastfmset(self, ctx):
|
||||
"""Instructions on how to set the api key."""
|
||||
message = (
|
||||
"1. Visit the [LastFM](https://www.last.fm/api/) website and click on 'Get an API Account'.\n"
|
||||
"2. Fill in the application. Once completed do not exit the page. - "
|
||||
"Copy all information on the page and save it.\n"
|
||||
f"3. Enter the api key via `{ctx.clean_prefix}set api lastfm appid <appid_here>`\n"
|
||||
f"4. Enter the api secret via `{ctx.clean_prefix}set api lastfm secret <secret_here>`\n"
|
||||
f"--------\n"
|
||||
f"Some commands that use webscraping may require a login token.\n"
|
||||
f"1. Visit [LastFM](https://www.last.fm) site and login.\n"
|
||||
f"2. Open your browser's developer tools and go to the Storage tab.\n"
|
||||
f"3. Find the cookie named `sessionid` and copy the value.\n"
|
||||
f"4. Enter the api secret via `{ctx.clean_prefix}set api lastfm logintoken <token_here>`\n"
|
||||
)
|
||||
await ctx.maybe_send_embed(message)
|
||||
|
||||
@commands.command(name="crowns")
|
||||
@commands.check(tokencheck)
|
||||
@commands.guild_only()
|
||||
async def command_crowns(self, ctx, user: discord.Member = None):
|
||||
"""Check yourself or another users crowns."""
|
||||
user = user or ctx.author
|
||||
crowns = await self.config.guild(ctx.guild).crowns()
|
||||
crownartists = []
|
||||
for key in crowns:
|
||||
if crowns[key]["user"] == user.id:
|
||||
crownartists.append((key, crowns[key]["playcount"]))
|
||||
if crownartists is None:
|
||||
return await ctx.send(
|
||||
"You haven't acquired any crowns yet! "
|
||||
f"Use the `{ctx.clean_prefix}whoknows` command to claim crowns \N{CROWN}"
|
||||
)
|
||||
|
||||
rows = []
|
||||
for artist, playcount in sorted(crownartists, key=itemgetter(1), reverse=True):
|
||||
rows.append(f"**{artist}** with **{playcount}** {self.format_plays(playcount)}")
|
||||
|
||||
content = discord.Embed(
|
||||
title=f"Artist crowns for {user.name} — Total {len(crownartists)} crowns",
|
||||
color=user.color,
|
||||
)
|
||||
content.set_footer(text="Playcounts are updated on the whoknows command.")
|
||||
if not rows:
|
||||
return await ctx.send("You do not have any crowns.")
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
||||
|
||||
@command_fm.command(
|
||||
name="artist", usage="[timeframe] <toptracks|topalbums|overview> <artist name>"
|
||||
)
|
||||
async def command_artist(self, ctx, timeframe, datatype, *, artistname=""):
|
||||
"""Your top tracks or albums for specific artist.
|
||||
|
||||
Usage:
|
||||
[p]fm artist [timeframe] toptracks <artist name>
|
||||
[p]fm artist [timeframe] topalbums <artist name>
|
||||
[p]fm artist [timeframe] overview <artist name>"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
username = conf["lastfm_username"]
|
||||
|
||||
period, _ = self.get_period(timeframe)
|
||||
if period in [None, "today"]:
|
||||
artistname = " ".join([datatype, artistname]).strip()
|
||||
datatype = timeframe
|
||||
period = "overall"
|
||||
|
||||
artistname = self.remove_mentions(artistname)
|
||||
|
||||
if artistname == "":
|
||||
return await ctx.send("Missing artist name!")
|
||||
|
||||
if datatype in ["toptracks", "tt", "tracks", "track"]:
|
||||
datatype = "tracks"
|
||||
|
||||
elif datatype in ["topalbums", "talb", "albums", "album"]:
|
||||
datatype = "albums"
|
||||
|
||||
elif datatype in ["overview", "stats", "ov"]:
|
||||
return await self.artist_overview(ctx, period, artistname, username)
|
||||
|
||||
else:
|
||||
return await ctx.send_help()
|
||||
|
||||
artist, data = await self.artist_top(ctx, period, artistname, datatype, username)
|
||||
if artist is None or not data:
|
||||
artistname = escape(artistname)
|
||||
if period == "overall":
|
||||
return await ctx.send(f"You have never listened to **{artistname}**!")
|
||||
else:
|
||||
return await ctx.send(
|
||||
f"You have not listened to **{artistname}** in the past {period}s!"
|
||||
)
|
||||
|
||||
total = 0
|
||||
rows = []
|
||||
for i, (name, playcount) in enumerate(data, start=1):
|
||||
rows.append(
|
||||
f"`#{i:2}` **{playcount}** {self.format_plays(playcount)} — **{escape(name)}**"
|
||||
)
|
||||
total += playcount
|
||||
|
||||
artistname = urllib.parse.quote_plus(artistname)
|
||||
content = discord.Embed(color=await ctx.embed_color())
|
||||
content.set_thumbnail(url=artist["image_url"])
|
||||
content.set_author(
|
||||
name=f"{ctx.author.display_name} — "
|
||||
+ (f"{self.humanized_period(period)} " if period != "overall" else "")
|
||||
+ f"Top {datatype} by {artist['formatted_name']}",
|
||||
icon_url=ctx.author.display_avatar.url,
|
||||
url=f"https://last.fm/user/{username}/library/music/{artistname}/"
|
||||
f"+{datatype}?date_preset={self.period_http_format(period)}",
|
||||
)
|
||||
content.set_footer(
|
||||
text=f"Total {total} {self.format_plays(total)} across {len(rows)} {datatype}"
|
||||
)
|
||||
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages[:15], DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
||||
|
||||
@command_fm.command(name="last")
|
||||
async def command_last(self, ctx):
|
||||
"""
|
||||
Your weekly listening overview.
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in(conf)
|
||||
await self.listening_report(ctx, "week", conf["lastfm_username"])
|
||||
|
||||
@command_fm.command(name="lyrics", aliases=["lyr"])
|
||||
async def command_lyrics(self, ctx, *, track: str = None):
|
||||
"""Currently playing song or most recent song."""
|
||||
if track is None:
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in(conf)
|
||||
track, artist, albumname, image_url = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
|
||||
title = (
|
||||
f"**{escape(artist, formatting=True)}** — ***{escape(track, formatting=True)} ***"
|
||||
)
|
||||
|
||||
results, songtitle = await self.lyrics_musixmatch(f"{artist} {track}")
|
||||
if results is None:
|
||||
return await ctx.send(f'No lyrics for "{artist} {track}" found.')
|
||||
embeds = []
|
||||
results = list(pagify(results, page_length=2048))
|
||||
for i, page in enumerate(results, 1):
|
||||
content = discord.Embed(
|
||||
color=await ctx.embed_color(),
|
||||
description=page,
|
||||
title=title,
|
||||
)
|
||||
content.set_thumbnail(url=image_url)
|
||||
if len(results) > 1:
|
||||
content.set_footer(text=f"Page {i}/{len(results)}")
|
||||
|
||||
embeds.append(content)
|
||||
if len(embeds) > 1:
|
||||
await menu(ctx, embeds, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=embeds[0])
|
||||
else:
|
||||
|
||||
results, songtitle = await self.lyrics_musixmatch(track)
|
||||
if results is None:
|
||||
return await ctx.send(f'No lyrics for "{track}" found.')
|
||||
embeds = []
|
||||
results = list(pagify(results, page_length=2048))
|
||||
for i, page in enumerate(results, 1):
|
||||
content = discord.Embed(
|
||||
color=await ctx.embed_color(),
|
||||
title=f"***{escape(songtitle, formatting=True)} ***",
|
||||
description=page,
|
||||
)
|
||||
if len(results) > 1:
|
||||
content.set_footer(text=f"Page {i}/{len(results)}")
|
||||
embeds.append(content)
|
||||
if len(embeds) > 1:
|
||||
await menu(ctx, embeds, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=embeds[0])
|
||||
|
||||
@command_fm.command(name="streak")
|
||||
async def command_streak(self, ctx, user: discord.User = None):
|
||||
"""
|
||||
View how many times you've listened to something in a row
|
||||
|
||||
Only the most 200 recent plays are tracked
|
||||
"""
|
||||
if not user:
|
||||
user = ctx.author
|
||||
conf = await self.config.user(user).all()
|
||||
self.check_if_logged_in(conf, user == ctx.author)
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{"user": conf["lastfm_username"], "method": "user.getrecenttracks", "limit": 200},
|
||||
)
|
||||
tracks = data["recenttracks"]["track"]
|
||||
if not tracks or not isinstance(tracks, list):
|
||||
return await ctx.send("You have not listened to anything yet!")
|
||||
track_streak = [tracks[0]["name"], 1, True]
|
||||
artist_streak = [tracks[0]["artist"]["#text"], 1, True]
|
||||
album_streak = [tracks[0]["album"]["#text"], 1, True]
|
||||
ignore = True
|
||||
for x in tracks:
|
||||
if ignore:
|
||||
ignore = False
|
||||
continue
|
||||
if track_streak[2]:
|
||||
if x["name"] == track_streak[0]:
|
||||
track_streak[1] += 1
|
||||
else:
|
||||
track_streak[2] = False
|
||||
if artist_streak[2]:
|
||||
if x["artist"]["#text"] == artist_streak[0]:
|
||||
artist_streak[1] += 1
|
||||
else:
|
||||
artist_streak[2] = False
|
||||
if album_streak[2]:
|
||||
if x["album"]["#text"] == album_streak[0]:
|
||||
album_streak[1] += 1
|
||||
else:
|
||||
album_streak[2] = False
|
||||
|
||||
if not track_streak[2] and not artist_streak[2] and not album_streak[2]:
|
||||
break
|
||||
|
||||
if track_streak[1] == 1 and artist_streak[1] == 1 and album_streak[1] == 1:
|
||||
return await ctx.send("You have not listened to anything in a row.")
|
||||
embed = discord.Embed(color=await ctx.embed_color(), title=f"{user.name}'s streaks")
|
||||
embed.set_thumbnail(url=tracks[0]["image"][3]["#text"])
|
||||
if track_streak[1] > 1:
|
||||
embed.add_field(
|
||||
name="Track", value=f"{track_streak[1]} times in a row \n({track_streak[0][:50]})"
|
||||
)
|
||||
if artist_streak[1] > 1:
|
||||
embed.add_field(
|
||||
name="Artist",
|
||||
value=f"{artist_streak[1]} times in a row \n({artist_streak[0][:50]})",
|
||||
)
|
||||
if album_streak[1] > 1:
|
||||
embed.add_field(
|
||||
name="Album", value=f"{album_streak[1]} times in a row \n({album_streak[0][:50]})"
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
|
||||
async def cog_command_error(self, ctx, error):
|
||||
if hasattr(error, "original"):
|
||||
if isinstance(error.original, SilentDeAuthorizedError):
|
||||
return
|
||||
if isinstance(error.original, LastFMError):
|
||||
await ctx.send(str(error.original))
|
||||
return
|
||||
await ctx.bot.on_command_error(ctx, error, unhandled_by_cog=True)
|
145
lastfm/love.py
Normal file
145
lastfm/love.py
Normal file
|
@ -0,0 +1,145 @@
|
|||
import discord
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
|
||||
|
||||
from .abc import MixinMeta
|
||||
from .exceptions import *
|
||||
from .fmmixin import FMMixin
|
||||
|
||||
command_fm = FMMixin.command_fm
|
||||
command_fm_server = FMMixin.command_fm_server
|
||||
|
||||
|
||||
class LoveMixin(MixinMeta):
|
||||
"""Love Commands"""
|
||||
|
||||
async def love_or_unlove_song(self, track, artist, love, key):
|
||||
params = {
|
||||
"api_key": self.token,
|
||||
"artist": artist,
|
||||
"sk": key,
|
||||
"track": track,
|
||||
}
|
||||
if love:
|
||||
params["method"] = "track.love"
|
||||
else:
|
||||
params["method"] = "track.unlove"
|
||||
data = await self.api_post(params=params)
|
||||
return data
|
||||
|
||||
@command_fm.command(name="love", usage="<track name> | <artist name>")
|
||||
async def command_love(self, ctx, *, track=None):
|
||||
"""
|
||||
Love a song on last.fm.
|
||||
|
||||
Usage:
|
||||
[p]love
|
||||
[p]love <track name> | <artist name>
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in_and_sk(conf)
|
||||
if track:
|
||||
try:
|
||||
trackname, artistname = [x.strip() for x in track.split("|")]
|
||||
if trackname == "" or artistname == "":
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
return await ctx.send("\N{WARNING SIGN} Incorrect format! use `track | artist`")
|
||||
else:
|
||||
trackname, artistname, albumname, imageurl = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"username": conf["lastfm_username"],
|
||||
"method": "track.getInfo",
|
||||
"track": trackname,
|
||||
"artist": artistname,
|
||||
},
|
||||
)
|
||||
|
||||
if data["track"].get("userloved", "0") == "1":
|
||||
return await ctx.send(
|
||||
f"This song is already loved. Did you mean to run `{ctx.clean_prefix}fm unlove`?"
|
||||
)
|
||||
|
||||
result = await self.love_or_unlove_song(
|
||||
data["track"]["name"], data["track"]["artist"]["name"], True, conf["session_key"]
|
||||
)
|
||||
await self.maybe_send_403_msg(ctx, result)
|
||||
await ctx.send(f"Loved **{trackname[:50]}** by **{artistname[:50]}**")
|
||||
|
||||
@command_fm.command(name="unlove", usage="<track name> | <artist name>")
|
||||
async def command_unlove(self, ctx, *, track=None):
|
||||
"""
|
||||
Unlove a song on last.fm.
|
||||
|
||||
Usage:
|
||||
[p]unlove
|
||||
[p]unlove <track name> | <artist name>
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in_and_sk(conf)
|
||||
if track:
|
||||
try:
|
||||
trackname, artistname = [x.strip() for x in track.split("|")]
|
||||
if trackname == "" or artistname == "":
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
return await ctx.send("\N{WARNING SIGN} Incorrect format! use `track | artist`")
|
||||
else:
|
||||
trackname, artistname, albumname, imageurl = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"username": conf["lastfm_username"],
|
||||
"method": "track.getInfo",
|
||||
"track": trackname,
|
||||
"artist": artistname,
|
||||
},
|
||||
)
|
||||
|
||||
if data["track"].get("userloved", "0") == "0":
|
||||
return await ctx.send(
|
||||
f"This song is not loved. Did you mean to run `{ctx.clean_prefix}fm love`?"
|
||||
)
|
||||
|
||||
result = await self.love_or_unlove_song(
|
||||
data["track"]["name"], data["track"]["artist"]["name"], False, conf["session_key"]
|
||||
)
|
||||
await self.maybe_send_403_msg(ctx, result)
|
||||
await ctx.send(f"Unloved **{trackname[:50]}** by **{artistname[:50]}**")
|
||||
|
||||
@command_fm.command(name="loved")
|
||||
async def command_loved(self, ctx, user: discord.User = None):
|
||||
"""
|
||||
Get a list of loved songs for a user.
|
||||
|
||||
Usage:
|
||||
[p]loved
|
||||
[p]loved <user>
|
||||
"""
|
||||
if not user:
|
||||
user = ctx.author
|
||||
conf = await self.config.user(user).all()
|
||||
self.check_if_logged_in_and_sk(conf)
|
||||
data = await self.api_request(
|
||||
ctx, {"user": conf["lastfm_username"], "method": "user.getlovedtracks"}
|
||||
)
|
||||
tracks = data["lovedtracks"]["track"]
|
||||
if not tracks:
|
||||
return await ctx.send("You have not loved anything yet!")
|
||||
tracks = [f"{x['name']} by {x['artist']['name']}\n" for x in tracks]
|
||||
content = discord.Embed(color=await ctx.embed_color(), title=f"{user.name}'s loved tracks")
|
||||
|
||||
pages = await self.create_pages(content, tracks)
|
||||
for i, page in enumerate(pages):
|
||||
page.set_footer(text=f"Page {i + 1}/{len(pages)}")
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
159
lastfm/nowplaying.py
Normal file
159
lastfm/nowplaying.py
Normal file
|
@ -0,0 +1,159 @@
|
|||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from redbot.core.utils.chat_formatting import escape
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
|
||||
|
||||
from .abc import MixinMeta
|
||||
from .exceptions import *
|
||||
from .fmmixin import FMMixin
|
||||
|
||||
command_fm = FMMixin.command_fm
|
||||
command_fm_server = FMMixin.command_fm_server
|
||||
|
||||
|
||||
class NowPlayingMixin(MixinMeta):
|
||||
"""NowPlaying Commands"""
|
||||
|
||||
@command_fm.command(name="nowplaying", aliases=["np"])
|
||||
async def command_nowplaying(self, ctx, user: Optional[discord.Member] = None):
|
||||
"""Currently playing song or most recent song."""
|
||||
user = user or ctx.author
|
||||
async with ctx.typing():
|
||||
conf = await self.config.user(user).all()
|
||||
self.check_if_logged_in(conf, user == ctx.author)
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"user": conf["lastfm_username"],
|
||||
"method": "user.getrecenttracks",
|
||||
"limit": 1,
|
||||
},
|
||||
)
|
||||
user_attr = data["recenttracks"]["@attr"]
|
||||
tracks = data["recenttracks"]["track"]
|
||||
|
||||
if not tracks:
|
||||
return await ctx.send("You have not listened to anything yet!")
|
||||
try:
|
||||
artist = tracks[0]["artist"]["#text"]
|
||||
album = tracks[0]["album"]["#text"]
|
||||
track = tracks[0]["name"]
|
||||
image_url = tracks[0]["image"][-1]["#text"]
|
||||
url = tracks[0]["url"]
|
||||
except KeyError:
|
||||
artist = tracks["artist"]["#text"]
|
||||
album = tracks["album"]["#text"]
|
||||
track = tracks["name"]
|
||||
image_url = tracks["image"][-1]["#text"]
|
||||
url = tracks["url"]
|
||||
|
||||
content = discord.Embed(color=await self.bot.get_embed_color(ctx.channel), url=url)
|
||||
|
||||
content.description = f"**{escape(album, formatting=True)}**" if album else ""
|
||||
content.title = (
|
||||
f"**{escape(artist, formatting=True)}** — ***{escape(track, formatting=True)} ***"
|
||||
)
|
||||
content.set_thumbnail(url=image_url)
|
||||
|
||||
# tags and playcount
|
||||
trackdata = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"user": conf["lastfm_username"],
|
||||
"method": "track.getInfo",
|
||||
"artist": artist,
|
||||
"track": track,
|
||||
},
|
||||
)
|
||||
if trackdata is not None:
|
||||
loved = trackdata["track"].get("userloved", "0") == "1"
|
||||
if loved:
|
||||
content.title += " :heart:"
|
||||
tags = []
|
||||
try:
|
||||
trackdata = trackdata["track"]
|
||||
playcount = int(trackdata["userplaycount"])
|
||||
if playcount > 0:
|
||||
content.description += f"\n> {playcount} {self.format_plays(playcount)}"
|
||||
if isinstance(trackdata["toptags"], dict):
|
||||
for tag in trackdata["toptags"]["tag"]:
|
||||
if "name" in tag:
|
||||
tags.append(tag["name"])
|
||||
else:
|
||||
tags.append(tag)
|
||||
if tags:
|
||||
content.set_footer(text=", ".join(tags))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# play state
|
||||
state = "— Most recent track"
|
||||
try:
|
||||
if "@attr" in tracks[0]:
|
||||
if "nowplaying" in tracks[0]["@attr"]:
|
||||
state = "— Now Playing"
|
||||
except KeyError:
|
||||
if "@attr" in tracks:
|
||||
if "nowplaying" in tracks["@attr"]:
|
||||
state = "— Now Playing"
|
||||
|
||||
content.set_author(
|
||||
name=f"{user_attr['user']} {state}",
|
||||
icon_url=user.display_avatar.url,
|
||||
)
|
||||
if state == "— Most recent track":
|
||||
msg = "You aren't currently listening to anything, here is the most recent song found."
|
||||
else:
|
||||
msg = None
|
||||
await ctx.send(msg if msg is not None else None, embed=content)
|
||||
|
||||
@command_fm_server.command(name="nowplaying", aliases=["np"])
|
||||
async def command_servernp(self, ctx):
|
||||
"""What people on this server are listening to at the moment."""
|
||||
listeners = []
|
||||
tasks = []
|
||||
async with ctx.typing():
|
||||
userlist = await self.config.all_users()
|
||||
guildusers = [x.id for x in ctx.guild.members]
|
||||
userslist = [user for user in userlist if user in guildusers]
|
||||
for user in userslist:
|
||||
lastfm_username = userlist[user]["lastfm_username"]
|
||||
if lastfm_username is None:
|
||||
continue
|
||||
member = ctx.guild.get_member(user)
|
||||
if member is None:
|
||||
continue
|
||||
|
||||
tasks.append(self.get_current_track(ctx, lastfm_username, member, True))
|
||||
|
||||
total_linked = len(tasks)
|
||||
if tasks:
|
||||
data = await asyncio.gather(*tasks)
|
||||
data = [i for i in data if i]
|
||||
for name, artist, album, image, ref in data:
|
||||
if name is not None:
|
||||
listeners.append((name, artist, ref))
|
||||
else:
|
||||
return await ctx.send("Nobody on this server has connected their last.fm account yet!")
|
||||
|
||||
if not listeners:
|
||||
return await ctx.send("Nobody on this server is listening to anything at the moment!")
|
||||
|
||||
total_listening = len(listeners)
|
||||
rows = []
|
||||
for name, artist, member in listeners:
|
||||
rows.append(f"{member.mention} **{artist}** — ***{name}***")
|
||||
|
||||
content = discord.Embed(color=await ctx.embed_color())
|
||||
content.set_author(
|
||||
name=f"What is {ctx.guild.name} listening to?",
|
||||
icon_url=ctx.guild.icon.url if ctx.guild.icon else None,
|
||||
)
|
||||
content.set_footer(text=f"{total_listening} / {total_linked} Members")
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
110
lastfm/profile.py
Normal file
110
lastfm/profile.py
Normal file
|
@ -0,0 +1,110 @@
|
|||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.predicates import MessagePredicate
|
||||
|
||||
from .abc import MixinMeta
|
||||
from .exceptions import *
|
||||
from .fmmixin import FMMixin
|
||||
|
||||
command_fm = FMMixin.command_fm
|
||||
command_fm_server = FMMixin.command_fm_server
|
||||
from .utils.tokencheck import tokencheck_plus_secret
|
||||
|
||||
|
||||
class ProfileMixin(MixinMeta):
|
||||
"""Profile Commands"""
|
||||
|
||||
@command_fm.command(name="login", aliases=["set"])
|
||||
@commands.check(tokencheck_plus_secret)
|
||||
async def command_login(self, ctx):
|
||||
"""Authenticates your last.fm account."""
|
||||
params = {
|
||||
"api_key": self.token,
|
||||
"method": "auth.getToken",
|
||||
}
|
||||
hashed = self.hashRequest(params, self.secret)
|
||||
params["api_sig"] = hashed
|
||||
response = await self.api_request(ctx, params=params)
|
||||
|
||||
token = response["token"]
|
||||
link = f"https://www.last.fm/api/auth/?api_key={self.token}&token={token}"
|
||||
message = (
|
||||
f"Please click [here]({link}) to authorize me to access your account.\n\n"
|
||||
"You have 90 seconds to successfully authenticate."
|
||||
)
|
||||
embed = discord.Embed(
|
||||
title="Authorization", description=message, color=await ctx.embed_color()
|
||||
)
|
||||
|
||||
try:
|
||||
await ctx.author.send(embed=embed)
|
||||
except discord.Forbidden:
|
||||
await ctx.send("I can't DM you.")
|
||||
return
|
||||
|
||||
if ctx.guild:
|
||||
await ctx.send("Check your Direct Messages for instructions on how to log in.")
|
||||
|
||||
params = {"api_key": self.token, "method": "auth.getSession", "token": token}
|
||||
hashed = self.hashRequest(params, self.secret)
|
||||
params["api_sig"] = hashed
|
||||
for x in range(6):
|
||||
try:
|
||||
data = await self.api_request(ctx, params=params)
|
||||
break
|
||||
except LastFMError as e:
|
||||
if x == 5:
|
||||
message = "You took to long to log in. Rerun the command to try again."
|
||||
embed = discord.Embed(
|
||||
title="Authorization Timeout",
|
||||
description=message,
|
||||
color=await ctx.embed_color(),
|
||||
)
|
||||
await ctx.author.send(embed=embed)
|
||||
return
|
||||
await asyncio.sleep(15)
|
||||
|
||||
await self.config.user(ctx.author).lastfm_username.set(data["session"]["name"])
|
||||
await self.config.user(ctx.author).session_key.set(data["session"]["key"])
|
||||
message = f"Your username is now set as: `{data['session']['name']}`"
|
||||
embed = discord.Embed(title="Success!", description=message, color=await ctx.embed_color())
|
||||
await ctx.author.send(embed=embed)
|
||||
|
||||
@command_fm.command(name="logout", aliases=["unset"])
|
||||
async def command_logout(self, ctx):
|
||||
"""
|
||||
Deauthenticates your last.fm account.
|
||||
"""
|
||||
await ctx.send("Are you sure you want to log out? (yes/no)")
|
||||
try:
|
||||
pred = MessagePredicate.yes_or_no(ctx, user=ctx.message.author)
|
||||
await ctx.bot.wait_for("message", check=pred, timeout=60)
|
||||
except asyncio.TimeoutError:
|
||||
await ctx.send(
|
||||
"You took too long! Use the command again if you still want to log out."
|
||||
)
|
||||
return
|
||||
if pred.result:
|
||||
await self.config.user(ctx.author).clear()
|
||||
await ctx.send("Ok, I've logged you out.")
|
||||
if ctx.guild:
|
||||
async with self.config.guild(ctx.guild).crowns() as crowns:
|
||||
crownlist = []
|
||||
for crown in crowns:
|
||||
if crowns[crown]["user"] == ctx.author.id:
|
||||
crownlist.append(crown)
|
||||
for crown in crownlist:
|
||||
del crowns[crown]
|
||||
else:
|
||||
await ctx.send("Ok, I won't log you out.")
|
||||
|
||||
@command_fm.command(name="profile")
|
||||
async def command_profile(self, ctx, user: Optional[discord.Member] = None):
|
||||
"""Lastfm profile."""
|
||||
user = user or ctx.author
|
||||
conf = await self.config.user(user).all()
|
||||
self.check_if_logged_in(conf, user == ctx.author)
|
||||
await ctx.send(embed=await self.get_userinfo_embed(ctx, user, conf["lastfm_username"]))
|
120
lastfm/recent.py
Normal file
120
lastfm/recent.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
import asyncio
|
||||
|
||||
import discord
|
||||
from redbot.core.utils.chat_formatting import escape
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
|
||||
|
||||
from .abc import MixinMeta
|
||||
from .exceptions import *
|
||||
from .fmmixin import FMMixin
|
||||
|
||||
command_fm = FMMixin.command_fm
|
||||
command_fm_server = FMMixin.command_fm_server
|
||||
|
||||
|
||||
class RecentMixin(MixinMeta):
|
||||
"""Recent Commands"""
|
||||
|
||||
@command_fm.command(name="recent", aliases=["recents", "re"], usage="[amount]")
|
||||
async def command_recent(self, ctx, size: int = 15):
|
||||
"""Tracks you have recently listened to."""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
name = conf["lastfm_username"]
|
||||
self.check_if_logged_in(conf)
|
||||
async with ctx.typing():
|
||||
data = await self.api_request(
|
||||
ctx, {"user": name, "method": "user.getrecenttracks", "limit": size}
|
||||
)
|
||||
user_attr = data["recenttracks"]["@attr"]
|
||||
tracks = data["recenttracks"]["track"]
|
||||
|
||||
if not tracks or not isinstance(tracks, list):
|
||||
return await ctx.send("You have not listened to anything yet!")
|
||||
|
||||
rows = []
|
||||
for i, track in enumerate(tracks):
|
||||
if i >= size:
|
||||
break
|
||||
name = escape(track["name"], formatting=True)
|
||||
track_url = track["url"]
|
||||
artist_name = escape(track["artist"]["#text"], formatting=True)
|
||||
if track.get("@attr") and track["@attr"].get("nowplaying"):
|
||||
extra = ":musical_note:"
|
||||
else:
|
||||
extra = f"(<t:{track['date']['uts']}:R>)"
|
||||
rows.append(f"[**{artist_name}** — **{name}**]({track_url}) {extra}")
|
||||
|
||||
image_url = tracks[0]["image"][-1]["#text"]
|
||||
|
||||
content = discord.Embed(color=await self.bot.get_embed_color(ctx.channel))
|
||||
content.set_thumbnail(url=image_url)
|
||||
content.set_footer(text=f"Total scrobbles: {user_attr['total']}")
|
||||
content.set_author(
|
||||
name=f"{user_attr['user']} — Recent tracks",
|
||||
icon_url=ctx.message.author.display_avatar.url,
|
||||
)
|
||||
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages[:15], DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
||||
|
||||
@command_fm_server.command(name="recent", aliases=["recents", "re"], usage="[amount]")
|
||||
async def command_recent_server(self, ctx, size: int = 15):
|
||||
"""Tracks recently listened to in this server."""
|
||||
listeners = []
|
||||
tasks = []
|
||||
userlist = await self.config.all_users()
|
||||
guildusers = [x.id for x in ctx.guild.members]
|
||||
userslist = [user for user in userlist if user in guildusers]
|
||||
for user in userslist:
|
||||
lastfm_username = userlist[user]["lastfm_username"]
|
||||
if lastfm_username is None:
|
||||
continue
|
||||
member = ctx.guild.get_member(user)
|
||||
if member is None:
|
||||
continue
|
||||
|
||||
tasks.append(self.get_lastplayed(ctx, lastfm_username, member))
|
||||
|
||||
total_linked = len(tasks)
|
||||
total_listening = 0
|
||||
if tasks:
|
||||
data = await asyncio.gather(*tasks)
|
||||
for song, member_ref in data:
|
||||
if song is not None:
|
||||
if song.get("nowplaying"):
|
||||
total_listening += 1
|
||||
listeners.append((song, member_ref))
|
||||
else:
|
||||
return await ctx.send("Nobody on this server has connected their last.fm account yet!")
|
||||
|
||||
if not listeners:
|
||||
return await ctx.send("Nobody on this server is listening to anything at the moment!")
|
||||
|
||||
listeners = sorted(listeners, key=lambda l: l[0].get("date"), reverse=True)
|
||||
rows = []
|
||||
for song, member in listeners:
|
||||
suffix = ""
|
||||
if song.get("nowplaying"):
|
||||
suffix = ":musical_note: "
|
||||
else:
|
||||
suffix = f"(<t:{song.get('date')}:R>)"
|
||||
rows.append(
|
||||
f"{member.mention} [**{escape(song.get('artist'), formatting=True)}** — **{escape(song.get('name'), formatting=True)}**]({song.get('url')}) {suffix}"
|
||||
)
|
||||
|
||||
content = discord.Embed(color=await ctx.embed_color())
|
||||
content.set_author(
|
||||
name=f"What has {ctx.guild.name} been listening to?",
|
||||
icon_url = ctx.guild.icon.url if ctx.guild.icon else None,
|
||||
)
|
||||
content.set_footer(
|
||||
text=f"{total_listening} / {total_linked} Members are listening to music right now"
|
||||
)
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
162
lastfm/scrobbler.py
Normal file
162
lastfm/scrobbler.py
Normal file
|
@ -0,0 +1,162 @@
|
|||
import asyncio
|
||||
import contextlib
|
||||
import re
|
||||
|
||||
import arrow
|
||||
import discord
|
||||
import lavalink
|
||||
from redbot.core import commands
|
||||
|
||||
from .abc import MixinMeta
|
||||
from .exceptions import *
|
||||
from .fmmixin import FMMixin
|
||||
|
||||
command_fm = FMMixin.command_fm
|
||||
command_fm_server = FMMixin.command_fm_server
|
||||
|
||||
|
||||
class ScrobblerMixin(MixinMeta):
|
||||
def __init__(self):
|
||||
# This regex is from GitHub user TheWyn
|
||||
# Source: https://github.com/TheWyn/Wyn-RedV3Cogs/blob/master/lyrics/lyrics.py#L12-13
|
||||
self.regex = re.compile(
|
||||
(
|
||||
r"((\[)|(\()).*(of?ficial|feat\.?|"
|
||||
r"ft\.?|audio|video|explicit|clean|lyrics?|remix|HD).*(?(2)]|\))"
|
||||
),
|
||||
flags=re.I,
|
||||
)
|
||||
|
||||
@commands.command(name="scrobble", usage="<track name> | <artist name>")
|
||||
@commands.cooldown(1, 300, type=commands.BucketType.user)
|
||||
async def command_scrobble(self, ctx, *, track):
|
||||
"""
|
||||
Scrobble a song to last.fm.
|
||||
|
||||
Usage:
|
||||
[p]scrobble <track name> | <artist name>
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in_and_sk(conf)
|
||||
try:
|
||||
trackname, artistname = [x.strip() for x in track.split("|")]
|
||||
if trackname == "" or artistname == "":
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
return await ctx.send("\N{WARNING SIGN} Incorrect format! use `track | artist`")
|
||||
|
||||
result = await self.scrobble_song(
|
||||
trackname, artistname, ctx.author, ctx.author, conf["session_key"], False
|
||||
)
|
||||
await self.maybe_send_403_msg(ctx, result)
|
||||
await ctx.tick()
|
||||
|
||||
@command_fm.command(name="scrobbler")
|
||||
async def command_scrobbler(self, ctx):
|
||||
"""
|
||||
Toggles automatic scrobbling in VC.
|
||||
|
||||
Note: this also toggles the setting of now playing in VC as well.
|
||||
"""
|
||||
current = await self.config.user(ctx.author).scrobble()
|
||||
new = not current
|
||||
await self.config.user(ctx.author).scrobble.set(new)
|
||||
if new:
|
||||
await ctx.send("\N{WHITE HEAVY CHECK MARK} VC scrobbling enabled.")
|
||||
else:
|
||||
await ctx.send("\N{CROSS MARK} VC scrobbling disabled.")
|
||||
|
||||
async def scrobble_song(self, track, artist, user, requester, key, is_vc):
|
||||
params = {
|
||||
"api_key": self.token,
|
||||
"artist": artist,
|
||||
"method": "track.scrobble",
|
||||
"sk": key,
|
||||
"timestamp": str(arrow.utcnow().timestamp()),
|
||||
"track": track,
|
||||
}
|
||||
data = await self.api_post(params=params)
|
||||
if data[0] == 200 and is_vc:
|
||||
scrobbles = await self.config.user(user).scrobbles()
|
||||
if not scrobbles:
|
||||
scrobbles = 0
|
||||
scrobbles += 1
|
||||
await self.config.user(user).scrobbles.set(scrobbles)
|
||||
return data
|
||||
|
||||
async def set_nowplaying(self, track, artist, user, key):
|
||||
params = {
|
||||
"artist": artist,
|
||||
"method": "track.updateNowPlaying",
|
||||
"sk": key,
|
||||
"track": track,
|
||||
}
|
||||
data = await self.api_post(params=params)
|
||||
if data[0] == 403 and data[1]["error"] == 9:
|
||||
await self.config.user(user).session_key.clear()
|
||||
await self.config.user(user).lastfm_username.clear()
|
||||
with contextlib.suppress(discord.HTTPException):
|
||||
message = (
|
||||
"I was unable to scrobble your last song as it seems you have unauthorized me to do so.\n"
|
||||
"You can reauthorize me using the `fm login` command, but I have logged you out for now."
|
||||
)
|
||||
embed = discord.Embed(
|
||||
title="Authorization Failed",
|
||||
description=message,
|
||||
color=await self.bot.get_embed_color(user.dm_channel),
|
||||
)
|
||||
await user.send(embed=embed)
|
||||
|
||||
async def maybe_scrobble_song(
|
||||
self,
|
||||
user: discord.Member,
|
||||
guild: discord.Guild,
|
||||
track: lavalink.Track,
|
||||
artist_name: str,
|
||||
track_name: str,
|
||||
session_key: str,
|
||||
):
|
||||
four_minutes = 240
|
||||
half_track_length = int((track.length / 1000) / 2)
|
||||
|
||||
time_to_sleep = min(four_minutes, half_track_length)
|
||||
await asyncio.sleep(time_to_sleep)
|
||||
|
||||
try:
|
||||
player = lavalink.get_player(guild.id)
|
||||
except:
|
||||
return
|
||||
|
||||
if player.current and player.current.uri == track.uri:
|
||||
await self.scrobble_song(track_name, artist_name, user, guild.me, session_key, True)
|
||||
|
||||
@commands.Cog.listener(name="on_red_audio_track_start")
|
||||
async def listener_scrobbler_track_start(
|
||||
self, guild: discord.Guild, track: lavalink.Track, requester: discord.Member
|
||||
):
|
||||
if (
|
||||
not (guild and track)
|
||||
or int(track.length) <= 30000
|
||||
or not guild.me
|
||||
or not guild.me.voice
|
||||
):
|
||||
return
|
||||
|
||||
renamed_track = self.regex.sub("", track.title).strip()
|
||||
track_array = renamed_track.split("-")
|
||||
if len(track_array) == 1:
|
||||
track_array = (track.author, track_array[0])
|
||||
track_artist = track_array[0]
|
||||
track_title = track_array[1]
|
||||
voice_members = guild.me.voice.channel.members
|
||||
for member in voice_members:
|
||||
if member == guild.me or member.bot is True:
|
||||
continue
|
||||
user_settings = await self.config.user(member).all()
|
||||
if user_settings["scrobble"] and user_settings["session_key"]:
|
||||
await self.set_nowplaying(
|
||||
track_title, track_artist, member, user_settings["session_key"]
|
||||
)
|
||||
await self.maybe_scrobble_song(
|
||||
member, guild, track, track_artist, track_title, user_settings["session_key"]
|
||||
)
|
439
lastfm/tags.py
Normal file
439
lastfm/tags.py
Normal file
|
@ -0,0 +1,439 @@
|
|||
import discord
|
||||
from redbot.core.utils.chat_formatting import humanize_list, pagify
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
|
||||
|
||||
from .abc import MixinMeta
|
||||
from .exceptions import *
|
||||
from .fmmixin import FMMixin
|
||||
|
||||
command_fm = FMMixin.command_fm
|
||||
command_fm_server = FMMixin.command_fm_server
|
||||
|
||||
|
||||
class TagsMixin(MixinMeta):
|
||||
"""Tag Commands"""
|
||||
|
||||
@command_fm.group(name="tag")
|
||||
async def command_tag(self, ctx):
|
||||
"""Commands to tag things"""
|
||||
|
||||
@command_tag.group(name="track", aliases=["tracks", "song"])
|
||||
async def command_tag_track(self, ctx):
|
||||
"""Commands to tag tracks"""
|
||||
|
||||
@command_tag_track.command(name="add", usage="<tag>,[tag] | [track name] | [artist name]")
|
||||
async def command_tag_track_add(self, ctx, *, args):
|
||||
"""
|
||||
Add tags to a track
|
||||
|
||||
Tags are inputted as a comma separated list in the first group
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in_and_sk(conf)
|
||||
split_args = [x.strip() for x in args.split("|")]
|
||||
list_of_tags = [x.strip() for x in split_args[0].split(",")]
|
||||
list_of_tags = [x for x in list_of_tags if x][:10]
|
||||
if len(split_args) not in [1, 3] or not list_of_tags:
|
||||
return await ctx.send(
|
||||
"\N{WARNING SIGN} Incorrect format! use `<tag>,[tag] | [track] | [artist]`"
|
||||
)
|
||||
|
||||
if len(split_args) == 1:
|
||||
|
||||
trackname, artistname, albumname, imageurl = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
|
||||
else:
|
||||
trackname = split_args[1]
|
||||
artistname = split_args[2]
|
||||
|
||||
params = {
|
||||
"artist": artistname,
|
||||
"method": "track.addtags",
|
||||
"sk": conf["session_key"],
|
||||
"tags": ",".join(list_of_tags),
|
||||
"track": trackname,
|
||||
}
|
||||
data = await self.api_post(params=params)
|
||||
await self.maybe_send_403_msg(ctx, data)
|
||||
await ctx.send(
|
||||
f"Added **{len(list_of_tags)}** {'tag' if len(list_of_tags) == 1 else 'tags'}."
|
||||
)
|
||||
|
||||
@command_tag_track.command(name="remove", usage="<tag>,[tag] | [track name] | [artist name]")
|
||||
async def command_tag_track_remove(self, ctx, *, args):
|
||||
"""
|
||||
Remove tags from a track
|
||||
|
||||
Tags are inputted as a comma separated list in the first group
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in_and_sk(conf)
|
||||
split_args = [x.strip() for x in args.split("|")]
|
||||
list_of_tags = [x.strip() for x in split_args[0].split(",")]
|
||||
list_of_tags = [x for x in list_of_tags if x][:10]
|
||||
if len(split_args) not in [1, 3] or not list_of_tags:
|
||||
return await ctx.send(
|
||||
"\N{WARNING SIGN} Incorrect format! use `<tag>,[tag] | [track] | [artist]`"
|
||||
)
|
||||
|
||||
if len(split_args) == 1:
|
||||
|
||||
trackname, artistname, albumname, imageurl = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
|
||||
else:
|
||||
trackname = split_args[1]
|
||||
artistname = split_args[2]
|
||||
params = {
|
||||
"artist": artistname,
|
||||
"method": "track.removetags",
|
||||
"sk": conf["session_key"],
|
||||
"tags": ",".join(list_of_tags),
|
||||
"track": trackname,
|
||||
}
|
||||
data = await self.api_post(params=params)
|
||||
await self.maybe_send_403_msg(ctx, data)
|
||||
await ctx.send(
|
||||
f"Removed **{len(list_of_tags)}** {'tag' if len(list_of_tags) == 1 else 'tags'}."
|
||||
)
|
||||
|
||||
@command_tag_track.command(name="list", usage="[track name] | [artist name]")
|
||||
async def command_tag_track_list(self, ctx, *, args=None):
|
||||
"""
|
||||
List tags for a track
|
||||
|
||||
If no arguments are given, the tags for the last track you listened to will be listed
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in_and_sk(conf)
|
||||
if args:
|
||||
try:
|
||||
trackname, artistname = [x.strip() for x in args.split("|")]
|
||||
if trackname == "" or artistname == "":
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
return await ctx.send("\N{WARNING SIGN} Incorrect format! use `track | artist`")
|
||||
else:
|
||||
|
||||
trackname, artistname, albumname, imageurl = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
|
||||
params = {
|
||||
"artist": artistname,
|
||||
"method": "track.gettags",
|
||||
"sk": conf["session_key"],
|
||||
"track": trackname,
|
||||
}
|
||||
data = await self.api_post(params=params)
|
||||
await self.maybe_send_403_msg(ctx, data)
|
||||
if "tag" not in data[1]["tags"]:
|
||||
return await ctx.send("This track has no tags.")
|
||||
trackname = data[1]["tags"]["@attr"]["track"]
|
||||
artistname = data[1]["tags"]["@attr"]["artist"]
|
||||
embed = discord.Embed(
|
||||
title=f"Your tags for {trackname} by {artistname}",
|
||||
color=await ctx.embed_color(),
|
||||
)
|
||||
nicelooking = []
|
||||
for tag in data[1]["tags"]["tag"]:
|
||||
nicelooking.append(f"[{tag['name']}]({tag['url']})")
|
||||
message = humanize_list(nicelooking)
|
||||
pages = []
|
||||
for page in pagify(message, delims=[","]):
|
||||
pages.append(page)
|
||||
embeds = []
|
||||
for num, page in enumerate(pages):
|
||||
embed.description = page
|
||||
embeds.append(embed)
|
||||
if len(pages) > 1:
|
||||
embed.set_footer(text=f"Page {num + 1}/{len(pages)}")
|
||||
if len(embeds) == 1:
|
||||
await ctx.send(embed=embeds[0])
|
||||
else:
|
||||
await menu(ctx, embeds, DEFAULT_CONTROLS)
|
||||
|
||||
@command_tag.group(name="album", aliases=["albums"])
|
||||
async def command_tag_album(self, ctx):
|
||||
"""Commands to tag albums"""
|
||||
|
||||
@command_tag_album.command(name="add", usage="<tag>,[tag] | [album name] | [artist name]")
|
||||
async def command_tag_album_add(self, ctx, *, args):
|
||||
"""
|
||||
Add tags to an album
|
||||
|
||||
Tags are inputted as a comma separated list in the first group
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in_and_sk(conf)
|
||||
split_args = [x.strip() for x in args.split("|")]
|
||||
list_of_tags = [x.strip() for x in split_args[0].split(",")]
|
||||
list_of_tags = [x for x in list_of_tags if x][:10]
|
||||
if len(split_args) not in [1, 3] or not list_of_tags:
|
||||
return await ctx.send(
|
||||
"\N{WARNING SIGN} Incorrect format! use `<tag>,[tag] | [album] | [artist]`"
|
||||
)
|
||||
|
||||
if len(split_args) == 1:
|
||||
|
||||
trackname, artistname, albumname, imageurl = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
|
||||
else:
|
||||
albumname = split_args[1]
|
||||
artistname = split_args[2]
|
||||
|
||||
if not albumname:
|
||||
await ctx.send(
|
||||
"Your currently playing track does not have an album attached on last.fm."
|
||||
)
|
||||
return
|
||||
|
||||
params = {
|
||||
"artist": artistname,
|
||||
"method": "album.addtags",
|
||||
"sk": conf["session_key"],
|
||||
"tags": ",".join(list_of_tags),
|
||||
"album": albumname,
|
||||
}
|
||||
data = await self.api_post(params=params)
|
||||
await self.maybe_send_403_msg(ctx, data)
|
||||
await ctx.send(
|
||||
f"Added **{len(list_of_tags)}** {'tag' if len(list_of_tags) == 1 else 'tags'}."
|
||||
)
|
||||
|
||||
@command_tag_album.command(name="remove", usage="<tag>,[tag] | [album name] | [artist name]")
|
||||
async def command_tag_album_remove(self, ctx, *, args):
|
||||
"""
|
||||
Remove tags from an album
|
||||
|
||||
Tags are inputted as a comma separated list in the first group
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in_and_sk(conf)
|
||||
split_args = [x.strip() for x in args.split("|")]
|
||||
list_of_tags = [x.strip() for x in split_args[0].split(",")]
|
||||
list_of_tags = [x for x in list_of_tags if x][:10]
|
||||
if len(split_args) not in [1, 3] or not list_of_tags:
|
||||
return await ctx.send(
|
||||
"\N{WARNING SIGN} Incorrect format! use `<tag>,[tag] | [album] | [artist]`"
|
||||
)
|
||||
|
||||
if len(split_args) == 1:
|
||||
|
||||
trackname, artistname, albumname, imageurl = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
|
||||
else:
|
||||
albumname = split_args[1]
|
||||
artistname = split_args[2]
|
||||
|
||||
if not albumname:
|
||||
await ctx.send(
|
||||
"Your currently playing track does not have an album attached on last.fm."
|
||||
)
|
||||
return
|
||||
|
||||
params = {
|
||||
"artist": artistname,
|
||||
"method": "album.removetags",
|
||||
"sk": conf["session_key"],
|
||||
"tags": ",".join(list_of_tags),
|
||||
"album": albumname,
|
||||
}
|
||||
data = await self.api_post(params=params)
|
||||
await self.maybe_send_403_msg(ctx, data)
|
||||
await ctx.send(
|
||||
f"Removed **{len(list_of_tags)}** {'tag' if len(list_of_tags) == 1 else 'tags'}."
|
||||
)
|
||||
|
||||
@command_tag_album.command(name="list", usage="[album name] | [artist name]")
|
||||
async def command_tag_album_list(self, ctx, *, args=None):
|
||||
"""
|
||||
List tags for an album
|
||||
|
||||
If no arguments are given, the tags for the last album you listened to will be listed
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in_and_sk(conf)
|
||||
if args:
|
||||
try:
|
||||
albumname, artistname = [x.strip() for x in args.split("|")]
|
||||
if albumname == "" or artistname == "":
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
return await ctx.send("\N{WARNING SIGN} Incorrect format! use `track | artist`")
|
||||
else:
|
||||
|
||||
trackname, artistname, albumname, imageurl = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
|
||||
if not albumname:
|
||||
await ctx.send(
|
||||
"Your currently playing track does not have an album attached on last.fm."
|
||||
)
|
||||
return
|
||||
params = {
|
||||
"artist": artistname,
|
||||
"method": "album.gettags",
|
||||
"sk": conf["session_key"],
|
||||
"album": albumname,
|
||||
}
|
||||
data = await self.api_post(params=params)
|
||||
await self.maybe_send_403_msg(ctx, data)
|
||||
if "tag" not in data[1]["tags"]:
|
||||
return await ctx.send("This album has no tags.")
|
||||
albumname = data[1]["tags"]["@attr"]["album"]
|
||||
artistname = data[1]["tags"]["@attr"]["artist"]
|
||||
embed = discord.Embed(
|
||||
title=f"Your tags for {albumname} by {artistname}",
|
||||
color=await ctx.embed_color(),
|
||||
)
|
||||
nicelooking = []
|
||||
for tag in data[1]["tags"]["tag"]:
|
||||
nicelooking.append(f"[{tag['name']}]({tag['url']})")
|
||||
message = humanize_list(nicelooking)
|
||||
pages = []
|
||||
for page in pagify(message, delims=[","]):
|
||||
pages.append(page)
|
||||
embeds = []
|
||||
for num, page in enumerate(pages):
|
||||
embed.description = page
|
||||
embeds.append(embed)
|
||||
if len(pages) > 1:
|
||||
embed.set_footer(text=f"Page {num + 1}/{len(pages)}")
|
||||
if len(embeds) == 1:
|
||||
await ctx.send(embed=embeds[0])
|
||||
else:
|
||||
await menu(ctx, embeds, DEFAULT_CONTROLS)
|
||||
|
||||
@command_tag.group(name="artist")
|
||||
async def command_tag_artist(self, ctx):
|
||||
"""Commands to tag tracks"""
|
||||
|
||||
@command_tag_artist.command(name="add", usage="<tag>,[tag] | [artist name]")
|
||||
async def command_tag_artist_add(self, ctx, *, args):
|
||||
"""
|
||||
Add tags to an artist
|
||||
|
||||
Tags are inputted as a comma separated list in the first group
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in_and_sk(conf)
|
||||
split_args = [x.strip() for x in args.split("|")]
|
||||
list_of_tags = [x.strip() for x in split_args[0].split(",")]
|
||||
list_of_tags = [x for x in list_of_tags if x][:10]
|
||||
if len(split_args) not in [1, 2] or not list_of_tags:
|
||||
return await ctx.send(
|
||||
"\N{WARNING SIGN} Incorrect format! use `<tag>,[tag] | [artist]`"
|
||||
)
|
||||
|
||||
if len(split_args) == 1:
|
||||
|
||||
trackname, artistname, albumname, imageurl = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
|
||||
else:
|
||||
artistname = split_args[1]
|
||||
params = {
|
||||
"artist": artistname,
|
||||
"method": "artist.addtags",
|
||||
"sk": conf["session_key"],
|
||||
"tags": ",".join(list_of_tags),
|
||||
}
|
||||
data = await self.api_post(params=params)
|
||||
await self.maybe_send_403_msg(ctx, data)
|
||||
await ctx.send(
|
||||
f"Added **{len(list_of_tags)}** {'tag' if len(list_of_tags) == 1 else 'tags'}."
|
||||
)
|
||||
|
||||
@command_tag_artist.command(name="remove", usage="<tag>,[tag] | [artist name]")
|
||||
async def command_tag_artist_remove(self, ctx, *, args):
|
||||
"""
|
||||
Remove tags from an artist
|
||||
|
||||
Tags are inputted as a comma separated list in the first group
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in_and_sk(conf)
|
||||
split_args = [x.strip() for x in args.split("|")]
|
||||
list_of_tags = [x.strip() for x in split_args[0].split(",")]
|
||||
list_of_tags = [x for x in list_of_tags if x][:10]
|
||||
if len(split_args) not in [1, 2] or not list_of_tags:
|
||||
return await ctx.send(
|
||||
"\N{WARNING SIGN} Incorrect format! use `<tag>,[tag] | [artist]`"
|
||||
)
|
||||
|
||||
if len(split_args) == 1:
|
||||
|
||||
trackname, artistname, albumname, imageurl = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
|
||||
else:
|
||||
artistname = split_args[1]
|
||||
params = {
|
||||
"artist": artistname,
|
||||
"method": "artist.removetags",
|
||||
"sk": conf["session_key"],
|
||||
"tags": ",".join(list_of_tags),
|
||||
}
|
||||
data = await self.api_post(params=params)
|
||||
await self.maybe_send_403_msg(ctx, data)
|
||||
await ctx.send(
|
||||
f"Removed **{len(list_of_tags)}** {'tag' if len(list_of_tags) == 1 else 'tags'}."
|
||||
)
|
||||
|
||||
@command_tag_artist.command(name="list", usage="[artist name]")
|
||||
async def command_tag_artist_list(self, ctx, *, artist=None):
|
||||
"""
|
||||
List tags for an artist
|
||||
|
||||
If no arguments are given, the tags for the last track you listened to will be listed
|
||||
"""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in_and_sk(conf)
|
||||
if not artist:
|
||||
|
||||
trackname, artistname, albumname, imageurl = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
|
||||
params = {
|
||||
"artist": artistname,
|
||||
"method": "artist.gettags",
|
||||
"sk": conf["session_key"],
|
||||
}
|
||||
data = await self.api_post(params=params)
|
||||
await self.maybe_send_403_msg(ctx, data)
|
||||
if "tag" not in data[1]["tags"]:
|
||||
return await ctx.send("This artist has no tags.")
|
||||
artistname = data[1]["tags"]["@attr"]["artist"]
|
||||
embed = discord.Embed(
|
||||
title=f"Your tags for {artistname}",
|
||||
color=await ctx.embed_color(),
|
||||
)
|
||||
nicelooking = []
|
||||
for tag in data[1]["tags"]["tag"]:
|
||||
nicelooking.append(f"[{tag['name']}]({tag['url']})")
|
||||
message = humanize_list(nicelooking)
|
||||
pages = []
|
||||
for page in pagify(message, delims=[","]):
|
||||
pages.append(page)
|
||||
embeds = []
|
||||
for num, page in enumerate(pages):
|
||||
embed.description = page
|
||||
embeds.append(embed)
|
||||
if len(pages) > 1:
|
||||
embed.set_footer(text=f"Page {num + 1}/{len(pages)}")
|
||||
if len(embeds) == 1:
|
||||
await ctx.send(embed=embeds[0])
|
||||
else:
|
||||
await menu(ctx, embeds, DEFAULT_CONTROLS)
|
360
lastfm/top.py
Normal file
360
lastfm/top.py
Normal file
|
@ -0,0 +1,360 @@
|
|||
import asyncio
|
||||
|
||||
import discord
|
||||
from redbot.core.utils.chat_formatting import escape
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
|
||||
|
||||
from .abc import MixinMeta
|
||||
from .exceptions import *
|
||||
from .fmmixin import FMMixin
|
||||
|
||||
command_fm = FMMixin.command_fm
|
||||
command_fm_server = FMMixin.command_fm_server
|
||||
|
||||
|
||||
class TopMixin(MixinMeta):
|
||||
"""Top Artist/Album/Track Commands"""
|
||||
|
||||
@command_fm.command(name="topartists", aliases=["ta"], usage="[timeframe] [amount]")
|
||||
async def command_topartists(self, ctx, *args):
|
||||
"""Most listened artists."""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in(conf)
|
||||
async with ctx.typing():
|
||||
arguments = self.parse_arguments(args)
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"user": conf["lastfm_username"],
|
||||
"method": "user.gettopartists",
|
||||
"period": arguments["period"],
|
||||
"limit": arguments["amount"],
|
||||
},
|
||||
)
|
||||
user_attr = data["topartists"]["@attr"]
|
||||
artists = data["topartists"]["artist"]
|
||||
|
||||
if not artists:
|
||||
return await ctx.send("You have not listened to any artists yet!")
|
||||
|
||||
rows = []
|
||||
for i, artist in enumerate(artists, start=1):
|
||||
name = escape(artist["name"], formatting=True)
|
||||
plays = artist["playcount"]
|
||||
rows.append(f"`#{i:2}` **{plays}** {self.format_plays(plays)} — **{name}**")
|
||||
|
||||
image_url = await self.scrape_artist_image(artists[0]["name"], ctx)
|
||||
|
||||
content = discord.Embed(color=await self.bot.get_embed_color(ctx.channel))
|
||||
content.set_thumbnail(url=image_url)
|
||||
content.set_footer(text=f"Total unique artists: {user_attr['total']}")
|
||||
content.set_author(
|
||||
name=f"{user_attr['user']} — {self.humanized_period(arguments['period']).capitalize()} top artists",
|
||||
icon_url=ctx.message.author.display_avatar.url,
|
||||
)
|
||||
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages[:15], DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
||||
|
||||
@command_fm.command(name="topalbums", aliases=["talb"], usage="[timeframe] [amount]")
|
||||
async def command_topalbums(self, ctx, *args):
|
||||
"""Most listened albums."""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in(conf)
|
||||
arguments = self.parse_arguments(args)
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"user": conf["lastfm_username"],
|
||||
"method": "user.gettopalbums",
|
||||
"period": arguments["period"],
|
||||
"limit": arguments["amount"],
|
||||
},
|
||||
)
|
||||
user_attr = data["topalbums"]["@attr"]
|
||||
albums = data["topalbums"]["album"]
|
||||
|
||||
if not albums:
|
||||
return await ctx.send("You have not listened to any albums yet!")
|
||||
|
||||
rows = []
|
||||
for i, album in enumerate(albums, start=1):
|
||||
name = escape(album["name"], formatting=True)
|
||||
artist_name = escape(album["artist"]["name"], formatting=True)
|
||||
plays = album["playcount"]
|
||||
rows.append(
|
||||
f"`#{i:2}` **{plays}** {self.format_plays(plays)} — **{artist_name}** — ***{name}***"
|
||||
)
|
||||
|
||||
image_url = albums[0]["image"][-1]["#text"]
|
||||
|
||||
content = discord.Embed(color=await self.bot.get_embed_color(ctx.channel))
|
||||
content.set_thumbnail(url=image_url)
|
||||
content.set_footer(text=f"Total unique albums: {user_attr['total']}")
|
||||
content.set_author(
|
||||
name=f"{user_attr['user']} — {self.humanized_period(arguments['period']).capitalize()} top albums",
|
||||
icon_url=ctx.message.author.display_avatar.url,
|
||||
)
|
||||
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages[:15], DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
||||
|
||||
@command_fm.command(name="toptracks", aliases=["tt"], usage="[timeframe] [amount]")
|
||||
async def command_toptracks(self, ctx, *args):
|
||||
"""Most listened tracks."""
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in(conf)
|
||||
async with ctx.typing():
|
||||
arguments = self.parse_arguments(args)
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"user": conf["lastfm_username"],
|
||||
"method": "user.gettoptracks",
|
||||
"period": arguments["period"],
|
||||
"limit": arguments["amount"],
|
||||
},
|
||||
)
|
||||
user_attr = data["toptracks"]["@attr"]
|
||||
tracks = data["toptracks"]["track"]
|
||||
|
||||
if not tracks:
|
||||
return await ctx.send("You have not listened to anything yet!")
|
||||
|
||||
rows = []
|
||||
for i, track in enumerate(tracks, start=1):
|
||||
name = escape(track["name"], formatting=True)
|
||||
artist_name = escape(track["artist"]["name"], formatting=True)
|
||||
plays = track["playcount"]
|
||||
rows.append(
|
||||
f"`#{i:2}` **{plays}** {self.format_plays(plays)} — **{artist_name}** — ***{name}***"
|
||||
)
|
||||
trackdata = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"user": name,
|
||||
"method": "track.getInfo",
|
||||
"artist": tracks[0]["artist"]["name"],
|
||||
"track": tracks[0]["name"],
|
||||
},
|
||||
)
|
||||
content = discord.Embed(color=await self.bot.get_embed_color(ctx.channel))
|
||||
try:
|
||||
if trackdata is None:
|
||||
raise KeyError
|
||||
image_url = trackdata["track"]["album"]["image"][-1]["#text"]
|
||||
# image_url_small = trackdata['track']['album']['image'][1]['#text']
|
||||
# image_colour = await color_from_image_url(image_url_small)
|
||||
except KeyError:
|
||||
image_url = await self.scrape_artist_image(tracks[0]["artist"]["name"], ctx)
|
||||
# image_colour = await color_from_image_url(image_url)
|
||||
|
||||
content.set_thumbnail(url=image_url)
|
||||
|
||||
content.set_footer(text=f"Total unique tracks: {user_attr['total']}")
|
||||
content.set_author(
|
||||
name=f"{user_attr['user']} — {self.humanized_period(arguments['period']).capitalize()} top tracks",
|
||||
icon_url=ctx.message.author.display_avatar.url,
|
||||
)
|
||||
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages[:15], DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
||||
|
||||
@command_fm_server.command(name="topartists", aliases=["ta"])
|
||||
async def command_servertopartists(self, ctx):
|
||||
"""Most listened artists in the server."""
|
||||
tasks = []
|
||||
userlist = await self.config.all_users()
|
||||
guildusers = [x.id for x in ctx.guild.members]
|
||||
userslist = [user for user in userlist if user in guildusers]
|
||||
for user in userslist:
|
||||
lastfm_username = userlist[user]["lastfm_username"]
|
||||
if lastfm_username is None:
|
||||
continue
|
||||
member = ctx.guild.get_member(user)
|
||||
if member is None:
|
||||
continue
|
||||
|
||||
tasks.append(
|
||||
self.get_server_top(
|
||||
ctx,
|
||||
lastfm_username,
|
||||
"artist",
|
||||
"overall",
|
||||
100,
|
||||
)
|
||||
)
|
||||
if not tasks:
|
||||
return await ctx.send("No users have logged in to LastFM!")
|
||||
async with ctx.typing():
|
||||
mapping = {}
|
||||
total_users = 0
|
||||
total_plays = 0
|
||||
data = await asyncio.gather(*tasks)
|
||||
for user in data:
|
||||
if user is None:
|
||||
continue
|
||||
total_users += 1
|
||||
for user_data in user:
|
||||
artist_name = user_data["name"]
|
||||
artist_plays = int(user_data["playcount"])
|
||||
total_plays += artist_plays
|
||||
if artist_name in mapping:
|
||||
mapping[artist_name] += artist_plays
|
||||
else:
|
||||
mapping[artist_name] = artist_plays
|
||||
|
||||
rows = []
|
||||
for i, (artist, playcount) in enumerate(
|
||||
sorted(mapping.items(), key=lambda x: x[1], reverse=True), start=1
|
||||
):
|
||||
name = escape(artist, formatting=True)
|
||||
plays = playcount
|
||||
rows.append(f"`#{i:2}` **{plays}** {self.format_plays(plays)} — **{name}**")
|
||||
|
||||
content = discord.Embed(
|
||||
title=f"Most listened to artists in {ctx.guild}",
|
||||
color=await self.bot.get_embed_color(ctx.channel),
|
||||
)
|
||||
content.set_footer(text=f"Top 100 artists of {total_users} users.")
|
||||
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages[:15], DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
||||
|
||||
@command_fm_server.command(name="topalbums", aliases=["talb"])
|
||||
async def command_servertopalbums(self, ctx):
|
||||
"""Most listened albums in the server."""
|
||||
tasks = []
|
||||
userlist = await self.config.all_users()
|
||||
guildusers = [x.id for x in ctx.guild.members]
|
||||
userslist = [user for user in userlist if user in guildusers]
|
||||
for user in userslist:
|
||||
lastfm_username = userlist[user]["lastfm_username"]
|
||||
if lastfm_username is None:
|
||||
continue
|
||||
member = ctx.guild.get_member(user)
|
||||
if member is None:
|
||||
continue
|
||||
|
||||
tasks.append(
|
||||
self.get_server_top(
|
||||
ctx,
|
||||
lastfm_username,
|
||||
"album",
|
||||
"overall",
|
||||
100,
|
||||
)
|
||||
)
|
||||
if not tasks:
|
||||
return await ctx.send("No users have logged in to LastFM!")
|
||||
async with ctx.typing():
|
||||
mapping = {}
|
||||
total_users = 0
|
||||
total_plays = 0
|
||||
data = await asyncio.gather(*tasks)
|
||||
for user in data:
|
||||
if user is None:
|
||||
continue
|
||||
total_users += 1
|
||||
for user_data in user:
|
||||
name = f'**{escape(user_data["artist"]["name"], formatting=True)}** — **{escape(user_data["name"], formatting=True)}**'
|
||||
plays = int(user_data["playcount"])
|
||||
total_plays += plays
|
||||
if name in mapping:
|
||||
mapping[name] += plays
|
||||
else:
|
||||
mapping[name] = plays
|
||||
|
||||
rows = []
|
||||
for i, (album, playcount) in enumerate(
|
||||
sorted(mapping.items(), key=lambda x: x[1], reverse=True), start=1
|
||||
):
|
||||
plays = playcount
|
||||
rows.append(f"`#{i:2}` **{plays}** {self.format_plays(plays)} — {album}")
|
||||
|
||||
content = discord.Embed(
|
||||
title=f"Most listened to albums in {ctx.guild}",
|
||||
color=await self.bot.get_embed_color(ctx.channel),
|
||||
)
|
||||
content.set_footer(text=f"Top 100 albums of {total_users} users.")
|
||||
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages[:15], DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
||||
|
||||
@command_fm_server.command(name="toptracks", aliases=["tt"])
|
||||
async def command_servertoptracks(self, ctx):
|
||||
"""Most listened tracks in the server."""
|
||||
tasks = []
|
||||
userlist = await self.config.all_users()
|
||||
guildusers = [x.id for x in ctx.guild.members]
|
||||
userslist = [user for user in userlist if user in guildusers]
|
||||
for user in userslist:
|
||||
lastfm_username = userlist[user]["lastfm_username"]
|
||||
if lastfm_username is None:
|
||||
continue
|
||||
member = ctx.guild.get_member(user)
|
||||
if member is None:
|
||||
continue
|
||||
|
||||
tasks.append(
|
||||
self.get_server_top(
|
||||
ctx,
|
||||
lastfm_username,
|
||||
"track",
|
||||
"overall",
|
||||
100,
|
||||
)
|
||||
)
|
||||
if not tasks:
|
||||
return await ctx.send("No users have logged in to LastFM!")
|
||||
async with ctx.typing():
|
||||
mapping = {}
|
||||
total_users = 0
|
||||
total_plays = 0
|
||||
data = await asyncio.gather(*tasks)
|
||||
for user in data:
|
||||
if user is None:
|
||||
continue
|
||||
total_users += 1
|
||||
for user_data in user:
|
||||
name = f'**{escape(user_data["artist"]["name"], formatting=True)}** — **{escape(user_data["name"], formatting=True)}**'
|
||||
plays = int(user_data["playcount"])
|
||||
total_plays += plays
|
||||
if name in mapping:
|
||||
mapping[name] += plays
|
||||
else:
|
||||
mapping[name] = plays
|
||||
|
||||
rows = []
|
||||
for i, (track, playcount) in enumerate(
|
||||
sorted(mapping.items(), key=lambda x: x[1], reverse=True), start=1
|
||||
):
|
||||
plays = playcount
|
||||
rows.append(f"`#{i:2}` **{plays}** {self.format_plays(plays)} — {track}")
|
||||
|
||||
content = discord.Embed(
|
||||
title=f"Most listened to tracks in {ctx.guild}",
|
||||
color=await self.bot.get_embed_color(ctx.channel),
|
||||
)
|
||||
content.set_footer(text=f"Top 100 tracks of {total_users} users.")
|
||||
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages[:15], DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
0
lastfm/utils/__init__.py
Normal file
0
lastfm/utils/__init__.py
Normal file
159
lastfm/utils/api.py
Normal file
159
lastfm/utils/api.py
Normal file
|
@ -0,0 +1,159 @@
|
|||
import contextlib
|
||||
|
||||
import aiohttp
|
||||
import arrow
|
||||
|
||||
from ..exceptions import *
|
||||
|
||||
|
||||
class APIMixin:
|
||||
async def api_request(self, ctx, params, supress_errors=False):
|
||||
"""Get json data from the lastfm api"""
|
||||
url = "http://ws.audioscrobbler.com/2.0/"
|
||||
params["api_key"] = self.token
|
||||
params["format"] = "json"
|
||||
async with self.session.get(url, params=params) as response:
|
||||
with contextlib.suppress(aiohttp.ContentTypeError):
|
||||
content = await response.json()
|
||||
if "error" in content or response.status != 200:
|
||||
if supress_errors:
|
||||
return
|
||||
raise LastFMError(
|
||||
f"Last.fm returned an error: {content.get('message')} | Error code {content.get('error')}"
|
||||
)
|
||||
return content
|
||||
|
||||
async def api_post(self, params):
|
||||
"""Post data to the lastfm api"""
|
||||
url = "http://ws.audioscrobbler.com/2.0/"
|
||||
params["api_key"] = self.token
|
||||
hashed = self.hashRequest(params, self.secret)
|
||||
params["api_sig"] = hashed
|
||||
params["format"] = "json"
|
||||
async with self.session.post(url, params=params) as response:
|
||||
with contextlib.suppress(aiohttp.ContentTypeError):
|
||||
content = await response.json()
|
||||
return response.status, content
|
||||
|
||||
async def fetch(self, ctx, url, params=None, handling="json"):
|
||||
if params is None:
|
||||
params = {}
|
||||
cookies = {'sessionid': self.login_token}
|
||||
async with self.session.get(url, params=params, cookies=cookies) as response:
|
||||
|
||||
if handling == "json":
|
||||
return await response.json()
|
||||
if handling == "text":
|
||||
return await response.text()
|
||||
return await response
|
||||
|
||||
async def get_current_track(self, ctx, username, ref=None, supress_errors=False):
|
||||
data = await self.api_request(
|
||||
ctx, {"method": "user.getrecenttracks", "user": username, "limit": 1}, supress_errors
|
||||
)
|
||||
if not data:
|
||||
return
|
||||
tracks = data["recenttracks"]["track"]
|
||||
if type(tracks) == list:
|
||||
if tracks:
|
||||
track = tracks[0]
|
||||
else:
|
||||
if supress_errors:
|
||||
return
|
||||
raise NoScrobblesError("You haven't scrobbled anything yet.")
|
||||
else:
|
||||
track = tracks
|
||||
|
||||
if "@attr" in track and "nowplaying" in track["@attr"]:
|
||||
|
||||
name = track["name"]
|
||||
artist = track["artist"]["#text"]
|
||||
image = track["image"][-1]["#text"]
|
||||
album = None
|
||||
if "#text" in track["album"]:
|
||||
album = track["album"]["#text"]
|
||||
if ref:
|
||||
return name, artist, album, image, ref
|
||||
else:
|
||||
return name, artist, album, image
|
||||
|
||||
if not ref:
|
||||
if supress_errors:
|
||||
return
|
||||
raise NotScrobblingError("You aren't currently listening to anything.")
|
||||
else:
|
||||
return None, None, None, None, ref
|
||||
|
||||
async def get_server_top(self, ctx, username, request_type, period, limit=100):
|
||||
if request_type == "artist":
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"user": username,
|
||||
"method": "user.gettopartists",
|
||||
"limit": limit,
|
||||
"period": period,
|
||||
},
|
||||
True,
|
||||
)
|
||||
return data["topartists"]["artist"] if data is not None else None
|
||||
if request_type == "album":
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"user": username,
|
||||
"method": "user.gettopalbums",
|
||||
"limit": limit,
|
||||
"period": period,
|
||||
},
|
||||
True,
|
||||
)
|
||||
return data["topalbums"]["album"] if data is not None else None
|
||||
if request_type == "track":
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"user": username,
|
||||
"method": "user.gettoptracks",
|
||||
"limit": limit,
|
||||
"period": period,
|
||||
},
|
||||
True,
|
||||
)
|
||||
return data["toptracks"]["track"] if data is not None else None
|
||||
|
||||
async def get_lastplayed(self, ctx, username, ref):
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{"method": "user.getrecenttracks", "user": username, "limit": 1},
|
||||
True,
|
||||
)
|
||||
song = None
|
||||
if data:
|
||||
tracks = data["recenttracks"]["track"]
|
||||
if type(tracks) == list:
|
||||
if tracks:
|
||||
track = tracks[0]
|
||||
else:
|
||||
return None, ref
|
||||
else:
|
||||
track = tracks
|
||||
|
||||
nowplaying = False
|
||||
if track.get("@attr") and track["@attr"].get("nowplaying"):
|
||||
nowplaying = True
|
||||
|
||||
if track.get("date"):
|
||||
date = tracks[0]["date"]["uts"]
|
||||
else:
|
||||
date = arrow.utcnow().int_timestamp
|
||||
|
||||
song = {
|
||||
"name": track["name"],
|
||||
"artist": track["artist"]["#text"],
|
||||
"nowplaying": nowplaying,
|
||||
"date": int(date),
|
||||
"url": track["url"],
|
||||
}
|
||||
|
||||
return song, ref
|
399
lastfm/utils/base.py
Normal file
399
lastfm/utils/base.py
Normal file
|
@ -0,0 +1,399 @@
|
|||
import hashlib
|
||||
import re
|
||||
import urllib
|
||||
from copy import deepcopy
|
||||
|
||||
import arrow
|
||||
import discord
|
||||
import tabulate
|
||||
from bs4 import BeautifulSoup
|
||||
from redbot.core.utils.chat_formatting import box
|
||||
|
||||
from ..abc import *
|
||||
from ..exceptions import *
|
||||
from .api import APIMixin
|
||||
from .converters import ConvertersMixin
|
||||
from .scraping import ScrapingMixin
|
||||
|
||||
|
||||
class UtilsMixin(APIMixin, ConvertersMixin, ScrapingMixin):
|
||||
"""Utils"""
|
||||
|
||||
def remove_mentions(self, text):
|
||||
"""Remove mentions from string."""
|
||||
return (re.sub(r"<@\!?[0-9]+>", "", text)).strip()
|
||||
|
||||
async def artist_overview(self, ctx, period, artistname, fmname):
|
||||
"""Overall artist view"""
|
||||
albums = []
|
||||
tracks = []
|
||||
metadata = [None, None, None]
|
||||
artistinfo = await self.api_request(
|
||||
ctx, {"method": "artist.getInfo", "artist": artistname}
|
||||
)
|
||||
url = (
|
||||
f"https://last.fm/user/{fmname}/library/music/{artistname}"
|
||||
f"?date_preset={self.period_http_format(period)}"
|
||||
)
|
||||
data = await self.fetch(ctx, url, handling="text")
|
||||
soup = BeautifulSoup(data, "html.parser")
|
||||
try:
|
||||
albumsdiv, tracksdiv, _ = soup.findAll("tbody", {"data-playlisting-add-entries": ""})
|
||||
except ValueError:
|
||||
if period == "overall":
|
||||
return await ctx.send(f"You have never listened to **{artistname}**!")
|
||||
return await ctx.send(
|
||||
f"You have not listened to **{artistname}** in the past {period}s!"
|
||||
)
|
||||
|
||||
for container, destination in zip([albumsdiv, tracksdiv], [albums, tracks]):
|
||||
items = container.findAll("tr", {"class": "chartlist-row"})
|
||||
for item in items:
|
||||
name = item.find("td", {"class": "chartlist-name"}).find("a").get("title")
|
||||
playcount = (
|
||||
item.find("span", {"class": "chartlist-count-bar-value"})
|
||||
.text.replace("scrobbles", "")
|
||||
.replace("scrobble", "")
|
||||
.strip()
|
||||
)
|
||||
destination.append((name, playcount))
|
||||
|
||||
metadata_list = soup.find("ul", {"class": "metadata-list"})
|
||||
for i, metadata_item in enumerate(
|
||||
metadata_list.findAll("p", {"class": "metadata-display"})
|
||||
):
|
||||
metadata[i] = int(metadata_item.text.replace(",", ""))
|
||||
|
||||
artist = {
|
||||
"image_url": soup.find("span", {"class": "library-header-image"})
|
||||
.find("img")
|
||||
.get("src")
|
||||
.replace("avatar70s", "avatar300s"),
|
||||
"formatted_name": soup.find("h2", {"class": "library-header-title"}).text.strip(),
|
||||
}
|
||||
|
||||
similar = [a["name"] for a in artistinfo["artist"]["similar"]["artist"]]
|
||||
tags = [t["name"] for t in artistinfo["artist"]["tags"]["tag"]]
|
||||
|
||||
content = discord.Embed(color=await self.bot.get_embed_color(ctx.channel))
|
||||
content.set_thumbnail(url=artist["image_url"])
|
||||
content.set_author(
|
||||
name=f"{ctx.author.name} — {artist['formatted_name']} "
|
||||
+ (f"{self.humanized_period(period)} " if period != "overall" else "")
|
||||
+ "Overview",
|
||||
icon_url=ctx.author.display_avatar.url,
|
||||
url=f"https://last.fm/user/{fmname}/library/music/{urllib.parse.quote_plus(artistname)}?date_preset={self.period_http_format(period)}",
|
||||
)
|
||||
content.set_footer(text=f"{', '.join(tags)}")
|
||||
|
||||
crowns = await self.config.guild(ctx.guild).crowns()
|
||||
crown_holder = crowns.get(artistname.lower(), None)
|
||||
if crown_holder is None or crown_holder["user"] != ctx.author.id:
|
||||
crownstate = None
|
||||
else:
|
||||
crownstate = "👑"
|
||||
if crownstate is not None:
|
||||
stats = [[crownstate, str(metadata[0]), str(metadata[1]), str(metadata[2])]]
|
||||
headers = ["-", "Scrobbles", "Albums", "Tracks"]
|
||||
else:
|
||||
stats = [[str(metadata[0]), str(metadata[1]), str(metadata[2])]]
|
||||
headers = ["Scrobbles", "Albums", "Tracks"]
|
||||
content.description = box(tabulate.tabulate(stats, headers=headers), lang="prolog")
|
||||
|
||||
content.add_field(
|
||||
name="Top albums",
|
||||
value="\n".join(
|
||||
f"`#{i:2}` **{item}** ({playcount})"
|
||||
for i, (item, playcount) in enumerate(albums, start=1)
|
||||
),
|
||||
inline=True,
|
||||
)
|
||||
content.add_field(
|
||||
name="Top tracks",
|
||||
value="\n".join(
|
||||
f"`#{i:2}` **{item}** ({playcount})"
|
||||
for i, (item, playcount) in enumerate(tracks, start=1)
|
||||
),
|
||||
inline=True,
|
||||
)
|
||||
if similar:
|
||||
content.add_field(name="Similar artists", value=", ".join(similar), inline=False)
|
||||
await ctx.send(embed=content)
|
||||
|
||||
async def get_userinfo_embed(self, ctx, user, username):
|
||||
data = await self.api_request(ctx, {"user": username, "method": "user.getinfo"})
|
||||
if not data:
|
||||
raise LastFMError
|
||||
|
||||
username = data["user"]["name"]
|
||||
playcount = data["user"]["playcount"]
|
||||
profile_url = data["user"]["url"]
|
||||
profile_pic_url = data["user"]["image"][3]["#text"]
|
||||
vc_scrobbles = await self.config.user(user).scrobbles()
|
||||
timestamp = int(data["user"]["registered"]["unixtime"])
|
||||
exact_time = f"<t:{timestamp}>"
|
||||
relative_time = f"<t:{timestamp}:R>"
|
||||
|
||||
content = discord.Embed(
|
||||
title=f"\N{OPTICAL DISC} {username}", color=await ctx.embed_color()
|
||||
)
|
||||
content.add_field(name="Last.fm profile", value=f"[Link]({profile_url})", inline=True)
|
||||
content.add_field(
|
||||
name="Registered",
|
||||
value=f"{exact_time}\n({relative_time})",
|
||||
inline=True,
|
||||
)
|
||||
content.set_thumbnail(url=profile_pic_url)
|
||||
|
||||
footer = f"Total plays: {playcount}"
|
||||
|
||||
if vc_scrobbles:
|
||||
footer += f" | VC Plays: {vc_scrobbles}"
|
||||
|
||||
content.set_footer(text=footer)
|
||||
return content
|
||||
|
||||
async def listening_report(self, ctx, timeframe, name):
|
||||
current_day_floor = arrow.utcnow().floor("day")
|
||||
week = []
|
||||
for i in range(1, 8):
|
||||
dt = current_day_floor.shift(days=-i)
|
||||
week.append(
|
||||
{
|
||||
"dt": int(dt.timestamp()),
|
||||
"ts": int(dt.timestamp()),
|
||||
"ts_to": int(dt.shift(days=+1, minutes=-1).timestamp()),
|
||||
"day": dt.format("ddd, MMM Do"),
|
||||
"scrobbles": 0,
|
||||
}
|
||||
)
|
||||
|
||||
params = {
|
||||
"method": "user.getrecenttracks",
|
||||
"user": name,
|
||||
"from": week[-1]["ts"],
|
||||
"to": int(current_day_floor.shift(minutes=-1).timestamp()),
|
||||
"limit": 1000,
|
||||
}
|
||||
content = await self.api_request(ctx, params)
|
||||
tracks = content["recenttracks"]["track"]
|
||||
if not tracks or not isinstance(tracks, list):
|
||||
await ctx.send("No data found.")
|
||||
return
|
||||
|
||||
# get rid of nowplaying track if user is currently scrobbling.
|
||||
# for some reason even with from and to parameters it appears
|
||||
if tracks[0].get("@attr") is not None:
|
||||
tracks = tracks[1:]
|
||||
|
||||
day_counter = 1
|
||||
for trackdata in reversed(tracks):
|
||||
scrobble_ts = int(trackdata["date"]["uts"])
|
||||
if scrobble_ts > week[-day_counter]["ts_to"]:
|
||||
day_counter += 1
|
||||
|
||||
week[day_counter - 1]["scrobbles"] += 1
|
||||
|
||||
scrobbles_total = sum(day["scrobbles"] for day in week)
|
||||
scrobbles_average = round(scrobbles_total / len(week))
|
||||
|
||||
rows = []
|
||||
for day in week:
|
||||
rows.append(f"`{day['day']}`: **{day['scrobbles']}** Scrobbles")
|
||||
|
||||
content = discord.Embed(color=await self.bot.get_embed_color(ctx.channel))
|
||||
content.set_author(
|
||||
name=f"{ctx.author.display_name} | Last {timeframe.title()}",
|
||||
icon_url=ctx.author.display_avatar.url,
|
||||
)
|
||||
content.description = "\n".join(rows)
|
||||
content.add_field(
|
||||
name="Total scrobbles", value=f"{scrobbles_total} Scrobbles", inline=False
|
||||
)
|
||||
content.add_field(
|
||||
name="Avg. daily scrobbles", value=f"{scrobbles_average} Scrobbles", inline=False
|
||||
)
|
||||
await ctx.send(embed=content)
|
||||
|
||||
async def create_pages(self, content, rows, maxrows=15, maxpages=10):
|
||||
pages = []
|
||||
content.description = ""
|
||||
thisrow = 0
|
||||
rowcount = len(rows)
|
||||
for row in rows:
|
||||
thisrow += 1
|
||||
if len(content.description) + len(row) < 2000 and thisrow < maxrows + 1:
|
||||
content.description += f"\n{row}"
|
||||
rowcount -= 1
|
||||
else:
|
||||
thisrow = 1
|
||||
if len(pages) == maxpages - 1:
|
||||
content.description += f"\n*+ {rowcount} more entries...*"
|
||||
pages.append(content)
|
||||
content = None
|
||||
break
|
||||
|
||||
pages.append(content)
|
||||
content = deepcopy(content)
|
||||
content.description = f"{row}"
|
||||
rowcount -= 1
|
||||
if content is not None and not content.description == "":
|
||||
pages.append(content)
|
||||
|
||||
return pages
|
||||
|
||||
def hashRequest(self, obj, secretKey):
|
||||
"""
|
||||
This hashing function is courtesy of GitHub user huberf.
|
||||
It is licensed under the MIT license.
|
||||
Source: https://github.com/huberf/lastfm-scrobbler/blob/master/lastpy/__init__.py#L50-L60
|
||||
"""
|
||||
string = ""
|
||||
items = obj.keys()
|
||||
items = sorted(items)
|
||||
for i in items:
|
||||
string += i
|
||||
string += obj[i]
|
||||
string += secretKey
|
||||
stringToHash = string.encode("utf8")
|
||||
requestHash = hashlib.md5(stringToHash).hexdigest()
|
||||
return requestHash
|
||||
|
||||
def check_if_logged_in(self, conf, same_person=True):
|
||||
you_or_they = "You" if same_person else "They"
|
||||
if not conf["lastfm_username"]:
|
||||
raise NotLoggedInError(
|
||||
f"{you_or_they} need to log into a last.fm account. Please log in with `fm login`."
|
||||
)
|
||||
|
||||
def check_if_logged_in_and_sk(self, conf):
|
||||
if not conf["session_key"] and not conf["lastfm_username"]:
|
||||
raise NotLoggedInError(
|
||||
"You need to log into a last.fm account. Please log in with `fm login`."
|
||||
)
|
||||
if not conf["session_key"] and conf["lastfm_username"]:
|
||||
raise NeedToReauthorizeError(
|
||||
"You appear to be an old user of this cog. "
|
||||
"To use this command you will need to reauthorize with `fm login`."
|
||||
)
|
||||
|
||||
async def maybe_send_403_msg(self, ctx, data):
|
||||
if data[0] == 403 and data[1]["error"] == 9:
|
||||
await self.config.user(ctx.author).session_key.clear()
|
||||
await self.config.user(ctx.author).lastfm_username.clear()
|
||||
message = (
|
||||
"I was unable to add your tags as it seems you have unauthorized me to do so.\n"
|
||||
"You can reauthorize me using the `fm login` command, but I have logged you out for now."
|
||||
)
|
||||
embed = discord.Embed(
|
||||
title="Authorization Failed",
|
||||
description=message,
|
||||
color=await ctx.embed_color(),
|
||||
)
|
||||
await ctx.send(embed=embed)
|
||||
raise SilentDeAuthorizedError
|
||||
|
||||
async def get_playcount_track(self, ctx, username, artist, track, period, reference=None):
|
||||
if period != "overall":
|
||||
return await self.get_playcount_track_scraper(ctx, username, artist, track, period)
|
||||
|
||||
try:
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"method": "track.getinfo",
|
||||
"user": username,
|
||||
"track": track,
|
||||
"artist": artist,
|
||||
"autocorrect": 1,
|
||||
},
|
||||
)
|
||||
except LastFMError:
|
||||
data = {}
|
||||
|
||||
try:
|
||||
count = int(data["track"]["userplaycount"])
|
||||
except KeyError:
|
||||
count = 0
|
||||
try:
|
||||
artistname = data["track"]["artist"]["name"]
|
||||
trackname = data["track"]["name"]
|
||||
except KeyError:
|
||||
artistname = None
|
||||
trackname = None
|
||||
|
||||
try:
|
||||
image_url = data["track"]["album"]["image"][-1]["#text"]
|
||||
except KeyError:
|
||||
image_url = None
|
||||
|
||||
if reference is None:
|
||||
return count
|
||||
else:
|
||||
return count, reference, (artistname, trackname, image_url)
|
||||
|
||||
async def get_playcount_album(self, ctx, username, artist, album, period, reference=None):
|
||||
if period != "overall":
|
||||
return await self.get_playcount_album_scraper(ctx, username, artist, album, period)
|
||||
try:
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"method": "album.getinfo",
|
||||
"user": username,
|
||||
"album": album,
|
||||
"artist": artist,
|
||||
"autocorrect": 1,
|
||||
},
|
||||
)
|
||||
except LastFMError:
|
||||
data = {}
|
||||
try:
|
||||
count = int(data["album"]["userplaycount"])
|
||||
except (KeyError, TypeError):
|
||||
count = 0
|
||||
|
||||
try:
|
||||
artistname = data["album"]["artist"]
|
||||
albumname = data["album"]["name"]
|
||||
except KeyError:
|
||||
artistname = None
|
||||
albumname = None
|
||||
|
||||
try:
|
||||
image_url = data["album"]["image"][-1]["#text"]
|
||||
except KeyError:
|
||||
image_url = None
|
||||
|
||||
if reference is None:
|
||||
return count
|
||||
else:
|
||||
return count, reference, (artistname, albumname, image_url)
|
||||
|
||||
async def get_playcount(self, ctx, username, artist, period, reference=None):
|
||||
if period != "overall":
|
||||
return await self.get_playcount_scraper(ctx, username, artist, period)
|
||||
|
||||
try:
|
||||
data = await self.api_request(
|
||||
ctx,
|
||||
{
|
||||
"method": "artist.getinfo",
|
||||
"user": username,
|
||||
"artist": artist,
|
||||
"autocorrect": 1,
|
||||
},
|
||||
)
|
||||
except LastFMError:
|
||||
data = {}
|
||||
try:
|
||||
count = int(data["artist"]["stats"]["userplaycount"])
|
||||
name = data["artist"]["name"]
|
||||
except (KeyError, TypeError):
|
||||
count = 0
|
||||
name = None
|
||||
|
||||
if not reference:
|
||||
return count
|
||||
|
||||
return count, reference, name
|
127
lastfm/utils/converters.py
Normal file
127
lastfm/utils/converters.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
class ConvertersMixin:
|
||||
def format_plays(self, amount):
|
||||
if amount == 1:
|
||||
return "play"
|
||||
return "plays"
|
||||
|
||||
def get_period(self, timeframe):
|
||||
if timeframe in ["7day", "7days", "weekly", "week", "1week", "7d"]:
|
||||
period = "7day", "past week"
|
||||
elif timeframe in ["30day", "30days", "monthly", "month", "1month", "1m"]:
|
||||
period = "1month", "past month"
|
||||
elif timeframe in ["90day", "90days", "3months", "3month", "3m"]:
|
||||
period = "3month", "past 3 months"
|
||||
elif timeframe in ["180day", "180days", "6months", "6month", "halfyear", "hy", "6m"]:
|
||||
period = "6month", "past 6 months"
|
||||
elif timeframe in [
|
||||
"365day",
|
||||
"365days",
|
||||
"1year",
|
||||
"year",
|
||||
"12months",
|
||||
"12month",
|
||||
"y",
|
||||
"1y",
|
||||
"12m",
|
||||
]:
|
||||
period = "12month", "past year"
|
||||
elif timeframe in ["at", "alltime", "overall"]:
|
||||
period = "overall", "overall"
|
||||
else:
|
||||
period = None, None
|
||||
|
||||
return period
|
||||
|
||||
def humanized_period(self, period):
|
||||
if period == "7day":
|
||||
humanized = "weekly"
|
||||
elif period == "1month":
|
||||
humanized = "monthly"
|
||||
elif period == "3month":
|
||||
humanized = "past 3 months"
|
||||
elif period == "6month":
|
||||
humanized = "past 6 months"
|
||||
elif period == "12month":
|
||||
humanized = "yearly"
|
||||
else:
|
||||
humanized = "alltime"
|
||||
|
||||
return humanized
|
||||
|
||||
def period_http_format(self, period):
|
||||
period_format_map = {
|
||||
"7day": "LAST_7_DAYS",
|
||||
"1month": "LAST_30_DAYS",
|
||||
"3month": "LAST_90_DAYS",
|
||||
"6month": "LAST_180_DAYS",
|
||||
"12month": "LAST_365_DAYS",
|
||||
"overall": "ALL",
|
||||
}
|
||||
return period_format_map.get(period)
|
||||
|
||||
def parse_arguments(self, args):
|
||||
parsed = {"period": None, "amount": None}
|
||||
for a in args:
|
||||
if parsed["amount"] is None:
|
||||
try:
|
||||
parsed["amount"] = int(a)
|
||||
continue
|
||||
except ValueError:
|
||||
pass
|
||||
if parsed["period"] is None:
|
||||
parsed["period"], _ = self.get_period(a)
|
||||
|
||||
if parsed["period"] is None:
|
||||
parsed["period"] = "overall"
|
||||
if parsed["amount"] is None:
|
||||
parsed["amount"] = 15
|
||||
return parsed
|
||||
|
||||
def parse_chart_arguments(self, args):
|
||||
parsed = {
|
||||
"period": None,
|
||||
"amount": None,
|
||||
"width": None,
|
||||
"height": None,
|
||||
"method": None,
|
||||
"path": None,
|
||||
}
|
||||
for a in args:
|
||||
a = a.lower()
|
||||
if parsed["amount"] is None:
|
||||
try:
|
||||
size = a.split("x")
|
||||
parsed["width"] = int(size[0])
|
||||
if len(size) > 1:
|
||||
parsed["height"] = int(size[1])
|
||||
else:
|
||||
parsed["height"] = int(size[0])
|
||||
continue
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if parsed["method"] is None:
|
||||
if a in ["talb", "topalbums", "albums", "album"]:
|
||||
parsed["method"] = "user.gettopalbums"
|
||||
continue
|
||||
elif a in ["ta", "topartists", "artists", "artist"]:
|
||||
parsed["method"] = "user.gettopartists"
|
||||
continue
|
||||
elif a in ["re", "recent", "recents"]:
|
||||
parsed["method"] = "user.getrecenttracks"
|
||||
continue
|
||||
elif a in ["tracks", "track"]:
|
||||
parsed["method"] = "user.gettoptracks"
|
||||
|
||||
if parsed["period"] is None:
|
||||
parsed["period"], _ = self.get_period(a)
|
||||
|
||||
if parsed["period"] is None:
|
||||
parsed["period"] = "7day"
|
||||
if parsed["width"] is None:
|
||||
parsed["width"] = 3
|
||||
parsed["height"] = 3
|
||||
if parsed["method"] is None:
|
||||
parsed["method"] = "user.gettopalbums"
|
||||
parsed["amount"] = parsed["width"] * parsed["height"]
|
||||
return parsed
|
184
lastfm/utils/scraping.py
Normal file
184
lastfm/utils/scraping.py
Normal file
|
@ -0,0 +1,184 @@
|
|||
import asyncio
|
||||
import math
|
||||
import re
|
||||
import urllib
|
||||
from typing import Tuple
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
class ScrapingMixin:
|
||||
async def artist_top(self, ctx, period, artistname, datatype, name):
|
||||
"""Scrape either top tracks or top albums from lastfm library page."""
|
||||
if not self.login_token:
|
||||
return None, []
|
||||
url = (
|
||||
f"https://last.fm/user/{name}/library/music/{artistname}/"
|
||||
f"+{datatype}?date_preset={self.period_http_format(period)}"
|
||||
)
|
||||
data = await self.fetch(ctx, url, handling="text")
|
||||
soup = BeautifulSoup(data, "html.parser")
|
||||
data = []
|
||||
try:
|
||||
chartlist = soup.find("tbody", {"data-playlisting-add-entries": ""})
|
||||
except ValueError:
|
||||
return None, []
|
||||
|
||||
artist = {
|
||||
"image_url": soup.find("span", {"class": "library-header-image"})
|
||||
.find("img")
|
||||
.get("src")
|
||||
.replace("avatar70s", "avatar300s"),
|
||||
"formatted_name": soup.find("a", {"class": "library-header-crumb"}).text.strip(),
|
||||
}
|
||||
|
||||
items = chartlist.findAll("tr", {"class": "chartlist-row"})
|
||||
for item in items:
|
||||
name = item.find("td", {"class": "chartlist-name"}).find("a").get("title")
|
||||
playcount = (
|
||||
item.find("span", {"class": "chartlist-count-bar-value"})
|
||||
.text.replace("scrobbles", "")
|
||||
.replace("scrobble", "")
|
||||
.strip()
|
||||
)
|
||||
data.append((name, int(playcount.replace(",", ""))))
|
||||
|
||||
return artist, data
|
||||
|
||||
async def lyrics_musixmatch(self, artistsong) -> Tuple[str, str]:
|
||||
artistsong = re.sub("[^a-zA-Z0-9 \n.]", "", artistsong)
|
||||
artistsong = re.sub(r"\s+", " ", artistsong).strip()
|
||||
headers = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Arch Linux; Linux x86_64; rv:66.0) Gecko/20100101 Firefox/66.0"
|
||||
}
|
||||
async with self.session.get(
|
||||
"https://musixmatch.com/search/{}".format(artistsong).replace(" ", "%20"),
|
||||
headers=headers,
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
result = await resp.text()
|
||||
else:
|
||||
return None, None
|
||||
soup = BeautifulSoup(result, "html.parser")
|
||||
songurl = soup.find("a", {"class": "title"})
|
||||
if songurl is None:
|
||||
return None, None
|
||||
url = "https://www.musixmatch.com" + songurl["href"]
|
||||
async with self.session.get(url, headers=headers) as resp:
|
||||
result = await resp.text()
|
||||
soup = BeautifulSoup(result, "html.parser")
|
||||
lyrics = soup.text.split('"body":"')
|
||||
lyrics = lyrics[0]
|
||||
songname = lyrics.split("|")[0]
|
||||
lyrics = lyrics.split('","language"')[0]
|
||||
try:
|
||||
lyrics = lyrics.split("languages")[1]
|
||||
except IndexError:
|
||||
return None, None
|
||||
lyrics = lyrics.split("Report")[0]
|
||||
lyrics = lyrics.replace("\\n", "\n")
|
||||
lyrics = lyrics.replace("\\", "")
|
||||
lyrics = lyrics.replace("&", "&")
|
||||
lyrics = lyrics.replace("`", "'")
|
||||
lyrics = lyrics.strip()
|
||||
return lyrics, songname.strip()
|
||||
|
||||
async def scrape_artist_image(self, artist, ctx):
|
||||
url = f"https://www.last.fm/music/{urllib.parse.quote_plus(artist)}/+images"
|
||||
data = await self.fetch(ctx, url, handling="text")
|
||||
|
||||
soup = BeautifulSoup(data, "html.parser")
|
||||
if soup is None:
|
||||
return ""
|
||||
image = soup.find("img", {"class": "image-list-image"})
|
||||
if image is None:
|
||||
try:
|
||||
image = soup.find("li", {"class": "image-list-item-wrapper"}).find("a").find("img")
|
||||
except AttributeError:
|
||||
return ""
|
||||
return image["src"].replace("/avatar170s/", "/300x300/") if image else ""
|
||||
|
||||
async def scrape_artists_for_chart(self, ctx, username, period, amount):
|
||||
period_format_map = {
|
||||
"7day": "LAST_7_DAYS",
|
||||
"1month": "LAST_30_DAYS",
|
||||
"3month": "LAST_90_DAYS",
|
||||
"6month": "LAST_180_DAYS",
|
||||
"12month": "LAST_365_DAYS",
|
||||
"overall": "ALL",
|
||||
}
|
||||
tasks = []
|
||||
url = f"https://www.last.fm/user/{username}/library/artists"
|
||||
for i in range(1, math.ceil(amount / 50) + 1):
|
||||
params = {"date_preset": period_format_map[period], "page": i}
|
||||
task = asyncio.ensure_future(self.fetch(ctx, url, params, handling="text"))
|
||||
tasks.append(task)
|
||||
|
||||
responses = await asyncio.gather(*tasks)
|
||||
|
||||
images = []
|
||||
for data in responses:
|
||||
if len(images) >= amount:
|
||||
break
|
||||
else:
|
||||
soup = BeautifulSoup(data, "html.parser")
|
||||
imagedivs = soup.findAll("td", {"class": "chartlist-image"})
|
||||
images += [
|
||||
div.find("img")["src"].replace("/avatar70s/", "/300x300/") for div in imagedivs
|
||||
]
|
||||
|
||||
return images
|
||||
|
||||
async def get_similar_artists(self, artistname, ctx):
|
||||
similar = []
|
||||
url = f"https://last.fm/music/{artistname}"
|
||||
data = await self.fetch(ctx, url, handling="text")
|
||||
soup = BeautifulSoup(data, "html.parser")
|
||||
for artist in soup.findAll("h3", {"class": "artist-similar-artists-sidebar-item-name"}):
|
||||
similar.append(artist.find("a").text)
|
||||
listeners = (
|
||||
soup.find("li", {"class": "header-metadata-tnew-item--listeners"}).find("abbr").text
|
||||
)
|
||||
return similar, listeners
|
||||
|
||||
async def get_playcount_scraper(self, ctx, username, artistname, period):
|
||||
url = (
|
||||
f"https://last.fm/user/{username}/library/music/{artistname}"
|
||||
f"?date_preset={self.period_http_format(period)}"
|
||||
)
|
||||
data = await self.fetch(ctx, url, handling="text")
|
||||
soup = BeautifulSoup(data, "html.parser")
|
||||
divs = soup.findAll(class_="metadata-display")
|
||||
if not divs:
|
||||
return 0
|
||||
div = divs[0]
|
||||
plays = div.get_text()
|
||||
return int(plays.split(" ")[0].replace(",", ""))
|
||||
|
||||
async def get_playcount_track_scraper(self, ctx, username, artistname, trackname, period):
|
||||
url = (
|
||||
f"https://last.fm/user/{username}/library/music/{artistname}/_/{trackname}"
|
||||
f"?date_preset={self.period_http_format(period)}"
|
||||
)
|
||||
data = await self.fetch(ctx, url, handling="text")
|
||||
soup = BeautifulSoup(data, "html.parser")
|
||||
divs = soup.findAll(class_="metadata-display")
|
||||
if not divs:
|
||||
return 0
|
||||
div = divs[0]
|
||||
plays = div.get_text()
|
||||
return int(plays.split(" ")[0].replace(",", ""))
|
||||
|
||||
async def get_playcount_album_scraper(self, ctx, username, artistname, albumname, period):
|
||||
url = (
|
||||
f"https://last.fm/user/{username}/library/music/{artistname}/{albumname}"
|
||||
f"?date_preset={self.period_http_format(period)}"
|
||||
)
|
||||
data = await self.fetch(ctx, url, handling="text")
|
||||
soup = BeautifulSoup(data, "html.parser")
|
||||
divs = soup.findAll(class_="metadata-display")
|
||||
if not divs:
|
||||
return 0
|
||||
div = divs[0]
|
||||
plays = div.get_text()
|
||||
return int(plays.split(" ")[0].replace(",", ""))
|
10
lastfm/utils/tokencheck.py
Normal file
10
lastfm/utils/tokencheck.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
async def tokencheck(ctx):
|
||||
token = await ctx.bot.get_shared_api_tokens("lastfm")
|
||||
return bool(token.get("appid"))
|
||||
|
||||
|
||||
async def tokencheck_plus_secret(ctx):
|
||||
token = await ctx.bot.get_shared_api_tokens("lastfm")
|
||||
if token.get("appid") and token.get("secret"):
|
||||
return True
|
||||
return False
|
279
lastfm/whoknows.py
Normal file
279
lastfm/whoknows.py
Normal file
|
@ -0,0 +1,279 @@
|
|||
import asyncio
|
||||
|
||||
import discord
|
||||
from redbot.core import commands
|
||||
from redbot.core.utils.menus import DEFAULT_CONTROLS, menu
|
||||
|
||||
from .abc import MixinMeta
|
||||
from .exceptions import *
|
||||
from .utils.tokencheck import tokencheck
|
||||
|
||||
|
||||
class WhoKnowsMixin(MixinMeta):
|
||||
"""WhoKnows Commands"""
|
||||
|
||||
@commands.command(name="whoknows", usage="<artist name>", aliases=["wk"])
|
||||
@commands.check(tokencheck)
|
||||
@commands.guild_only()
|
||||
@commands.cooldown(2, 10, type=commands.BucketType.user)
|
||||
async def command_whoknows(self, ctx, *, artistname=None):
|
||||
"""Check who has listened to a given artist the most."""
|
||||
listeners = []
|
||||
tasks = []
|
||||
async with ctx.typing():
|
||||
userlist = await self.config.all_users()
|
||||
guildusers = [x.id for x in ctx.guild.members]
|
||||
userslist = [user for user in userlist if user in guildusers]
|
||||
if not artistname:
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in(conf)
|
||||
trackname, artistname, albumname, image_url = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
for user in userslist:
|
||||
lastfm_username = userlist[user]["lastfm_username"]
|
||||
if lastfm_username is None:
|
||||
continue
|
||||
member = ctx.guild.get_member(user)
|
||||
if member is None:
|
||||
continue
|
||||
|
||||
tasks.append(
|
||||
self.get_playcount(ctx, lastfm_username, artistname, "overall", member)
|
||||
)
|
||||
if tasks:
|
||||
data = await asyncio.gather(*tasks)
|
||||
data = [i for i in data if i]
|
||||
for playcount, user, name in data:
|
||||
artistname = name
|
||||
if playcount > 0:
|
||||
listeners.append((playcount, user))
|
||||
else:
|
||||
return await ctx.send(
|
||||
"Nobody on this server has connected their last.fm account yet!"
|
||||
)
|
||||
rows = []
|
||||
total = 0
|
||||
for i, (playcount, user) in enumerate(
|
||||
sorted(listeners, key=lambda p: p[0], reverse=True), start=1
|
||||
):
|
||||
if i == 1:
|
||||
rank = "\N{CROWN}"
|
||||
old_kingdata = await self.config.guild(ctx.guild).crowns()
|
||||
old_kingartist = old_kingdata.get(artistname.lower())
|
||||
if old_kingartist is not None:
|
||||
old_king = old_kingartist["user"]
|
||||
old_king = ctx.guild.get_member(old_king)
|
||||
else:
|
||||
old_king = None
|
||||
new_king = user
|
||||
play = playcount
|
||||
else:
|
||||
rank = f"`#{i:2}`"
|
||||
rows.append(
|
||||
f"{rank} **{user.name}** — **{playcount}** {self.format_plays(playcount)}"
|
||||
)
|
||||
total += playcount
|
||||
|
||||
if not rows:
|
||||
return await ctx.send(f"Nobody on this server has listened to **{artistname}**")
|
||||
|
||||
content = discord.Embed(
|
||||
title=f"Who knows **{artistname}**?",
|
||||
color=await self.bot.get_embed_color(ctx.channel),
|
||||
)
|
||||
image_url = await self.scrape_artist_image(artistname, ctx)
|
||||
content.set_thumbnail(url=image_url)
|
||||
content.set_footer(text=f"Collective plays: {total}")
|
||||
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
||||
if old_king is None:
|
||||
await ctx.send(f"> **{new_king.name}** just earned the **{artistname}** crown.")
|
||||
async with self.config.guild(ctx.guild).crowns() as crowns:
|
||||
crowns[artistname.lower()] = {"user": new_king.id, "playcount": play}
|
||||
if isinstance(old_king, discord.Member):
|
||||
if not (old_king.id == new_king.id):
|
||||
await ctx.send(
|
||||
f"> **{new_king.name}** just stole the **{artistname}** crown from **{old_king.name}**."
|
||||
)
|
||||
async with self.config.guild(ctx.guild).crowns() as crowns:
|
||||
crowns[artistname.lower()] = {"user": new_king.id, "playcount": play}
|
||||
if old_king.id == new_king.id:
|
||||
async with self.config.guild(ctx.guild).crowns() as crowns:
|
||||
crowns[artistname.lower()] = {"user": new_king.id, "playcount": play}
|
||||
|
||||
@commands.command(
|
||||
name="whoknowstrack", usage="<track name> | <artist name>", aliases=["wkt", "whoknowst"]
|
||||
)
|
||||
@commands.check(tokencheck)
|
||||
@commands.guild_only()
|
||||
@commands.cooldown(2, 15, type=commands.BucketType.user)
|
||||
async def command_whoknowstrack(self, ctx, *, track=None):
|
||||
"""
|
||||
Check who has listened to a given song the most.
|
||||
"""
|
||||
if not track:
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in(conf)
|
||||
trackname, artistname, albumname, image_url = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
else:
|
||||
try:
|
||||
trackname, artistname = [x.strip() for x in track.split("|")]
|
||||
if trackname == "" or artistname == "":
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
return await ctx.send("\N{WARNING SIGN} Incorrect format! use `track | artist`")
|
||||
|
||||
listeners = []
|
||||
tasks = []
|
||||
userlist = await self.config.all_users()
|
||||
guildusers = [x.id for x in ctx.guild.members]
|
||||
userslist = [user for user in userlist if user in guildusers]
|
||||
for user in userslist:
|
||||
lastfm_username = userlist[user]["lastfm_username"]
|
||||
member = ctx.guild.get_member(user)
|
||||
if member is None or lastfm_username is None:
|
||||
continue
|
||||
|
||||
tasks.append(
|
||||
self.get_playcount_track(
|
||||
ctx, lastfm_username, artistname, trackname, "overall", member
|
||||
)
|
||||
)
|
||||
|
||||
if tasks:
|
||||
data = await asyncio.gather(*tasks)
|
||||
data = [i for i in data if i]
|
||||
for playcount, user, metadata in data:
|
||||
artistname, trackname, image_url = metadata
|
||||
if artistname is None or trackname is None:
|
||||
return await ctx.send("Track could not be found on last.fm!")
|
||||
if playcount > 0:
|
||||
listeners.append((playcount, user))
|
||||
else:
|
||||
return await ctx.send("Nobody on this server has connected their last.fm account yet!")
|
||||
|
||||
rows = []
|
||||
total = 0
|
||||
for i, (playcount, user) in enumerate(
|
||||
sorted(listeners, key=lambda p: p[0], reverse=True), start=1
|
||||
):
|
||||
rows.append(
|
||||
f"`#{i:2}` **{user.name}** — **{playcount}** {self.format_plays(playcount)}"
|
||||
)
|
||||
total += playcount
|
||||
|
||||
if not rows:
|
||||
return await ctx.send(
|
||||
f"Nobody on this server has listened to **{trackname}** by **{artistname}**"
|
||||
)
|
||||
if image_url is None:
|
||||
image_url = await self.scrape_artist_image(artistname, ctx)
|
||||
|
||||
content = discord.Embed(
|
||||
title=f"Who knows **{trackname}**\n— by {artistname}",
|
||||
color=await self.bot.get_embed_color(ctx.channel),
|
||||
)
|
||||
content.set_thumbnail(url=image_url)
|
||||
content.set_footer(text=f"Collective plays: {total}")
|
||||
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
||||
|
||||
@commands.command(
|
||||
name="whoknowsalbum", aliases=["wka", "whoknowsa"], usage="<album name> | <artist name>"
|
||||
)
|
||||
@commands.check(tokencheck)
|
||||
@commands.guild_only()
|
||||
@commands.cooldown(2, 15, type=commands.BucketType.user)
|
||||
async def command_whoknowsalbum(self, ctx, *, album=None):
|
||||
"""
|
||||
Check who has listened to a given album the most.
|
||||
"""
|
||||
if not album:
|
||||
conf = await self.config.user(ctx.author).all()
|
||||
self.check_if_logged_in(conf)
|
||||
|
||||
trackname, artistname, albumname, image_url = await self.get_current_track(
|
||||
ctx, conf["lastfm_username"]
|
||||
)
|
||||
if not albumname:
|
||||
return await ctx.send(
|
||||
"Sorry, the track you're listening to doesn't have the album info provided."
|
||||
)
|
||||
else:
|
||||
try:
|
||||
albumname, artistname = [x.strip() for x in album.split("|")]
|
||||
if not albumname or not artistname:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
return await ctx.send("\N{WARNING SIGN} Incorrect format! use `album | artist`")
|
||||
|
||||
listeners = []
|
||||
tasks = []
|
||||
userlist = await self.config.all_users()
|
||||
guildusers = [x.id for x in ctx.guild.members]
|
||||
userslist = [user for user in userlist if user in guildusers]
|
||||
for user in userslist:
|
||||
lastfm_username = userlist[user]["lastfm_username"]
|
||||
member = ctx.guild.get_member(user)
|
||||
if member is None or lastfm_username is None:
|
||||
continue
|
||||
|
||||
tasks.append(
|
||||
self.get_playcount_album(
|
||||
ctx, lastfm_username, artistname, albumname, "overall", member
|
||||
)
|
||||
)
|
||||
|
||||
if tasks:
|
||||
data = await asyncio.gather(*tasks)
|
||||
data = [i for i in data if i]
|
||||
for playcount, user, metadata in data:
|
||||
artistname, albumname, image_url = metadata
|
||||
if artistname is None or albumname is None:
|
||||
return await ctx.send("Album could not be found on last.fm!")
|
||||
if playcount > 0:
|
||||
listeners.append((playcount, user))
|
||||
else:
|
||||
return await ctx.send("Nobody on this server has connected their last.fm account yet!")
|
||||
|
||||
rows = []
|
||||
total = 0
|
||||
for i, (playcount, user) in enumerate(
|
||||
sorted(listeners, key=lambda p: p[0], reverse=True), start=1
|
||||
):
|
||||
rows.append(
|
||||
f"`#{i:2}` **{user.name}** — **{playcount}** {self.format_plays(playcount)}"
|
||||
)
|
||||
total += playcount
|
||||
|
||||
if not rows:
|
||||
return await ctx.send(
|
||||
f"Nobody on this server has listened to **{albumname}** by **{artistname}**"
|
||||
)
|
||||
|
||||
if image_url is None:
|
||||
image_url = await self.scrape_artist_image(artistname, ctx)
|
||||
|
||||
content = discord.Embed(
|
||||
title=f"Who knows **{albumname}**\n— by {artistname}",
|
||||
color=await self.bot.get_embed_color(ctx.channel),
|
||||
)
|
||||
content.set_thumbnail(url=image_url)
|
||||
content.set_footer(text=f"Collective plays: {total}")
|
||||
|
||||
pages = await self.create_pages(content, rows)
|
||||
if len(pages) > 1:
|
||||
await menu(ctx, pages, DEFAULT_CONTROLS)
|
||||
else:
|
||||
await ctx.send(embed=pages[0])
|
108
lastfm/wordcloud.py
Normal file
108
lastfm/wordcloud.py
Normal file
|
@ -0,0 +1,108 @@
|
|||
from contextlib import suppress
|
||||
from io import BytesIO
|
||||
from typing import Optional
|
||||
|
||||
import discord
|
||||
from redbot.core import commands
|
||||
|
||||
from .abc import MixinMeta
|
||||
from .exceptions import *
|
||||
from .fmmixin import FMMixin
|
||||
|
||||
command_fm = FMMixin.command_fm
|
||||
command_fm_server = FMMixin.command_fm_server
|
||||
|
||||
with suppress(Exception):
|
||||
from wordcloud import WordCloud
|
||||
|
||||
|
||||
async def wordcloud_available(ctx):
|
||||
return "WordCloud" in globals().keys()
|
||||
|
||||
|
||||
class WordCloudMixin(MixinMeta):
|
||||
"""WordCloud Commands"""
|
||||
|
||||
def wordcloud_create(self):
|
||||
if "WordCloud" in globals().keys():
|
||||
self.wc = WordCloud(width=1920, height=1080, mode="RGBA", background_color=None)
|
||||
|
||||
@command_fm.group(name="wordcloud", aliases=["cloud", "wc"])
|
||||
@commands.check(wordcloud_available)
|
||||
@commands.bot_has_permissions(attach_files=True)
|
||||
async def command_wordcloud(self, ctx):
|
||||
"""WordCloud Commands
|
||||
|
||||
Original idea: http://lastfm.dontdrinkandroot.net"""
|
||||
|
||||
@command_wordcloud.command(name="artists", aliases=["artist"])
|
||||
@commands.max_concurrency(1, commands.BucketType.user)
|
||||
async def command_wordcloud_artists(self, ctx, user: Optional[discord.Member] = None):
|
||||
"""Get a picture with the most listened to artists."""
|
||||
user = user or ctx.author
|
||||
async with ctx.typing():
|
||||
conf = await self.config.user(user).all()
|
||||
self.check_if_logged_in(conf, user == ctx.author)
|
||||
name = conf["lastfm_username"]
|
||||
data = await self.api_request(
|
||||
ctx, {"user": name, "method": "user.gettopartists", "period": "overall"}
|
||||
)
|
||||
artists = data["topartists"]["artist"]
|
||||
if not artists:
|
||||
return await ctx.send(f"{name} has not listened to any artists yet!")
|
||||
data = {a["name"]: int(a["playcount"]) for a in artists}
|
||||
wc = await self.bot.loop.run_in_executor(None, self.wc.generate_from_frequencies, data)
|
||||
pic = BytesIO()
|
||||
pic.name = f"{name}_artists.png"
|
||||
wc.to_file(pic)
|
||||
pic.seek(0)
|
||||
await ctx.send(f"{name}'s artist cloud:", file=discord.File(pic))
|
||||
pic.close()
|
||||
|
||||
@command_wordcloud.command(name="tracks", aliases=["track"])
|
||||
@commands.max_concurrency(1, commands.BucketType.user)
|
||||
async def command_wordcloud_tracks(self, ctx, user: Optional[discord.Member] = None):
|
||||
"""Get a picture with the most listened to tracks."""
|
||||
user = user or ctx.author
|
||||
async with ctx.typing():
|
||||
conf = await self.config.user(user).all()
|
||||
self.check_if_logged_in(conf, user == ctx.author)
|
||||
name = conf["lastfm_username"]
|
||||
data = await self.api_request(
|
||||
ctx, {"user": name, "method": "user.gettoptracks", "period": "overall"}
|
||||
)
|
||||
tracks = data["toptracks"]["track"]
|
||||
if not tracks:
|
||||
return await ctx.send(f"{name} has not listened to any tracks yet!")
|
||||
data = {a["name"]: int(a["playcount"]) for a in tracks}
|
||||
wc = await self.bot.loop.run_in_executor(None, self.wc.generate_from_frequencies, data)
|
||||
pic = BytesIO()
|
||||
pic.name = f"{name}_tracks.png"
|
||||
wc.to_file(pic)
|
||||
pic.seek(0)
|
||||
await ctx.send(f"{name}'s track cloud:", file=discord.File(pic))
|
||||
pic.close()
|
||||
|
||||
@command_wordcloud.command(name="albums", aliases=["album"])
|
||||
@commands.max_concurrency(1, commands.BucketType.user)
|
||||
async def command_wordcloud_albums(self, ctx, user: Optional[discord.Member] = None):
|
||||
"""Get a picture with the most listened to albums."""
|
||||
user = user or ctx.author
|
||||
async with ctx.typing():
|
||||
conf = await self.config.user(user).all()
|
||||
self.check_if_logged_in(conf, user == ctx.author)
|
||||
name = conf["lastfm_username"]
|
||||
data = await self.api_request(
|
||||
ctx, {"user": name, "method": "user.gettopalbums", "period": "overall"}
|
||||
)
|
||||
albums = data["topalbums"]["album"]
|
||||
if not albums:
|
||||
return await ctx.send(f"{name} has not listened to any albums yet!")
|
||||
data = {a["name"]: int(a["playcount"]) for a in albums}
|
||||
wc = await self.bot.loop.run_in_executor(None, self.wc.generate_from_frequencies, data)
|
||||
pic = BytesIO()
|
||||
pic.name = f"{name}_albums.png"
|
||||
wc.to_file(pic)
|
||||
pic.seek(0)
|
||||
await ctx.send(f"{name}'s albums cloud:", file=discord.File(pic))
|
||||
pic.close()
|
Loading…
Add table
Reference in a new issue