back interdiff of round #1 and #0

display artist's top listeners #5

merged
opened by tsiry-sandratraina.com targeting main from feat/artist-listeners
files
apps
api
lexicons
pkl
src
lexicon
xrpc
app
rocksky
web
src
api
hooks
pages
artist
crates
analytics
src
handlers
types
REVERTED
apps/api/lexicons/artist/defs.json
···
"minimum": 0
}
}
-
},
-
"listenerViewBasic": {
-
"type": "object",
-
"properties": {
-
"id": {
-
"type": "string",
-
"description": "The unique identifier of the actor."
-
},
-
"did": {
-
"type": "string",
-
"description": "The DID of the listener."
-
},
-
"handle": {
-
"type": "string",
-
"description": "The handle of the listener."
-
},
-
"displayName": {
-
"type": "string",
-
"description": "The display name of the listener."
-
},
-
"avatar": {
-
"type": "string",
-
"description": "The URL of the listener's avatar image.",
-
"format": "uri"
-
},
-
"mostListenedSong": {
-
"type": "ref",
-
"ref": "app.rocksky.song.defs#songViewBasic"
-
}
-
}
}
}
}
···
"minimum": 0
}
}
}
}
}
ERROR
apps/api/lexicons/artist/getArtistListeners.json

Failed to calculate interdiff for this file.

REVERTED
apps/api/pkl/defs/artist/defs.pkl
···
}
}
-
-
["listenerViewBasic"] {
-
type = "object"
-
properties {
-
["id"] = new StringType {
-
type = "string"
-
description = "The unique identifier of the actor."
-
}
-
-
["did"] = new StringType {
-
type = "string"
-
description = "The DID of the listener."
-
}
-
-
["handle"] = new StringType {
-
type = "string"
-
description = "The handle of the listener."
-
}
-
-
["displayName"] = new StringType {
-
type = "string"
-
description = "The display name of the listener."
-
}
-
-
["avatar"] = new StringType {
-
type = "string"
-
format = "uri"
-
description = "The URL of the listener's avatar image."
-
}
-
-
["mostListenedSong"] = new Ref {
-
ref = "app.rocksky.song.defs#songViewBasic"
-
}
-
-
}
-
}
}
···
}
}
}
ERROR
apps/api/pkl/defs/artist/getArtistListeners.pkl

Failed to calculate interdiff for this file.

ERROR
apps/api/src/lexicon/index.ts

Failed to calculate interdiff for this file.

REVERTED
apps/api/src/lexicon/lexicons.ts
···
},
},
},
-
listenerViewBasic: {
-
type: 'object',
-
properties: {
-
id: {
-
type: 'string',
-
description: 'The unique identifier of the actor.',
-
},
-
did: {
-
type: 'string',
-
description: 'The DID of the listener.',
-
},
-
handle: {
-
type: 'string',
-
description: 'The handle of the listener.',
-
},
-
displayName: {
-
type: 'string',
-
description: 'The display name of the listener.',
-
},
-
avatar: {
-
type: 'string',
-
description: "The URL of the listener's avatar image.",
-
format: 'uri',
-
},
-
mostListenedSong: {
-
type: 'ref',
-
ref: 'lex:app.rocksky.song.defs#songViewBasic',
-
},
-
},
-
},
},
},
AppRockskyArtistGetArtistAlbums: {
···
},
},
},
-
AppRockskyArtistGetArtistListeners: {
-
lexicon: 1,
-
id: 'app.rocksky.artist.getArtistListeners',
-
defs: {
-
main: {
-
type: 'query',
-
description: 'Get artist listeners',
-
parameters: {
-
type: 'params',
-
required: ['uri'],
-
properties: {
-
uri: {
-
type: 'string',
-
description: 'The URI of the artist to retrieve listeners from',
-
format: 'at-uri',
-
},
-
},
-
},
-
output: {
-
encoding: 'application/json',
-
schema: {
-
type: 'object',
-
properties: {
-
listeners: {
-
type: 'array',
-
items: {
-
type: 'ref',
-
ref: 'lex:app.rocksky.artist.defs#listenerViewBasic',
-
},
-
},
-
},
-
},
-
},
-
},
-
},
-
},
AppRockskyArtistGetArtists: {
lexicon: 1,
id: 'app.rocksky.artist.getArtists',
···
AppRockskyArtistDefs: 'app.rocksky.artist.defs',
AppRockskyArtistGetArtistAlbums: 'app.rocksky.artist.getArtistAlbums',
AppRockskyArtistGetArtist: 'app.rocksky.artist.getArtist',
-
AppRockskyArtistGetArtistListeners: 'app.rocksky.artist.getArtistListeners',
AppRockskyArtistGetArtists: 'app.rocksky.artist.getArtists',
AppRockskyArtistGetArtistTracks: 'app.rocksky.artist.getArtistTracks',
AppRockskyChartsDefs: 'app.rocksky.charts.defs',
···
},
},
},
},
},
AppRockskyArtistGetArtistAlbums: {
···
},
},
},
AppRockskyArtistGetArtists: {
lexicon: 1,
id: 'app.rocksky.artist.getArtists',
···
AppRockskyArtistDefs: 'app.rocksky.artist.defs',
AppRockskyArtistGetArtistAlbums: 'app.rocksky.artist.getArtistAlbums',
AppRockskyArtistGetArtist: 'app.rocksky.artist.getArtist',
AppRockskyArtistGetArtists: 'app.rocksky.artist.getArtists',
AppRockskyArtistGetArtistTracks: 'app.rocksky.artist.getArtistTracks',
AppRockskyChartsDefs: 'app.rocksky.charts.defs',
REVERTED
apps/api/src/lexicon/types/app/rocksky/artist/defs.ts
···
import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
-
import type * as AppRockskySongDefs from '../song/defs'
export interface ArtistViewBasic {
/** The unique identifier of the artist. */
···
export function validateArtistViewDetailed(v: unknown): ValidationResult {
return lexicons.validate('app.rocksky.artist.defs#artistViewDetailed', v)
}
-
-
export interface ListenerViewBasic {
-
/** The unique identifier of the actor. */
-
id?: string
-
/** The DID of the listener. */
-
did?: string
-
/** The handle of the listener. */
-
handle?: string
-
/** The display name of the listener. */
-
displayName?: string
-
/** The URL of the listener's avatar image. */
-
avatar?: string
-
mostListenedSong?: AppRockskySongDefs.SongViewBasic
-
[k: string]: unknown
-
}
-
-
export function isListenerViewBasic(v: unknown): v is ListenerViewBasic {
-
return (
-
isObj(v) &&
-
hasProp(v, '$type') &&
-
v.$type === 'app.rocksky.artist.defs#listenerViewBasic'
-
)
-
}
-
-
export function validateListenerViewBasic(v: unknown): ValidationResult {
-
return lexicons.validate('app.rocksky.artist.defs#listenerViewBasic', v)
-
}
···
import { lexicons } from '../../../../lexicons'
import { isObj, hasProp } from '../../../../util'
import { CID } from 'multiformats/cid'
export interface ArtistViewBasic {
/** The unique identifier of the artist. */
···
export function validateArtistViewDetailed(v: unknown): ValidationResult {
return lexicons.validate('app.rocksky.artist.defs#artistViewDetailed', v)
}
ERROR
apps/api/src/lexicon/types/app/rocksky/artist/getArtistListeners.ts

Failed to calculate interdiff for this file.

ERROR
crates/analytics/src/handlers/artists.rs

Failed to calculate interdiff for this file.

ERROR
crates/analytics/src/handlers/mod.rs

Failed to calculate interdiff for this file.

REVERTED
crates/analytics/src/types/artist.rs
···
pub unique_listeners: Option<i32>,
}
-
#[derive(Debug, Serialize, Deserialize, Default)]
-
pub struct ArtistBasic {
-
pub id: String,
-
pub name: String,
-
pub uri: Option<String>,
-
}
-
-
#[derive(Debug, Serialize, Deserialize, Default)]
-
pub struct ArtistListener {
-
pub artist: String,
-
pub listener_rank: i64,
-
pub user_id: String,
-
pub display_name: String,
-
pub did: String,
-
pub handle: String,
-
pub avatar: String,
-
pub total_artist_plays: i64,
-
pub most_played_track: String,
-
pub most_played_track_uri: String,
-
pub track_play_count: i64,
-
}
-
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct GetArtistsParams {
pub user_did: Option<String>,
···
pub struct GetArtistAlbumsParams {
pub artist_id: String,
}
-
-
#[derive(Debug, Serialize, Deserialize, Default)]
-
pub struct GetArtistListenersParams {
-
pub artist_id: String,
-
pub pagination: Option<Pagination>,
-
}
···
pub unique_listeners: Option<i32>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct GetArtistsParams {
pub user_did: Option<String>,
···
pub struct GetArtistAlbumsParams {
pub artist_id: String,
}
NEW
apps/api/src/xrpc/app/rocksky/artist/getArtistListeners.ts
···
···
+
import type { Context } from "context";
+
import { Effect, pipe } from "effect";
+
import type { Server } from "lexicon";
+
import type { ListenerViewBasic } from "lexicon/types/app/rocksky/artist/defs";
+
import type { QueryParams } from "lexicon/types/app/rocksky/artist/getArtistListeners";
+
+
export default function (server: Server, ctx: Context) {
+
const getArtistListeners = (params) =>
+
pipe(
+
{ params, ctx },
+
retrieve,
+
Effect.flatMap(presentation),
+
Effect.retry({ times: 3 }),
+
Effect.timeout("10 seconds"),
+
Effect.catchAll((err) => {
+
console.error(err);
+
return Effect.succeed({ listeners: [] });
+
})
+
);
+
server.app.rocksky.artist.getArtistListeners({
+
handler: async ({ params }) => {
+
const result = await Effect.runPromise(getArtistListeners(params));
+
return {
+
encoding: "application/json",
+
body: result,
+
};
+
},
+
});
+
}
+
+
const retrieve = ({
+
params,
+
ctx,
+
}: {
+
params: QueryParams;
+
ctx: Context;
+
}): Effect.Effect<{ data: ArtistListener[] }, Error> => {
+
return Effect.tryPromise({
+
try: () =>
+
ctx.analytics.post("library.getArtistListeners", {
+
artist_id: params.uri,
+
}),
+
catch: (error) =>
+
new Error(`Failed to retrieve artist's listeners: ${error}`),
+
});
+
};
+
+
const presentation = ({
+
data,
+
}: {
+
data: ArtistListener[];
+
}): Effect.Effect<{ listeners: ListenerViewBasic[] }, never> => {
+
return Effect.sync(() => ({
+
listeners: data.map((item) => ({
+
id: item.user_id,
+
did: item.did,
+
handle: item.handle,
+
displayName: item.display_name,
+
avatar: item.avatar,
+
mostListenedSong: {
+
title: item.most_played_track,
+
uri: item.most_played_track_uri,
+
playCount: item.track_play_count,
+
},
+
totalPlays: item.total_artist_plays,
+
rank: item.listener_rank,
+
})),
+
}));
+
};
+
+
type ArtistListener = {
+
artist: string;
+
avatar: string;
+
did: string;
+
display_name: string;
+
handle: string;
+
listener_rank: number;
+
most_played_track: string;
+
most_played_track_uri: string;
+
total_artist_plays: number;
+
track_play_count: number;
+
user_id: string;
+
};
NEW
apps/api/src/xrpc/index.ts
···
import updateApikey from "./app/rocksky/apikey/updateApikey";
import getArtist from "./app/rocksky/artist/getArtist";
import getArtistAlbums from "./app/rocksky/artist/getArtistAlbums";
import getArtists from "./app/rocksky/artist/getArtists";
import getArtistTracks from "./app/rocksky/artist/getArtistTracks";
import getScrobblesChart from "./app/rocksky/charts/getScrobblesChart";
···
getArtist(server, ctx);
getArtistAlbums(server, ctx);
getArtists(server, ctx);
getArtistTracks(server, ctx);
getScrobblesChart(server, ctx);
downloadFileFromDropbox(server, ctx);
···
import updateApikey from "./app/rocksky/apikey/updateApikey";
import getArtist from "./app/rocksky/artist/getArtist";
import getArtistAlbums from "./app/rocksky/artist/getArtistAlbums";
+
import getArtistListeners from "./app/rocksky/artist/getArtistListeners";
import getArtists from "./app/rocksky/artist/getArtists";
import getArtistTracks from "./app/rocksky/artist/getArtistTracks";
import getScrobblesChart from "./app/rocksky/charts/getScrobblesChart";
···
getArtist(server, ctx);
getArtistAlbums(server, ctx);
getArtists(server, ctx);
+
getArtistListeners(server, ctx);
getArtistTracks(server, ctx);
getScrobblesChart(server, ctx);
downloadFileFromDropbox(server, ctx);
NEW
apps/web/src/api/library.ts
···
export const getArtistTracks = async (
uri: string,
-
limit = 10,
): Promise<
{
id: string;
···
> => {
const response = await client.get(
"/xrpc/app.rocksky.artist.getArtistTracks",
-
{ params: { uri, limit } },
);
return response.data.tracks;
};
export const getArtistAlbums = async (
uri: string,
-
limit = 10,
): Promise<
{
id: string;
···
> => {
const response = await client.get(
"/xrpc/app.rocksky.artist.getArtistAlbums",
-
{ params: { uri, limit } },
);
return response.data.albums;
};
···
"/xrpc/app.rocksky.actor.getActorLovedSongs",
{
params: { did, limit, offset },
-
},
);
return response.data.tracks;
};
···
});
return response.data;
};
···
export const getArtistTracks = async (
uri: string,
+
limit = 10
): Promise<
{
id: string;
···
> => {
const response = await client.get(
"/xrpc/app.rocksky.artist.getArtistTracks",
+
{ params: { uri, limit } }
);
return response.data.tracks;
};
export const getArtistAlbums = async (
uri: string,
+
limit = 10
): Promise<
{
id: string;
···
> => {
const response = await client.get(
"/xrpc/app.rocksky.artist.getArtistAlbums",
+
{ params: { uri, limit } }
);
return response.data.albums;
};
···
"/xrpc/app.rocksky.actor.getActorLovedSongs",
{
params: { did, limit, offset },
+
}
);
return response.data.tracks;
};
···
});
return response.data;
};
+
+
export const getArtistListeners = async (uri: string, limit: number) => {
+
const response = await client.get(
+
"/xrpc/app.rocksky.artist.getArtistListeners",
+
{ params: { uri, limit } }
+
);
+
return response.data;
+
};
NEW
apps/web/src/hooks/useLibrary.tsx
···
import { useQuery } from "@tanstack/react-query";
import {
-
getAlbum,
-
getAlbums,
-
getArtist,
-
getArtistAlbums,
-
getArtists,
-
getArtistTracks,
-
getLovedTracks,
-
getSongByUri,
-
getTracks,
} from "../api/library";
export const useSongByUriQuery = (uri: string) =>
-
useQuery({
-
queryKey: ["songByUri", uri],
-
queryFn: () => getSongByUri(uri),
-
enabled: !!uri,
-
});
export const useArtistTracksQuery = (uri: string, limit = 10) =>
-
useQuery({
-
queryKey: ["artistTracks", uri, limit],
-
queryFn: () => getArtistTracks(uri, limit),
-
enabled: !!uri,
-
});
export const useArtistAlbumsQuery = (uri: string, limit = 10) =>
-
useQuery({
-
queryKey: ["artistAlbums", uri, limit],
-
queryFn: () => getArtistAlbums(uri, limit),
-
enabled: !!uri,
-
});
export const useArtistsQuery = (did: string, offset = 0, limit = 30) =>
-
useQuery({
-
queryKey: ["artists", did, offset, limit],
-
queryFn: () => getArtists(did, offset, limit),
-
enabled: !!did,
-
select: (data) =>
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-
data?.artists.map((x: any) => ({
-
...x,
-
scrobbles: x.playCount,
-
})),
-
});
export const useAlbumsQuery = (did: string, offset = 0, limit = 12) =>
-
useQuery({
-
queryKey: ["albums", did, offset, limit],
-
queryFn: () => getAlbums(did, offset, limit),
-
enabled: !!did,
-
select: (data) =>
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-
data?.albums.map((x: any) => ({
-
...x,
-
scrobbles: x.playCount,
-
})),
-
});
export const useTracksQuery = (did: string, offset = 0, limit = 20) =>
-
useQuery({
-
queryKey: ["tracks", did, offset, limit],
-
queryFn: () => getTracks(did, offset, limit),
-
enabled: !!did,
-
select: (data) =>
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-
data?.tracks.map((x: any) => ({
-
...x,
-
scrobbles: x.playCount,
-
})),
-
});
export const useLovedTracksQuery = (did: string, offset = 0, limit = 20) =>
-
useQuery({
-
queryKey: ["lovedTracks", did, offset, limit],
-
queryFn: () => getLovedTracks(did, offset, limit),
-
enabled: !!did,
-
});
export const useAlbumQuery = (did: string, rkey: string) =>
-
useQuery({
-
queryKey: ["album", did, rkey],
-
queryFn: () => getAlbum(did, rkey),
-
enabled: !!did && !!rkey,
-
});
export const useArtistQuery = (did: string, rkey: string) =>
-
useQuery({
-
queryKey: ["artist", did, rkey],
-
queryFn: () => getArtist(did, rkey),
-
enabled: !!did && !!rkey,
-
});
···
import { useQuery } from "@tanstack/react-query";
import {
+
getAlbum,
+
getAlbums,
+
getArtist,
+
getArtistAlbums,
+
getArtistListeners,
+
getArtists,
+
getArtistTracks,
+
getLovedTracks,
+
getSongByUri,
+
getTracks,
} from "../api/library";
export const useSongByUriQuery = (uri: string) =>
+
useQuery({
+
queryKey: ["songByUri", uri],
+
queryFn: () => getSongByUri(uri),
+
enabled: !!uri,
+
});
export const useArtistTracksQuery = (uri: string, limit = 10) =>
+
useQuery({
+
queryKey: ["artistTracks", uri, limit],
+
queryFn: () => getArtistTracks(uri, limit),
+
enabled: !!uri,
+
});
export const useArtistAlbumsQuery = (uri: string, limit = 10) =>
+
useQuery({
+
queryKey: ["artistAlbums", uri, limit],
+
queryFn: () => getArtistAlbums(uri, limit),
+
enabled: !!uri,
+
});
export const useArtistsQuery = (did: string, offset = 0, limit = 30) =>
+
useQuery({
+
queryKey: ["artists", did, offset, limit],
+
queryFn: () => getArtists(did, offset, limit),
+
enabled: !!did,
+
select: (data) =>
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
+
data?.artists.map((x: any) => ({
+
...x,
+
scrobbles: x.playCount,
+
})),
+
});
export const useAlbumsQuery = (did: string, offset = 0, limit = 12) =>
+
useQuery({
+
queryKey: ["albums", did, offset, limit],
+
queryFn: () => getAlbums(did, offset, limit),
+
enabled: !!did,
+
select: (data) =>
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
+
data?.albums.map((x: any) => ({
+
...x,
+
scrobbles: x.playCount,
+
})),
+
});
export const useTracksQuery = (did: string, offset = 0, limit = 20) =>
+
useQuery({
+
queryKey: ["tracks", did, offset, limit],
+
queryFn: () => getTracks(did, offset, limit),
+
enabled: !!did,
+
select: (data) =>
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
+
data?.tracks.map((x: any) => ({
+
...x,
+
scrobbles: x.playCount,
+
})),
+
});
export const useLovedTracksQuery = (did: string, offset = 0, limit = 20) =>
+
useQuery({
+
queryKey: ["lovedTracks", did, offset, limit],
+
queryFn: () => getLovedTracks(did, offset, limit),
+
enabled: !!did,
+
});
export const useAlbumQuery = (did: string, rkey: string) =>
+
useQuery({
+
queryKey: ["album", did, rkey],
+
queryFn: () => getAlbum(did, rkey),
+
enabled: !!did && !!rkey,
+
});
export const useArtistQuery = (did: string, rkey: string) =>
+
useQuery({
+
queryKey: ["artist", did, rkey],
+
queryFn: () => getArtist(did, rkey),
+
enabled: !!did && !!rkey,
+
});
+
+
export const useArtistListenersQuery = (uri: string, limit = 10) =>
+
useQuery({
+
queryKey: ["artistListeners", uri, limit],
+
queryFn: () => getArtistListeners(uri, limit),
+
enabled: !!uri,
+
select: (data) => data.listeners,
+
});
NEW
apps/web/src/pages/artist/Artist.tsx
···
import ArtistIcon from "../../components/Icons/Artist";
import Shout from "../../components/Shout/Shout";
import {
-
useArtistAlbumsQuery,
-
useArtistQuery,
-
useArtistTracksQuery,
} from "../../hooks/useLibrary";
import Main from "../../layouts/Main";
import Albums from "./Albums";
import PopularSongs from "./PopularSongs";
const Group = styled.div`
···
`;
const Artist = () => {
-
const { did, rkey } = useParams({ strict: false });
-
-
const uri = `at://${did}/app.rocksky.artist/${rkey}`;
-
const artistResult = useArtistQuery(did!, rkey!);
-
const artistTracksResult = useArtistTracksQuery(uri);
-
const artistAlbumsResult = useArtistAlbumsQuery(uri);
-
-
const artist = useAtomValue(artistAtom);
-
const setArtist = useSetAtom(artistAtom);
-
const [topTracks, setTopTracks] = useState<
-
{
-
id: string;
-
title: string;
-
artist: string;
-
albumArtist: string;
-
albumArt: string;
-
uri: string;
-
scrobbles: number;
-
albumUri?: string;
-
artistUri?: string;
-
}[]
-
>([]);
-
const [topAlbums, setTopAlbums] = useState<
-
{
-
id: string;
-
title: string;
-
artist: string;
-
albumArt: string;
-
artistUri: string;
-
uri: string;
-
}[]
-
>([]);
-
-
useEffect(() => {
-
if (artistResult.isLoading || artistResult.isError) {
-
return;
-
}
-
-
if (!artistResult.data || !did) {
-
return;
-
}
-
-
setArtist({
-
id: artistResult.data.id,
-
name: artistResult.data.name,
-
born: artistResult.data.born,
-
bornIn: artistResult.data.bornIn,
-
died: artistResult.data.died,
-
listeners: artistResult.data.uniqueListeners,
-
scrobbles: artistResult.data.playCount,
-
picture: artistResult.data.picture,
-
tags: artistResult.data.tags,
-
uri: artistResult.data.uri,
-
spotifyLink: artistResult.data.spotifyLink,
-
});
-
// eslint-disable-next-line react-hooks/exhaustive-deps
-
}, [artistResult.data, artistResult.isLoading, artistResult.isError, did]);
-
-
useEffect(() => {
-
if (artistTracksResult.isLoading || artistTracksResult.isError) {
-
return;
-
}
-
-
if (!artistTracksResult.data || !did) {
-
return;
-
}
-
-
setTopTracks(
-
artistTracksResult.data.map((track) => ({
-
...track,
-
scrobbles: track.playCount || 1,
-
})),
-
);
-
}, [
-
artistTracksResult.data,
-
artistTracksResult.isLoading,
-
artistTracksResult.isError,
-
did,
-
]);
-
-
useEffect(() => {
-
if (artistAlbumsResult.isLoading || artistAlbumsResult.isError) {
-
return;
-
}
-
-
if (!artistAlbumsResult.data || !did) {
-
return;
-
}
-
-
setTopAlbums(artistAlbumsResult.data);
-
}, [
-
artistAlbumsResult.data,
-
artistAlbumsResult.isLoading,
-
artistAlbumsResult.isError,
-
did,
-
]);
-
-
const loading =
-
artistResult.isLoading ||
-
artistTracksResult.isLoading ||
-
artistAlbumsResult.isLoading;
-
return (
-
<Main>
-
<div className="pb-[100px] pt-[50px]">
-
<Group>
-
<div className="mr-[20px]">
-
{artist?.picture && !loading && (
-
<Avatar name={artist?.name} src={artist?.picture} size="150px" />
-
)}
-
{!artist?.picture && !loading && (
-
<div className="w-[150px] h-[150px] rounded-[80px] bg-[rgba(243, 243, 243, 0.725)] flex items-center justify-center">
-
<div
-
style={{
-
height: 60,
-
width: 60,
-
}}
-
>
-
<ArtistIcon color="rgba(66, 87, 108, 0.65)" />
-
</div>
-
</div>
-
)}
-
</div>
-
{artist && !loading && (
-
<div style={{ flex: 1 }}>
-
<HeadingMedium
-
marginTop={"20px"}
-
marginBottom={0}
-
className="!text-[var(--color-text)]"
-
>
-
{artist?.name}
-
</HeadingMedium>
-
<div className="mt-[20px] flex flex-row">
-
<div className="mr-[20px]">
-
<LabelMedium
-
margin={0}
-
className="!text-[var(--color-text-muted)]"
-
>
-
Listeners
-
</LabelMedium>
-
<HeadingXSmall
-
margin={0}
-
className="!text-[var(--color-text)]"
-
>
-
{numeral(artist?.listeners).format("0,0")}
-
</HeadingXSmall>
-
</div>
-
<div>
-
<LabelMedium
-
margin={0}
-
className="!text-[var(--color-text-muted)]"
-
>
-
Scrobbles
-
</LabelMedium>
-
<HeadingXSmall
-
margin={0}
-
className="!text-[var(--color-text)]"
-
>
-
{numeral(artist?.scrobbles).format("0,0")}
-
</HeadingXSmall>
-
</div>
-
<div className="flex items-center justify-end flex-1 mr-[10px]">
-
<a
-
href={`https://pdsls.dev/at/${uri.replace("at://", "")}`}
-
target="_blank"
-
className="text-[var(--color-text)] no-underline bg-[var(--color-default-button)] rounded-[10px] p-[16px] pl-[25px] pr-[25px]"
-
>
-
<ExternalLink
-
size={24}
-
className="mr-[10px] text-[var(--color-text)]"
-
/>
-
View on PDSls
-
</a>
-
</div>
-
</div>
-
</div>
-
)}
-
</Group>
-
-
<PopularSongs topTracks={topTracks} />
-
<Albums topAlbums={topAlbums} />
-
-
<Shout type="artist" />
-
</div>
-
</Main>
-
);
};
export default Artist;
···
import ArtistIcon from "../../components/Icons/Artist";
import Shout from "../../components/Shout/Shout";
import {
+
useArtistAlbumsQuery,
+
useArtistListenersQuery,
+
useArtistQuery,
+
useArtistTracksQuery,
} from "../../hooks/useLibrary";
import Main from "../../layouts/Main";
import Albums from "./Albums";
+
import ArtistListeners from "./ArtistListeners";
import PopularSongs from "./PopularSongs";
const Group = styled.div`
···
`;
const Artist = () => {
+
const { did, rkey } = useParams({ strict: false });
+
+
const uri = `at://${did}/app.rocksky.artist/${rkey}`;
+
const artistResult = useArtistQuery(did!, rkey!);
+
const artistTracksResult = useArtistTracksQuery(uri);
+
const artistAlbumsResult = useArtistAlbumsQuery(uri);
+
const artistListenersResult = useArtistListenersQuery(uri);
+
+
const artist = useAtomValue(artistAtom);
+
const setArtist = useSetAtom(artistAtom);
+
const [topTracks, setTopTracks] = useState<
+
{
+
id: string;
+
title: string;
+
artist: string;
+
albumArtist: string;
+
albumArt: string;
+
uri: string;
+
scrobbles: number;
+
albumUri?: string;
+
artistUri?: string;
+
}[]
+
>([]);
+
const [topAlbums, setTopAlbums] = useState<
+
{
+
id: string;
+
title: string;
+
artist: string;
+
albumArt: string;
+
artistUri: string;
+
uri: string;
+
}[]
+
>([]);
+
+
useEffect(() => {
+
if (artistResult.isLoading || artistResult.isError) {
+
return;
+
}
+
+
if (!artistResult.data || !did) {
+
return;
+
}
+
+
setArtist({
+
id: artistResult.data.id,
+
name: artistResult.data.name,
+
born: artistResult.data.born,
+
bornIn: artistResult.data.bornIn,
+
died: artistResult.data.died,
+
listeners: artistResult.data.uniqueListeners,
+
scrobbles: artistResult.data.playCount,
+
picture: artistResult.data.picture,
+
tags: artistResult.data.tags,
+
uri: artistResult.data.uri,
+
spotifyLink: artistResult.data.spotifyLink,
+
});
+
// eslint-disable-next-line react-hooks/exhaustive-deps
+
}, [artistResult.data, artistResult.isLoading, artistResult.isError, did]);
+
+
useEffect(() => {
+
if (artistTracksResult.isLoading || artistTracksResult.isError) {
+
return;
+
}
+
+
if (!artistTracksResult.data || !did) {
+
return;
+
}
+
+
setTopTracks(
+
artistTracksResult.data.map((track) => ({
+
...track,
+
scrobbles: track.playCount || 1,
+
})),
+
);
+
}, [
+
artistTracksResult.data,
+
artistTracksResult.isLoading,
+
artistTracksResult.isError,
+
did,
+
]);
+
+
useEffect(() => {
+
if (artistAlbumsResult.isLoading || artistAlbumsResult.isError) {
+
return;
+
}
+
+
if (!artistAlbumsResult.data || !did) {
+
return;
+
}
+
+
setTopAlbums(artistAlbumsResult.data);
+
}, [
+
artistAlbumsResult.data,
+
artistAlbumsResult.isLoading,
+
artistAlbumsResult.isError,
+
did,
+
]);
+
+
const loading =
+
artistResult.isLoading ||
+
artistTracksResult.isLoading ||
+
artistAlbumsResult.isLoading;
+
return (
+
<Main>
+
<div className="pb-[100px] pt-[50px]">
+
<Group>
+
<div className="mr-[20px]">
+
{artist?.picture && !loading && (
+
<Avatar name={artist?.name} src={artist?.picture} size="150px" />
+
)}
+
{!artist?.picture && !loading && (
+
<div className="w-[150px] h-[150px] rounded-[80px] bg-[rgba(243, 243, 243, 0.725)] flex items-center justify-center">
+
<div
+
style={{
+
height: 60,
+
width: 60,
+
}}
+
>
+
<ArtistIcon color="rgba(66, 87, 108, 0.65)" />
+
</div>
+
</div>
+
)}
+
</div>
+
{artist && !loading && (
+
<div style={{ flex: 1 }}>
+
<HeadingMedium
+
marginTop={"20px"}
+
marginBottom={0}
+
className="!text-[var(--color-text)]"
+
>
+
{artist?.name}
+
</HeadingMedium>
+
<div className="mt-[20px] flex flex-row">
+
<div className="mr-[20px]">
+
<LabelMedium
+
margin={0}
+
className="!text-[var(--color-text-muted)]"
+
>
+
Listeners
+
</LabelMedium>
+
<HeadingXSmall
+
margin={0}
+
className="!text-[var(--color-text)]"
+
>
+
{numeral(artist?.listeners).format("0,0")}
+
</HeadingXSmall>
+
</div>
+
<div>
+
<LabelMedium
+
margin={0}
+
className="!text-[var(--color-text-muted)]"
+
>
+
Scrobbles
+
</LabelMedium>
+
<HeadingXSmall
+
margin={0}
+
className="!text-[var(--color-text)]"
+
>
+
{numeral(artist?.scrobbles).format("0,0")}
+
</HeadingXSmall>
+
</div>
+
<div className="flex items-center justify-end flex-1 mr-[10px]">
+
<a
+
href={`https://pdsls.dev/at/${uri.replace("at://", "")}`}
+
target="_blank"
+
className="text-[var(--color-text)] no-underline bg-[var(--color-default-button)] rounded-[10px] p-[16px] pl-[25px] pr-[25px]"
+
>
+
<ExternalLink
+
size={24}
+
className="mr-[10px] text-[var(--color-text)]"
+
/>
+
View on PDSls
+
</a>
+
</div>
+
</div>
+
</div>
+
)}
+
</Group>
+
+
<PopularSongs topTracks={topTracks} />
+
<Albums topAlbums={topAlbums} />
+
<ArtistListeners listeners={artistListenersResult.data} />
+
<Shout type="artist" />
+
</div>
+
</Main>
+
);
};
export default Artist;
NEW
apps/web/src/pages/artist/ArtistListeners/ArtistListeners.tsx
···
···
+
import { Link } from "@tanstack/react-router";
+
import { Avatar } from "baseui/avatar";
+
import { HeadingSmall } from "baseui/typography";
+
+
interface ArtistListenersProps {
+
listeners: {
+
id: string;
+
did: string;
+
handle: string;
+
displayName: string;
+
avatar: string;
+
mostListenedSong: {
+
title: string;
+
uri: string;
+
playCount: number;
+
};
+
totalPlays: number;
+
rank: number;
+
}[];
+
}
+
+
function ArtistListeners(props: ArtistListenersProps) {
+
return (
+
<>
+
<HeadingSmall
+
marginBottom={"15px"}
+
className="!text-[var(--color-text)] !mb-[30px]"
+
>
+
Listeners
+
</HeadingSmall>
+
{props.listeners?.map((item) => (
+
<div
+
key={item.id}
+
className="mb-[30px] flex flex-row items-center gap-[20px]"
+
>
+
<Link
+
to={`/profile/${item.handle}` as string}
+
className="no-underline"
+
>
+
<Avatar src={item.avatar} name={item.displayName} size={"60px"} />
+
</Link>
+
<div>
+
<Link
+
to={`/profile/${item.handle}` as string}
+
className="text-[var(--color-text)] hover:underline no-underline"
+
style={{ fontWeight: 600 }}
+
>
+
@{item.handle}
+
</Link>
+
<div className="!text-[14px] mt-[5px]">
+
Listens to{" "}
+
{item.mostListenedSong.uri && (
+
<Link
+
to={`${item.mostListenedSong.uri?.split("at:/")[1].replace("app.rocksky.", "")}`}
+
className="text-[var(--color-primary)] hover:underline no-underline"
+
>
+
{item.mostListenedSong.title}
+
</Link>
+
)}
+
{!item.mostListenedSong.uri && (
+
<div style={{ fontWeight: 600 }}>
+
{item.mostListenedSong.title}
+
</div>
+
)}{" "}
+
a lot
+
</div>
+
</div>
+
</div>
+
))}
+
</>
+
);
+
}
+
+
export default ArtistListeners;
NEW
apps/web/src/pages/artist/ArtistListeners/index.tsx
···
···
+
import ArtistListeners from "./ArtistListeners";
+
+
export default ArtistListeners;