lfcircle: modularise more things, fix erroneous print

This commit is contained in:
Mark Joshwel 2024-05-11 16:20:05 +00:00
parent 83d7892039
commit 97e541d147

View file

@ -31,6 +31,7 @@ For more information, please refer to <http://unlicense.org/>
""" """
from argparse import ArgumentParser from argparse import ArgumentParser
from bisect import insort
from datetime import datetime, timedelta from datetime import datetime, timedelta
from enum import Enum from enum import Enum
from functools import wraps from functools import wraps
@ -38,15 +39,15 @@ from sys import stderr
from textwrap import indent from textwrap import indent
from time import sleep from time import sleep
from traceback import format_exception from traceback import format_exception
from typing import Callable, NamedTuple, ParamSpec, TypeVar from typing import Callable, Final, NamedTuple, ParamSpec, TypeVar
from urllib.parse import unquote from urllib.parse import unquote
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from requests import Response from requests import Response, get
from requests import get as _get
USER_AGENT = ( FORMAT_TELEGRAM_PREFIX: Final[str] = " "
"Mozilla/5.0 (compatible; lfcircle; https://github.com/markjoshwel/lfcircle)" USER_AGENT: Final[str] = (
"Mozilla/5.0 " "(compatible; lfcircle; https://github.com/markjoshwel/lfcircle)"
) )
@ -176,7 +177,6 @@ class Limiter:
"""helper to class to not bomb last.hq""" """helper to class to not bomb last.hq"""
max_per_second: int = 1 max_per_second: int = 1
user_agent: str = USER_AGENT
last_call: datetime | None = None last_call: datetime | None = None
def limit( def limit(
@ -250,20 +250,19 @@ class ListeningReport(NamedTuple):
leaderboard_tracks_pos: int, leaderboard_tracks_pos: int,
leaderboard_n: int, leaderboard_n: int,
) -> str: ) -> str:
basket: list[str] = []
prefix: str = ""
text: str = "" text: str = ""
match behaviour.format: match behaviour.format:
case FormatTypeEnum.ASCII: case FormatTypeEnum.ASCII:
basket: list[str] = []
# intro # intro
basket.append( basket.append(
(_prefix := f"{leaderboard_pos}. ") (_prefix := f"{leaderboard_pos}. ")
+ f"{self.user} — Σ{self.listening_time_hours}h; {self.scrobbles_daily_avg}s/d " + f"{self.user} — Σ{self.listening_time_hours}h; {self.scrobbles_daily_avg}s/d "
) )
basket.append( basket.append(
indent(f"<{self.url}>", prefix=(prefix := " " * len(_prefix))) indent(f"<{self.url}>", prefix=(prefix := " " * len(_prefix))) + "\n"
+ "\n"
) )
rmax = len(f"#{leaderboard_n}") rmax = len(f"#{leaderboard_n}")
@ -307,7 +306,7 @@ class ListeningReport(NamedTuple):
# detail 4: total period tracks count # detail 4: total period tracks count
d4_l = indent( d4_l = indent(
f"{self.artists_count} tracks".ljust(len(ls)) f"{self.tracks_count} tracks".ljust(len(ls))
+ " (" + " ("
+ f"#{leaderboard_tracks_pos}".rjust(rmax) + f"#{leaderboard_tracks_pos}".rjust(rmax)
+ ") : ", + ") : ",
@ -328,8 +327,7 @@ class ListeningReport(NamedTuple):
text = "\n".join(basket[:3] + [s.lower() for s in basket[3:]]) text = "\n".join(basket[:3] + [s.lower() for s in basket[3:]])
case FormatTypeEnum.TELEGRAM: case FormatTypeEnum.TELEGRAM:
basket: list[str] = [] prefix = FORMAT_TELEGRAM_PREFIX
prefix: str = " "
# intro # intro
basket.append( basket.append(
@ -394,8 +392,13 @@ def get_listening_report(
limiter: Limiter, limiter: Limiter,
behaviour: Behaviour, behaviour: Behaviour,
) -> ListeningReport: ) -> ListeningReport:
target_url: str = f"https://www.last.fm/user/{target}/listening-report/week" target_url: str = f"https://www.last.fm/user/{target}/listening-report/week"
page_res: Response = limiter.limit(_get)(target_url)
page_res: Response = limiter.limit(get)(
target_url,
headers={"User-Agent": USER_AGENT},
)
if page_res.status_code != 200: if page_res.status_code != 200:
raise Exception( raise Exception(
@ -422,9 +425,22 @@ def get_listening_report(
) )
def _int(number: str) -> int:
n = (
number.replace(",", "")
.replace("scrobbles", "")
.strip()
.lstrip("days,")
.rstrip("hours")
.strip()
)
assert n.isnumeric()
return int(n)
def _get_scrobbles_count(page: BeautifulSoup) -> int: def _get_scrobbles_count(page: BeautifulSoup) -> int:
assert (_1 := page.select_one(".report-headline-total")) is not None assert (_1 := page.select_one(".report-headline-total")) is not None
return int(_1.text.strip().replace(",", "")) return _int(_1.text)
def _get_scrobbles_daily_avg(page: BeautifulSoup) -> int: def _get_scrobbles_daily_avg(page: BeautifulSoup) -> int:
@ -434,7 +450,7 @@ def _get_scrobbles_daily_avg(page: BeautifulSoup) -> int:
continue continue
assert (_1 := fact.select_one(".quick-fact-data-value")) is not None assert (_1 := fact.select_one(".quick-fact-data-value")) is not None
return int(_1.text.strip().replace(",", "")) return _int(_1.text)
else: else:
raise Exception(f"could not find '{needle}' fact, {len(facts)=}") raise Exception(f"could not find '{needle}' fact, {len(facts)=}")
@ -447,10 +463,10 @@ def _get_listening_time_hours(page: BeautifulSoup) -> int:
continue continue
assert (_d1 := fact.select_one(".quick-fact-data-value")) is not None assert (_d1 := fact.select_one(".quick-fact-data-value")) is not None
days: int = int(_d1.text.strip().replace(",", "")) days: int = _int(_d1.text)
assert (_h1 := fact.select_one(".quick-fact-data-detail")) is not None assert (_h1 := fact.select_one(".quick-fact-data-detail")) is not None
hours: int = int(_h1.text.strip().lstrip("days,").rstrip("hours").strip()) hours: int = _int(_h1.text)
return (days * 24) + hours return (days * 24) + hours
@ -461,7 +477,7 @@ def _get_listening_time_hours(page: BeautifulSoup) -> int:
def _get_overview_scrobbles(page: BeautifulSoup, needle: str) -> int: def _get_overview_scrobbles(page: BeautifulSoup, needle: str) -> int:
assert (_1 := page.select_one(needle)) is not None assert (_1 := page.select_one(needle)) is not None
assert (_2 := _1.select_one(".top-item-overview__scrobbles")) is not None assert (_2 := _1.select_one(".top-item-overview__scrobbles")) is not None
return int(_2.text.strip().replace(",", "")) return _int(_2.text)
def _get_artists_count(page: BeautifulSoup) -> int: def _get_artists_count(page: BeautifulSoup) -> int:
@ -494,7 +510,7 @@ def _get_top_overview(
things.append( things.append(
ThingWithScrobbles( ThingWithScrobbles(
name=_12.text.strip(), name=_12.text.strip(),
scrobbles=int(_13.text.strip().replace(",", "")), scrobbles=_int(_13.text),
url=f"https://www.last.fm{_14.attrs.get('href', '/')}", url=f"https://www.last.fm{_14.attrs.get('href', '/')}",
) )
) )
@ -504,9 +520,7 @@ def _get_top_overview(
if len(top.select(select_needle)) == 0: if len(top.select(select_needle)) == 0:
continue continue
assert ( assert (_n := top.select(".listening-report-secondary-top-item-name")) is not None
_n := top.select(".listening-report-secondary-top-item-name")
) is not None
assert ( assert (
_v := top.select(".listening-report-secondary-top-item-value") _v := top.select(".listening-report-secondary-top-item-value")
) is not None ) is not None
@ -514,7 +528,7 @@ def _get_top_overview(
for n, v in zip( for n, v in zip(
[x.text.strip() for x in _n], [x.text.strip() for x in _n],
[int(y.text.strip()) for y in _v], [_int(y.text) for y in _v],
): ):
things.append(ThingWithScrobbles(name=n, scrobbles=v)) things.append(ThingWithScrobbles(name=n, scrobbles=v))
@ -583,8 +597,18 @@ def _get_tracks_top_new(page: BeautifulSoup) -> ThingWithScrobbles:
return _get_top_new_thing(page=page, select_needle=".top-new-item-type__track") return _get_top_new_thing(page=page, select_needle=".top-new-item-type__track")
def _sorter(r: ListeningReport) -> int: def _rank(
return r.listening_time_hours + r.scrobbles_count r: ListeningReport, rs: list[ListeningReport], k: Callable[[ListeningReport], int]
):
ranking: list[ListeningReport] = []
for _r in rs:
insort(ranking, _r, key=k)
for i, _r in enumerate(reversed(ranking), start=1):
if _r.user == r.user:
return i
else:
return 0
def make_circle_report( def make_circle_report(
@ -604,69 +628,38 @@ def make_circle_report(
text.append(behaviour.header + "\n") text.append(behaviour.header + "\n")
for leaderboard_pos, report in enumerate( for leaderboard_pos, report in enumerate(
reversed(sorted(listening_reports, key=_sorter)), reversed(
sorted(
listening_reports,
key=lambda r: r.listening_time_hours + r.scrobbles_count,
)
),
start=1, start=1,
): ):
leaderboard_scrobble_pos: int = 0
for leaderboard_scrobble_pos, _report in enumerate(
reversed(
sorted(
listening_reports,
key=lambda r: r.scrobbles_count,
)
),
start=1,
):
if report == _report:
break
leaderboard_artists_pos: int = 0
for leaderboard_artists_pos, _report in enumerate(
reversed(
sorted(
listening_reports,
key=lambda r: r.artists_count,
)
),
start=1,
):
if report == _report:
break
leaderboard_albums_pos: int = 0
for leaderboard_albums_pos, _report in enumerate(
reversed(
sorted(
listening_reports,
key=lambda r: r.albums_count,
)
),
start=1,
):
if report == _report:
break
leaderboard_tracks_pos: int = 0
for leaderboard_tracks_pos, _report in enumerate(
reversed(
sorted(
listening_reports,
key=lambda r: r.tracks_count,
)
),
start=1,
):
if report == _report:
break
text.append( text.append(
report.to_str( report.to_str(
behaviour=behaviour, behaviour=behaviour,
leaderboard_pos=leaderboard_pos, leaderboard_pos=leaderboard_pos,
leaderboard_scrobble_pos=leaderboard_scrobble_pos, leaderboard_scrobble_pos=_rank(
leaderboard_artists_pos=leaderboard_artists_pos, r=report,
leaderboard_albums_pos=leaderboard_albums_pos, rs=listening_reports,
leaderboard_tracks_pos=leaderboard_tracks_pos, k=lambda r: r.listening_time_hours + r.scrobbles_count,
),
leaderboard_artists_pos=_rank(
r=report,
rs=listening_reports,
k=lambda r: r.listening_time_hours + r.artists_count,
),
leaderboard_albums_pos=_rank(
r=report,
rs=listening_reports,
k=lambda r: r.listening_time_hours + r.albums_count,
),
leaderboard_tracks_pos=_rank(
r=report,
rs=listening_reports,
k=lambda r: r.listening_time_hours + r.tracks_count,
),
leaderboard_n=len(listening_reports), leaderboard_n=len(listening_reports),
) )
+ "\n" + "\n"
@ -702,12 +695,12 @@ def cli() -> None:
else: else:
print( print(
f"{i + 1}/{len(behaviour.targets)}", f"got {target}'s reports... ({i + 1}/{len(behaviour.targets)})",
file=stderr, file=stderr,
end="\r",
) )
print(reports[-1], file=stderr) if behaviour.verbose else ... print(reports[-1], file=stderr) if behaviour.verbose else ...
print(file=stderr)
print(make_circle_report(listening_reports=reports, behaviour=behaviour)) print(make_circle_report(listening_reports=reports, behaviour=behaviour))