mirror of
https://github.com/apricote/Listory.git
synced 2026-01-13 21:21:02 +00:00
feat(api): poll listens less often if user is inactive
To save on Spotify API requests we have two different classes of polling intervals: - all users are polled at least every 10 minutes, this is a safe interval and no listens will be ever missed - if a user listened to a song within the last 60 minutes, we poll every minute to ensure that the UI shows new listens immediately
This commit is contained in:
parent
b9f92bbdfa
commit
14478a5418
3 changed files with 59 additions and 6 deletions
|
|
@ -57,6 +57,16 @@ export class ListensService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMostRecentListenPerUser(): Promise<Listen[]> {
|
||||||
|
return this.listenRepository
|
||||||
|
.createQueryBuilder("listen")
|
||||||
|
.leftJoinAndSelect("listen.user", "user")
|
||||||
|
.distinctOn(["user.id"])
|
||||||
|
.orderBy({ "user.id": "ASC", "listen.playedAt": "DESC" })
|
||||||
|
.limit(1)
|
||||||
|
.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
getScopedQueryBuilder(): ListenScopes {
|
getScopedQueryBuilder(): ListenScopes {
|
||||||
return this.listenRepository.scoped;
|
return this.listenRepository.scoped;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
UpdateSpotifyLibraryJob,
|
UpdateSpotifyLibraryJob,
|
||||||
} from "./jobs";
|
} from "./jobs";
|
||||||
import { JobService } from "@apricote/nest-pg-boss";
|
import { JobService } from "@apricote/nest-pg-boss";
|
||||||
|
import { Span } from "nestjs-otel";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SchedulerService implements OnApplicationBootstrap {
|
export class SchedulerService implements OnApplicationBootstrap {
|
||||||
|
|
@ -35,15 +36,38 @@ export class SchedulerService implements OnApplicationBootstrap {
|
||||||
await this.superviseImportJobsJobService.schedule("*/1 * * * *", {}, {});
|
await this.superviseImportJobsJobService.schedule("*/1 * * * *", {}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Span()
|
||||||
@CrawlerSupervisorJob.Handle()
|
@CrawlerSupervisorJob.Handle()
|
||||||
async superviseImportJobs(): Promise<void> {
|
async superviseImportJobs(): Promise<void> {
|
||||||
this.logger.log("Starting crawler jobs");
|
this.logger.log("Starting crawler jobs");
|
||||||
const users = await this.spotifyService.getCrawlableUserInfo();
|
const userInfo = await this.spotifyService.getCrawlableUserInfo();
|
||||||
|
|
||||||
|
// To save on Spotify API requests we have two different classes of polling intervals:
|
||||||
|
// - all users are polled at least every 10 minutes, this is a safe interval
|
||||||
|
// and no listens will be ever missed
|
||||||
|
// - if a user listened to a song within the last 60 minutes, we poll every
|
||||||
|
// minute to ensure that the UI shows new listens immediately
|
||||||
|
const POLL_RATE_INACTIVE_SEC = 10 * 60;
|
||||||
|
const POLL_RATE_ACTIVE_SEC = 1 * 60;
|
||||||
|
|
||||||
|
const INACTIVE_CUTOFF_MSEC = 60 * 60 * 1000;
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
users.map((user) =>
|
userInfo.map(({ user, lastListen }) => {
|
||||||
this.importSpotifyJobService.sendOnce({ userID: user.id }, {}, user.id)
|
let pollRate = POLL_RATE_INACTIVE_SEC;
|
||||||
)
|
|
||||||
|
const timeSinceLastListen = new Date().getTime() - lastListen.getTime();
|
||||||
|
if (timeSinceLastListen < INACTIVE_CUTOFF_MSEC) {
|
||||||
|
pollRate = POLL_RATE_ACTIVE_SEC;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.importSpotifyJobService.sendThrottled(
|
||||||
|
{ userID: user.id },
|
||||||
|
{},
|
||||||
|
pollRate,
|
||||||
|
user.id
|
||||||
|
);
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,27 @@ export class SpotifyService {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Span()
|
@Span()
|
||||||
async getCrawlableUserInfo(): Promise<User[]> {
|
async getCrawlableUserInfo(): Promise<{ user: User; lastListen: Date }[]> {
|
||||||
return this.usersService.findAll();
|
// All of this is kinda inefficient, we do two db queries and join in code,
|
||||||
|
// i can't be bothered to do this properly in the db for now.
|
||||||
|
// Should be refactored if listory gets hundreds of users (lol).
|
||||||
|
|
||||||
|
const [users, listens] = await Promise.all([
|
||||||
|
this.usersService.findAll(),
|
||||||
|
this.listensService.getMostRecentListenPerUser(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return users.map((user) => {
|
||||||
|
const lastListen = listens.find((listen) => listen.user.id === user.id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
// Return 1970 if no listen exists
|
||||||
|
lastListen: lastListen ? lastListen.playedAt : new Date(0),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ImportSpotifyJob.Handle()
|
@ImportSpotifyJob.Handle()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue