mirror of
https://github.com/apricote/Listory.git
synced 2026-01-13 21:21:02 +00:00
feat(api): fetch listens from spotify
This commit is contained in:
parent
f253a66f86
commit
f2065d3f1f
54 changed files with 1180 additions and 256 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { SpotifyModule } from './spotify/spotify.module';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { SpotifyModule } from "./spotify/spotify.module";
|
||||
|
||||
@Module({
|
||||
imports: [SpotifyModule]
|
||||
|
|
|
|||
67
src/sources/spotify/spotify-api/entities/album-object.ts
Normal file
67
src/sources/spotify/spotify-api/entities/album-object.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { PagingObject } from "./paging-object";
|
||||
import { SimplifiedTrackObject } from "./simplified-track-object";
|
||||
import { SimplifiedArtistObject } from "./simplified-artist-object";
|
||||
|
||||
// tslint:disable: variable-name
|
||||
|
||||
export class AlbumObject {
|
||||
/**
|
||||
* A list of the genres used to classify the album.
|
||||
* For example: "Prog Rock" , "Post-Grunge".
|
||||
* (If not yet classified, the array is empty.)
|
||||
*/
|
||||
genres: string[];
|
||||
|
||||
/**
|
||||
* The artists of the album.
|
||||
* Each artist object includes a link in href to more detailed information about the artist.
|
||||
*/
|
||||
artists: SimplifiedArtistObject[];
|
||||
|
||||
/**
|
||||
* A link to the Web API endpoint providing full details of the album.
|
||||
*/
|
||||
href: string;
|
||||
|
||||
/**
|
||||
* The Spotify ID for the album.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The label for the album.
|
||||
*/
|
||||
label: string;
|
||||
|
||||
/**
|
||||
* The name of the album.
|
||||
* In case of an album takedown, the value may be an empty string.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The object type: "album".
|
||||
*/
|
||||
type: "album";
|
||||
|
||||
/**
|
||||
* The Spotify URI for the album.
|
||||
*/
|
||||
uri: string;
|
||||
|
||||
/**
|
||||
* The date the album was first released, for example `1981`.
|
||||
* Depending on the precision, it might be shown as `1981-12` or `1981-12-15`.
|
||||
*/
|
||||
release_date: string;
|
||||
|
||||
/**
|
||||
* The precision with which `release_date` value is known: `year` , `month` , or `day`.
|
||||
*/
|
||||
release_date_precision: "year" | "month" | "day";
|
||||
|
||||
/**
|
||||
* The tracks of the album.
|
||||
*/
|
||||
tracks: PagingObject<SimplifiedTrackObject>;
|
||||
}
|
||||
32
src/sources/spotify/spotify-api/entities/artist-object.ts
Normal file
32
src/sources/spotify/spotify-api/entities/artist-object.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export class ArtistObject {
|
||||
/**
|
||||
* A list of the genres the artist is associated with.
|
||||
* For example: "Prog Rock" , "Post-Grunge".
|
||||
* (If not yet classified, the array is empty.)
|
||||
*/
|
||||
genres: string[];
|
||||
/**
|
||||
* A link to the Web API endpoint providing full details of the artist.
|
||||
*/
|
||||
href: string;
|
||||
|
||||
/**
|
||||
* The Spotify ID for the artist.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The name of the artist.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The object type: "artist".
|
||||
*/
|
||||
type: "artist";
|
||||
|
||||
/**
|
||||
* The Spotify URI for the artist.
|
||||
*/
|
||||
uri: string;
|
||||
}
|
||||
25
src/sources/spotify/spotify-api/entities/context-object.ts
Normal file
25
src/sources/spotify/spotify-api/entities/context-object.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { ExternalUrlObject } from "./external-url-object";
|
||||
|
||||
// tslint:disable variable-name
|
||||
|
||||
export class ContextObject {
|
||||
/**
|
||||
* External URLs for this context.
|
||||
*/
|
||||
external_urls: ExternalUrlObject;
|
||||
|
||||
/**
|
||||
* A link to the Web API endpoint providing full details of the track.
|
||||
*/
|
||||
href: string;
|
||||
|
||||
/**
|
||||
* The object type, e.g. "artist", "playlist", "album".
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* The Spotify URI for the context.
|
||||
*/
|
||||
uri: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
export class ExternalUrlObject {
|
||||
// No documentation for this exists
|
||||
}
|
||||
36
src/sources/spotify/spotify-api/entities/paging-object.ts
Normal file
36
src/sources/spotify/spotify-api/entities/paging-object.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
export class PagingObject<T = any> {
|
||||
/**
|
||||
* A link to the Web API endpoint returning the full result of the request
|
||||
*/
|
||||
href: string;
|
||||
|
||||
/**
|
||||
* The requested data
|
||||
*/
|
||||
items: T[];
|
||||
|
||||
/**
|
||||
* The maximum number of items in the response (as set in the query or by default).
|
||||
*/
|
||||
limit: number;
|
||||
|
||||
/**
|
||||
* URL to the next page of items. ( null if none)
|
||||
*/
|
||||
next: string;
|
||||
|
||||
/**
|
||||
* The offset of the items returned (as set in the query or by default)
|
||||
*/
|
||||
offset: number;
|
||||
|
||||
/**
|
||||
* URL to the previous page of items. ( null if none)
|
||||
*/
|
||||
previous: string;
|
||||
|
||||
/**
|
||||
* The total number of items available to return.
|
||||
*/
|
||||
total: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { ContextObject } from "./context-object";
|
||||
import { SimplifiedTrackObject } from "./simplified-track-object";
|
||||
|
||||
// tslint:disable variable-name
|
||||
|
||||
export class PlayHistoryObject {
|
||||
/**
|
||||
* The context the track was played from.
|
||||
*/
|
||||
context: ContextObject;
|
||||
|
||||
/**
|
||||
* The date and time the track was played.
|
||||
*/
|
||||
played_at: string;
|
||||
|
||||
/**
|
||||
* The track the user listened to.
|
||||
*/
|
||||
track: SimplifiedTrackObject;
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// tslint:disable: variable-name
|
||||
|
||||
export class SimplifiedAlbumObject {
|
||||
album_type: "album" | "single" | "compilation";
|
||||
|
||||
/**
|
||||
* A link to the Web API endpoint providing full details of the album.
|
||||
*/
|
||||
href: string;
|
||||
|
||||
/**
|
||||
* The Spotify ID for the album.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The name of the album. In case of an album takedown, the value may be an empty string.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The object type: "album".
|
||||
*/
|
||||
type: "album";
|
||||
|
||||
/**
|
||||
* The Spotify URI for the album.
|
||||
*/
|
||||
uri: string;
|
||||
|
||||
/**
|
||||
* The date the album was first released, for example `1981`.
|
||||
* Depending on the precision, it might be shown as `1981-12` or `1981-12-15`.
|
||||
*/
|
||||
release_date: string;
|
||||
|
||||
/**
|
||||
* The precision with which `release_date` value is known: `year` , `month` , or `day`.
|
||||
*/
|
||||
release_date_precision: "year" | "month" | "day";
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
export class SimplifiedArtistObject {
|
||||
/**
|
||||
* A link to the Web API endpoint providing full details of the artist.
|
||||
*/
|
||||
href: string;
|
||||
|
||||
/**
|
||||
* The Spotify ID for the artist.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The name of the artist.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The object type: "artist".
|
||||
*/
|
||||
type: "artist";
|
||||
|
||||
/**
|
||||
* The Spotify URI for the artist.
|
||||
*/
|
||||
uri: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
export class SimplifiedTrackObject {
|
||||
/**
|
||||
* The Spotify ID for the track.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The name of the track.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The Spotify URI for the track.
|
||||
*/
|
||||
uri: string;
|
||||
}
|
||||
46
src/sources/spotify/spotify-api/entities/track-object.ts
Normal file
46
src/sources/spotify/spotify-api/entities/track-object.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { SimplifiedAlbumObject } from "./simplified-album-object";
|
||||
import { SimplifiedArtistObject } from "./simplified-artist-object";
|
||||
|
||||
// tslint:disable: variable-name
|
||||
|
||||
export class TrackObject {
|
||||
/**
|
||||
* The album on which the track appears. The album object includes a link in href to full information about the album.
|
||||
*/
|
||||
album: SimplifiedAlbumObject;
|
||||
|
||||
/**
|
||||
* The album on which the track appears. The album object includes a link in href to full information about the album.
|
||||
*/
|
||||
artists: SimplifiedArtistObject[];
|
||||
|
||||
/**
|
||||
* The track length in milliseconds.
|
||||
*/
|
||||
duration_ms: number;
|
||||
|
||||
/**
|
||||
* A link to the Web API endpoint providing full details of the track.
|
||||
*/
|
||||
href: string;
|
||||
|
||||
/**
|
||||
* The Spotify ID for the track.
|
||||
*/
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The name of the track.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* The object type: "track".
|
||||
*/
|
||||
type: "track";
|
||||
|
||||
/**
|
||||
* The Spotify URI for the track.
|
||||
*/
|
||||
uri: string;
|
||||
}
|
||||
16
src/sources/spotify/spotify-api/spotify-api.module.ts
Normal file
16
src/sources/spotify/spotify-api/spotify-api.module.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { HttpModule, Module } from "@nestjs/common";
|
||||
import { SpotifyApiService } from "./spotify-api.service";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
HttpModule.registerAsync({
|
||||
useFactory: () => ({
|
||||
timeout: 5000,
|
||||
baseURL: "https://api.spotify.com/"
|
||||
})
|
||||
})
|
||||
],
|
||||
providers: [SpotifyApiService],
|
||||
exports: [SpotifyApiService]
|
||||
})
|
||||
export class SpotifyApiModule {}
|
||||
18
src/sources/spotify/spotify-api/spotify-api.service.spec.ts
Normal file
18
src/sources/spotify/spotify-api/spotify-api.service.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SpotifyApiService } from './spotify-api.service';
|
||||
|
||||
describe('SpotifyApiService', () => {
|
||||
let service: SpotifyApiService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [SpotifyApiService],
|
||||
}).compile();
|
||||
|
||||
service = module.get<SpotifyApiService>(SpotifyApiService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
81
src/sources/spotify/spotify-api/spotify-api.service.ts
Normal file
81
src/sources/spotify/spotify-api/spotify-api.service.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import { HttpService, Injectable } from "@nestjs/common";
|
||||
import { SpotifyConnection } from "../spotify-connection.entity";
|
||||
import { AlbumObject } from "./entities/album-object";
|
||||
import { ArtistObject } from "./entities/artist-object";
|
||||
import { PagingObject } from "./entities/paging-object";
|
||||
import { PlayHistoryObject } from "./entities/play-history-object";
|
||||
import { TrackObject } from "./entities/track-object";
|
||||
|
||||
@Injectable()
|
||||
export class SpotifyApiService {
|
||||
constructor(private readonly httpService: HttpService) {}
|
||||
|
||||
async getRecentlyPlayedTracks({
|
||||
accessToken,
|
||||
lastRefreshTime
|
||||
}: SpotifyConnection): Promise<PlayHistoryObject[]> {
|
||||
console.log("SpotifyApiService#getRecentlyPlayedTracks");
|
||||
|
||||
const parameters: { limit: number; after?: number } = {
|
||||
limit: 50
|
||||
};
|
||||
|
||||
if (lastRefreshTime) {
|
||||
parameters.after = lastRefreshTime.getTime();
|
||||
}
|
||||
|
||||
console.log(
|
||||
"getRecentlyPlayedTracks parameters",
|
||||
parameters,
|
||||
lastRefreshTime
|
||||
);
|
||||
|
||||
const history = await this.httpService
|
||||
.get<PagingObject<PlayHistoryObject>>(`v1/me/player/recently-played`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
params: parameters
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
return history.data.items;
|
||||
}
|
||||
|
||||
async getArtist(
|
||||
accessToken: string,
|
||||
spotifyID: string
|
||||
): Promise<ArtistObject> {
|
||||
console.log("SpotifyApiService#getArtist");
|
||||
const artist = await this.httpService
|
||||
.get<ArtistObject>(`v1/artists/${spotifyID}`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` }
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
return artist.data;
|
||||
}
|
||||
|
||||
async getAlbum(accessToken: string, spotifyID: string): Promise<AlbumObject> {
|
||||
console.log("SpotifyApiService#getAlbum");
|
||||
|
||||
const album = await this.httpService
|
||||
.get<AlbumObject>(`v1/albums/${spotifyID}`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` }
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
console.log("getAlbum", { data: album.data });
|
||||
return album.data;
|
||||
}
|
||||
|
||||
async getTrack(accessToken: string, spotifyID: string): Promise<TrackObject> {
|
||||
console.log("SpotifyApiService#getTrack");
|
||||
|
||||
const track = await this.httpService
|
||||
.get<TrackObject>(`v1/tracks/${spotifyID}`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` }
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
return track.data;
|
||||
}
|
||||
}
|
||||
16
src/sources/spotify/spotify-auth/spotify-auth.module.ts
Normal file
16
src/sources/spotify/spotify-auth/spotify-auth.module.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { HttpModule, Module } from "@nestjs/common";
|
||||
import { SpotifyAuthService } from "./spotify-auth.service";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
HttpModule.registerAsync({
|
||||
useFactory: () => ({
|
||||
timeout: 5000,
|
||||
baseURL: "https://accounts.spotify.com/"
|
||||
})
|
||||
})
|
||||
],
|
||||
providers: [SpotifyAuthService],
|
||||
exports: [SpotifyAuthService]
|
||||
})
|
||||
export class SpotifyAuthModule {}
|
||||
51
src/sources/spotify/spotify-auth/spotify-auth.service.ts
Normal file
51
src/sources/spotify/spotify-auth/spotify-auth.service.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { HttpService, Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { SpotifyConnection } from "../spotify-connection.entity";
|
||||
|
||||
@Injectable()
|
||||
export class SpotifyAuthService {
|
||||
private readonly clientID: string;
|
||||
private readonly clientSecret: string;
|
||||
|
||||
constructor(
|
||||
private readonly httpService: HttpService,
|
||||
config: ConfigService
|
||||
) {
|
||||
this.clientID = config.get<string>("SPOTIFY_CLIENT_ID");
|
||||
this.clientSecret = config.get<string>("SPOTIFY_CLIENT_SECRET");
|
||||
}
|
||||
|
||||
async clientCredentialsGrant(): Promise<string> {
|
||||
const response = await this.httpService
|
||||
.post<{ access_token: string }>(
|
||||
`api/token`,
|
||||
"grant_type=client_credentials",
|
||||
{
|
||||
auth: {
|
||||
username: this.clientID,
|
||||
password: this.clientSecret
|
||||
}
|
||||
}
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
return response.data.access_token;
|
||||
}
|
||||
|
||||
async refreshAccessToken(connection: SpotifyConnection): Promise<string> {
|
||||
const response = await this.httpService
|
||||
.post<any>(
|
||||
`api/token`,
|
||||
`grant_type=refresh_token&refresh_token=${connection.refreshToken}`,
|
||||
{
|
||||
auth: {
|
||||
username: this.clientID,
|
||||
password: this.clientSecret
|
||||
}
|
||||
}
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
return response.data.access_token;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,4 +9,7 @@ export class SpotifyConnection {
|
|||
|
||||
@Column()
|
||||
refreshToken: string;
|
||||
|
||||
@Column({ type: "timestamp", nullable: true })
|
||||
lastRefreshTime?: Date;
|
||||
}
|
||||
|
|
|
|||
15
src/sources/spotify/spotify-library-details.entity.ts
Normal file
15
src/sources/spotify/spotify-library-details.entity.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Column } from "typeorm";
|
||||
|
||||
export class SpotifyLibraryDetails {
|
||||
@Column()
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
uri: string;
|
||||
|
||||
@Column()
|
||||
type: string;
|
||||
|
||||
@Column()
|
||||
href: string;
|
||||
}
|
||||
|
|
@ -1,9 +1,19 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { SpotifyService } from './spotify.service';
|
||||
import { SpotifyApiModule } from './spotify-api/spotify-api.module';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { UsersModule } from "src/users/users.module";
|
||||
import { MusicLibraryModule } from "../../music-library/music-library.module";
|
||||
import { SpotifyApiModule } from "./spotify-api/spotify-api.module";
|
||||
import { SpotifyService } from "./spotify.service";
|
||||
import { ListensModule } from "../../listens/listens.module";
|
||||
import { SpotifyAuthModule } from "./spotify-auth/spotify-auth.module";
|
||||
|
||||
@Module({
|
||||
providers: [SpotifyService],
|
||||
imports: [SpotifyApiModule]
|
||||
imports: [
|
||||
UsersModule,
|
||||
ListensModule,
|
||||
MusicLibraryModule,
|
||||
SpotifyApiModule,
|
||||
SpotifyAuthModule
|
||||
],
|
||||
providers: [SpotifyService]
|
||||
})
|
||||
export class SpotifyModule {}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { SpotifyService } from './spotify.service';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { SpotifyService } from "./spotify.service";
|
||||
|
||||
describe('SpotifyService', () => {
|
||||
describe("SpotifyService", () => {
|
||||
let service: SpotifyService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [SpotifyService],
|
||||
providers: [SpotifyService]
|
||||
}).compile();
|
||||
|
||||
service = module.get<SpotifyService>(SpotifyService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,255 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import {Interval} from "@nestjs/cron"
|
||||
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";
|
||||
|
||||
@Injectable()
|
||||
export class SpotifyService {
|
||||
@Interval
|
||||
private appAccessToken: string | null;
|
||||
private appAccessTokenInProgress: Promise<void> | null;
|
||||
|
||||
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> {
|
||||
console.log("SpotifyService#getRecentlyPlayedTracks");
|
||||
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,
|
||||
accessToken
|
||||
});
|
||||
await this.processUser(user, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (playHistory.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
playHistory.map(async history => {
|
||||
const track = await this.importTrack(history.track.id);
|
||||
|
||||
this.listensService.createListen({
|
||||
user,
|
||||
track,
|
||||
playedAt: new Date(history.played_at)
|
||||
});
|
||||
|
||||
console.log(
|
||||
`Found Listen!: ${user.displayName} listened to "${track.name}" by "${track.artists}"`
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
const newestPlayTime = new Date(
|
||||
playHistory
|
||||
.map(history => history.played_at)
|
||||
.sort()
|
||||
.pop()
|
||||
);
|
||||
|
||||
console.log("newestPlayTime", {
|
||||
newestPlayTime,
|
||||
times: playHistory.map(history => history.played_at).sort()
|
||||
});
|
||||
|
||||
this.usersService.updateSpotifyConnection(user, {
|
||||
...user.spotify,
|
||||
lastRefreshTime: newestPlayTime
|
||||
});
|
||||
}
|
||||
|
||||
async importTrack(
|
||||
spotifyID: string,
|
||||
retryOnExpiredToken: boolean = true
|
||||
): Promise<Track> {
|
||||
const track = await this.musicLibraryService.findTrack({
|
||||
spotify: { id: spotifyID }
|
||||
});
|
||||
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)
|
||||
)
|
||||
)
|
||||
]);
|
||||
|
||||
return this.musicLibraryService.createTrack({
|
||||
name: spotifyTrack.name,
|
||||
album,
|
||||
artists,
|
||||
spotify: {
|
||||
id: spotifyTrack.id,
|
||||
uri: spotifyTrack.uri,
|
||||
type: spotifyTrack.type,
|
||||
href: spotifyTrack.href
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async importAlbum(
|
||||
spotifyID: string,
|
||||
retryOnExpiredToken: boolean = true
|
||||
): Promise<Album> {
|
||||
const album = await this.musicLibraryService.findAlbum({
|
||||
spotify: { id: spotifyID }
|
||||
});
|
||||
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,
|
||||
href: spotifyAlbum.href
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async importArtist(
|
||||
spotifyID: string,
|
||||
retryOnExpiredToken: boolean = true
|
||||
): Promise<Artist> {
|
||||
const artist = await this.musicLibraryService.findArtist({
|
||||
spotify: { id: spotifyID }
|
||||
});
|
||||
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,
|
||||
href: spotifyArtist.href
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue