mirror of
https://github.com/apricote/Listory.git
synced 2026-01-13 21:21:02 +00:00
fix(api): missed listens from spotify
Listory could miss some listens when the Spotify Api misbehaves. Sometimes the listens added to the last-recently-played endpoint are showing up are out-of-order. Because of our optimization to only retrieve listens newer than the lastRefreshTime we would skip retrieving those out-of-order listens. By always retrieving the maximum 50 listens we can be reasonably sure that we got all the listens. We also had to improve the handling of duplicate listens, as we now have a lot of them, curtesy of removing the lastRefreshTime optimization.
This commit is contained in:
parent
52a0dc79ce
commit
5ca3437b59
5 changed files with 76 additions and 43 deletions
|
|
@ -1,8 +1,16 @@
|
|||
import { Track } from "../../music-library/track.entity";
|
||||
import { User } from "../../users/user.entity";
|
||||
import { Listen } from "../listen.entity";
|
||||
|
||||
export class CreateListenDto {
|
||||
// tslint:disable max-classes-per-file
|
||||
|
||||
export class CreateListenRequestDto {
|
||||
track: Track;
|
||||
user: User;
|
||||
playedAt: Date;
|
||||
}
|
||||
|
||||
export class CreateListenResponseDto {
|
||||
listen: Listen;
|
||||
isDuplicate: boolean;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
// tslint:disable: max-classes-per-file
|
||||
import { EntityRepository, Repository, SelectQueryBuilder } from "typeorm";
|
||||
import { User } from "../users/user.entity";
|
||||
import { Listen } from "./listen.entity";
|
||||
import { Interval } from "../reports/interval";
|
||||
import { User } from "../users/user.entity";
|
||||
import {
|
||||
CreateListenRequestDto,
|
||||
CreateListenResponseDto,
|
||||
} from "./dto/create-listen.dto";
|
||||
import { Listen } from "./listen.entity";
|
||||
|
||||
export class ListenScopes extends SelectQueryBuilder<Listen> {
|
||||
/**
|
||||
|
|
@ -32,4 +36,35 @@ export class ListenRepository extends Repository<Listen> {
|
|||
get scoped(): ListenScopes {
|
||||
return new ListenScopes(this.createQueryBuilder("listen"));
|
||||
}
|
||||
|
||||
async insertNoConflict({
|
||||
user,
|
||||
track,
|
||||
playedAt,
|
||||
}: CreateListenRequestDto): Promise<CreateListenResponseDto> {
|
||||
const result = await this.createQueryBuilder()
|
||||
.insert()
|
||||
.values({
|
||||
user,
|
||||
track,
|
||||
playedAt,
|
||||
})
|
||||
.onConflict('("playedAt", "trackId", "userId") DO NOTHING')
|
||||
.execute();
|
||||
|
||||
const [insertedRowIdentifier] = result.identifiers;
|
||||
|
||||
if (!insertedRowIdentifier) {
|
||||
// We did not insert a new listen, it already existed
|
||||
return {
|
||||
listen: await this.findOne({ where: { user, track, playedAt } }),
|
||||
isDuplicate: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
listen: await this.findOne(insertedRowIdentifier.id),
|
||||
isDuplicate: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { getTime, parseISO, getUnixTime } from "date-fns";
|
||||
import {
|
||||
IPaginationOptions,
|
||||
paginate,
|
||||
Pagination,
|
||||
} from "nestjs-typeorm-paginate";
|
||||
import { CreateListenDto } from "./dto/create-listen.dto";
|
||||
import {
|
||||
CreateListenRequestDto,
|
||||
CreateListenResponseDto,
|
||||
} from "./dto/create-listen.dto";
|
||||
import { GetListensDto } from "./dto/get-listens.dto";
|
||||
import { Listen } from "./listen.entity";
|
||||
import { ListenRepository, ListenScopes } from "./listen.repository";
|
||||
|
|
@ -18,26 +20,14 @@ export class ListensService {
|
|||
user,
|
||||
track,
|
||||
playedAt,
|
||||
}: CreateListenDto): Promise<Listen> {
|
||||
const listen = this.listenRepository.create();
|
||||
}: CreateListenRequestDto): Promise<CreateListenResponseDto> {
|
||||
const response = await this.listenRepository.insertNoConflict({
|
||||
user,
|
||||
track,
|
||||
playedAt,
|
||||
});
|
||||
|
||||
listen.user = user;
|
||||
listen.track = track;
|
||||
listen.playedAt = playedAt;
|
||||
|
||||
try {
|
||||
await this.listenRepository.save(listen);
|
||||
} catch (err) {
|
||||
if (err.code === "23505") {
|
||||
return this.listenRepository.findOne({
|
||||
where: { user, track, playedAt },
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
return listen;
|
||||
return response;
|
||||
}
|
||||
|
||||
async getListens(
|
||||
|
|
|
|||
|
|
@ -18,10 +18,6 @@ export class SpotifyApiService {
|
|||
limit: 50,
|
||||
};
|
||||
|
||||
if (lastRefreshTime) {
|
||||
parameters.after = lastRefreshTime.getTime();
|
||||
}
|
||||
|
||||
const history = await this.httpService
|
||||
.get<PagingObject<PlayHistoryObject>>(`v1/me/player/recently-played`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Injectable } from "@nestjs/common";
|
||||
import { Interval, Timeout } from "@nestjs/schedule";
|
||||
import { Interval } from "@nestjs/schedule";
|
||||
import { ListensService } from "../../listens/listens.service";
|
||||
import { Logger } from "../../logger/logger.service";
|
||||
import { Album } from "../../music-library/album.entity";
|
||||
|
|
@ -80,19 +80,21 @@ export class SpotifyService {
|
|||
playHistory.map(async (history) => {
|
||||
const track = await this.importTrack(history.track.id);
|
||||
|
||||
this.listensService.createListen({
|
||||
const { isDuplicate } = await this.listensService.createListen({
|
||||
user,
|
||||
track,
|
||||
playedAt: new Date(history.played_at),
|
||||
});
|
||||
|
||||
this.logger.debug(
|
||||
`New listen found! ${user.id} listened to "${
|
||||
track.name
|
||||
}" by ${track.artists
|
||||
?.map((artist) => `"${artist.name}"`)
|
||||
.join(", ")}`
|
||||
);
|
||||
if (!isDuplicate) {
|
||||
this.logger.debug(
|
||||
`New listen found! ${user.id} listened to "${
|
||||
track.name
|
||||
}" by ${track.artists
|
||||
?.map((artist) => `"${artist.name}"`)
|
||||
.join(", ")}`
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
|
@ -103,12 +105,14 @@ export class SpotifyService {
|
|||
.pop()
|
||||
);
|
||||
|
||||
this.logger.debug(
|
||||
`Updating last refresh time for user ${
|
||||
user.id
|
||||
}, new time: ${newestPlayTime.toISOString()}`
|
||||
);
|
||||
|
||||
/**
|
||||
* lastRefreshTime was previously used to only get new listens from Spotify
|
||||
* but the Spotify WEB Api was sometimes not adding the listens in the right
|
||||
* order, causing us to miss some listens.
|
||||
*
|
||||
* The variable will still be set, in case we want to add the functionality
|
||||
* again.
|
||||
*/
|
||||
this.usersService.updateSpotifyConnection(user, {
|
||||
...user.spotify,
|
||||
lastRefreshTime: newestPlayTime,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue