Skip to content

Commit

Permalink
feat(source): Start implementing recent from API
Browse files Browse the repository at this point in the history
  • Loading branch information
FoxxMD committed Jan 31, 2024
1 parent c807611 commit d88ac1b
Show file tree
Hide file tree
Showing 9 changed files with 107 additions and 14 deletions.
19 changes: 17 additions & 2 deletions src/backend/server/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ export const setupApi = (app: ExpressWithAsync, logger: Logger, initialLogOutput
hasAuthInteraction: requiresAuthInteraction,
authed,
players: 'players' in x ? (x as MemorySource).playersToObject() : {},
sot: ('playerSourceOfTruth' in x) ? x.playerSourceOfTruth : SOURCE_SOT.HISTORY
sot: ('playerSourceOfTruth' in x) ? x.playerSourceOfTruth : SOURCE_SOT.HISTORY,
supportsUpstreamRecentlyPlayed: x.supportsUpstreamRecentlyPlayed
};
if(!x.isReady()) {
if(x.buildOK === false) {
Expand Down Expand Up @@ -264,11 +265,25 @@ export const setupApi = (app: ExpressWithAsync, logger: Logger, initialLogOutput
const {
// @ts-expect-error TS(2339): Property 'scrobbleSource' does not exist on type '... Remove this comment to see the full error message
scrobbleSource: source,
query: {
upstream = 'false'
}
} = req;

let result: PlayObject[] = [];
if (source !== undefined) {
result = (source as AbstractSource).getFlatRecentlyDiscoveredPlays();
if (upstream === 'true' || upstream === '1') {
if (!(source as AbstractSource).supportsUpstreamRecentlyPlayed) {
return res.status(409).json({message: 'Fetching upstream recently played is not supported for this source'});
}
try {
result = await (source as AbstractSource).getUpstreamRecentlyPlayed();
} catch (e) {
return res.status(500).json({message: e.message});
}
} else {
result = (source as AbstractSource).getFlatRecentlyDiscoveredPlays();
}
}

return res.json(result);
Expand Down
11 changes: 11 additions & 0 deletions src/backend/sources/AbstractSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ export default abstract class AbstractSource implements Authenticatable {
pollRetries: number = 0;
tracksDiscovered: number = 0;

supportsUpstreamRecentlyPlayed: boolean = false;
supportsUpstreamNowPlaying: boolean = false;

emitter: EventEmitter;

protected recentDiscoveredPlays: GroupedFixedPlays = new TupleMap<DeviceId, PlayUserId, FixedSizeList<ProgressAwarePlayObject>>();
Expand Down Expand Up @@ -219,6 +222,14 @@ export default abstract class AbstractSource implements Authenticatable {
return [];
}

getUpstreamRecentlyPlayed = async (options: RecentlyPlayedOptions = {}): Promise<PlayObject[]> => {
throw new Error('Not implemented');
}

getUpstreamNowPlaying = async(): Promise<PlayObject[]> => {
throw new Error('Not implemented');
}

// by default if the track was recently played it is valid
// this is useful for sources where the track doesn't have complete information like Subsonic
// TODO make this more descriptive? or move it elsewhere
Expand Down
37 changes: 33 additions & 4 deletions src/backend/sources/LastfmSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { LastfmSourceConfig } from "../common/infrastructure/config/source/lastf
import dayjs from "dayjs";
import { isNodeNetworkException } from "../common/errors/NodeErrors.js";
import {ErrorWithCause} from "pony-cause";
import request from "superagent";
import request, {options} from "superagent";

export default class LastfmSource extends MemorySource {

Expand All @@ -31,6 +31,8 @@ export default class LastfmSource extends MemorySource {
super('lastfm', name, {...config, data: {interval, maxInterval, ...restData}}, internal, emitter);
this.canPoll = true;
this.canBacklog = true;
this.supportsUpstreamRecentlyPlayed = true;
this.supportsUpstreamNowPlaying = true;
this.api = new LastfmApiClient(name, {...config.data, configDir: internal.configDir, localUrl: internal.localUrl});
this.playerSourceOfTruth = SOURCE_SOT.HISTORY;
this.logger.info(`Note: The player for this source is an analogue for the 'Now Playing' status exposed by ${this.type} which is NOT used for scrobbling. Instead, the 'recently played' or 'history' information provided by this source is used for scrobbles.`)
Expand Down Expand Up @@ -71,7 +73,7 @@ export default class LastfmSource extends MemorySource {
}


getRecentlyPlayed = async(options: RecentlyPlayedOptions = {}): Promise<PlayObject[]> => {
getLastfmRecentTrack = async(options: RecentlyPlayedOptions = {}): Promise<[PlayObject[], PlayObject[]]> => {
const {limit = 20} = options;
const resp = await this.api.callApi<UserGetRecentTracksResponse>((client: any) => client.userGetRecentTracks({
user: this.api.user,
Expand Down Expand Up @@ -119,8 +121,35 @@ export default class LastfmSource extends MemorySource {
// so we'll just ignore it in the context of recent tracks since really we only want "tracks that have already finished being played" anyway
const history = plays.filter(x => x.meta.nowPlaying !== true);
const now = plays.filter(x => x.meta.nowPlaying === true);
this.processRecentPlays(now);
return history;
return [history, now];
}

getRecentlyPlayed = async(options: RecentlyPlayedOptions = {}): Promise<PlayObject[]> => {
try {
const [history, now] = await this.getLastfmRecentTrack(options);
this.processRecentPlays(now);
return history;
} catch (e) {
throw e;
}
}

getUpstreamRecentlyPlayed = async (options: RecentlyPlayedOptions = {}): Promise<PlayObject[]> => {
try {
const [history, now] = await this.getLastfmRecentTrack(options);
return history;
} catch (e) {
throw e;
}
}

getUpstreamNowPlaying = async (): Promise<PlayObject[]> => {
try {
const [history, now] = await this.getLastfmRecentTrack();
return now;
} catch (e) {
throw e;
}
}

protected getBackloggedPlays = async () => {
Expand Down
9 changes: 9 additions & 0 deletions src/backend/sources/SpotifySource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export default class SpotifySource extends MemorySource {
this.workingCredsPath = `${this.configDir}/currentCreds-${name}.json`;
this.canPoll = true;
this.canBacklog = true;
this.supportsUpstreamRecentlyPlayed = true;
}

static formatPlayObj(obj: PlayHistoryObject | CurrentlyPlayingObject, options: FormatPlayObjectOptions = {}): PlayObject {
Expand Down Expand Up @@ -350,6 +351,14 @@ export default class SpotifySource extends MemorySource {
return result.body.items.map((x: any) => SpotifySource.formatPlayObj(x)).sort(sortByOldestPlayDate);
}

getUpstreamRecentlyPlayed = async (options: RecentlyPlayedOptions = {}): Promise<PlayObject[]> => {
try {
return await this.getPlayHistory(options);
} catch (e) {
throw e;
}
}

getNowPlaying = async () => {
const func = (api: SpotifyWebApi) => api.getMyCurrentPlayingTrack();
const playingRes = await this.callApi<ReturnType<typeof this.spotifyApi.getMyCurrentPlayingTrack>>(func);
Expand Down
11 changes: 9 additions & 2 deletions src/client/components/Tooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import {PropsWithChildren, ReactElement} from "react";
import clsx from "clsx";

export interface TooltipProps {
message: string | ReactElement
classNames?: string[]
style?: object
}

const defaultStyle = {};

const Tooltip = (props: PropsWithChildren<TooltipProps>) => {
const {children, message} = props;
const {children, message, classNames = [], style = defaultStyle } = props;
const classes = ['group','relative','flex'];
clsx(classes.concat(classNames))
return (
<div className="group relative flex">
<div className={clsx(classes.concat(classNames))} style={style}>
{children}
<span
className="absolute top-5 scale-0 transition-all rounded bg-gray-800 p-2 text-xs text-white group-hover:scale-100">{message}</span>
Expand Down
9 changes: 8 additions & 1 deletion src/client/components/statusCard/SourceStatusCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ const SourceStatusCard = (props: SourceStatusCardData) => {
hasAuthInteraction,
type,
players = {},
sot
sot,
supportsUpstreamRecentlyPlayed
} = data;
if(type === 'listenbrainz' || type === 'lastfm') {
header = `${display} (Source)`;
Expand All @@ -65,6 +66,11 @@ const SourceStatusCard = (props: SourceStatusCardData) => {

const discovered = (!hasAuth || authed) ? <Link to={`/recent?type=${type}&name=${name}`}>Tracks Discovered</Link> : <span>Tracks Discovered</span>;

let upstreamRecent = null;
if(supportsUpstreamRecentlyPlayed && (!hasAuth || authed)) {
upstreamRecent = <div><Link to={`/recent?type=${type}&name=${name}&upstream=1`}>See Recent from Source API</Link></div>;
}

if((!hasAuth || authed) && canPoll) {
startSourceElement = <div onClick={poll} className="capitalize underline cursor-pointer">{status === 'Polling' ? 'Restart' : 'Start'}</div>
}
Expand All @@ -73,6 +79,7 @@ const SourceStatusCard = (props: SourceStatusCardData) => {
body = (<div className="statusCardBody">
{platformIds.map(x => <Player key={x} data={players[x]} sot={sot}/>)}
<div>{discovered}: {tracksDiscovered}</div>
{upstreamRecent}
{canPoll && hasAuthInteraction ? <a target="_blank" href={`/api/source/auth?name=${name}&type=${type}`}>(Re)authenticate</a> : null}
</div>);
}
Expand Down
20 changes: 17 additions & 3 deletions src/client/recent/RecentPage.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
import React from 'react';
import React, {Fragment} from 'react';
import PlayDisplay from "../components/PlayDisplay";
import {recentIncludes} from "../../core/Atomic";
import {useSearchParams} from "react-router-dom";
import {useGetRecentQuery} from "./recentDucks";
import Tooltip from "../components/Tooltip";
import {faQuestionCircle} from "@fortawesome/free-solid-svg-icons";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {data} from "autoprefixer";

const displayOpts = {
include: recentIncludes,
includeWeb: true
}

const apiTip = <Fragment>
<div>Data that is directly returned by the Source API.</div>
<div>If you do not see your recent plays in this data it is likely the Source's data is lagging behind your actual activity.</div>
</Fragment>

const recent = () => {
let [searchParams, setSearchParams] = useSearchParams();
const {
data = [],
error,
isLoading,
isSuccess
} = useGetRecentQuery({name: searchParams.get('name'), type: searchParams.get('type')});
} = useGetRecentQuery({name: searchParams.get('name'), type: searchParams.get('type'), upstream: searchParams.get('upstream')});

const isUpstream = searchParams.get('upstream') === '1';

return (
<div className="grid">
<div className="shadow-md rounded bg-gray-500 text-white">
<div className="p-3 font-semibold bg-gray-700 text-white">
<h2>Recently Played
<h2>Recently Played{isUpstream ? ' from Source API' : null}{isUpstream ? <Tooltip message={apiTip}
classNames={['ml-2']}
style={{display: 'inline-flex', width: '35%'}}><FontAwesomeIcon color="white" icon={faQuestionCircle}/></Tooltip> : null}
</h2>
</div>
<div className="p-5">
{/*{isUpstream ? <span className="mb-3">Below is data directly returned by the Source API. MS uses</span> : null}*/}
{isSuccess && !isLoading && data.length === 0 ? 'No recently played tracks!' : null}
<ul>{data.map(x => <li key={x.index}><PlayDisplay data={x} buildOptions={displayOpts}/></li>)}</ul>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/client/recent/recentDucks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ export const recentApi = createApi({
reducerPath: 'recentApi',
baseQuery: fetchBaseQuery({ baseUrl: './api/' }),
endpoints: (builder) => ({
getRecent: builder.query<RecentResponse, {name: string, type: string}>({
query: (params) => `recent?name=${params.name}&type=${params.type}`,
getRecent: builder.query<RecentResponse, {name: string, type: string, upstream?: string}>({
query: (params) => `recent?name=${params.name}&type=${params.type}&upstream=${params.upstream ?? 0}`,
transformResponse: (response: RecentResponse, meta, arg) => {
return response.map((x, index) => ({...x, index: index + 1}))
}
Expand Down
1 change: 1 addition & 0 deletions src/core/Atomic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface SourceStatusData {
authed: boolean;
players: Record<string, SourcePlayerJson>
sot: SOURCE_SOT_TYPES
supportsUpstreamRecentlyPlayed: boolean;
}

export interface ClientStatusData {
Expand Down

0 comments on commit d88ac1b

Please sign in to comment.