diff --git a/frontend/css/entity-pages.less b/frontend/css/entity-pages.less
index 3e88564ea5..215badc0cb 100644
--- a/frontend/css/entity-pages.less
+++ b/frontend/css/entity-pages.less
@@ -169,6 +169,9 @@
justify-items: center;
grid-template-columns: repeat(auto-fill, minmax(190px, max-content));
grid-template-rows: repeat(2, 1fr);
+ &.single-row {
+ grid-template-rows: repeat(1, 1fr);
+ }
.cover-art {
aspect-ratio: 1;
background: lightgrey;
diff --git a/frontend/js/src/artist/ArtistPage.tsx b/frontend/js/src/artist/ArtistPage.tsx
index ccbff5ccf7..b2f68f8701 100644
--- a/frontend/js/src/artist/ArtistPage.tsx
+++ b/frontend/js/src/artist/ArtistPage.tsx
@@ -7,7 +7,7 @@ import {
faPlayCircle,
faUserAstronaut,
} from "@fortawesome/free-solid-svg-icons";
-import { chain, isEmpty, isUndefined, partition, sortBy } from "lodash";
+import { chain, isEmpty, isUndefined, orderBy, groupBy, sortBy } from "lodash";
import { sanitize } from "dompurify";
import {
Link,
@@ -19,7 +19,7 @@ import {
import { Helmet } from "react-helmet";
import { useQuery } from "@tanstack/react-query";
import NiceModal from "@ebay/nice-modal-react";
-import GlobalAppContext from "../utils/GlobalAppContext";
+import { faCalendar } from "@fortawesome/free-regular-svg-icons";
import { getReviewEventContent } from "../utils/utils";
import TagsComponent from "../tags/TagsComponent";
import ListenCard from "../common/listens/ListenCard";
@@ -39,11 +39,43 @@ import { RouteQuery } from "../utils/Loader";
import { useBrainzPlayerDispatch } from "../common/brainzplayer/BrainzPlayerContext";
import SimilarArtistComponent from "../explore/music-neighborhood/components/SimilarArtist";
import CBReviewModal from "../cb-review/CBReviewModal";
+import Pill from "../components/Pill";
+
+function SortingButtons({
+ sort,
+ setSort,
+}: {
+ sort: "release_date" | "total_listen_count";
+ setSort: (sort: "release_date" | "total_listen_count") => void;
+}): JSX.Element {
+ return (
+
+
setSort("release_date")}
+ >
+
+
+
setSort("total_listen_count")}
+ >
+
+
+
+ );
+}
+
+interface ReleaseGroupWithSecondaryTypes extends ReleaseGroup {
+ secondary_types: string[];
+}
export type ArtistPageProps = {
popularRecordings: PopularRecording[];
artist: MusicBrainzArtist;
- releaseGroups: ReleaseGroup[];
+ releaseGroups: ReleaseGroupWithSecondaryTypes[];
similarArtists: {
artists: SimilarArtist[];
topReleaseGroupColor: ReleaseColor | undefined;
@@ -53,9 +85,10 @@ export type ArtistPageProps = {
coverArt?: string;
};
+const COVER_ART_SINGLE_ROW_COUNT = 8;
+
export default function ArtistPage(): JSX.Element {
const _ = useLoaderData();
- const { APIService } = React.useContext(GlobalAppContext);
const location = useLocation();
const params = useParams() as { artistMBID: string };
const { artistMBID } = params;
@@ -84,11 +117,44 @@ export default function ArtistPage(): JSX.Element {
WikipediaExtract
>();
- const [albumsByThisArtist, alsoAppearsOn] = partition(
+ const [sort, setSort] = React.useState<"release_date" | "total_listen_count">(
+ "release_date"
+ );
+
+ const rgGroups = groupBy(
releaseGroups,
- (rg) => rg.artists[0].artist_mbid === artist?.artist_mbid
+ (rg) =>
+ (rg.type ?? "Other") +
+ (rg.secondary_types?.[0] ? ` + ${rg.secondary_types?.[0]}` : "")
);
+ const sortReleaseGroups = (
+ releaseGroupsInput: ReleaseGroupWithSecondaryTypes[]
+ ) =>
+ orderBy(
+ releaseGroupsInput,
+ [
+ sort === "release_date" ? (rg) => rg.date || "" : "total_listen_count",
+ sort === "release_date" ? "total_listen_count" : (rg) => rg.date || "",
+ "name",
+ ],
+ ["desc", "desc", "asc"]
+ );
+
+ const typeOrder = ["Album", "Single", "EP", "Broadcast", "Other"];
+ const sortedRgGroupsKeys = sortBy(Object.keys(rgGroups), [
+ (type) => typeOrder.indexOf(type.split(" + ")[0]),
+ (type) => type.split(" + ")[1] ?? "",
+ ]);
+
+ const groupedReleaseGroups: Record<
+ string,
+ ReleaseGroupWithSecondaryTypes[]
+ > = {};
+ sortedRgGroupsKeys.forEach((type) => {
+ groupedReleaseGroups[type] = sortReleaseGroups(rgGroups[type]);
+ });
+
React.useEffect(() => {
async function fetchReviews() {
try {
@@ -407,20 +473,21 @@ export default function ArtistPage(): JSX.Element {
)}
-
-
Albums
-
- {albumsByThisArtist.map(getReleaseCard)}
-
-
- {Boolean(alsoAppearsOn?.length) && (
+ {Object.entries(groupedReleaseGroups).map(([type, rgGroup]) => (
-
Also appears on
-
- {alsoAppearsOn.map(getReleaseCard)}
+
+
{type}
+
+
+
+ {rgGroup.map(getReleaseCard)}
- )}
+ ))}
{similarArtists && similarArtists.artists.length > 0 ? (
diff --git a/listenbrainz/webserver/views/entity_pages.py b/listenbrainz/webserver/views/entity_pages.py
index a913105670..9d6edd813c 100644
--- a/listenbrainz/webserver/views/entity_pages.py
+++ b/listenbrainz/webserver/views/entity_pages.py
@@ -20,17 +20,6 @@
release_group_bp = Blueprint("release-group", __name__)
-def get_release_group_sort_key(release_group):
- """ Return a tuple that sorts release group by total_listen_count and then by date """
- release_date = release_group.get("date")
- if release_date is None:
- release_date = datetime.min
- else:
- release_date = datetime.strptime(release_date, "%Y-%m-%d")
-
- return release_group["total_listen_count"] or 0, release_date
-
-
def get_cover_art_for_artist(release_groups):
""" Get the cover art for an artist using a list of their release groups """
covers = []
@@ -170,8 +159,6 @@ def artist_entity(artist_mbid):
release_group["total_user_count"] = pop["total_user_count"]
release_groups.append(release_group)
- release_groups.sort(key=get_release_group_sort_key, reverse=True)
-
listening_stats = get_entity_listener(db_conn, "artists", artist_mbid, "all_time")
if listening_stats is None:
listening_stats = {