Ruby-Cogs/lastfm/utils/base.py
2025-03-26 10:59:44 -04:00

399 lines
14 KiB
Python

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