diff --git a/Dockerfile b/Dockerfile index d77864b..2933762 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ RUN npm install -g npm && \ COPY ./tsconfig*.json ./ COPY ./nest-cli.json ./ COPY ./src ./src -COPY ./packages ./packages +# COPY ./packages ./packages COPY ./test ./test RUN npm run build diff --git a/docker-compose.network.yml b/docker-compose.network.yml index 91840b3..14422fa 100644 --- a/docker-compose.network.yml +++ b/docker-compose.network.yml @@ -33,7 +33,7 @@ services: - redis volumes: - ./src:/app/src - - ./packages:/app/packages + # - ./packages:/app/packages - ./package.json:/app/package.json command: npm run start:network @@ -46,6 +46,8 @@ services: - POSTGRES_DB=devdatabase - POSTGRES_USER=devuser - POSTGRES_PASSWORD=devpass + ports: + - 5432:5432 networks: - cluster @@ -70,12 +72,60 @@ services: # - pgadmin-network-data:/var/lib/pgadmin # networks: # - cluster + + pgadmin: + # Config adapted from: https://stackoverflow.com/a/77519799/10914922 + image: dpage/pgadmin4 + restart: unless-stopped + attach: false + ports: + - '8888:80' + environment: + - PGADMIN_DEFAULT_EMAIL=admin@example.com + - PGADMIN_DEFAULT_PASSWORD=changeme + - PGADMIN_CONFIG_SERVER_MODE=False + - PGADMIN_CONFIG_MASTER_PASSWORD_REQUIRED=False + user: root + depends_on: + - postgres + entrypoint: /bin/sh -c "chmod 600 /pgpass; /entrypoint.sh;" + volumes: + - pgadmin-jukebox-dev:/var/lib/pgadmin + configs: + - source: servers.json + target: /pgadmin4/servers.json + - source: pgpass + target: /pgpass + + networks: + - cluster volumes: jukebox-network-pg-data: - pgadmin-network-data: + pgadmin-jukebox-dev: networks: cluster: name: clubs_cluster external: true + +configs: + pgpass: + content: jbx-network-db:5432:*:devuser:devpass + servers.json: + content: | + { + "Servers": { + "1": { + "Group": "Servers", + "Name": "Docker", + "Host": "jbx-network-db", + "Port": 5432, + "MaintenanceDB": "devdatabase", + "Username": "devuser", + "PassFile": "/pgpass", + "SSLMode": "prefer", + "Favorite": true + } + } + } diff --git a/docker-compose.yml b/docker-compose.yml index e0c5b6f..640b208 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,18 +35,18 @@ services: container_name: jbx-dev-db volumes: - jukebox-pg-data:/var/lib/postgresql/data - # - ./scripts/create-multiple-postgres-databases:/docker-entrypoint-initdb.d + - ./scripts/create-multiple-postgres-databases.sh:/docker-entrypoint-initdb.d/create-multiple-postgres-databases.sh environment: - - POSTGRES_DB=devdatabase - # - POSTGRES_MULTIPLE_DATABASES=devdatabase,test + # - POSTGRES_DB=devdatabase + - POSTGRES_MULTIPLE_DATABASES=devdatabase,test - POSTGRES_USER=devuser - POSTGRES_PASSWORD=devpass redis: image: redis:alpine container_name: jbx-redis - ports: - - 6379:6379 + # ports: + # - 6379:6379 volumes: jukebox-pg-data: diff --git a/package.json b/package.json index 3a31196..a01d77b 100644 --- a/package.json +++ b/package.json @@ -16,13 +16,14 @@ "start:prod": "node dist/src/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\"", "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", - "test": "jest --detectOpenHandles", - "test:watch": "jest --watch", - "test:cov": "jest --detectOpenHandles --coverage", - "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest -c test/jest-e2e.json --detectOpenHandles", + "test": "NODE_ENV=test jest --detectOpenHandles", + "test:watch": "NODE_ENV=test jest --watch", + "test:cov": "NODE_ENV=test jest --detectOpenHandles --coverage", + "test:debug": "NODE_ENV=test node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "NODE_ENV=test jest -c test/jest-e2e.json --detectOpenHandles", "seed": "ts-node -r tsconfig-paths/register src/scripts/seed.ts", "prepare": "husky" + }, "dependencies": { "@nestjs/axios": "^4.0.1", diff --git a/packages/jukebox-types/README.md b/packages/jukebox-types/README.md deleted file mode 100644 index 4b476b7..0000000 --- a/packages/jukebox-types/README.md +++ /dev/null @@ -1,56 +0,0 @@ -# Jukebox Types - -## Interface Standards - -All models have the following fields: - -```ts -// Default fields for all models -interface IModel { - id: number - created_at: string - updated_at: string -} -``` - -For a model `Example`, the following types would be (with sample documentation): - -```ts -/** - * Base object, has all fields from database - */ -interface IExample extends IModel {} - -/** - * Fields used to create object - * - * @see {@link IExample} - */ -interface IExampleCreate extends ICreate {} - -/** - * Fields used to update object - * - * @see {@link IExample} - */ -interface IExampleUpdate extends IUpdate {} - -// ==================== -// Optional Interfaces -// ==================== -/** - * Aggregate related data for object - * - * @see {@link IExample} - */ -interface IExampleDetails extends IExample {} -``` - -## Conventions - -For a model `Example`, abide by the following: - -- Objects generated from ORMs or Schemas would be called `Example` -- Interfaces called `IExample` -- All fields in object are snake case, regardless of the tech stack used. This should be especially enforced in REST APIs. -- For sub interfaces of the main interface, include `@see {@link IExample}` to allow easier access to the the main interfaces docs in the editor. The `@see` is used to notate an additional reference, and `@link` creates a hyperlink to the original object. The brackets are used to combine the decorators properly. diff --git a/packages/jukebox-types/index.d.ts b/packages/jukebox-types/index.d.ts deleted file mode 100644 index c9ad6b5..0000000 --- a/packages/jukebox-types/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from './types/base' -export * from './types/jukebox' -export * from './types/user' -export * from './types/track' -export * from './types/clubs' diff --git a/packages/jukebox-types/package.json b/packages/jukebox-types/package.json deleted file mode 100644 index 48805a5..0000000 --- a/packages/jukebox-types/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "jukebox-types", - "version": "0.1.4", - "description": "Types for Jukebox", - "repository": "https://github.com/ufosc/Jukebox-Server", - "main": "", - "types": "index.d.ts", - "devDependencies": {}, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "Isaac Hunter", - "license": "ISC" -} diff --git a/packages/jukebox-types/types/README.md b/packages/jukebox-types/types/README.md deleted file mode 100644 index 3792e07..0000000 --- a/packages/jukebox-types/types/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Jukebox Types - -## Interface Standards - -All models have the following fields: - -```ts -// Default fields for all models -interface IModel { - id: string - created_at: string - updated_at: string -} -``` - -For a model `Example`, the following types would be: - -```ts -// Base object, has all fields from database -interface IExample extends IModel {} -// Fields used to create object -interface IExampleCreate extends ICreate {} -// Fields used to update object -interface IExampleUpdate extends IUpdate {} - -/** Optional Interfaces */ -// Aggregate related data for object -interface IExampleDetails extends IExample {} -``` - -## Conventions - -For a model `Example`, abide by the following: - -- Objects generated from ORMs or Schemas would be called `Example` -- Interfaces called `IExample` -- All fields in object are snake case, regardless of the tech stack used. This should be especially enforced in REST APIs. diff --git a/packages/jukebox-types/types/base.d.ts b/packages/jukebox-types/types/base.d.ts deleted file mode 100644 index 42e62f6..0000000 --- a/packages/jukebox-types/types/base.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -declare interface IModel { - id: number - created_at: string // Serialized Date - updated_at: string // Serialized Date -} - -declare type ICreate = Partial> -declare type IUpdate = Partial> diff --git a/packages/jukebox-types/types/jukebox.d.ts b/packages/jukebox-types/types/jukebox.d.ts deleted file mode 100644 index 5c369c3..0000000 --- a/packages/jukebox-types/types/jukebox.d.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * Manages player and track queue information for a club. - */ -declare interface IJukebox extends IModel { - name: string - club_id: number - links: IJukeboxLink[] -} - -/** - * Fields required to create a new jukebox. - * - * @see {@link IJukebox} - */ -declare interface IJukeboxCreate extends ICreate { - name: string - club_id: number -} - -/** - * Fields used for updating a jukebox. - * - * @see {@link IJukebox} - */ -declare interface IJukeboxUpdate extends IUpdate { - name: string -} - -/** - * The type of music app that is linked to the jukebox. - * - * In the future, we may support apps like Apple Music, Amazon Music, etc. - */ -declare type JukeboxLinkType = 'spotify' - -/** - * Represents a connection between a jukebox and a music app. - * - * This is used to get the appropriate credentials for the player - * on the frontend. It would be a security risk to return the credentials - * for all of the links with the jukebox information. - */ -declare interface IJukeboxLink extends IModel { - type: JukeboxLinkType - email: string - active: boolean -} - -/** - * Fields required to create a new jukebox link. - * - * @see {@link IJukeboxLink} - */ -declare interface IJukeboxLinkCreate extends ICreate { - type: JukeboxLinkType - email: string - active: boolean -} - -/** - * Fields used for updating a jukebox link. - * - * @see {@link IJukeboxLink} - */ -declare interface IJukeboxLinkUpdate extends IUpdate { - active: boolean - email: never - type: never -} - -/** - * Spotify credentials needed to authenticate with the Spotify API - * and the Spotify Web Player. - */ -declare interface ISpotifyAccount extends IModel { - access_token: string - refresh_token: string - user_id: number - spotify_email: string - expires_in: number - expires_at: string - token_type: string -} - -/** - * Information about a track that is currently playing, if applicable. - */ -declare interface IPlayerState { - jukebox_id: number - current_track?: IQueuedTrack - progress: number - is_playing: boolean -} - -/** - * Information passed to socket subscribers when the player state changes. - * Contains minimal information when needed to send frequently. - * - * @see {@link IPlayerState} - */ -declare interface IPlayerUpdate extends Partial { - jukebox_id: number - current_track?: Partial -} - -/** - * Full state of the player, including the track queue. - * Used if needed to reduce the number of requests. - * - * @see {@link IPlayerState} - */ -declare interface IPlayerQueueState extends IPlayerState { - next_tracks: IQueuedTrack[] -} - -/** - * Represents the state of the spotify web player. - * - * @see {@link IPlayerState} - */ -declare interface IPlayerAuxState extends IPlayerState { - current_track?: IPlayerTrack | null -} - -/** - * Updates sent to the server from the player. - * Contains additional information to tell the server - * what actions it should take upon update, if any. - * - * @see {@link IPlayerState} - */ -declare interface IPlayerAuxUpdate extends IPlayerAuxState { - changed_tracks?: boolean -} - -/** - * Fields needed for a user to interact with a jukebox. - * - * @see {@link IJukeboxInteraction} - */ -declare interface IJukeboxInteractionCreate { - action: 'like' | 'dislike' - - /** Position in the queue, 0 for currently playing, 1 for top of queue. */ - queue_index: number -} - -/** - * Fields used to represent a user interaction with a - * track in the jukebox - whether in the queue or - * in the current player. - */ - -declare interface IJukeboxInteraction extends IJukeboxInteractionCreate { - jukebox_id: number - user: IUser -} diff --git a/packages/jukebox-types/types/track.d.ts b/packages/jukebox-types/types/track.d.ts deleted file mode 100644 index 85bfa46..0000000 --- a/packages/jukebox-types/types/track.d.ts +++ /dev/null @@ -1,250 +0,0 @@ -declare interface ITrackImage { - url: string - height?: number | null - width?: number | null -} - -/** - * Base fields for a track's artist. - * This is used in both the Spotify API and the Spotify player. - */ -declare interface IArtistInline { - name: string - uri: string -} - -/** - * Base fields for a track's album. - * This is used in both the Spotify API and the Spotify player. - */ -declare interface IAlbumInline { - uri: string - name: string - images: ITrackImage[] -} - -/** - * Base track information - * that is included in both the spotify player and spotify api. - */ -declare interface ITrack { - id: string - uri: string - name: string - type: 'track' | 'episode' | 'ad' - duration_ms: number - album: IAlbumInline - artists: IArtistInline[] -} - -/** - * Track information returned from - * the Spotify web player. - */ -declare interface IPlayerTrack extends ITrack { - // id?: string | null - uid: string - linked_from: { uri: string | null; id: string | null } - media_type: 'audio' | 'video' - track_type: 'audio' | 'video' - content_type?: 'music' - is_playable: boolean - metadata?: any -} - -/* -Sample response from player: -{ - "id": "5Pdd4QCr0rREXM03zBM2Eh", - "uri": "spotify:track:5Pdd4QCr0rREXM03zBM2Eh", - "type": "track", - "uid": "38356466633337313163333137613865", - "linked_from": { - "uri": null, - "id": null - }, - "media_type": "audio", - "track_type": "audio", - "content_type": "music", - "name": "Walk Idiot Walk", - "duration_ms": 211860, - "artists": [ - { - "name": "The Hives", - "uri": "spotify:artist:4DToQR3aKrHQSSRzSz8Nzt" - } - ], - "album": { - "uri": "spotify:album:2Qo8MVIOIyrBrqgoCsHCXV", - "name": "Tyrannosaurus Hives", - "images": [ - { - "url": "https://i.scdn.co/image/ab67616d0000b27368fb48c259aa205c9f18117f", - "height": 640, - "width": 640 - }, - { - "url": "https://i.scdn.co/image/ab67616d0000485168fb48c259aa205c9f18117f", - "height": 64, - "width": 64 - }, - { - "url": "https://i.scdn.co/image/ab67616d00001e0268fb48c259aa205c9f18117f", - "height": 300, - "width": 300 - } - ] - }, - "is_playable": true, - "metadata": {} -} -*/ - -/** - * Artists returned in track information from server. - */ -declare interface IArtistInlineDetails extends IArtistInline { - id: string - type: 'artist' - external_urls?: { spotify: string } - href: string -} - -/** - * Album fields returned in track information from the server. - */ -declare interface IAlbumInlineDetails extends IAlbumInline { - id: string - album_type: 'album' | 'single' - artists: IArtistInlineDetails[] - available_markets: string[] - external_urls?: { spotify: string } - href: string - release_date: string - release_date_precision: 'day' - total_tracks: number -} - -/** - * Track information returned from the server, - * includes additional info from the Spotify API. - */ -declare interface ITrackDetails extends ITrack { - artists: IArtistInlineDetails[] - album: IAlbumInlineDetails - disc_number: number - explicit: boolean - popularity: number - preview_url: string | null - track_number: number - external_ids?: { isrc: string } - external_urls?: { spotify: string } -} - -/** -Sample response from API: -{ - "album": { - "album_type": "album", - "artists": [ - { - "external_urls": { - "spotify": "https://open.spotify.com/artist/4DToQR3aKrHQSSRzSz8Nzt" - }, - "href": "https://api.spotify.com/v1/artists/4DToQR3aKrHQSSRzSz8Nzt", - "id": "4DToQR3aKrHQSSRzSz8Nzt", - "name": "The Hives", - "type": "artist", - "uri": "spotify:artist:4DToQR3aKrHQSSRzSz8Nzt" - } - ], - "available_markets": [ - "AR", - "AU", - ... - ], - "external_urls": { - "spotify": "https://open.spotify.com/album/2Qo8MVIOIyrBrqgoCsHCXV" - }, - "href": "https://api.spotify.com/v1/albums/2Qo8MVIOIyrBrqgoCsHCXV", - "id": "2Qo8MVIOIyrBrqgoCsHCXV", - "images": [ - { - "url": "https://i.scdn.co/image/ab67616d0000b27368fb48c259aa205c9f18117f", - "width": 640, - "height": 640 - }, - { - "url": "https://i.scdn.co/image/ab67616d00001e0268fb48c259aa205c9f18117f", - "width": 300, - "height": 300 - }, - { - "url": "https://i.scdn.co/image/ab67616d0000485168fb48c259aa205c9f18117f", - "width": 64, - "height": 64 - } - ], - "name": "Tyrannosaurus Hives", - "release_date": "2004-01-01", - "release_date_precision": "day", - "total_tracks": 12, - "type": "album", - "uri": "spotify:album:2Qo8MVIOIyrBrqgoCsHCXV" - }, - "artists": [ - { - "external_urls": { - "spotify": "https://open.spotify.com/artist/4DToQR3aKrHQSSRzSz8Nzt" - }, - "href": "https://api.spotify.com/v1/artists/4DToQR3aKrHQSSRzSz8Nzt", - "id": "4DToQR3aKrHQSSRzSz8Nzt", - "name": "The Hives", - "type": "artist", - "uri": "spotify:artist:4DToQR3aKrHQSSRzSz8Nzt" - } - ], - "available_markets": [ - "AR", - "AU", - ... - ], - "disc_number": 1, - "duration_ms": 211813, - "explicit": false, - "external_ids": { - "isrc": "GBAKW0400379" - }, - "external_urls": { - "spotify": "https://open.spotify.com/track/5Pdd4QCr0rREXM03zBM2Eh" - }, - "href": "https://api.spotify.com/v1/tracks/5Pdd4QCr0rREXM03zBM2Eh", - "id": "5Pdd4QCr0rREXM03zBM2Eh", - "is_local": false, - "name": "Walk Idiot Walk", - "popularity": 52, - "preview_url": null, - "track_number": 3, - "type": "track", - "uri": "spotify:track:5Pdd4QCr0rREXM03zBM2Eh" -} -*/ - -/** - * How users can interact with tracks in the queue. - */ -declare interface ITrackInteractions { - likes: number - dislikes: number -} - -/** - * Information about a track that is queued. - */ -declare interface IQueuedTrack { - track: ITrackDetails - queue_id: string - recommended_by?: string - spotify_queued?: boolean - interactions: ITrackInteractions -} diff --git a/scripts/create-multiple-postgres-databases.sh b/scripts/create-multiple-postgres-databases.sh old mode 100644 new mode 100755 index 94577d8..6cc5e15 --- a/scripts/create-multiple-postgres-databases.sh +++ b/scripts/create-multiple-postgres-databases.sh @@ -1,23 +1,25 @@ #!/bin/bash # Reference: https://github.com/mrts/docker-postgresql-multiple-databases/blob/master/create-multiple-postgresql-databases.sh -set -e -set -u +set -ue function create_user_and_database() { local database=$1 + local user=$POSTGRES_USER + echo " Creating user and database '$database'" - psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL - CREATE USER $database; + echo " Assigning user '$user'" + psql -v ON_ERROR_STOP=1 --username "$user" <<-EOSQL CREATE DATABASE $database; - GRANT ALL PRIVILEGES ON DATABASE $database TO $database; -EOSQL + GRANT ALL PRIVILEGES ON DATABASE $database TO $user; + EOSQL } if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then echo "Multiple database creation requested: $POSTGRES_MULTIPLE_DATABASES" + for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do create_user_and_database $db done - echo "Multiple databases created" + echo "Multiple databases created successfully" fi \ No newline at end of file diff --git a/src/config/constants.ts b/src/config/constants.ts index cd84ec8..5669cdd 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -32,4 +32,4 @@ export const DB_HOST = process.env.DB_HOST export const DB_PORT = +(process.env.DB_PORT ?? '5432') export const DB_USER = process.env.DB_USER ?? 'devuser' export const DB_PASS = process.env.DB_PASS ?? 'devpass' -export const DB_NAME = process.env.DB_NAME ?? 'devdatabase' +export const DB_NAME = NODE_ENV !== 'test' ? (process.env.DB_NAME ?? 'devdatabase') : 'test' diff --git a/src/jukebox/account-link/account-link.controller.ts b/src/jukebox/account-link/account-link.controller.ts index 37a6d22..0138f24 100644 --- a/src/jukebox/account-link/account-link.controller.ts +++ b/src/jukebox/account-link/account-link.controller.ts @@ -1,14 +1,14 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, UseGuards } from '@nestjs/common' -import { AccountLinkService } from './account-link.service' -import { CreateAccountLinkDto, UpdateAccountLinkDto } from './dto/account-link.dto' +import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common' import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger' import { NumberPipe } from 'src/pipes/int-pipe.pipe' import { Roles } from 'src/utils/decorators/roles.decorator' import { RolesGuard } from 'src/utils/guards/roles.guard' +import { AccountLinkService } from './account-link.service' +import { CreateAccountLinkDto, UpdateAccountLinkDto } from './dto/account-link.dto' @ApiTags('AccountLink') @ApiBearerAuth() -@Controller('jukebox/jukeboxes/:jukebox_id/account-link') +@Controller('jukebox/jukeboxes/:jukebox_id/account-links') export class AccountLinkController { constructor(private readonly accountLinkService: AccountLinkService) {} @@ -36,6 +36,19 @@ export class AccountLinkController { return this.accountLinkService.findAll(jukeboxId) } + @Roles('admin') + @UseGuards(RolesGuard) + @Get('active') + @ApiOperation({ + summary: '[ADMIN] Get an active account link for a jukebox', + }) + getActiveAccount( + @Param('jukebox_id', new NumberPipe('jukebox_id')) jukeboxId: number, + @Query('refresh') refresh: boolean = false, + ) { + return this.accountLinkService.getActiveAccount(jukeboxId, refresh) + } + @Roles('member') @UseGuards(RolesGuard) @Get(':id') @@ -60,7 +73,7 @@ export class AccountLinkController { @Param('jukebox_id', new NumberPipe('jukebox_id')) jukeboxId: number, @Body() updateAccountLinkDto: UpdateAccountLinkDto, ) { - return this.accountLinkService.update(id, updateAccountLinkDto) + return this.accountLinkService.update(+id, updateAccountLinkDto) } @Roles('admin') @@ -75,14 +88,4 @@ export class AccountLinkController { ) { return this.accountLinkService.remove(id) } - - @Roles('admin') - @UseGuards(RolesGuard) - @Get('active') - @ApiOperation({ - summary: '[ADMIN] Get an active account link for a jukebox', - }) - getActiveAccount(@Param('jukebox_id', new NumberPipe('jukebox_id')) jukeboxId: number) { - return this.accountLinkService.getActiveAccount(jukeboxId) - } } diff --git a/src/jukebox/account-link/account-link.module.ts b/src/jukebox/account-link/account-link.module.ts index f156a75..fda1fc8 100644 --- a/src/jukebox/account-link/account-link.module.ts +++ b/src/jukebox/account-link/account-link.module.ts @@ -1,17 +1,25 @@ import { Module } from '@nestjs/common' -import { AccountLinkService } from './account-link.service' -import { AccountLinkController } from './account-link.controller' import { TypeOrmModule } from '@nestjs/typeorm' -import { AccountLink } from './entities/account-link.entity' -import { JukeboxService } from '../jukebox.service' -import { Jukebox } from '../entities/jukebox.entity' import { NetworkService } from 'src/network/network.service' +import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' +import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' import { AxiosProvider } from 'src/utils/mock' +import { Jukebox } from '../entities/jukebox.entity' +import { JukeboxService } from '../jukebox.service' +import { AccountLinkController } from './account-link.controller' +import { AccountLinkService } from './account-link.service' +import { AccountLink } from './entities/account-link.entity' @Module({ - imports: [TypeOrmModule.forFeature([AccountLink, Jukebox])], + imports: [TypeOrmModule.forFeature([AccountLink, SpotifyAccount, Jukebox])], controllers: [AccountLinkController], - providers: [AccountLinkService, JukeboxService, NetworkService, AxiosProvider], + providers: [ + AccountLinkService, + SpotifyAuthService, + JukeboxService, + NetworkService, + AxiosProvider, + ], exports: [AccountLinkService], }) export class AccountLinkModule {} diff --git a/src/jukebox/account-link/account-link.service.ts b/src/jukebox/account-link/account-link.service.ts index fd63fad..b6b9ad0 100644 --- a/src/jukebox/account-link/account-link.service.ts +++ b/src/jukebox/account-link/account-link.service.ts @@ -1,20 +1,25 @@ -import { Injectable, NotFoundException, NotImplementedException } from '@nestjs/common' -import { AccountLinkDto, CreateAccountLinkDto, UpdateAccountLinkDto } from './dto/account-link.dto' -import { QueryFailedError, Repository } from 'typeorm' +import { Injectable, Logger, NotFoundException } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' -import { AccountLink } from './entities/account-link.entity' import { plainToInstance } from 'class-transformer' +import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' +import { QueryFailedError, Repository } from 'typeorm' +import { AccountLinkDto, CreateAccountLinkDto, UpdateAccountLinkDto } from './dto/account-link.dto' +import { AccountLink } from './entities/account-link.entity' @Injectable() export class AccountLinkService { - constructor(@InjectRepository(AccountLink) private accountLinkRepo: Repository) {} + constructor( + private spotifyAuthService: SpotifyAuthService, + @InjectRepository(AccountLink) private accountLinkRepo: Repository, + ) {} async create( jukebox_id: number, createAccountLinkDto: CreateAccountLinkDto, ): Promise { const preLink = this.accountLinkRepo.create({ - ...createAccountLinkDto, + active: createAccountLinkDto.active, + spotify_account: { id: createAccountLinkDto.spotify_account_id }, jukebox: { id: jukebox_id }, }) @@ -32,7 +37,7 @@ export class AccountLinkService { link = await this.accountLinkRepo.findOne({ where: { jukebox: { id: jukebox_id }, - spotify_account: { id: createAccountLinkDto.spotify_account.id }, + spotify_account: { id: createAccountLinkDto.spotify_account_id }, }, relations: { spotify_account: true }, loadRelationIds: { relations: ['jukebox'] }, @@ -52,7 +57,7 @@ export class AccountLinkService { async findAll(jukebox_id: number): Promise { const links = await this.accountLinkRepo.find({ where: { jukebox: { id: jukebox_id } }, - relations: { spotify_account: true }, + relations: { spotify_account: true, jukebox: true }, loadRelationIds: { relations: ['jukebox'] }, }) return plainToInstance(AccountLinkDto, links) @@ -72,7 +77,12 @@ export class AccountLinkService { } async update(id: number, updateAccountLinkDto: UpdateAccountLinkDto): Promise { - await this.accountLinkRepo.update({ id }, updateAccountLinkDto) + const payload: any = { active: updateAccountLinkDto.active } + if (updateAccountLinkDto.spotify_account_id != null) { + payload.spotify_account = { id: updateAccountLinkDto.spotify_account_id } + } + + await this.accountLinkRepo.update({ id }, payload) const link = await this.findOne(id) return plainToInstance(AccountLinkDto, link) } @@ -84,10 +94,14 @@ export class AccountLinkService { return plainToInstance(AccountLinkDto, link) } - async getActiveAccount(jukeboxId: number): Promise { + async refreshAccountLink(accountLink: AccountLink) { + await this.spotifyAuthService.refreshSpotifyAccount(accountLink.spotify_account) + } + + async getActiveAccount(jukeboxId: number, refresh?: boolean): Promise { const link = await this.accountLinkRepo.findOne({ where: { jukebox: { id: jukeboxId }, active: true }, - relations: { spotify_account: true }, + relations: { spotify_account: true, jukebox: true }, loadRelationIds: { relations: ['jukebox'] }, }) @@ -95,6 +109,11 @@ export class AccountLinkService { throw new NotFoundException('Could not find active account link for jukeboxId: ' + jukeboxId) } + if (refresh) { + Logger.log(`Refreshing active account link for jukebox ${jukeboxId}...`) + await this.refreshAccountLink(link) + } + return plainToInstance(AccountLinkDto, link) } } diff --git a/src/jukebox/account-link/dto/account-link.dto.ts b/src/jukebox/account-link/dto/account-link.dto.ts index 4c4bb8f..7e0658b 100644 --- a/src/jukebox/account-link/dto/account-link.dto.ts +++ b/src/jukebox/account-link/dto/account-link.dto.ts @@ -1,20 +1,13 @@ import { PartialType } from '@nestjs/swagger' -import { Expose, Type } from 'class-transformer' -import { IsBoolean, IsNumber, IsOptional, ValidateNested } from 'class-validator' +import { Expose } from 'class-transformer' import { EntityDtoBase } from 'src/config/dtos' import { SpotifyAccountDto } from 'src/spotify/dto' export class CreateAccountLinkDto { - @IsOptional() - @IsNumber() - jukebox_id?: number + @Expose({ name: 'spotify_account' }) + spotify_account_id: number - @ValidateNested() - @Type(() => SpotifyAccountDto) - spotify_account: SpotifyAccountDto - - @IsOptional() - @IsBoolean() + @Expose() active?: boolean } diff --git a/src/jukebox/account-link/entities/account-link.entity.ts b/src/jukebox/account-link/entities/account-link.entity.ts index 858b6d2..a8a4310 100644 --- a/src/jukebox/account-link/entities/account-link.entity.ts +++ b/src/jukebox/account-link/entities/account-link.entity.ts @@ -1,6 +1,6 @@ import { EntityBase } from 'src/config/entities' import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' -import { Column, Entity, Index, ManyToOne } from 'typeorm' +import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm' import { Jukebox } from '../../entities/jukebox.entity' @Entity('account_link') diff --git a/src/jukebox/account-link/tests/account-link.controller.spec.ts b/src/jukebox/account-link/tests/account-link.controller.spec.ts index cf23e47..aa64d54 100644 --- a/src/jukebox/account-link/tests/account-link.controller.spec.ts +++ b/src/jukebox/account-link/tests/account-link.controller.spec.ts @@ -1,21 +1,20 @@ +import { NotFoundException } from '@nestjs/common' import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' +import { TypeOrmModule } from '@nestjs/typeorm' +import { DatabaseModule } from 'src/config/database.module' +import type { JukeboxDto } from 'src/jukebox/dto/jukebox.dto' +import { Jukebox } from 'src/jukebox/entities/jukebox.entity' +import { JukeboxService } from 'src/jukebox/jukebox.service' +import { NetworkService } from 'src/network/network.service' +import type { CreateSpotifyAccountDto } from 'src/spotify/dto' +import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' +import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' +import { MockAxiosProvider, MockCacheProvider, mockSpotifyAccount } from 'src/utils/mock' import { AccountLinkController } from '../account-link.controller' import { AccountLinkService } from '../account-link.service' -import type { CreateSpotifyAccountDto } from 'src/spotify/dto' -import { SpotifyAccountDto } from 'src/spotify/dto' import type { CreateAccountLinkDto } from '../dto' -import { NotFoundException } from '@nestjs/common' -import { AxiosMockProvider, MockCacheProvider, mockSpotifyAccount } from 'src/utils/mock' -import { DatabaseModule } from 'src/config/database.module' -import { TypeOrmModule } from '@nestjs/typeorm' import { AccountLink } from '../entities/account-link.entity' -import { JukeboxService } from 'src/jukebox/jukebox.service' -import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' -import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' -import { Jukebox } from 'src/jukebox/entities/jukebox.entity' -import type { JukeboxDto } from 'src/jukebox/dto/jukebox.dto' -import { NetworkService } from 'src/network/network.service' describe('AccountLinkController', () => { let controller: AccountLinkController @@ -38,7 +37,7 @@ describe('AccountLinkController', () => { ): Promise => { const spotify_account = await spotifyAuthService.addAccount(createSpotifyAccountDto) return { - spotify_account, + spotify_account_id: spotify_account.id, active: true, } } @@ -48,7 +47,7 @@ describe('AccountLinkController', () => { imports: [DatabaseModule, TypeOrmModule.forFeature([AccountLink, SpotifyAccount, Jukebox])], controllers: [AccountLinkController], providers: [ - AxiosMockProvider, + MockAxiosProvider, MockCacheProvider, AccountLinkService, SpotifyAuthService, @@ -85,7 +84,7 @@ describe('AccountLinkController', () => { const result1 = await controller.create(jukeboxId1, testAccountLinkDto1) expect(result1.jukebox_id).toEqual(jukeboxId1) expect(result1.active).toBeTruthy() - expect(result1.spotify_account).toEqual(testAccountLinkDto1.spotify_account) + expect(result1.spotify_account.id).toEqual(testAccountLinkDto1.spotify_account_id) const result2 = await controller.create(jukeboxId1, testAccountLinkDto1) expect(result1).toEqual(result2) @@ -114,18 +113,18 @@ describe('AccountLinkController', () => { const testAccountLinkDto2 = await createTestAccountLink() const result = await controller.create(jukeboxId1, testAccountLinkDto1) - expect(result.spotify_account).toEqual(testAccountLinkDto1.spotify_account) + expect(result.spotify_account.id).toEqual(testAccountLinkDto1.spotify_account_id) const updated1 = await controller.update(result.id, jukeboxId1, testAccountLinkDto2) - expect(updated1.spotify_account).toEqual(testAccountLinkDto2.spotify_account) + expect(updated1.spotify_account.id).toEqual(testAccountLinkDto2.spotify_account_id) const updated2 = await controller.update(result.id, jukeboxId1, { ...testAccountLinkDto2, - spotify_account: undefined, + spotify_account_id: undefined, active: false, }) expect(updated2.active).toBeFalsy() - expect(updated2.spotify_account).toEqual(testAccountLinkDto2.spotify_account) + expect(updated2.spotify_account.id).toEqual(testAccountLinkDto2.spotify_account_id) }) it('should remove a spotify account from a jukebox', async () => { @@ -133,7 +132,7 @@ describe('AccountLinkController', () => { const link = await controller.create(jukeboxId1, testAccountLinkDto) expect(link.jukebox_id).toEqual(jukeboxId1) expect(link.active).toBeTruthy() - expect(link.spotify_account).toEqual(testAccountLinkDto.spotify_account) + expect(link.spotify_account.id).toEqual(testAccountLinkDto.spotify_account_id) await controller.remove(link.id, jukeboxId1) await expect(controller.findOne(link.id, jukeboxId1)).rejects.toThrow(NotFoundException) @@ -148,7 +147,7 @@ describe('AccountLinkController', () => { await controller.update(link1.id, jukeboxId1, { ...testAccountLinkDto1, - spotify_account: undefined, + spotify_account_id: undefined, active: false, }) @@ -166,13 +165,13 @@ describe('AccountLinkController', () => { await controller.update(link1.id, jukeboxId1, { ...testAccountLinkDto1, - spotify_account: undefined, + spotify_account_id: undefined, active: false, }) await controller.update(link2.id, jukeboxId1, { ...testAccountLinkDto2, - spotify_account: undefined, + spotify_account_id: undefined, active: false, }) diff --git a/src/jukebox/account-link/tests/account-link.service.spec.ts b/src/jukebox/account-link/tests/account-link.service.spec.ts index d23f6de..0e8d046 100644 --- a/src/jukebox/account-link/tests/account-link.service.spec.ts +++ b/src/jukebox/account-link/tests/account-link.service.spec.ts @@ -1,8 +1,11 @@ import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' -import { AccountLinkService } from '../account-link.service' -import { DatabaseModule } from 'src/config/database.module' import { TypeOrmModule } from '@nestjs/typeorm' +import { DatabaseModule } from 'src/config/database.module' +import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' +import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' +import { MockAxiosProvider } from 'src/utils/mock' +import { AccountLinkService } from '../account-link.service' import { AccountLink } from '../entities/account-link.entity' describe('AccountLinkService', () => { @@ -10,8 +13,8 @@ describe('AccountLinkService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [DatabaseModule, TypeOrmModule.forFeature([AccountLink])], - providers: [AccountLinkService], + imports: [DatabaseModule, TypeOrmModule.forFeature([AccountLink, SpotifyAccount])], + providers: [MockAxiosProvider, AccountLinkService, SpotifyAuthService], }).compile() service = module.get(AccountLinkService) diff --git a/src/jukebox/juke-session/dto/juke-session.dto.ts b/src/jukebox/juke-session/dto/juke-session.dto.ts index 90d4433..df94e7a 100644 --- a/src/jukebox/juke-session/dto/juke-session.dto.ts +++ b/src/jukebox/juke-session/dto/juke-session.dto.ts @@ -6,7 +6,7 @@ import { EntityDtoBase } from 'src/config/dtos' export class JukeSessionDto extends EntityDtoBase { // Transforms JukeboxSession Entity Into Just The Jukebox Id @Expose() - @Transform(({ obj }) => obj.jukebox?.id) + @Transform(({ obj }) => obj.jukebox.id) jukebox_id: number @Expose() diff --git a/src/jukebox/juke-session/juke-session.controller.ts b/src/jukebox/juke-session/juke-session.controller.ts index 49bfac0..44f33ee 100644 --- a/src/jukebox/juke-session/juke-session.controller.ts +++ b/src/jukebox/juke-session/juke-session.controller.ts @@ -1,16 +1,31 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, Query, UseGuards } from '@nestjs/common' +import { + Body, + Controller, + Delete, + Get, + Param, + Patch, + Post, + Query, + UseGuards, + UseInterceptors, +} from '@nestjs/common' import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger' +import { AuthInterceptor } from 'src/auth/auth.interceptor' +import { NumberPipe } from 'src/pipes/int-pipe.pipe' +import { UserDto } from 'src/shared' import { Serialize } from 'src/utils' +import { CurrentUser } from 'src/utils/decorators' +import { Roles } from 'src/utils/decorators/roles.decorator' +import { RolesGuard } from 'src/utils/guards/roles.guard' import { CreateJukeSessionDto, JukeSessionDto, UpdateJukeSessionDto } from './dto/juke-session.dto' import { CreateJukeSessionMembershipDto, JukeSessionMembershipDto } from './dto/membership.dto' import { JukeSessionService } from './juke-session.service' -import { NumberPipe } from 'src/pipes/int-pipe.pipe' -import { Roles } from 'src/utils/decorators/roles.decorator' -import { RolesGuard } from 'src/utils/guards/roles.guard' @ApiTags('JukeSession') @ApiBearerAuth() -@Controller('jukebox/jukeboxes/:jukebox_id/juke-session') +@Controller('jukebox/jukeboxes/:jukebox_id/juke-sessions') +@UseInterceptors(AuthInterceptor) export class JukeSessionController { constructor(private readonly jukeSessionService: JukeSessionService) {} @@ -88,6 +103,19 @@ export class JukeSessionController { return this.jukeSessionService.endSession(id) } + @Roles('member') + @UseGuards(RolesGuard) + @Get(':id/membership/') + @Serialize(JukeSessionMembershipDto) + @ApiOperation({ summary: 'Get Juke Session Membership for Current User' }) + getJukeSessionMembership( + @Param('jukebox_id', new NumberPipe('jukebox_id')) jukeboxId: number, + @Param('id', new NumberPipe('id')) id: number, + @CurrentUser() user: UserDto, + ) { + return this.jukeSessionService.getMembershipForUser(id, user.id) + } + @Roles('admin') @UseGuards(RolesGuard) @Post(':id/members/') @@ -101,6 +129,15 @@ export class JukeSessionController { return this.jukeSessionService.createMembership(id, body) } + @Roles('member') + @UseGuards(RolesGuard) + @Post(':id/members/join/') + @Serialize(JukeSessionMembershipDto) + @ApiOperation({ summary: 'Add Juke Session Member' }) + joinJukeSession(@Param('id', new NumberPipe('id')) id: number, @CurrentUser() user: UserDto) { + return this.jukeSessionService.createMembership(id, { user_id: user.id }) + } + @Roles('member') @UseGuards(RolesGuard) @Post(':id/members/code') diff --git a/src/jukebox/juke-session/juke-session.service.ts b/src/jukebox/juke-session/juke-session.service.ts index 89577a3..5054845 100644 --- a/src/jukebox/juke-session/juke-session.service.ts +++ b/src/jukebox/juke-session/juke-session.service.ts @@ -1,6 +1,8 @@ import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { plainToInstance } from 'class-transformer' +import { BASE_URL, CLUBS_URL } from 'src/config' +import { NetworkService } from 'src/network/network.service' import { QueryFailedError, Repository } from 'typeorm' import { CreateJukeSessionDto, JukeSessionDto, UpdateJukeSessionDto } from './dto/juke-session.dto' import { @@ -11,8 +13,6 @@ import { import { JukeSession } from './entities/juke-session.entity' import { JukeSessionMembership } from './entities/membership.entity' import { generateJoinCode } from './utils/generate-join-code' -import { NetworkService } from 'src/network/network.service' -import { BASE_URL, CLUBS_URL } from 'src/config' @Injectable() export class JukeSessionService { @@ -124,7 +124,10 @@ export class JukeSessionService { } async findAll(jukeboxId: number): Promise { - const sessions = await this.jukeSessionRepo.find({ where: { jukebox: { id: jukeboxId } } }) + const sessions = await this.jukeSessionRepo.find({ + where: { jukebox: { id: jukeboxId } }, + relations: { jukebox: true }, + }) return sessions.map((session) => plainToInstance(JukeSessionDto, session)) } @@ -203,6 +206,20 @@ export class JukeSessionService { return plainToInstance(JukeSessionMembershipDto, membership) } + async getMembershipForUser( + jukeSessionId: number, + userId: number, + ): Promise { + const membership = await this.membershipRepo.findOne({ + where: { user_id: userId, juke_session: { id: jukeSessionId } }, + }) + if (!membership) { + throw new NotFoundException(`User ${userId} is not a member of juke session ${jukeSessionId}`) + } + + return plainToInstance(JukeSessionMembershipDto, membership) + } + async deleteMembership(membershipId: number): Promise { const membership = await this.getMembership(membershipId) await this.membershipRepo.delete({ id: membershipId }) @@ -245,6 +262,7 @@ export class JukeSessionService { async getCurrentSession(jukeboxId: number): Promise { const session = await this.jukeSessionRepo.findOne({ where: { jukebox: { id: jukeboxId }, is_active: true }, + relations: { jukebox: true }, }) if (!session) { throw new NotFoundException(`No Current Juke session Found for jukebox ${jukeboxId}`) diff --git a/src/jukebox/juke-session/tests/juke-session.controller.spec.ts b/src/jukebox/juke-session/tests/juke-session.controller.spec.ts index df82707..6b8d0e0 100644 --- a/src/jukebox/juke-session/tests/juke-session.controller.spec.ts +++ b/src/jukebox/juke-session/tests/juke-session.controller.spec.ts @@ -3,6 +3,8 @@ import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' import { DatabaseModule } from 'src/config/database.module' +import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' +import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' import type { JukeboxDto } from 'src/jukebox/dto/jukebox.dto' import { Jukebox } from 'src/jukebox/entities/jukebox.entity' import { JukeboxService } from 'src/jukebox/jukebox.service' @@ -10,20 +12,19 @@ import { PlayerInteraction } from 'src/jukebox/player/entity/player-interaction. import { PlayerService } from 'src/jukebox/player/player.service' import { QueuedTrack } from 'src/jukebox/queue/entities/queued-track.entity' import { QueueService } from 'src/jukebox/queue/queue.service' +import { NetworkService } from 'src/network/network.service' +import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' import { SpotifyService } from 'src/spotify/spotify.service' import type { TrackDto } from 'src/track/dto/track.dto' import { Track } from 'src/track/entities/track.entity' import { TrackService } from 'src/track/track.service' -import { AxiosMockProvider, MockCacheProvider } from 'src/utils/mock' +import { MockAxiosProvider, MockCacheProvider } from 'src/utils/mock' import type { CreateJukeSessionDto } from '../dto/juke-session.dto' import { JukeSession } from '../entities/juke-session.entity' import { JukeSessionMembership } from '../entities/membership.entity' import { JukeSessionController } from '../juke-session.controller' import { JukeSessionService } from '../juke-session.service' -import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' -import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' -import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' -import { NetworkService } from 'src/network/network.service' +import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' const getEndAtDate = (hours = 2) => new Date(new Date().getTime() + 1000 * 60 * 60 * hours) @@ -73,7 +74,7 @@ describe('JukeSessionController', () => { ], controllers: [JukeSessionController], providers: [ - AxiosMockProvider, + MockAxiosProvider, MockCacheProvider, JukeSessionService, PlayerService, @@ -84,6 +85,7 @@ describe('JukeSessionController', () => { TrackService, AccountLinkService, SpotifyService, + SpotifyAuthService, ], }).compile() @@ -104,6 +106,9 @@ describe('JukeSessionController', () => { artists: ['Acme Music'], release_year: 2025, spotify_id: 'abc123', + duration_ms: 0, + is_explicit: false, + preview_url: null, }) }) diff --git a/src/jukebox/jukebox.controller.ts b/src/jukebox/jukebox.controller.ts index 9e0b697..30f1945 100644 --- a/src/jukebox/jukebox.controller.ts +++ b/src/jukebox/jukebox.controller.ts @@ -24,7 +24,7 @@ export class JukeboxController { @UseGuards(RolesGuard) @Get() @ApiOperation({ summary: '[MEMBER] Find all jukeboxes for a club id' }) - findAll(@Query('clubId', new NumberPipe('clubId')) clubId: number) { + findAll(@Query('club_id', new NumberPipe('clubId')) clubId: number) { return this.jukeboxService.findAll(clubId) } diff --git a/src/jukebox/jukebox.service.ts b/src/jukebox/jukebox.service.ts index b67e72b..7df525f 100644 --- a/src/jukebox/jukebox.service.ts +++ b/src/jukebox/jukebox.service.ts @@ -1,16 +1,11 @@ -import { - Injectable, - InternalServerErrorException, - NotFoundException, - NotImplementedException, -} from '@nestjs/common' +import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' import { plainToInstance } from 'class-transformer' +import { CLUBS_URL } from 'src/config' +import { NetworkService } from 'src/network/network.service' import { Repository } from 'typeorm' import { CreateJukeboxDto, JukeboxDto, UpdateJukeboxDto } from './dto/jukebox.dto' import { Jukebox } from './entities/jukebox.entity' -import { NetworkService } from 'src/network/network.service' -import { CLUBS_URL } from 'src/config' @Injectable() export class JukeboxService { @@ -32,7 +27,11 @@ export class JukeboxService { const clubs = (await this.networkService.sendRequest( `${CLUBS_URL}/api/v1/club/clubs/?is_admin=true`, 'GET', - )) as { status: number; description: string; data: { id: number; name: string }[] } + )) as { + status: number + description: string + data: { id: number; name: string; alias?: string }[] + } if (clubs.status !== 200) { throw new InternalServerErrorException( @@ -51,7 +50,10 @@ export class JukeboxService { return plainToInstance(JukeboxDto, result) } - await this.create({ name: clubDetails.name + ' Jukebox', club_id: clubId }) + await this.create({ + name: (clubDetails.alias ?? clubDetails.name) + ' Jukebox', + club_id: clubId, + }) result = await this.jukeboxRepo.find({ where: { club_id: clubId } }) } diff --git a/src/jukebox/player/player.controller.ts b/src/jukebox/player/player.controller.ts index e7ceec6..b902ee8 100644 --- a/src/jukebox/player/player.controller.ts +++ b/src/jukebox/player/player.controller.ts @@ -37,10 +37,9 @@ export class PlayerController { @ApiOperation({ summary: '[ADMIN] Set player device' }) setPlayerDevice( @Param('jukebox_id', new NumberPipe('jukebox_id')) jukeboxId: number, - @ActiveAccount() account: AccountLinkDto, @Body() body: SetPlayerDeviceDto, ) { - return this.playerService.setPlayerDeviceId(jukeboxId, account, body) + return this.playerService.setPlayerDeviceId(jukeboxId, body) } @Roles('member') @@ -63,9 +62,8 @@ export class PlayerController { @ApiOperation({ summary: '[ADMIN] Execute player action' }) async executeAction( @Param('jukebox_id', new NumberPipe('jukebox_id')) jukeboxId: number, - @ActiveAccount() account: AccountLinkDto, @Body() body: PlayerActionDto, ) { - return this.playerService.executeAction(jukeboxId, account, body) + return this.playerService.executeAction(jukeboxId, body) } } diff --git a/src/jukebox/player/player.service.ts b/src/jukebox/player/player.service.ts index b0e1f55..8946038 100644 --- a/src/jukebox/player/player.service.ts +++ b/src/jukebox/player/player.service.ts @@ -6,7 +6,7 @@ import { UserDto } from 'src/shared' import { SpotifyService } from 'src/spotify/spotify.service' import { TrackDto } from 'src/track/dto/track.dto' import { Repository } from 'typeorm' -import { AccountLinkDto } from '../account-link/dto' +import { AccountLinkService } from '../account-link/account-link.service' import { QueuedTrackDto } from '../queue/dto' import { QueueService } from '../queue/queue.service' import { ActionType, PlayerActionDto, PlayerStateDto, SetPlayerDeviceDto } from './dto' @@ -18,6 +18,7 @@ export class PlayerService { @InjectRepository(PlayerInteraction) private repo: Repository, @Inject(CACHE_MANAGER) private cache: Cache, private spotifyService: SpotifyService, + private accountLinkService: AccountLinkService, private queueService: QueueService, ) {} @@ -71,13 +72,9 @@ export class PlayerService { * Transfer playback to the device with id in spotify. * Save this id as the current device id in the player state. */ - async setPlayerDeviceId( - jukeboxId: number, - activeAccount: AccountLinkDto, - payload: SetPlayerDeviceDto, - ): Promise { + async setPlayerDeviceId(jukeboxId: number, payload: SetPlayerDeviceDto): Promise { const { device_id } = payload - + const activeAccount = await this.accountLinkService.getActiveAccount(jukeboxId) await this.spotifyService.setPlayerDevice(activeAccount.spotify_account, device_id) return await this.updatePlayerState(jukeboxId, { current_device_id: device_id }) } @@ -175,14 +172,14 @@ export class PlayerService { * Change the playback state of the player in spotify, * update player state cache. */ - async executeAction(jukeboxId: number, activeAccount: AccountLinkDto, action: PlayerActionDto) { + async executeAction(jukeboxId: number, action: PlayerActionDto) { const { action_type } = action const { current_device_id, juke_session_id } = await this.getPlayerState(+jukeboxId) if (!current_device_id) { throw new BadRequestException('Current device is not set, transfer playback to control audio') } - + const activeAccount = await this.accountLinkService.getActiveAccount(jukeboxId) const { spotify_account } = activeAccount switch (action_type) { diff --git a/src/jukebox/player/tests/player.controller.spec.ts b/src/jukebox/player/tests/player.controller.spec.ts index 72b9184..030a755 100644 --- a/src/jukebox/player/tests/player.controller.spec.ts +++ b/src/jukebox/player/tests/player.controller.spec.ts @@ -15,6 +15,8 @@ import { JukeSession } from 'src/jukebox/juke-session/entities/juke-session.enti import { JukeSessionService } from 'src/jukebox/juke-session/juke-session.service' import { JukeSessionMembership } from 'src/jukebox/juke-session/entities/membership.entity' import { NetworkService } from 'src/network/network.service' +import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' +import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' import { JukeboxService } from 'src/jukebox/jukebox.service' import { Jukebox } from 'src/jukebox/entities/jukebox.entity' @@ -33,11 +35,13 @@ describe('PlayerController', () => { Jukebox, JukeSession, JukeSessionMembership, + SpotifyAccount, ]), ], providers: [ JukeboxService, PlayerService, + SpotifyAuthService, AccountLinkService, QueueService, SpotifyService, diff --git a/src/jukebox/player/tests/player.service.spec.ts b/src/jukebox/player/tests/player.service.spec.ts index b5da00a..bcb1ce4 100644 --- a/src/jukebox/player/tests/player.service.spec.ts +++ b/src/jukebox/player/tests/player.service.spec.ts @@ -5,6 +5,8 @@ import { TypeOrmModule } from '@nestjs/typeorm' import type { Cache } from 'cache-manager' import { plainToInstance } from 'class-transformer' import { DatabaseModule } from 'src/config/database.module' +import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' +import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' import type { JukeboxDto } from 'src/jukebox/dto/jukebox.dto' import { Jukebox } from 'src/jukebox/entities/jukebox.entity' import type { JukeSessionDto } from 'src/jukebox/juke-session/dto/juke-session.dto' @@ -16,18 +18,17 @@ import { JukeboxService } from 'src/jukebox/jukebox.service' import { QueuedTrackDto } from 'src/jukebox/queue/dto' import { QueuedTrack } from 'src/jukebox/queue/entities/queued-track.entity' import { QueueService } from 'src/jukebox/queue/queue.service' +import { NetworkService } from 'src/network/network.service' +import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' +import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' import { SpotifyService } from 'src/spotify/spotify.service' import type { TrackDto } from 'src/track/dto/track.dto' import { Track } from 'src/track/entities/track.entity' import { TrackService } from 'src/track/track.service' -import { AxiosMockProvider, MockCacheProvider, mockUser } from 'src/utils/mock' +import { MockAxiosProvider, MockCacheProvider, mockUser } from 'src/utils/mock' import type { PlayerStateDto } from '../dto' import { InteractionType, PlayerInteraction } from '../entity/player-interaction.entity' import { PlayerService } from '../player.service' -import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' -import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' -import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' -import { NetworkService } from 'src/network/network.service' describe('PlayerService', () => { let service: PlayerService @@ -76,7 +77,7 @@ describe('PlayerService', () => { ]), ], providers: [ - AxiosMockProvider, + MockAxiosProvider, MockCacheProvider, PlayerService, SpotifyService, @@ -87,6 +88,7 @@ describe('PlayerService', () => { TrackService, AccountLinkService, SpotifyService, + SpotifyAuthService, ], }).compile() @@ -111,6 +113,9 @@ describe('PlayerService', () => { artists: ['Acme Music'], release_year: 2025, spotify_id: 'abc123', + duration_ms: 0, + is_explicit: false, + preview_url: null, }) queuedTrack = await createTestQueuedTrack() diff --git a/src/jukebox/queue/tests/queue.controller.spec.ts b/src/jukebox/queue/tests/queue.controller.spec.ts index 06fb062..7260005 100644 --- a/src/jukebox/queue/tests/queue.controller.spec.ts +++ b/src/jukebox/queue/tests/queue.controller.spec.ts @@ -2,29 +2,29 @@ import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' import { DatabaseModule } from 'src/config/database.module' -import { Track } from 'src/track/entities/track.entity' -import { TrackService } from 'src/track/track.service' -import { QueuedTrack } from '../entities/queued-track.entity' -import { QueueController } from '../queue.controller' -import { QueueService } from '../queue.service' -import { JukeSessionMembership } from 'src/jukebox/juke-session/entities/membership.entity' -import { JukeSession } from 'src/jukebox/juke-session/entities/juke-session.entity' -import { Jukebox } from 'src/jukebox/entities/jukebox.entity' -import { JukeSessionService } from 'src/jukebox/juke-session/juke-session.service' -import { JukeboxService } from 'src/jukebox/jukebox.service' +import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' +import type { AccountLinkDto } from 'src/jukebox/account-link/dto' +import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' import type { JukeboxDto } from 'src/jukebox/dto/jukebox.dto' +import { Jukebox } from 'src/jukebox/entities/jukebox.entity' import type { JukeSessionDto } from 'src/jukebox/juke-session/dto/juke-session.dto' import type { JukeSessionMembershipDto } from 'src/jukebox/juke-session/dto/membership.dto' -import { mockCreateTrack } from 'src/utils/mock/mock-create-track' -import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' -import { SpotifyService } from 'src/spotify/spotify.service' -import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' +import { JukeSession } from 'src/jukebox/juke-session/entities/juke-session.entity' +import { JukeSessionMembership } from 'src/jukebox/juke-session/entities/membership.entity' +import { JukeSessionService } from 'src/jukebox/juke-session/juke-session.service' +import { JukeboxService } from 'src/jukebox/jukebox.service' +import { NetworkService } from 'src/network/network.service' import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' -import { AxiosMockProvider, mockSpotifyAccount } from 'src/utils/mock' -import type { AccountLinkDto } from 'src/jukebox/account-link/dto' import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' -import { NetworkService } from 'src/network/network.service' -import { mockTrackDetails } from 'src/utils/mock/mock-itrack-details' +import { SpotifyService } from 'src/spotify/spotify.service' +import { Track } from 'src/track/entities/track.entity' +import { TrackService } from 'src/track/track.service' +import { MockAxiosProvider, mockSpotifyAccount } from 'src/utils/mock' +import { mockCreateTrack } from 'src/utils/mock/mock-create-track' +import { mockTrackDetails } from 'src/utils/mock/mock-track-details' +import { QueuedTrack } from '../entities/queued-track.entity' +import { QueueController } from '../queue.controller' +import { QueueService } from '../queue.service' describe('QueueController', () => { let controller: QueueController @@ -70,7 +70,7 @@ describe('QueueController', () => { ], controllers: [QueueController], providers: [ - AxiosMockProvider, + MockAxiosProvider, QueueService, TrackService, JukeSessionService, @@ -104,7 +104,7 @@ describe('QueueController', () => { user_id: 1, }) accountLink = await accountLinkService.create(jukebox.id, { - spotify_account: await spotifyAuthService.addAccount(mockSpotifyAccount), + spotify_account_id: (await spotifyAuthService.addAccount(mockSpotifyAccount)).id, }) sessionId1 = jukeSession1.id diff --git a/src/jukebox/queue/tests/queue.service.spec.ts b/src/jukebox/queue/tests/queue.service.spec.ts index 7731f72..e00d99d 100644 --- a/src/jukebox/queue/tests/queue.service.spec.ts +++ b/src/jukebox/queue/tests/queue.service.spec.ts @@ -2,29 +2,29 @@ import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' import { DatabaseModule } from 'src/config/database.module' -import { QueuedTrack } from '../entities/queued-track.entity' -import { QueueService } from '../queue.service' -import { Track } from 'src/track/entities/track.entity' -import { JukeSessionService } from 'src/jukebox/juke-session/juke-session.service' -import { JukeSession } from 'src/jukebox/juke-session/entities/juke-session.entity' -import { JukeSessionMembership } from 'src/jukebox/juke-session/entities/membership.entity' -import { SpotifyService } from 'src/spotify/spotify.service' import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' -import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' +import type { AccountLinkDto } from 'src/jukebox/account-link/dto' import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' -import { AxiosMockProvider, mockSpotifyAccount } from 'src/utils/mock' import type { JukeboxDto } from 'src/jukebox/dto/jukebox.dto' +import { Jukebox } from 'src/jukebox/entities/jukebox.entity' import type { JukeSessionDto } from 'src/jukebox/juke-session/dto/juke-session.dto' import type { JukeSessionMembershipDto } from 'src/jukebox/juke-session/dto/membership.dto' -import { TrackService } from 'src/track/track.service' +import { JukeSession } from 'src/jukebox/juke-session/entities/juke-session.entity' +import { JukeSessionMembership } from 'src/jukebox/juke-session/entities/membership.entity' +import { JukeSessionService } from 'src/jukebox/juke-session/juke-session.service' import { JukeboxService } from 'src/jukebox/jukebox.service' -import { Jukebox } from 'src/jukebox/entities/jukebox.entity' +import { NetworkService } from 'src/network/network.service' +import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' +import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' +import { SpotifyService } from 'src/spotify/spotify.service' +import { Track } from 'src/track/entities/track.entity' +import { TrackService } from 'src/track/track.service' +import { MockAxiosProvider, mockSpotifyAccount } from 'src/utils/mock' import { mockCreateTrack } from 'src/utils/mock/mock-create-track' +import { mockTrackDetails } from 'src/utils/mock/mock-track-details' +import { QueuedTrack } from '../entities/queued-track.entity' import { QueueController } from '../queue.controller' -import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' -import type { AccountLinkDto } from 'src/jukebox/account-link/dto' -import { NetworkService } from 'src/network/network.service' -import { mockTrackDetails } from 'src/utils/mock/mock-itrack-details' +import { QueueService } from '../queue.service' describe('QueueService', () => { let controller: QueueController @@ -70,7 +70,7 @@ describe('QueueService', () => { ], controllers: [QueueController], providers: [ - AxiosMockProvider, + MockAxiosProvider, QueueService, TrackService, JukeSessionService, @@ -106,7 +106,7 @@ describe('QueueService', () => { user_id: 1, }) accountLink = await accountLinkService.create(jukebox.id, { - spotify_account: await spotifyAuthService.addAccount(mockSpotifyAccount), + spotify_account_id: (await spotifyAuthService.addAccount(mockSpotifyAccount)).id, }) sessionId1 = jukeSession1.id diff --git a/src/spotify/spotify.controller.ts b/src/spotify/spotify.controller.ts index 9d0ebde..3d19533 100644 --- a/src/spotify/spotify.controller.ts +++ b/src/spotify/spotify.controller.ts @@ -4,11 +4,11 @@ import { Response } from 'express' import { AuthInterceptor } from 'src/auth/auth.interceptor' import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' import { JukeboxService } from 'src/jukebox/jukebox.service' +import { NumberPipe } from 'src/pipes/int-pipe.pipe' import { UserDto } from 'src/shared' import { CurrentUser } from 'src/utils/decorators/current-user.decorator' import { SpotifyAuthService } from './spotify-auth.service' import { SpotifyService } from './spotify.service' -import { NumberPipe } from 'src/pipes/int-pipe.pipe' @ApiTags('Spotify') @ApiBearerAuth() @@ -44,7 +44,7 @@ export class SpotifyController { if (jukeboxId != null) { // await this.jukeboxService.addLinkToJukebox(jukeboxId, account) this.accountLinkService.create(jukeboxId, { - spotify_account: account, + spotify_account_id: account.id, active: true, }) } @@ -56,15 +56,18 @@ export class SpotifyController { } } - @Get('links/') + @Get('accounts/') @UseInterceptors(AuthInterceptor) async getSpotifyLinks(@CurrentUser() user: UserDto) { return this.spotifyAuthService.findUserAccounts(user.id) } - @Delete('links/:id/') + @Delete('accounts/:id/') @UseInterceptors(AuthInterceptor) - async deleteSpotifyLink(@Param('id', new NumberPipe('id')) id: number) { + async deleteSpotifyLink( + @CurrentUser() user: UserDto, + @Param('id', new NumberPipe('id')) id: number, + ) { const link = await this.spotifyAuthService.removeAccount(id) return link } diff --git a/src/spotify/spotify.service.ts b/src/spotify/spotify.service.ts index 432ed9f..77601a2 100644 --- a/src/spotify/spotify.service.ts +++ b/src/spotify/spotify.service.ts @@ -25,11 +25,11 @@ export class SpotifyService extends SpotifyBaseService implements ISpotifyServic return await sdk.currentUser.profile() } - async getTrack(spotifyAuth: SpotifyTokensDto, trackId: string): Promise { + async getTrack(spotifyAuth: SpotifyTokensDto, trackId: string) { const sdk = this.getSdk(spotifyAuth) const track = await sdk.tracks.get(trackId) - return track as ITrackDetails + return track } async queueTrack(spotifyAuth: SpotifyTokensDto, track_uri: string) { diff --git a/src/spotify/tests/spotify.controller.spec.ts b/src/spotify/tests/spotify.controller.spec.ts index dded5ff..da61e1e 100644 --- a/src/spotify/tests/spotify.controller.spec.ts +++ b/src/spotify/tests/spotify.controller.spec.ts @@ -3,15 +3,15 @@ import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' import { DatabaseModule } from 'src/config/database.module' import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' +import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' +import { Jukebox } from 'src/jukebox/entities/jukebox.entity' import { JukeboxService } from 'src/jukebox/jukebox.service' import { NetworkModule } from 'src/network/network.module' -import { AxiosMockProvider } from 'src/utils/mock' +import { MockAxiosProvider } from 'src/utils/mock' import { SpotifyAccount } from '../entities/spotify-account.entity' import { SpotifyAuthService } from '../spotify-auth.service' import { SpotifyController } from '../spotify.controller' import { SpotifyService } from '../spotify.service' -import { Jukebox } from 'src/jukebox/entities/jukebox.entity' -import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' describe('SpotifyController', () => { let controller: SpotifyController @@ -28,7 +28,7 @@ describe('SpotifyController', () => { SpotifyAuthService, SpotifyService, AccountLinkService, - AxiosMockProvider, + MockAxiosProvider, JukeboxService, ], }).compile() diff --git a/src/spotify/tests/spotify.service.spec.ts b/src/spotify/tests/spotify.service.spec.ts index c96d58f..58c6c47 100644 --- a/src/spotify/tests/spotify.service.spec.ts +++ b/src/spotify/tests/spotify.service.spec.ts @@ -1,10 +1,9 @@ import { BadRequestException } from '@nestjs/common' -import type { TestingModule } from '@nestjs/testing' -import { Test } from '@nestjs/testing' +import { Test, TestingModule } from '@nestjs/testing' import { getRepositoryToken, TypeOrmModule } from '@nestjs/typeorm' import { Axios } from 'axios' import { DatabaseModule } from 'src/config/database.module' -import type { Repository } from 'typeorm' +import { Repository } from 'typeorm' import { SpotifyAccount } from '../entities/spotify-account.entity' import { SpotifyAuthService } from '../spotify-auth.service' diff --git a/src/track/dto/track.dto.ts b/src/track/dto/track.dto.ts index 16c3593..c8fb00b 100644 --- a/src/track/dto/track.dto.ts +++ b/src/track/dto/track.dto.ts @@ -1,7 +1,7 @@ import { PartialType } from '@nestjs/swagger' +import { Expose } from 'class-transformer' import { EntityDtoBase } from 'src/config/dtos' import { Track } from '../entities/track.entity' -import { Expose } from 'class-transformer' export class TrackDto extends EntityDtoBase { name: string @@ -10,6 +10,9 @@ export class TrackDto extends EntityDtoBase { artists: string[] spotify_id: string spotify_uri: string + duration_ms: number + is_explicit: boolean + preview_url: string | null } export class CreateTrackDto { @@ -30,5 +33,14 @@ export class CreateTrackDto { @Expose() spotify_uri?: string + + @Expose() + duration_ms: number + + @Expose() + is_explicit: boolean + + @Expose() + preview_url: string | null } export class UpdateTrackDto extends PartialType(CreateTrackDto) {} diff --git a/src/track/tests/track.controller.spec.ts b/src/track/tests/track.controller.spec.ts index 2b8f43a..524b9ae 100644 --- a/src/track/tests/track.controller.spec.ts +++ b/src/track/tests/track.controller.spec.ts @@ -1,19 +1,18 @@ -import type { TestingModule } from '@nestjs/testing' -import { Test } from '@nestjs/testing' +import { Test, TestingModule } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' import { DatabaseModule } from 'src/config/database.module' -import { Track } from '../entities/track.entity' -import { TrackController } from '../track.controller' -import { TrackService } from '../track.service' -import { Jukebox } from 'src/jukebox/entities/jukebox.entity' +import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' -import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' -import { AxiosMockProvider } from 'src/utils/mock' +import { Jukebox } from 'src/jukebox/entities/jukebox.entity' import { JukeboxService } from 'src/jukebox/jukebox.service' -import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' -import { SpotifyService } from 'src/spotify/spotify.service' -import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' import { NetworkService } from 'src/network/network.service' +import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' +import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' +import { SpotifyService } from 'src/spotify/spotify.service' +import { MockAxiosProvider } from 'src/utils/mock' +import { Track } from '../entities/track.entity' +import { TrackController } from '../track.controller' +import { TrackService } from '../track.service' describe('TrackController', () => { let controller: TrackController @@ -26,7 +25,7 @@ describe('TrackController', () => { ], controllers: [TrackController], providers: [ - AxiosMockProvider, + MockAxiosProvider, TrackService, JukeboxService, NetworkService, diff --git a/src/track/tests/track.service.spec.ts b/src/track/tests/track.service.spec.ts index 039157a..f2d4e63 100644 --- a/src/track/tests/track.service.spec.ts +++ b/src/track/tests/track.service.spec.ts @@ -2,21 +2,21 @@ import type { TestingModule } from '@nestjs/testing' import { Test } from '@nestjs/testing' import { TypeOrmModule } from '@nestjs/typeorm' import { DatabaseModule } from 'src/config/database.module' -import { Track } from '../entities/track.entity' -import { TrackService } from '../track.service' -import { mockCreateTrack } from 'src/utils/mock/mock-create-track' -import { Jukebox } from 'src/jukebox/entities/jukebox.entity' -import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' -import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' -import { JukeboxService } from 'src/jukebox/jukebox.service' import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' -import { SpotifyService } from 'src/spotify/spotify.service' -import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' -import { AxiosMockProvider, mockSpotifyAccount } from 'src/utils/mock' -import type { JukeboxDto } from 'src/jukebox/dto/jukebox.dto' -import { mockTrackDetails } from 'src/utils/mock/mock-itrack-details' import type { AccountLinkDto } from 'src/jukebox/account-link/dto' +import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' +import type { JukeboxDto } from 'src/jukebox/dto/jukebox.dto' +import { Jukebox } from 'src/jukebox/entities/jukebox.entity' +import { JukeboxService } from 'src/jukebox/jukebox.service' import { NetworkService } from 'src/network/network.service' +import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' +import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' +import { SpotifyService } from 'src/spotify/spotify.service' +import { MockAxiosProvider, mockSpotifyAccount } from 'src/utils/mock' +import { mockCreateTrack } from 'src/utils/mock/mock-create-track' +import { mockTrackDetails } from 'src/utils/mock/mock-track-details' +import { Track } from '../entities/track.entity' +import { TrackService } from '../track.service' describe('TrackService', () => { let service: TrackService @@ -35,7 +35,7 @@ describe('TrackService', () => { TypeOrmModule.forFeature([Track, Jukebox, AccountLink, SpotifyAccount]), ], providers: [ - AxiosMockProvider, + MockAxiosProvider, TrackService, JukeboxService, NetworkService, @@ -57,7 +57,7 @@ describe('TrackService', () => { jukebox = await jukeboxService.create({ name: 'Test Jukebox', club_id: 1 }) accountLink = await accountLinkService.create(jukebox.id, { - spotify_account: await spotifyAuthService.addAccount(mockSpotifyAccount), + spotify_account_id: (await spotifyAuthService.addAccount(mockSpotifyAccount)).id, }) }) @@ -72,7 +72,9 @@ describe('TrackService', () => { const foundTrack = await service.getTrack(track.spotify_id, jukebox.id) expect(foundTrack.spotify_id).toEqual(track.spotify_id) - await expect(service.create({ ...mockCreateTrack, spotify_uri: '' })).rejects.toThrow(Error) + expect( + async () => await service.create({ ...mockCreateTrack, spotify_uri: '' }), + ).rejects.toThrow(Error) }) it('should create a local reference to a track if it does not exist', async () => { diff --git a/src/track/track.controller.ts b/src/track/track.controller.ts index 69f9bd8..858e32d 100644 --- a/src/track/track.controller.ts +++ b/src/track/track.controller.ts @@ -1,9 +1,9 @@ -import { Body, Controller, Get, Query, UseGuards } from '@nestjs/common' -import { TrackService } from './track.service' +import { Body, Controller, Get, NotImplementedException, Query, UseGuards } from '@nestjs/common' import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger' import { JukeboxSearchDto } from 'src/jukebox/dto/jukebox-search.dto' import { Roles } from 'src/utils/decorators/roles.decorator' import { RolesGuard } from 'src/utils/guards/roles.guard' +import { TrackService } from './track.service' @ApiTags('Track') @ApiBearerAuth() @@ -16,6 +16,7 @@ export class TrackController { @Get() @ApiOperation({ summary: '[MEMBER] Search tracks from the Spotify API' }) searchTracks(@Query('jukeboxId') jukeboxId: string, @Body() body: JukeboxSearchDto) { - return this.trackService.searchTracks(+jukeboxId, body) + // return this.trackService.searchTracks(+jukeboxId, body) + throw new NotImplementedException() } } diff --git a/src/track/track.module.ts b/src/track/track.module.ts index 020d026..e2a7c21 100644 --- a/src/track/track.module.ts +++ b/src/track/track.module.ts @@ -1,27 +1,31 @@ import { Module } from '@nestjs/common' import { TypeOrmModule } from '@nestjs/typeorm' -import { Track } from './entities/track.entity' -import { TrackController } from './track.controller' -import { TrackService } from './track.service' -import { SpotifyService } from 'src/spotify/spotify.service' import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' -import { AxiosProvider } from 'src/utils/mock' import { AccountLink } from 'src/jukebox/account-link/entities/account-link.entity' -import { JukeboxService } from 'src/jukebox/jukebox.service' import { Jukebox } from 'src/jukebox/entities/jukebox.entity' +import { JukeboxService } from 'src/jukebox/jukebox.service' import { NetworkService } from 'src/network/network.service' +import { SpotifyAccount } from 'src/spotify/entities/spotify-account.entity' +import { SpotifyAuthService } from 'src/spotify/spotify-auth.service' +import { SpotifyService } from 'src/spotify/spotify.service' +import { AxiosProvider } from 'src/utils/mock' +import { Track } from './entities/track.entity' +import { TrackController } from './track.controller' +import { TrackService } from './track.service' @Module({ - imports: [TypeOrmModule.forFeature([Track, AccountLink, Jukebox])], + imports: [TypeOrmModule.forFeature([Track, AccountLink, Jukebox, SpotifyAccount])], controllers: [TrackController], providers: [ AxiosProvider, NetworkService, TrackService, SpotifyService, + SpotifyAuthService, AccountLinkService, JukeboxService, ], + exports: [TrackService], }) export class TrackModule {} diff --git a/src/track/track.service.ts b/src/track/track.service.ts index ad5f212..ddcd553 100644 --- a/src/track/track.service.ts +++ b/src/track/track.service.ts @@ -1,12 +1,11 @@ -import { Injectable, NotImplementedException } from '@nestjs/common' +import { Injectable } from '@nestjs/common' import { InjectRepository } from '@nestjs/typeorm' +import { plainToInstance } from 'class-transformer' +import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' +import { SpotifyService } from 'src/spotify/spotify.service' import { Repository } from 'typeorm' import { CreateTrackDto, TrackDto } from './dto/track.dto' import { Track } from './entities/track.entity' -import { plainToInstance } from 'class-transformer' -import { SpotifyService } from 'src/spotify/spotify.service' -import { AccountLinkService } from 'src/jukebox/account-link/account-link.service' -import { JukeboxSearchDto } from 'src/jukebox/dto/jukebox-search.dto' @Injectable() export class TrackService { @@ -82,17 +81,15 @@ export class TrackService { album: trackDetails.album.name, artists: trackDetails.artists.map((artist) => artist.name), spotify_uri: trackDetails.uri, + duration_ms: trackDetails.duration_ms, + is_explicit: trackDetails.explicit, + preview_url: trackDetails.preview_url, }) } return plainToInstance(TrackDto, result ?? track) } - async searchTracks(jukeboxId: number, payload: JukeboxSearchDto) { - const link = await this.accountLinkService.getActiveAccount(jukeboxId) - return await this.spotifyService.searchTracks(link.spotify_account, payload) - } - /** * FOR TESTING PURPOSES ONLY so null constraint is not violated. NEVER use this in production * diff --git a/src/types/index.d.ts b/src/types/index.d.ts deleted file mode 100644 index 5f12ade..0000000 --- a/src/types/index.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -// declare interface IModel { -// id: number -// created_at: Date -// updated_at: Date -// } diff --git a/src/types/jukebox.d.ts b/src/types/jukebox.d.ts deleted file mode 100644 index 8d8c9eb..0000000 --- a/src/types/jukebox.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -// declare interface IJukebox extends IModel { -// name: string -// club_id: number -// links: IJukeboxLink[] -// } - -// declare type JukeboxLinkType = 'spotify' - -// declare interface IJukeboxLink extends IModel { -// type: JukeboxLinkType -// email: string -// active: boolean -// } - -// declare interface ISpotifyAccount { -// id: number -// access_token: string -// refresh_token: string -// user_id: number -// spotify_email: string -// expires_in: number -// expires_at: Date -// token_type: string -// } - -// declare interface IJukeboxLinkAccount extends IJukeboxLink { -// account: ISpotifyAccount -// } - -// declare interface IPlayerState { -// jukebox_id: number -// current_track?: ITrackMeta -// progress: number -// is_playing: boolean -// } - -// /** -// * State of the current player stored in Redis -// */ -// declare interface IPlayerMetaState extends IPlayerState { -// /** Next up in Spotify's queue */ -// default_next_tracks: ITrack[] -// } - -// /** -// * The state of the player broadcast to socket subscribers -// */ -// declare interface IPlayerQueueState extends IPlayerState { -// next_tracks: ITrack[] -// } - -// declare interface IPlayerAuxUpdate extends IPlayerMetaState { -// changed_tracks?: boolean -// } -// declare interface IPlayerUpdate extends IPlayerQueueState {} - -// declare interface IPlayerAction extends Partial { -// current_track?: Partial -// } diff --git a/src/types/spotify-auth.d.ts b/src/types/spotify-auth.d.ts deleted file mode 100644 index 355e730..0000000 --- a/src/types/spotify-auth.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -// declare interface ISpotifyAccount extends IModel { -// access_token: string -// user_id: number -// spotify_email: string -// expires_in: number -// expires_at: Date -// token_type: string -// } diff --git a/src/types/spotify.d.ts b/src/types/spotify.d.ts deleted file mode 100644 index 917b7c9..0000000 --- a/src/types/spotify.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -// declare interface ITrack extends Spotify.Track {} - -// declare interface ITrackMeta extends ITrack { -// queue_id: string -// recommended_by?: string -// spotify_queued?: boolean -// likes?: number -// dislikes?: number -// } diff --git a/src/types/user.d.ts b/src/types/user.d.ts deleted file mode 100644 index 0accbaf..0000000 --- a/src/types/user.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -// declare interface IUser { -// id: number -// email: string -// first_name: string -// last_name: string -// username: string -// } diff --git a/src/utils/guards/roles.guard.ts b/src/utils/guards/roles.guard.ts index 08f6492..eee5504 100644 --- a/src/utils/guards/roles.guard.ts +++ b/src/utils/guards/roles.guard.ts @@ -1,15 +1,15 @@ import { - Injectable, + BadRequestException, CanActivate, ExecutionContext, + Injectable, InternalServerErrorException, - BadRequestException, } from '@nestjs/common' import { Reflector } from '@nestjs/core' -import { CLUBS_URL, NODE_ENV } from 'src/config' +import { CLUBS_URL } from 'src/config' +import { JukeboxService } from 'src/jukebox/jukebox.service' import { NetworkService } from 'src/network/network.service' import { Role } from '../decorators/roles.decorator' -import { JukeboxService } from 'src/jukebox/jukebox.service' import { TokenGuard } from './token.guard' @Injectable() @@ -28,7 +28,7 @@ export class RolesGuard implements CanActivate { const request = context.switchToHttp().getRequest() const { body, query, params } = request - let clubId = body?.club_id ?? query?.clubId ?? params?.club_id ?? null + let clubId = body?.club_id ?? query?.club_id ?? params?.club_id ?? null if (clubId === null) { const jukeboxId = params?.jukebox_id ?? query?.jukeboxId ?? null diff --git a/src/utils/guards/token.guard.ts b/src/utils/guards/token.guard.ts index 1178ee9..19eead6 100644 --- a/src/utils/guards/token.guard.ts +++ b/src/utils/guards/token.guard.ts @@ -1,7 +1,7 @@ import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common' +import { Request } from 'express' import { NODE_ENV } from 'src/config' import { NetworkService } from 'src/network/network.service' -import { Request } from 'express' @Injectable() export class TokenGuard implements CanActivate { diff --git a/src/utils/mock/mock-axios-provider.ts b/src/utils/mock/mock-axios-provider.ts index c20e021..7364e30 100644 --- a/src/utils/mock/mock-axios-provider.ts +++ b/src/utils/mock/mock-axios-provider.ts @@ -5,7 +5,7 @@ export const AxiosProvider = { useValue: Axios.create(), } -export const AxiosMockProvider = { +export const MockAxiosProvider = { provide: Axios.Axios, useValue: Axios.create(), } diff --git a/src/utils/mock/mock-create-track.ts b/src/utils/mock/mock-create-track.ts index a55a3e2..b996646 100644 --- a/src/utils/mock/mock-create-track.ts +++ b/src/utils/mock/mock-create-track.ts @@ -7,4 +7,7 @@ export const mockCreateTrack: CreateTrackDto = { release_year: 2000, spotify_id: '3AJwUDP919kvQ9QcozQPxg', spotify_uri: 'spotify:track:3AJwUDP919kvQ9QcozQPxg', + duration_ms: 2000, + is_explicit: false, + preview_url: 'https://i.scdn.co/image/ab67616d0000b273abcdabcdabcdabcdabcdabcd', } diff --git a/src/utils/mock/mock-itrack-details.ts b/src/utils/mock/mock-track-details.ts similarity index 73% rename from src/utils/mock/mock-itrack-details.ts rename to src/utils/mock/mock-track-details.ts index ef2b14e..aa3438d 100644 --- a/src/utils/mock/mock-itrack-details.ts +++ b/src/utils/mock/mock-track-details.ts @@ -1,4 +1,6 @@ -export const mockTrackDetails: ITrackDetails = { +import type { Track } from '@spotify/web-api-ts-sdk' + +export const mockTrackDetails: Track = { id: '1', name: 'Yellow', duration_ms: 269000, @@ -9,7 +11,7 @@ export const mockTrackDetails: ITrackDetails = { id: '4gzpq5DPGxSnKTe4SA8HAU', name: 'Coldplay', external_urls: { spotify: 'https://open.spotify.com/artist/4gzpq5DPGxSnKTe4SA8HAU' }, - } as unknown as IArtistInlineDetails, + } as Track['artists'][number], ], album: { id: '4aawyAB9vmqN3uQ7FjRGTy', @@ -23,12 +25,21 @@ export const mockTrackDetails: ITrackDetails = { }, ], external_urls: { spotify: 'https://open.spotify.com/album/4aawyAB9vmqN3uQ7FjRGTy' }, - } as unknown as IAlbumInlineDetails, + } as Track['album'], disc_number: 1, explicit: false, popularity: 78, preview_url: 'https://p.scdn.co/mp3-preview/abcdefabcdefabcdefabcdef', track_number: 5, - external_ids: { isrc: 'GBAYE0000598' }, + external_ids: { + isrc: 'GBAYE0000598', + upc: '', + ean: '', + }, external_urls: { spotify: 'https://open.spotify.com/track/3AJwUDP919kvQ9QcozQPxg' }, + available_markets: [], + episode: false, + href: '', + is_local: false, + track: true, } diff --git a/tsconfig.json b/tsconfig.json index 9be9624..dacab7a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,8 +20,8 @@ "forceConsistentCasingInFileNames": false, "noFallthroughCasesInSwitch": false, "types": [ - "jukebox-types", - "./src/types", + // "jukebox-types", + // "./src/types", "@spotify/web-api-ts-sdk", "jest" ],