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 { Track } from "../../music-library/track.entity";
|
||||||
import { User } from "../../users/user.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;
|
track: Track;
|
||||||
user: User;
|
user: User;
|
||||||
playedAt: Date;
|
playedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CreateListenResponseDto {
|
||||||
|
listen: Listen;
|
||||||
|
isDuplicate: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,12 @@
|
||||||
// tslint:disable: max-classes-per-file
|
// tslint:disable: max-classes-per-file
|
||||||
import { EntityRepository, Repository, SelectQueryBuilder } from "typeorm";
|
import { EntityRepository, Repository, SelectQueryBuilder } from "typeorm";
|
||||||
import { User } from "../users/user.entity";
|
|
||||||
import { Listen } from "./listen.entity";
|
|
||||||
import { Interval } from "../reports/interval";
|
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> {
|
export class ListenScopes extends SelectQueryBuilder<Listen> {
|
||||||
/**
|
/**
|
||||||
|
|
@ -32,4 +36,35 @@ export class ListenRepository extends Repository<Listen> {
|
||||||
get scoped(): ListenScopes {
|
get scoped(): ListenScopes {
|
||||||
return new ListenScopes(this.createQueryBuilder("listen"));
|
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 { Injectable } from "@nestjs/common";
|
||||||
import { getTime, parseISO, getUnixTime } from "date-fns";
|
|
||||||
import {
|
import {
|
||||||
IPaginationOptions,
|
IPaginationOptions,
|
||||||
paginate,
|
paginate,
|
||||||
Pagination,
|
Pagination,
|
||||||
} from "nestjs-typeorm-paginate";
|
} 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 { GetListensDto } from "./dto/get-listens.dto";
|
||||||
import { Listen } from "./listen.entity";
|
import { Listen } from "./listen.entity";
|
||||||
import { ListenRepository, ListenScopes } from "./listen.repository";
|
import { ListenRepository, ListenScopes } from "./listen.repository";
|
||||||
|
|
@ -18,26 +20,14 @@ export class ListensService {
|
||||||
user,
|
user,
|
||||||
track,
|
track,
|
||||||
playedAt,
|
playedAt,
|
||||||
}: CreateListenDto): Promise<Listen> {
|
}: CreateListenRequestDto): Promise<CreateListenResponseDto> {
|
||||||
const listen = this.listenRepository.create();
|
const response = await this.listenRepository.insertNoConflict({
|
||||||
|
user,
|
||||||
|
track,
|
||||||
|
playedAt,
|
||||||
|
});
|
||||||
|
|
||||||
listen.user = user;
|
return response;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getListens(
|
async getListens(
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,6 @@ export class SpotifyApiService {
|
||||||
limit: 50,
|
limit: 50,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (lastRefreshTime) {
|
|
||||||
parameters.after = lastRefreshTime.getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
const history = await this.httpService
|
const history = await this.httpService
|
||||||
.get<PagingObject<PlayHistoryObject>>(`v1/me/player/recently-played`, {
|
.get<PagingObject<PlayHistoryObject>>(`v1/me/player/recently-played`, {
|
||||||
headers: { Authorization: `Bearer ${accessToken}` },
|
headers: { Authorization: `Bearer ${accessToken}` },
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Injectable } from "@nestjs/common";
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Interval, Timeout } from "@nestjs/schedule";
|
import { Interval } from "@nestjs/schedule";
|
||||||
import { ListensService } from "../../listens/listens.service";
|
import { ListensService } from "../../listens/listens.service";
|
||||||
import { Logger } from "../../logger/logger.service";
|
import { Logger } from "../../logger/logger.service";
|
||||||
import { Album } from "../../music-library/album.entity";
|
import { Album } from "../../music-library/album.entity";
|
||||||
|
|
@ -80,19 +80,21 @@ export class SpotifyService {
|
||||||
playHistory.map(async (history) => {
|
playHistory.map(async (history) => {
|
||||||
const track = await this.importTrack(history.track.id);
|
const track = await this.importTrack(history.track.id);
|
||||||
|
|
||||||
this.listensService.createListen({
|
const { isDuplicate } = await this.listensService.createListen({
|
||||||
user,
|
user,
|
||||||
track,
|
track,
|
||||||
playedAt: new Date(history.played_at),
|
playedAt: new Date(history.played_at),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.logger.debug(
|
if (!isDuplicate) {
|
||||||
`New listen found! ${user.id} listened to "${
|
this.logger.debug(
|
||||||
track.name
|
`New listen found! ${user.id} listened to "${
|
||||||
}" by ${track.artists
|
track.name
|
||||||
?.map((artist) => `"${artist.name}"`)
|
}" by ${track.artists
|
||||||
.join(", ")}`
|
?.map((artist) => `"${artist.name}"`)
|
||||||
);
|
.join(", ")}`
|
||||||
|
);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -103,12 +105,14 @@ export class SpotifyService {
|
||||||
.pop()
|
.pop()
|
||||||
);
|
);
|
||||||
|
|
||||||
this.logger.debug(
|
/**
|
||||||
`Updating last refresh time for user ${
|
* lastRefreshTime was previously used to only get new listens from Spotify
|
||||||
user.id
|
* but the Spotify WEB Api was sometimes not adding the listens in the right
|
||||||
}, new time: ${newestPlayTime.toISOString()}`
|
* 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, {
|
this.usersService.updateSpotifyConnection(user, {
|
||||||
...user.spotify,
|
...user.spotify,
|
||||||
lastRefreshTime: newestPlayTime,
|
lastRefreshTime: newestPlayTime,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue