diff --git a/lfcircle.py b/lfcircle.py index 8a853ee..2b1dda0 100644 --- a/lfcircle.py +++ b/lfcircle.py @@ -32,6 +32,7 @@ For more information, please refer to from argparse import ArgumentParser from bisect import insort +from collections import Counter from datetime import datetime, timedelta from enum import Enum from functools import wraps @@ -239,6 +240,7 @@ class ListeningReport(NamedTuple): albums_top_new: ThingWithScrobbles tracks_top_new: ThingWithScrobbles listening_time_hours: int + tags: dict[str, tuple[int, ...]] def to_str( self, @@ -262,7 +264,8 @@ class ListeningReport(NamedTuple): + f"{self.user} — Σ{self.listening_time_hours}h; {self.scrobbles_daily_avg}s/d " ) basket.append( - indent(f"<{self.url}>", prefix=(prefix := " " * len(_prefix))) + "\n" + indent(f"<{self.url}>", prefix=(prefix := " " * len(_prefix))) + + "\n" ) rmax = len(f"#{leaderboard_n}") @@ -320,6 +323,29 @@ class ListeningReport(NamedTuple): ) basket.append(d4_l + d4_r + d4_url) + # detail 5: top five tags + if len(self.tags) > 0: + tag_counter = Counter[str]() + + for tag_name, tag_values in self.tags.items(): + tag_counter.update({tag_name: sum(tag_values)}) + + basket.append( + indent( + ( + "Top tags".ljust(len(d4_l) - 6) + + " : " + + ", ".join( + [ + f"{tn} ({tc})" + for tn, tc in tag_counter.most_common(5) + ] + ) + ), + prefix=prefix, + ) + ) + if not behaviour.lowercase: text = "\n".join(basket) @@ -340,7 +366,7 @@ class ListeningReport(NamedTuple): # detail 2: total period artist count basket.append( - f"{prefix}{self.artists_count} artists (#{leaderboard_artists_pos}): " + f"\n{prefix}{self.artists_count} artists (#{leaderboard_artists_pos}): " + ( f"[{self.artists[0].name}]({self.artists[0].url})" if behaviour.all_the_links @@ -370,6 +396,21 @@ class ListeningReport(NamedTuple): ) ) + # detail 5: top tags + if len(self.tags) > 0: + tag_counter = Counter[str]() + + for tag_name, tag_values in self.tags.items(): + tag_counter.update({tag_name: sum(tag_values)}) + + basket.append( + f"\n{prefix}Top tags" + + ": " + + ", ".join( + [f"{tn} ({tc})" for tn, tc in tag_counter.most_common(5)] + ) + ) + if not behaviour.lowercase: text = "\n".join(basket) @@ -392,7 +433,6 @@ def get_listening_report( limiter: Limiter, behaviour: Behaviour, ) -> ListeningReport: - target_url: str = f"https://www.last.fm/user/{target}/listening-report/week" page_res: Response = limiter.limit(get)( @@ -422,6 +462,7 @@ def get_listening_report( albums_top_new=_get_albums_top_new(page), tracks_top_new=_get_tracks_top_new(page), listening_time_hours=_get_listening_time_hours(page), + tags=_get_tags(page), ) @@ -520,7 +561,9 @@ def _get_top_overview( if len(top.select(select_needle)) == 0: continue - assert (_n := top.select(".listening-report-secondary-top-item-name")) is not None + assert ( + _n := top.select(".listening-report-secondary-top-item-name") + ) is not None assert ( _v := top.select(".listening-report-secondary-top-item-value") ) is not None @@ -597,6 +640,34 @@ def _get_tracks_top_new(page: BeautifulSoup) -> ThingWithScrobbles: return _get_top_new_thing(page=page, select_needle=".top-new-item-type__track") +def _get_tags(page: BeautifulSoup) -> dict[str, tuple[int, ...]]: + tags: dict[str, tuple[int, ...]] = {} + + assert (_1 := page.select_one("#top-tags-over-time")) is not None + assert (_2 := _1.select_one(".js-top-tags-over-time-table")) is not None + assert (_3 := _2.select_one("tbody")) is not None + + for tr in _3.select("tr"): + tag_name: str = "" + tag_counts: list[int] = [] + + for i, td in enumerate(tr.select("td")): + if i == 0: + tag_name = td.text.strip() + + else: + assert ( + num := td.text.strip() + ).isnumeric(), ( + "second tag table td onwards should be numeric, but isn't" + ) + tag_counts.append(int(num)) + + tags.update({tag_name: tuple(tag_counts)}) + + return tags + + def _rank( r: ListeningReport, rs: list[ListeningReport], k: Callable[[ListeningReport], int] ): diff --git a/poetry.lock b/poetry.lock index 624b7eb..fc40316 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "beautifulsoup4" @@ -330,13 +330,13 @@ files = [ [[package]] name = "platformdirs" -version = "4.2.1" +version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ - {file = "platformdirs-4.2.1-py3-none-any.whl", hash = "sha256:17d5a1161b3fd67b390023cb2d3b026bbd40abde6fdb052dfbd3a29c3ba22ee1"}, - {file = "platformdirs-4.2.1.tar.gz", hash = "sha256:031cd18d4ec63ec53e82dceaac0417d218a6863f7745dfcc9efe7793b7039bdf"}, + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, ] [package.extras] @@ -346,13 +346,13 @@ type = ["mypy (>=1.8)"] [[package]] name = "requests" -version = "2.31.0" +version = "2.32.2" description = "Python HTTP for Humans." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, - {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, + {file = "requests-2.32.2-py3-none-any.whl", hash = "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c"}, + {file = "requests-2.32.2.tar.gz", hash = "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289"}, ] [package.dependencies] @@ -400,13 +400,13 @@ files = [ [[package]] name = "types-beautifulsoup4" -version = "4.12.0.20240504" +version = "4.12.0.20240511" description = "Typing stubs for beautifulsoup4" optional = false python-versions = ">=3.8" files = [ - {file = "types-beautifulsoup4-4.12.0.20240504.tar.gz", hash = "sha256:d7b7af4ccc52fc22784d33a529695e34329a9bdd5db6a649c9b25cb2c3a148d5"}, - {file = "types_beautifulsoup4-4.12.0.20240504-py3-none-any.whl", hash = "sha256:84e04e4473b3c79da04d9d1f89d20a38e5cc92f9c080a8e1f9d36a287b350465"}, + {file = "types-beautifulsoup4-4.12.0.20240511.tar.gz", hash = "sha256:004f6096fdd83b19cdbf6cb10e4eae57b10205eccc365d0a69d77da836012e28"}, + {file = "types_beautifulsoup4-4.12.0.20240511-py3-none-any.whl", hash = "sha256:7ceda66a93ba28d759d5046d7fec9f4cad2f563a77b3a789efc90bcadafeefd1"}, ] [package.dependencies] @@ -425,13 +425,13 @@ files = [ [[package]] name = "types-requests" -version = "2.31.0.20240406" +version = "2.32.0.20240523" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" files = [ - {file = "types-requests-2.31.0.20240406.tar.gz", hash = "sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1"}, - {file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"}, + {file = "types-requests-2.32.0.20240523.tar.gz", hash = "sha256:26b8a6de32d9f561192b9942b41c0ab2d8010df5677ca8aa146289d11d505f57"}, + {file = "types_requests-2.32.0.20240523-py3-none-any.whl", hash = "sha256:f19ed0e2daa74302069bbbbf9e82902854ffa780bc790742a810a9aaa52f65ec"}, ] [package.dependencies] @@ -439,13 +439,13 @@ urllib3 = ">=2" [[package]] name = "typing-extensions" -version = "4.11.0" +version = "4.12.0" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, - {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, + {file = "typing_extensions-4.12.0-py3-none-any.whl", hash = "sha256:b349c66bea9016ac22978d800cfff206d5f9816951f12a7d0ec5578b0a819594"}, + {file = "typing_extensions-4.12.0.tar.gz", hash = "sha256:8cbcdc8606ebcb0d95453ad7dc5065e6237b6aa230a31e81d0f440c30fed5fd8"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 9fe1240..7ab2092 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "lfcircle" -version = "0.1.2" +version = "0.1.3" description = "last.fm statistics generator for your friend circle!" authors = ["Mark Joshwel "] license = "Unlicense"