2020-02-02 19:21:58 +01:00
|
|
|
import { Injectable } from "@nestjs/common";
|
|
|
|
|
import { Interval } from "@nestjs/schedule";
|
|
|
|
|
import { ListensService } from "../../listens/listens.service";
|
|
|
|
|
import { Album } from "../../music-library/album.entity";
|
|
|
|
|
import { Artist } from "../../music-library/artist.entity";
|
|
|
|
|
import { MusicLibraryService } from "../../music-library/music-library.service";
|
|
|
|
|
import { Track } from "../../music-library/track.entity";
|
|
|
|
|
import { User } from "../../users/user.entity";
|
|
|
|
|
import { UsersService } from "../../users/users.service";
|
|
|
|
|
import { AlbumObject } from "./spotify-api/entities/album-object";
|
|
|
|
|
import { ArtistObject } from "./spotify-api/entities/artist-object";
|
|
|
|
|
import { PlayHistoryObject } from "./spotify-api/entities/play-history-object";
|
|
|
|
|
import { TrackObject } from "./spotify-api/entities/track-object";
|
|
|
|
|
import { SpotifyApiService } from "./spotify-api/spotify-api.service";
|
|
|
|
|
import { SpotifyAuthService } from "./spotify-auth/spotify-auth.service";
|
2020-02-01 16:11:48 +01:00
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
|
export class SpotifyService {
|
2020-02-02 19:21:58 +01:00
|
|
|
private appAccessToken: string | null;
|
|
|
|
|
private appAccessTokenInProgress: Promise<void> | null;
|
2020-02-01 16:11:48 +01:00
|
|
|
|
2020-02-02 19:21:58 +01:00
|
|
|
constructor(
|
|
|
|
|
private readonly usersService: UsersService,
|
|
|
|
|
private readonly listensService: ListensService,
|
|
|
|
|
private readonly musicLibraryService: MusicLibraryService,
|
|
|
|
|
private readonly spotifyApi: SpotifyApiService,
|
|
|
|
|
private readonly spotifyAuth: SpotifyAuthService
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
@Interval(20 * 1000)
|
|
|
|
|
async getRecentlyPlayedTracks(): Promise<void> {
|
|
|
|
|
const users = await this.usersService.findAll();
|
|
|
|
|
|
|
|
|
|
for (const user of users) {
|
|
|
|
|
await this.processUser(user);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async processUser(
|
|
|
|
|
user: User,
|
|
|
|
|
retryOnExpiredToken: boolean = true
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
let playHistory: PlayHistoryObject[];
|
|
|
|
|
try {
|
|
|
|
|
playHistory = await this.spotifyApi.getRecentlyPlayedTracks(user.spotify);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err.response && err.response.status === 401 && retryOnExpiredToken) {
|
|
|
|
|
const accessToken = await this.spotifyAuth.refreshAccessToken(
|
|
|
|
|
user.spotify
|
|
|
|
|
);
|
|
|
|
|
await this.usersService.updateSpotifyConnection(user, {
|
|
|
|
|
...user.spotify,
|
2020-05-02 17:17:20 +02:00
|
|
|
accessToken,
|
2020-02-02 19:21:58 +01:00
|
|
|
});
|
|
|
|
|
await this.processUser(user, false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (playHistory.length === 0) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Promise.all(
|
2020-05-02 17:17:20 +02:00
|
|
|
playHistory.map(async (history) => {
|
2020-02-02 19:21:58 +01:00
|
|
|
const track = await this.importTrack(history.track.id);
|
|
|
|
|
|
|
|
|
|
this.listensService.createListen({
|
|
|
|
|
user,
|
|
|
|
|
track,
|
2020-05-02 17:17:20 +02:00
|
|
|
playedAt: new Date(history.played_at),
|
2020-02-02 19:21:58 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
|
`Found Listen!: ${user.displayName} listened to "${track.name}" by "${track.artists}"`
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const newestPlayTime = new Date(
|
|
|
|
|
playHistory
|
2020-05-02 17:17:20 +02:00
|
|
|
.map((history) => history.played_at)
|
2020-02-02 19:21:58 +01:00
|
|
|
.sort()
|
|
|
|
|
.pop()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
console.log("newestPlayTime", {
|
|
|
|
|
newestPlayTime,
|
2020-05-02 17:17:20 +02:00
|
|
|
times: playHistory.map((history) => history.played_at).sort(),
|
2020-02-02 19:21:58 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this.usersService.updateSpotifyConnection(user, {
|
|
|
|
|
...user.spotify,
|
2020-05-02 17:17:20 +02:00
|
|
|
lastRefreshTime: newestPlayTime,
|
2020-02-02 19:21:58 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async importTrack(
|
|
|
|
|
spotifyID: string,
|
|
|
|
|
retryOnExpiredToken: boolean = true
|
|
|
|
|
): Promise<Track> {
|
|
|
|
|
const track = await this.musicLibraryService.findTrack({
|
2020-05-02 17:17:20 +02:00
|
|
|
spotify: { id: spotifyID },
|
2020-02-02 19:21:58 +01:00
|
|
|
});
|
|
|
|
|
if (track) {
|
|
|
|
|
return track;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let spotifyTrack: TrackObject;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
spotifyTrack = await this.spotifyApi.getTrack(
|
|
|
|
|
this.appAccessToken,
|
|
|
|
|
spotifyID
|
|
|
|
|
);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err.response && err.response.status === 401) {
|
|
|
|
|
await this.refreshAppAccessToken();
|
|
|
|
|
|
|
|
|
|
return this.importTrack(spotifyID, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const [album, artists] = await Promise.all([
|
|
|
|
|
this.importAlbum(spotifyTrack.album.id),
|
|
|
|
|
Promise.all(
|
|
|
|
|
spotifyTrack.artists.map(({ id: artistID }) =>
|
|
|
|
|
this.importArtist(artistID)
|
|
|
|
|
)
|
2020-05-02 17:17:20 +02:00
|
|
|
),
|
2020-02-02 19:21:58 +01:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return this.musicLibraryService.createTrack({
|
|
|
|
|
name: spotifyTrack.name,
|
|
|
|
|
album,
|
|
|
|
|
artists,
|
|
|
|
|
spotify: {
|
|
|
|
|
id: spotifyTrack.id,
|
|
|
|
|
uri: spotifyTrack.uri,
|
|
|
|
|
type: spotifyTrack.type,
|
2020-05-02 17:17:20 +02:00
|
|
|
href: spotifyTrack.href,
|
|
|
|
|
},
|
2020-02-02 19:21:58 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async importAlbum(
|
|
|
|
|
spotifyID: string,
|
|
|
|
|
retryOnExpiredToken: boolean = true
|
|
|
|
|
): Promise<Album> {
|
|
|
|
|
const album = await this.musicLibraryService.findAlbum({
|
2020-05-02 17:17:20 +02:00
|
|
|
spotify: { id: spotifyID },
|
2020-02-02 19:21:58 +01:00
|
|
|
});
|
|
|
|
|
if (album) {
|
|
|
|
|
return album;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let spotifyAlbum: AlbumObject;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
spotifyAlbum = await this.spotifyApi.getAlbum(
|
|
|
|
|
this.appAccessToken,
|
|
|
|
|
spotifyID
|
|
|
|
|
);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err.response && err.response.status === 401) {
|
|
|
|
|
await this.refreshAppAccessToken();
|
|
|
|
|
|
|
|
|
|
return this.importAlbum(spotifyID, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const artists = await Promise.all(
|
|
|
|
|
spotifyAlbum.artists.map(({ id: artistID }) =>
|
|
|
|
|
this.importArtist(artistID)
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return this.musicLibraryService.createAlbum({
|
|
|
|
|
name: spotifyAlbum.name,
|
|
|
|
|
artists,
|
|
|
|
|
spotify: {
|
|
|
|
|
id: spotifyAlbum.id,
|
|
|
|
|
uri: spotifyAlbum.uri,
|
|
|
|
|
type: spotifyAlbum.type,
|
2020-05-02 17:17:20 +02:00
|
|
|
href: spotifyAlbum.href,
|
|
|
|
|
},
|
2020-02-02 19:21:58 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async importArtist(
|
|
|
|
|
spotifyID: string,
|
|
|
|
|
retryOnExpiredToken: boolean = true
|
|
|
|
|
): Promise<Artist> {
|
|
|
|
|
const artist = await this.musicLibraryService.findArtist({
|
2020-05-02 17:17:20 +02:00
|
|
|
spotify: { id: spotifyID },
|
2020-02-02 19:21:58 +01:00
|
|
|
});
|
|
|
|
|
if (artist) {
|
|
|
|
|
return artist;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let spotifyArtist: ArtistObject;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
spotifyArtist = await this.spotifyApi.getArtist(
|
|
|
|
|
this.appAccessToken,
|
|
|
|
|
spotifyID
|
|
|
|
|
);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err.response && err.response.status === 401) {
|
|
|
|
|
await this.refreshAppAccessToken();
|
|
|
|
|
|
|
|
|
|
return this.importArtist(spotifyID, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.musicLibraryService.createArtist({
|
|
|
|
|
name: spotifyArtist.name,
|
|
|
|
|
spotify: {
|
|
|
|
|
id: spotifyArtist.id,
|
|
|
|
|
uri: spotifyArtist.uri,
|
|
|
|
|
type: spotifyArtist.type,
|
2020-05-02 17:17:20 +02:00
|
|
|
href: spotifyArtist.href,
|
|
|
|
|
},
|
2020-02-02 19:21:58 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async refreshAppAccessToken(): Promise<void> {
|
|
|
|
|
if (!this.appAccessTokenInProgress) {
|
|
|
|
|
console.log("refreshAppAccessToken");
|
|
|
|
|
this.appAccessTokenInProgress = new Promise(async (resolve, reject) => {
|
|
|
|
|
try {
|
|
|
|
|
const newAccessToken = await this.spotifyAuth.clientCredentialsGrant();
|
|
|
|
|
this.appAccessToken = newAccessToken;
|
|
|
|
|
resolve();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
reject(err);
|
|
|
|
|
} finally {
|
|
|
|
|
this.appAccessTokenInProgress = null;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
console.log(
|
|
|
|
|
"refreshAppAccessToken already in progress, awaiting its result"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return this.appAccessTokenInProgress;
|
|
|
|
|
}
|
2020-02-01 16:11:48 +01:00
|
|
|
}
|