Skip to content

Commit

Permalink
LB-1563: add API endpoints to retrieve total listen and listener count (
Browse files Browse the repository at this point in the history
#2865)

* LB-1563: add API endpoints to retrieve total listen and listener count

We calculate total listen count and total count of unique listeners of recordings, artists,
releases and release groups using both ListenBrainz and MLHD+ data. This information is
displayed on the relevant entity pages. Add API endpoints to retrieve the data independently
as well.

* fix docstrings
  • Loading branch information
amCap1712 committed May 8, 2024
1 parent a0639d6 commit 6507d13
Show file tree
Hide file tree
Showing 7 changed files with 220 additions and 18 deletions.
1 change: 1 addition & 0 deletions docs/users/api/index.rst
Expand Up @@ -74,6 +74,7 @@ Reference
playlist
recordings
statistics
popularity
metadata
social
recommendation
Expand Down
12 changes: 12 additions & 0 deletions docs/users/api/popularity.rst
@@ -0,0 +1,12 @@
.. _popularity-api:

Popularity
==========

The popularity APIs return the total listen and listeners count for various entities and also a way to query top entities
for a given artist.

.. autoflask:: listenbrainz.webserver:create_app_rtfd()
:blueprints: popularity_api_v1
:include-empty-docstring:
:undoc-static:
4 changes: 2 additions & 2 deletions frontend/js/src/utils/APIService.ts
Expand Up @@ -1613,7 +1613,7 @@ export default class APIService {
getTopRecordingsForArtist = async (
artistMBID: string
): Promise<RecordingType[]> => {
const url = `${this.APIBaseURI}/popularity/top-recordings-for-artist?artist_mbid=${artistMBID}`;
const url = `${this.APIBaseURI}/popularity/top-recordings-for-artist/${artistMBID}`;
const response = await fetch(url);
await this.checkStatus(response);
return response.json();
Expand All @@ -1622,7 +1622,7 @@ export default class APIService {
getTopReleaseGroupsForArtist = async (
artistMBID: string
): Promise<ReleaseGroupType[]> => {
const url = `${this.APIBaseURI}/popularity/top-release-groups-for-artist?artist_mbid=${artistMBID}`;
const url = `${this.APIBaseURI}/popularity/top-release-groups-for-artist/${artistMBID}`;
const response = await fetch(url);
await this.checkStatus(response);
return response.json();
Expand Down
4 changes: 3 additions & 1 deletion listenbrainz/db/popularity.py
Expand Up @@ -156,8 +156,10 @@ def get_counts(ts_conn, entity, mbids):
entity_mbid = "release_group_mbid"
elif entity == "release":
entity_mbid = "release_mbid"
elif entity == "artist":
entity_mbid = "artist_mbid"
else:
return []
return [], {}

query = SQL("""
WITH mbids (mbid) AS (
Expand Down
1 change: 1 addition & 0 deletions listenbrainz/webserver/views/entity_pages.py
@@ -1,6 +1,7 @@
from datetime import datetime

from flask import Blueprint, render_template, current_app, jsonify
from werkzeug.exceptions import BadRequest

from listenbrainz.art.cover_art_generator import CoverArtGenerator
from listenbrainz.db import popularity, similarity
Expand Down
4 changes: 2 additions & 2 deletions listenbrainz/webserver/views/metadata_api.py
Expand Up @@ -155,13 +155,13 @@ def metadata_recording_post():

for mbid in recording_mbids:
if not is_valid_uuid(mbid):
raise APIBadRequest(f"Recording mbid {mbid} is not valid.")
raise APIBadRequest(f"recording_mbid {mbid} is not valid.")

if len(recording_mbids) == 0:
raise APIBadRequest("At least one valid recording_mbid must be present.")

if len(recording_mbids) > MAX_ITEMS_PER_GET:
raise APIBadRequest("Maximum number of recordings_mbids that can be fetchs at once is %s" % MAX_ITEMS_PER_GET)
raise APIBadRequest("Maximum number of recordings_mbids that can be fetched at once is %s" % MAX_ITEMS_PER_GET)

result = fetch_metadata(recording_mbids, incs)
return jsonify(result)
Expand Down
212 changes: 199 additions & 13 deletions listenbrainz/webserver/views/popularity_api.py
Expand Up @@ -5,18 +5,18 @@
from listenbrainz.webserver import ts_conn, db_conn
from listenbrainz.webserver.decorators import crossdomain
from listenbrainz.webserver.errors import APIBadRequest, APIInternalServerError
from listenbrainz.webserver.views.api_tools import is_valid_uuid
from listenbrainz.webserver.views.api_tools import is_valid_uuid, MAX_ITEMS_PER_GET

popularity_api_bp = Blueprint('popularity_api_v1', __name__)


@popularity_api_bp.get("/top-recordings-for-artist")
@popularity_api_bp.get("/top-recordings-for-artist/<artist_mbid>")
@crossdomain
@ratelimit()
def top_recordings():
def top_recordings_for_artist(artist_mbid):
""" Get the top recordings by listen count for a given artist. The response is of the following format:
.. code:: json
.. code-block:: json
[
{
Expand All @@ -41,12 +41,9 @@ def top_recordings():
}
]
:param artist_mbid: the mbid of the artist to get top recordings for
:type artist_mbid: ``str``
:statuscode 200: you have data!
:statuscode 400: invalid artist_mbid argument
:statuscode 400: invalid artist_mbid
"""
artist_mbid = request.args.get("artist_mbid")
if not is_valid_uuid(artist_mbid):
raise APIBadRequest(f"artist_mbid: '{artist_mbid}' is not a valid uuid")

Expand All @@ -58,13 +55,13 @@ def top_recordings():
raise APIInternalServerError("Failed to fetch metadata for recordings. Please try again.")


@popularity_api_bp.get("/top-release-groups-for-artist")
@popularity_api_bp.get("/top-release-groups-for-artist/<artist_mbid>")
@crossdomain
@ratelimit()
def top_release_groups():
def top_release_groups_for_artist(artist_mbid):
""" Get the top release groups by listen count for a given artist. The response is of the following format:
.. code:: json
.. code-block:: json
[
{
Expand Down Expand Up @@ -104,9 +101,8 @@ def top_release_groups():
:param artist_mbid: the mbid of the artist to get top release groups for
:type artist_mbid: ``str``
:statuscode 200: you have data!
:statuscode 400: invalid artist_mbid argument
:statuscode 400: invalid artist_mbid
"""
artist_mbid = request.args.get("artist_mbid")
if not is_valid_uuid(artist_mbid):
raise APIBadRequest(f"artist_mbid: '{artist_mbid}' is not a valid uuid")

Expand All @@ -116,3 +112,193 @@ def top_release_groups():
except Exception:
current_app.logger.error("Error while fetching metadata for release groups: ", exc_info=True)
raise APIInternalServerError("Failed to fetch metadata for release groups. Please try again.")


def fetch_entity_popularity_counts(entity):
""" Validate API request and retrieve popularity counts for the requested entities """
entity_mbid_key = f"{entity}_mbids"
try:
entity_mbids = request.json[entity_mbid_key]
except KeyError:
raise APIBadRequest(f"{entity_mbid_key} JSON element must be present and contain a list of {entity_mbid_key}")

for mbid in entity_mbids:
if not is_valid_uuid(mbid):
raise APIBadRequest(f"{entity}_mbid {mbid} is not valid.")

if len(entity_mbids) == 0:
raise APIBadRequest(f"At least one valid {entity}_mbid must be present.")

if len(entity_mbids) > MAX_ITEMS_PER_GET:
raise APIBadRequest(f"Maximum number of {entity_mbid_key} that can be fetched at once is %s" % MAX_ITEMS_PER_GET)

popularity_data, _ = popularity.get_counts(ts_conn, entity, entity_mbids)
return popularity_data


@popularity_api_bp.post("/recording")
@crossdomain
@ratelimit()
def popularity_recording():
""" Get the total listen count and total unique listeners count for a given recording.
A JSON document with a list of recording_mbids and inc string must be POSTed. Up to
:data:`~webserver.views.api.MAX_ITEMS_PER_GET` items can be requested at once. Example:
.. code-block:: json
{
"recording_mbids": [
"13dd61c7-ce73-4e97-9f0c-9f0e53144411",
"22ad712e-ce73-9f0c-4e97-9f0e53144411"
]
}
The response maintains the order of the recording mbids supplied and also includes any recordings
for which the data was not found with counts set to null. Example:
.. code-block:: json
[
{
"recording_mbid": "13dd61c7-ce73-4e97-9f0c-9f0e53144411",
"total_listen_count": 1000,
"total_user_count": 10
},
{
"recording_mbid": "22ad712e-ce73-9f0c-4e97-9f0e53144411",
"total_listen_count": null,
"total_user_count": null
}
]
:statuscode 200: you have data!
:statuscode 400: invalid recording_mbid(s)
"""
return fetch_entity_popularity_counts("recording")


@popularity_api_bp.post("/artist")
@crossdomain
@ratelimit()
def popularity_artist():
""" Get the total listen count and total unique listeners count for a given artist.
A JSON document with a list of artists and inc string must be POSTed. Up to
:data:`~webserver.views.api.MAX_ITEMS_PER_GET` items can be requested at once. Example:
.. code-block:: json
{
"artist_mbids": [
"13dd61c7-ce73-4e97-9f0c-9f0e53144411",
"22ad712e-ce73-9f0c-4e97-9f0e53144411"
]
}
The response maintains the order of the artist mbids supplied and also includes any artists
for which the data was not found with counts set to null. Example:
.. code-block:: json
[
{
"artist_mbid": "13dd61c7-ce73-4e97-9f0c-9f0e53144411",
"total_listen_count": 1000,
"total_user_count": 10
},
{
"artist_mbid": "22ad712e-ce73-9f0c-4e97-9f0e53144411",
"total_listen_count": null,
"total_user_count": null
}
]
:statuscode 200: you have data!
:statuscode 400: invalid artist_mbid(s)
"""
return fetch_entity_popularity_counts("artist")


@popularity_api_bp.post("/release")
@crossdomain
@ratelimit()
def popularity_release():
""" Get the total listen count and total unique listeners count for a given release.
A JSON document with a list of releases and inc string must be POSTed. Up to
:data:`~webserver.views.api.MAX_ITEMS_PER_GET` items can be requested at once. Example:
.. code-block:: json
{
"release_mbids": [
"13dd61c7-ce73-4e97-9f0c-9f0e53144411",
"22ad712e-ce73-9f0c-4e97-9f0e53144411"
]
}
The response maintains the order of the release mbids supplied and also includes any releases
for which the data was not found with counts set to null. Example:
.. code-block:: json
[
{
"release_mbid": "13dd61c7-ce73-4e97-9f0c-9f0e53144411",
"total_listen_count": 1000,
"total_user_count": 10
},
{
"release_mbid": "22ad712e-ce73-9f0c-4e97-9f0e53144411",
"total_listen_count": null,
"total_user_count": null
}
]
:statuscode 200: you have data!
:statuscode 400: invalid release_mbid(s)
"""
return fetch_entity_popularity_counts("release")


@popularity_api_bp.post("/release-group")
@crossdomain
@ratelimit()
def popularity_release_group():
""" Get the total listen count and total unique listeners count for a given release group.
A JSON document with a list of release groups and inc string must be POSTed. Up to
:data:`~webserver.views.api.MAX_ITEMS_PER_GET` items can be requested at once. Example:
.. code-block:: json
{
"release_group_mbids": [
"13dd61c7-ce73-4e97-9f0c-9f0e53144411",
"22ad712e-ce73-9f0c-4e97-9f0e53144411"
]
}
The response maintains the order of the release group mbids supplied and also includes any release groups
for which the data was not found with counts set to null. Example:
.. code-block:: json
[
{
"release_group_mbid": "13dd61c7-ce73-4e97-9f0c-9f0e53144411",
"total_listen_count": 1000,
"total_user_count": 10
},
{
"release_group_mbid": "22ad712e-ce73-9f0c-4e97-9f0e53144411",
"total_listen_count": null,
"total_user_count": null
}
]
:statuscode 200: you have data!
:statuscode 400: invalid release_group_mbid(s)
"""
return fetch_entity_popularity_counts("release_group")

0 comments on commit 6507d13

Please sign in to comment.