feat: add top-artists report

This commit is contained in:
Julian Tölle 2020-05-31 23:26:06 +02:00
parent 6a6ba493f6
commit 6fc10c40ca
18 changed files with 345 additions and 30 deletions

View file

@ -0,0 +1,18 @@
import { IsEnum, IsISO8601, ValidateIf } from "class-validator";
import { User } from "../../users/user.entity";
import { TimePreset } from "../timePreset.enum";
export class GetTopArtistsReportDto {
user: User;
@IsEnum(TimePreset)
timePreset: TimePreset;
@ValidateIf((o) => o.timePreset === TimePreset.CUSTOM)
@IsISO8601()
customTimeStart: string;
@ValidateIf((o) => o.timePreset === TimePreset.CUSTOM)
@IsISO8601()
customTimeEnd: string;
}

View file

@ -0,0 +1,8 @@
import { Artist } from "../../music-library/artist.entity";
export class TopArtistsReportDto {
items: {
artist: Artist;
count: number;
}[];
}

View file

@ -3,7 +3,9 @@ import { Auth } from "src/auth/decorators/auth.decorator";
import { ReqUser } from "../auth/decorators/req-user.decorator";
import { User } from "../users/user.entity";
import { GetListenReportDto } from "./dto/get-listen-report.dto";
import { GetTopArtistsReportDto } from "./dto/get-top-artists-report.dto";
import { ListenReportDto } from "./dto/listen-report.dto";
import { TopArtistsReportDto } from "./dto/top-artists-report.dto";
import { ReportsService } from "./reports.service";
@Controller("api/v1/reports")
@ -18,4 +20,13 @@ export class ReportsController {
): Promise<ListenReportDto> {
return this.reportsService.getListens({ ...options, user });
}
@Get("top-artists")
@Auth()
async getTopArtists(
@Query() options: Omit<GetTopArtistsReportDto, "user">,
@ReqUser() user: User
): Promise<TopArtistsReportDto> {
return this.reportsService.getTopArtists({ ...options, user });
}
}

View file

@ -17,7 +17,10 @@ import {
} from "date-fns";
import { ListensService } from "../listens/listens.service";
import { GetListenReportDto } from "./dto/get-listen-report.dto";
import { GetTopArtistsReportDto } from "./dto/get-top-artists-report.dto";
import { ListenReportDto } from "./dto/listen-report.dto";
import { TopArtistsReportDto } from "./dto/top-artists-report.dto";
import { Interval } from "./interval";
import { Timeframe } from "./timeframe.enum";
import { TimePreset } from "./timePreset.enum";
@ -55,6 +58,8 @@ const timePresetToDays: { [x in TimePreset]: number } = {
[TimePreset.CUSTOM]: 0, // Not used for this
};
const PAGINATION_LIMIT_UNLIMITED = 10000000;
@Injectable()
export class ReportsService {
constructor(private readonly listensService: ListensService) {}
@ -73,7 +78,7 @@ export class ReportsService {
user,
filter: { time: interval },
page: 1,
limit: 10000000,
limit: PAGINATION_LIMIT_UNLIMITED,
});
const reportInterval: Interval = {
@ -91,6 +96,54 @@ export class ReportsService {
return { items: reportItems, timeStart, timeEnd, timeFrame };
}
async getTopArtists(
options: GetTopArtistsReportDto
): Promise<TopArtistsReportDto> {
const { user, timePreset, customTimeStart, customTimeEnd } = options;
const interval = this.getIntervalFromPreset({
timePreset,
customTimeStart,
customTimeEnd,
});
const { items: listens } = await this.listensService.getListens({
user,
filter: { time: interval },
page: 1,
limit: PAGINATION_LIMIT_UNLIMITED,
});
// Declare types for metrics calculation
type Item = TopArtistsReportDto["items"][0];
type Accumulator = {
[x: string]: Item;
};
const items: TopArtistsReportDto["items"] = Object.values<Item>(
listens
.flatMap((listen) => listen.track.artists)
.reduce<Accumulator>((counters, artist) => {
if (!counters[artist.id]) {
counters[artist.id] = {
artist,
count: 0,
};
}
counters[artist.id].count += 1;
return counters;
}, {})
)
.sort((a, b) => a.count - b.count)
.reverse() // sort descending
.slice(0, 20); // TODO: Make configurable
return { items };
}
private getIntervalFromPreset(options: {
timePreset: TimePreset;
customTimeStart?: string;