1 Commits

Author SHA1 Message Date
GitBluub
e4a8eba699 feat: gen cover in populate wip 2023-11-15 13:08:16 +01:00
300 changed files with 9852 additions and 13000 deletions

View File

@@ -16,10 +16,3 @@ GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google
SMTP_TRANSPORT=smtps://toto:tata@relay SMTP_TRANSPORT=smtps://toto:tata@relay
MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>' MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>'
IGNORE_MAILS=true IGNORE_MAILS=true
API_KEYS=SCOROTEST,ROBOTO,SCORO,POPULATE
API_KEY_SCORO_TEST=SCOROTEST
API_KEY_ROBOT=ROBOTO
API_KEY_SCORO=SCORO
API_KEY_POPULATE=POPULATE
MEILI_MASTER_KEY="ghvjkgisbgkbgskegblfqbgjkebbhgwkjfb"
# vi: ft=sh

View File

@@ -1 +0,0 @@
14e241db37c4080bc0bd87363cf7a57ef8379f46

View File

@@ -1,18 +1,164 @@
name: Deploy name: CI
on: on:
pull_request:
types:
- closed
branches:
- main
push: push:
branches: branches:
- main - '*'
pull_request:
branches: [ main ]
jobs: jobs:
deployment:
## Build Back ##
Build_Back:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event.pull_request.merged == true timeout-minutes: 10
defaults:
run:
working-directory: ./back
environment: Staging
steps:
- uses: actions/checkout@v3
- name: Build Docker
run: docker build -t testback .
## Build App ##
Check_Front:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./front
environment: Staging
steps:
- uses: actions/checkout@v3
- name: Install Yarn
run: npm install -g yarn
- name: Install dependencies
run: yarn install
- name: Type Check
run: yarn tsc
- name: Check Prettier
run: yarn pretty:check .
- name: Run Linter
run: yarn lint
Build_Front:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./front
environment: Staging
steps:
- uses: actions/checkout@v3
- name: Install Yarn
run: npm install -g yarn
- name: Install dependencies
run: yarn install
- name: 🏗 Setup Expo
uses: expo/expo-github-action@v7
with:
expo-version: latest
eas-version: 3.3.1
token: ${{ secrets.EXPO_TOKEN }}
- name: Build Web App
uses: docker/build-push-action@v3
with:
context: ./front
push: false
tags: ${{steps.meta_front.outputs.tags}}
build-args: |
API_URL=${{secrets.API_URL}}
SCORO_URL=${{secrets.SCORO_URL}}
- name: Build Android APK
run: |
eas build -p android --profile production --local --non-interactive
mv *.apk chromacase.apk
- name: Upload Artifact
if: github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v3
with:
name: chromacase.apk
path: front/
## Test Backend ##
Test_Back:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [ Build_Back ]
environment: Staging
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- name: Copy env file to github secret env file
run: cp .env.example .env
- name: Start the service
run: docker-compose up -d back db
- name: Perform healthchecks
run: |
docker-compose ps -a
docker-compose logs
wget --retry-connrefused http://localhost:3000 || (docker-compose logs && exit 1)
- name: Run scorometer tests
run: |
pip install -r scorometer/requirements.txt
cd scorometer/tests && ./runner.sh
- name: Run robot tests
run: |
pip install -r back/test/robot/requirements.txt
robot -d out back/test/robot/
- uses: actions/upload-artifact@v3
if: always()
with:
name: results
path: out
- name: Write results to Pull Request and Summary
if: always() && github.event_name == 'pull_request'
uses: joonvena/robotframework-reporter-action@v2.1
with:
report_path: out/
gh_access_token: ${{ secrets.GITHUB_TOKEN }}
only_summary: false
- name: Write results to Summary
if: always() && github.event_name != 'pull_request'
uses: joonvena/robotframework-reporter-action@v2.1
with:
report_path: out/
gh_access_token: ${{ secrets.GITHUB_TOKEN }}
only_summary: true
- name: Remove .env && stop the service
run: docker-compose down && rm .env
## Test App ##
## Deployement ##
Deployement_Docker:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
needs: [ Test_Back ]
environment: Production
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -57,7 +203,6 @@ jobs:
build-args: | build-args: |
API_URL=${{secrets.API_URL}} API_URL=${{secrets.API_URL}}
SCORO_URL=${{secrets.SCORO_URL}} SCORO_URL=${{secrets.SCORO_URL}}
- name: Docker meta scorometer - name: Docker meta scorometer
id: meta_scorometer id: meta_scorometer
uses: docker/metadata-action@v4 uses: docker/metadata-action@v4

View File

@@ -1,101 +0,0 @@
name: "Back"
on:
pull_request:
branches: [ main ]
jobs:
changes:
runs-on: ubuntu-latest
# Required permissions
permissions:
pull-requests: read
# Set job outputs to values from filter step
outputs:
back: ${{ steps.filter.outputs.back }}
front: ${{ steps.filter.outputs.front }}
scorometer: ${{ steps.filter.outputs.scorometer }}
steps:
# For pull requests it's not necessary to checkout the code
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
back:
- 'back/**'
- '.github/workflows/back.yml'
front:
- 'front/**'
- '.github/workflows/front.yml'
scorometer:
- 'scorometer/**'
- '.github/workflows/scoro.yml'
back_build:
runs-on: ubuntu-latest
timeout-minutes: 10
needs: changes
if: ${{ needs.changes.outputs.back == 'true' }}
defaults:
run:
working-directory: ./back
steps:
- uses: actions/checkout@v3
- name: Build Docker
run: docker build -t testback .
back_test:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [ back_build ]
if: ${{ needs.changes.outputs.back == 'true' }}
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- name: Copy env file to github secret env file
run: cp .env.example .env
- name: Build and start the service
run: docker-compose up -d meilisearch back db
- name: Perform healthchecks
run: |
docker-compose ps -a
docker-compose logs
wget --retry-connrefused http://localhost:3000 || (docker-compose logs && exit 1)
- name: Run robot tests
run: |
export API_KEY_ROBOT=ROBOTO
pip install -r back/test/robot/requirements.txt
robot -d out back/test/robot/
- uses: actions/upload-artifact@v3
if: always()
with:
name: results
path: out
- name: Write results to Pull Request and Summary
if: always() && github.event_name == 'pull_request'
uses: joonvena/robotframework-reporter-action@v2.1
with:
report_path: out/
gh_access_token: ${{ secrets.GITHUB_TOKEN }}
only_summary: false
- name: Write results to Summary
if: always() && github.event_name != 'pull_request'
uses: joonvena/robotframework-reporter-action@v2.1
with:
report_path: out/
gh_access_token: ${{ secrets.GITHUB_TOKEN }}
only_summary: true
- name: stop the service
run: docker-compose down

View File

@@ -1,98 +0,0 @@
name: "Front"
on:
pull_request:
branches: [ main ]
jobs:
changes:
runs-on: ubuntu-latest
# Required permissions
permissions:
pull-requests: read
# Set job outputs to values from filter step
outputs:
back: ${{ steps.filter.outputs.back }}
front: ${{ steps.filter.outputs.front }}
scorometer: ${{ steps.filter.outputs.scorometer }}
steps:
# For pull requests it's not necessary to checkout the code
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
back:
- 'back/**'
- '.github/workflows/back.yml'
front:
- 'front/**'
- '.github/workflows/front.yml'
scorometer:
- 'scorometer/**'
- '.github/workflows/scoro.yml'
front_check:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./front
needs: changes
if: ${{ needs.changes.outputs.front == 'true' }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'yarn'
cache-dependency-path: front/yarn.lock
- run: yarn install --frozen-lockfile
- name: type check
run: yarn tsc
- name: prettier
run: yarn pretty:check .
- name: eslint
run: yarn lint
front_build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./front
if: ${{ needs.changes.outputs.front == 'true' }}
needs: [ front_check ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '16'
cache: 'yarn'
cache-dependency-path: front/yarn.lock
- run: yarn install --frozen-lockfile
- name: 🏗 Setup Expo
uses: expo/expo-github-action@v8
with:
expo-version: latest
eas-version: latest
token: ${{ secrets.EXPO_TOKEN }}
- name: Build Web App
uses: docker/build-push-action@v3
with:
context: ./front
push: false
tags: ${{steps.meta_front.outputs.tags}}
build-args: |
API_URL=${{secrets.API_URL}}
SCORO_URL=${{secrets.SCORO_URL}}
- name: Build Android APK
run: |
eas build -p android --profile production --local --non-interactive
mv *.apk chromacase.apk
- name: Upload Artifact
if: github.ref == 'refs/heads/main'
uses: actions/upload-artifact@v3
with:
name: chromacase.apk
path: front/

View File

@@ -1,63 +0,0 @@
name: "Scoro"
on:
pull_request:
branches: [ main ]
jobs:
changes:
runs-on: ubuntu-latest
# Required permissions
permissions:
pull-requests: read
# Set job outputs to values from filter step
outputs:
back: ${{ steps.filter.outputs.back }}
front: ${{ steps.filter.outputs.front }}
scorometer: ${{ steps.filter.outputs.scorometer }}
steps:
# For pull requests it's not necessary to checkout the code
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
back:
- 'back/**'
- '.github/workflows/back.yml'
front:
- 'front/**'
- '.github/workflows/front.yml'
scorometer:
- 'scorometer/**'
- '.github/workflows/scoro.yml'
scoro_test:
runs-on: ubuntu-latest
needs: changes
if: ${{ needs.changes.outputs.scorometer == 'true' }}
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
fetch-depth: 0
- name: Copy env file to github secret env file
run: cp .env.example .env
- name: Build and start the service
run: docker-compose up -d meilisearch back db
- name: Perform healthchecks
run: |
docker-compose ps -a
docker-compose logs
wget --retry-connrefused http://localhost:3000 || (docker-compose logs && exit 1)
- name: Run scorometer tests
run: |
export API_KEY_SCORO_TEST=SCOROTEST
export API_KEY_SCORO=SCORO
pip install -r scorometer/requirements.txt
cd scorometer/tests && ./runner.sh
- name: stop the service
run: docker-compose down

1
.gitignore vendored
View File

@@ -16,4 +16,3 @@ node_modules/
.data .data
.DS_Store .DS_Store
_gen _gen
venv

View File

@@ -1,19 +0,0 @@
#!/bin/bash
# Iterate through subfolders
find . -type d | while read -r dir; do
# Check if .midi file exists in the subfolder
midi_file=$(find "$dir" -maxdepth 1 -type f -name '*.midi' | head -n 1)
if [ -n "$midi_file" ]; then
# Create output file name (melody.mp3) in the same subfolder
output_file="${dir}/melody.mp3"
# Run the given command
#timidity "$midi_file" -Ow -o - | ffmpeg -i - -acodec libmp3lame -ab 64k "$output_file"
fluidsynth -a alsa -T raw -F - "$midi_file" | ffmpeg -f s32le -i - "$output_file"
echo "Converted: $midi_file to $output_file"
fi
done

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -8,10 +8,6 @@ from mido import MidiFile
from configparser import ConfigParser from configparser import ConfigParser
url = os.environ.get("API_URL") url = os.environ.get("API_URL")
api_key = os.environ.get("API_KEY_POPULATE")
auth_headers = {}
auth_headers["Authorization"] = f"API Key {api_key}"
def getOrCreateAlbum(name, artistId): def getOrCreateAlbum(name, artistId):
if not name: if not name:
@@ -19,7 +15,7 @@ def getOrCreateAlbum(name, artistId):
res = requests.post(f"{url}/album", json={ res = requests.post(f"{url}/album", json={
"name": name, "name": name,
"artist": artistId, "artist": artistId,
},headers=auth_headers) })
out = res.json() out = res.json()
print(out) print(out)
return out["id"] return out["id"]
@@ -29,7 +25,7 @@ def getOrCreateGenre(names):
for name in names.split(","): for name in names.split(","):
res = requests.post(f"{url}/genre", json={ res = requests.post(f"{url}/genre", json={
"name": name, "name": name,
},headers=auth_headers) })
out = res.json() out = res.json()
print(out) print(out)
ids += [out["id"]] ids += [out["id"]]
@@ -39,21 +35,26 @@ def getOrCreateGenre(names):
def getOrCreateArtist(name): def getOrCreateArtist(name):
res = requests.post(f"{url}/artist", json={ res = requests.post(f"{url}/artist", json={
"name": name, "name": name,
},headers=auth_headers) })
out = res.json() out = res.json()
print(out) print(out)
return out["id"] return out["id"]
def gen_cover():
def populateFile(path, midi, mxl): def populateFile(path, midi, mxl):
config = ConfigParser() config = ConfigParser()
config.read(path) config.read(path)
mid = MidiFile(midi) mid = MidiFile(midi)
common = os.path.commonpath([midi, mxl])
png_path = f"{common}/illustration.png"
if not os.path.exists(png_path):
gen_cover(common)
metadata = config["Metadata"]; metadata = config["Metadata"];
difficulties = dict(config["Difficulties"]) difficulties = dict(config["Difficulties"])
difficulties["length"] = round((mid.length), 2) difficulties["length"] = round((mid.length), 2)
artistId = getOrCreateArtist(metadata["Artist"]) artistId = getOrCreateArtist(metadata["Artist"])
print(f"Populating {metadata['Name']}") print(f"Populating {metadata['Name']}")
print(auth_headers)
res = requests.post(f"{url}/song", json={ res = requests.post(f"{url}/song", json={
"name": metadata["Name"], "name": metadata["Name"],
"midiPath": f"/assets/{midi}", "midiPath": f"/assets/{midi}",
@@ -62,11 +63,10 @@ def populateFile(path, midi, mxl):
"artist": artistId, "artist": artistId,
"album": getOrCreateAlbum(metadata["Album"], artistId), "album": getOrCreateAlbum(metadata["Album"], artistId),
"genre": getOrCreateGenre(metadata["Genre"]), "genre": getOrCreateGenre(metadata["Genre"]),
"illustrationPath": f"/assets/{os.path.commonpath([midi, mxl])}/illustration.png" "illustrationPath": f"/assets/{png_path}"
}, headers=auth_headers) })
print(res.json()) print(res.json())
def main(): def main():
global url global url
if url == None: if url == None:

View File

@@ -1,2 +0,0 @@
mido
requests

View File

@@ -1,4 +1,4 @@
{ {
"singleQuote": false, "singleQuote": true,
"trailingComma": "all" "trailingComma": "all"
} }

View File

@@ -1,3 +1,3 @@
FROM node:18.10.0 FROM node:17
WORKDIR /app WORKDIR /app
CMD npm i ; npx prisma generate ; npx prisma migrate dev ; npm run start:dev CMD npx prisma generate ; npx prisma migrate dev ; npm run start:dev

3201
back/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,19 +35,13 @@
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/passport": "^1.0.12", "@types/passport": "^1.0.12",
"bcryptjs": "^2.4.3", "bcryptjs": "^2.4.3",
"canvas": "^2.11.2",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.13.2",
"cross-blob": "^3.0.2",
"fs": "^0.0.1-security",
"jsdom": "^22.1.0",
"json-logger-service": "^9.0.1", "json-logger-service": "^9.0.1",
"meilisearch": "^0.35.0", "class-validator": "^0.14.0",
"node-fetch": "^2.6.12", "node-fetch": "^2.6.12",
"nodemailer": "^6.9.5", "nodemailer": "^6.9.5",
"opensheetmusicdisplay": "^1.8.4",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"passport-headerapikey": "^1.2.2",
"passport-jwt": "^4.0.1", "passport-jwt": "^4.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"prisma-class-generator": "^0.2.7", "prisma-class-generator": "^0.2.7",
@@ -87,8 +81,7 @@
"moduleFileExtensions": [ "moduleFileExtensions": [
"js", "js",
"json", "json",
"ts", "ts"
"mjs"
], ],
"rootDir": "src", "rootDir": "src",
"testRegex": ".*\\.spec\\.ts$", "testRegex": ".*\\.spec\\.ts$",

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "totalScore" INTEGER NOT NULL DEFAULT 0;

View File

@@ -24,7 +24,6 @@ model User {
googleID String? @unique googleID String? @unique
isGuest Boolean @default(false) isGuest Boolean @default(false)
partyPlayed Int @default(0) partyPlayed Int @default(0)
totalScore Int @default(0)
LessonHistory LessonHistory[] LessonHistory LessonHistory[]
SongHistory SongHistory[] SongHistory SongHistory[]
searchHistory SearchHistory[] searchHistory SearchHistory[]

View File

@@ -12,24 +12,23 @@ import {
Query, Query,
Req, Req,
UseGuards, UseGuards,
} from "@nestjs/common"; } from '@nestjs/common';
import { ApiOkResponsePlaginated, Plage } from "src/models/plage"; import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
import { CreateAlbumDto } from "./dto/create-album.dto"; import { CreateAlbumDto } from './dto/create-album.dto';
import { AlbumService } from "./album.service"; import { AlbumService } from './album.service';
import { Request } from "express"; import { Request } from 'express';
import { Prisma, Album } from "@prisma/client"; import { Prisma, Album } from '@prisma/client';
import { ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger"; import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
import { FilterQuery } from "src/utils/filter.pipe"; import { FilterQuery } from 'src/utils/filter.pipe';
import { Album as _Album } from "src/_gen/prisma-class/album"; import { Album as _Album } from 'src/_gen/prisma-class/album';
import { IncludeMap, mapInclude } from "src/utils/include"; import { IncludeMap, mapInclude } from 'src/utils/include';
import { AuthGuard } from "@nestjs/passport"; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
@Controller("album") @Controller('album')
@ApiTags("album") @ApiTags('album')
@UseGuards(ChromaAuthGuard) @UseGuards(JwtAuthGuard)
export class AlbumController { export class AlbumController {
static filterableFields: string[] = ["+id", "name", "+artistId"]; static filterableFields: string[] = ['+id', 'name', '+artistId'];
static includableFields: IncludeMap<Prisma.AlbumInclude> = { static includableFields: IncludeMap<Prisma.AlbumInclude> = {
artist: true, artist: true,
Song: true, Song: true,
@@ -39,7 +38,7 @@ export class AlbumController {
@Post() @Post()
@ApiOperation({ @ApiOperation({
description: "Register a new album, should not be used by frontend", description: 'Register a new album, should not be used by frontend',
}) })
async create(@Body() createAlbumDto: CreateAlbumDto) { async create(@Body() createAlbumDto: CreateAlbumDto) {
try { try {
@@ -56,26 +55,26 @@ export class AlbumController {
} }
} }
@Delete(":id") @Delete(':id')
@ApiOperation({ description: "Delete an album by id" }) @ApiOperation({ description: 'Delete an album by id' })
async remove(@Param("id", ParseIntPipe) id: number) { async remove(@Param('id', ParseIntPipe) id: number) {
try { try {
return await this.albumService.deleteAlbum({ id }); return await this.albumService.deleteAlbum({ id });
} catch { } catch {
throw new NotFoundException("Invalid ID"); throw new NotFoundException('Invalid ID');
} }
} }
@Get() @Get()
@ApiOkResponsePlaginated(_Album) @ApiOkResponsePlaginated(_Album)
@ApiOperation({ description: "Get all albums paginated" }) @ApiOperation({ description: 'Get all albums paginated' })
async findAll( async findAll(
@Req() req: Request, @Req() req: Request,
@FilterQuery(AlbumController.filterableFields) @FilterQuery(AlbumController.filterableFields)
where: Prisma.AlbumWhereInput, where: Prisma.AlbumWhereInput,
@Query("include") include: string, @Query('include') include: string,
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number, @Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number, @Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Album>> { ): Promise<Plage<Album>> {
const ret = await this.albumService.albums({ const ret = await this.albumService.albums({
skip, skip,
@@ -86,20 +85,20 @@ export class AlbumController {
return new Plage(ret, req); return new Plage(ret, req);
} }
@Get(":id") @Get(':id')
@ApiOperation({ description: "Get an album by id" }) @ApiOperation({ description: 'Get an album by id' })
@ApiOkResponse({ type: _Album }) @ApiOkResponse({ type: _Album })
async findOne( async findOne(
@Req() req: Request, @Req() req: Request,
@Query("include") include: string, @Query('include') include: string,
@Param("id", ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
) { ) {
const res = await this.albumService.album( const res = await this.albumService.album(
{ id }, { id },
mapInclude(include, req, AlbumController.includableFields), mapInclude(include, req, AlbumController.includableFields),
); );
if (res === null) throw new NotFoundException("Album not found"); if (res === null) throw new NotFoundException('Album not found');
return res; return res;
} }
} }

View File

@@ -1,7 +1,7 @@
import { Module } from "@nestjs/common"; import { Module } from '@nestjs/common';
import { PrismaModule } from "src/prisma/prisma.module"; import { PrismaModule } from 'src/prisma/prisma.module';
import { AlbumController } from "./album.controller"; import { AlbumController } from './album.controller';
import { AlbumService } from "./album.service"; import { AlbumService } from './album.service';
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule],

View File

@@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { Prisma, Album } from "@prisma/client"; import { Prisma, Album } from '@prisma/client';
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from 'src/prisma/prisma.service';
@Injectable() @Injectable()
export class AlbumService { export class AlbumService {

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from "class-validator"; import { IsNotEmpty } from 'class-validator';
export class CreateAlbumDto { export class CreateAlbumDto {
@IsNotEmpty() @IsNotEmpty()

View File

@@ -1,8 +1,8 @@
import { Test, TestingModule } from "@nestjs/testing"; import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from "./app.controller"; import { AppController } from './app.controller';
import { AppService } from "./app.service"; import { AppService } from './app.service';
describe("AppController", () => { describe('AppController', () => {
let appController: AppController; let appController: AppController;
beforeEach(async () => { beforeEach(async () => {
@@ -14,9 +14,9 @@ describe("AppController", () => {
appController = app.get<AppController>(AppController); appController = app.get<AppController>(AppController);
}); });
describe("root", () => { describe('root', () => {
it('should return "Hello World!"', () => { it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe("Hello World!"); expect(appController.getHello()).toBe('Hello World!');
}); });
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { Controller, Get } from "@nestjs/common"; import { Controller, Get } from '@nestjs/common';
import { AppService } from "./app.service"; import { AppService } from './app.service';
import { ApiOkResponse } from "@nestjs/swagger"; import { ApiOkResponse } from '@nestjs/swagger';
@Controller() @Controller()
export class AppController { export class AppController {
@@ -8,7 +8,7 @@ export class AppController {
@Get() @Get()
@ApiOkResponse({ @ApiOkResponse({
description: "Return a hello world message, used as a health route", description: 'Return a hello world message, used as a health route',
}) })
getHello(): string { getHello(): string {
return this.appService.getHello(); return this.appService.getHello();

View File

@@ -1,21 +1,20 @@
import { Module } from "@nestjs/common"; import { Module } from '@nestjs/common';
import { AppController } from "./app.controller"; import { AppController } from './app.controller';
import { AppService } from "./app.service"; import { AppService } from './app.service';
import { PrismaService } from "./prisma/prisma.service"; import { PrismaService } from './prisma/prisma.service';
import { UsersModule } from "./users/users.module"; import { UsersModule } from './users/users.module';
import { PrismaModule } from "./prisma/prisma.module"; import { PrismaModule } from './prisma/prisma.module';
import { AuthModule } from "./auth/auth.module"; import { AuthModule } from './auth/auth.module';
import { SongModule } from "./song/song.module"; import { SongModule } from './song/song.module';
import { LessonModule } from "./lesson/lesson.module"; import { LessonModule } from './lesson/lesson.module';
import { SettingsModule } from "./settings/settings.module"; import { SettingsModule } from './settings/settings.module';
import { ArtistService } from "./artist/artist.service"; import { ArtistService } from './artist/artist.service';
import { GenreModule } from "./genre/genre.module"; import { GenreModule } from './genre/genre.module';
import { ArtistModule } from "./artist/artist.module"; import { ArtistModule } from './artist/artist.module';
import { AlbumModule } from "./album/album.module"; import { AlbumModule } from './album/album.module';
import { SearchModule } from "./search/search.module"; import { SearchModule } from './search/search.module';
import { HistoryModule } from "./history/history.module"; import { HistoryModule } from './history/history.module';
import { MailerModule } from "@nestjs-modules/mailer"; import { MailerModule } from '@nestjs-modules/mailer';
import { ScoresModule } from "./scores/scores.module";
@Module({ @Module({
imports: [ imports: [
@@ -30,7 +29,6 @@ import { ScoresModule } from "./scores/scores.module";
SearchModule, SearchModule,
SettingsModule, SettingsModule,
HistoryModule, HistoryModule,
ScoresModule,
MailerModule.forRoot({ MailerModule.forRoot({
transport: process.env.SMTP_TRANSPORT, transport: process.env.SMTP_TRANSPORT,
defaults: { defaults: {

View File

@@ -1,8 +1,8 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
@Injectable() @Injectable()
export class AppService { export class AppService {
getHello(): string { getHello(): string {
return "Hello World!"; return 'Hello World!';
} }
} }

View File

@@ -14,31 +14,30 @@ import {
Req, Req,
StreamableFile, StreamableFile,
UseGuards, UseGuards,
} from "@nestjs/common"; } from '@nestjs/common';
import { ApiOkResponsePlaginated, Plage } from "src/models/plage"; import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
import { CreateArtistDto } from "./dto/create-artist.dto"; import { CreateArtistDto } from './dto/create-artist.dto';
import { Request } from "express"; import { Request } from 'express';
import { ArtistService } from "./artist.service"; import { ArtistService } from './artist.service';
import { Prisma, Artist } from "@prisma/client"; import { Prisma, Artist } from '@prisma/client';
import { import {
ApiNotFoundResponse, ApiNotFoundResponse,
ApiOkResponse, ApiOkResponse,
ApiOperation, ApiOperation,
ApiTags, ApiTags,
} from "@nestjs/swagger"; } from '@nestjs/swagger';
import { createReadStream, existsSync } from "fs"; import { createReadStream, existsSync } from 'fs';
import { FilterQuery } from "src/utils/filter.pipe"; import { FilterQuery } from 'src/utils/filter.pipe';
import { Artist as _Artist } from "src/_gen/prisma-class/artist"; import { Artist as _Artist } from 'src/_gen/prisma-class/artist';
import { IncludeMap, mapInclude } from "src/utils/include"; import { IncludeMap, mapInclude } from 'src/utils/include';
import { Public } from "src/auth/public"; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { AuthGuard } from "@nestjs/passport"; import { Public } from 'src/auth/public';
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
@Controller("artist") @Controller('artist')
@ApiTags("artist") @ApiTags('artist')
@UseGuards(ChromaAuthGuard) @UseGuards(JwtAuthGuard)
export class ArtistController { export class ArtistController {
static filterableFields = ["+id", "name"]; static filterableFields = ['+id', 'name'];
static includableFields: IncludeMap<Prisma.ArtistInclude> = { static includableFields: IncludeMap<Prisma.ArtistInclude> = {
Song: true, Song: true,
Album: true, Album: true,
@@ -48,7 +47,7 @@ export class ArtistController {
@Post() @Post()
@ApiOperation({ @ApiOperation({
description: "Register a new artist, should not be used by frontend", description: 'Register a new artist, should not be used by frontend',
}) })
async create(@Body() dto: CreateArtistDto) { async create(@Body() dto: CreateArtistDto) {
try { try {
@@ -58,26 +57,26 @@ export class ArtistController {
} }
} }
@Delete(":id") @Delete(':id')
@ApiOperation({ description: "Delete an artist by id" }) @ApiOperation({ description: 'Delete an artist by id' })
async remove(@Param("id", ParseIntPipe) id: number) { async remove(@Param('id', ParseIntPipe) id: number) {
try { try {
return await this.service.delete({ id }); return await this.service.delete({ id });
} catch { } catch {
throw new NotFoundException("Invalid ID"); throw new NotFoundException('Invalid ID');
} }
} }
@Get(":id/illustration") @Get(':id/illustration')
@ApiOperation({ description: "Get an artist's illustration" }) @ApiOperation({ description: "Get an artist's illustration" })
@ApiNotFoundResponse({ description: "Artist or illustration not found" }) @ApiNotFoundResponse({ description: 'Artist or illustration not found' })
@Public() @Public()
async getIllustration(@Param("id", ParseIntPipe) id: number) { async getIllustration(@Param('id', ParseIntPipe) id: number) {
const artist = await this.service.get({ id }); const artist = await this.service.get({ id });
if (!artist) throw new NotFoundException("Artist not found"); if (!artist) throw new NotFoundException('Artist not found');
const path = `/assets/artists/${artist.name}/illustration.png`; const path = `/assets/artists/${artist.name}/illustration.png`;
if (!existsSync(path)) if (!existsSync(path))
throw new NotFoundException("Illustration not found"); throw new NotFoundException('Illustration not found');
try { try {
const file = createReadStream(path); const file = createReadStream(path);
@@ -88,15 +87,15 @@ export class ArtistController {
} }
@Get() @Get()
@ApiOperation({ description: "Get all artists paginated" }) @ApiOperation({ description: 'Get all artists paginated' })
@ApiOkResponsePlaginated(_Artist) @ApiOkResponsePlaginated(_Artist)
async findAll( async findAll(
@Req() req: Request, @Req() req: Request,
@FilterQuery(ArtistController.filterableFields) @FilterQuery(ArtistController.filterableFields)
where: Prisma.ArtistWhereInput, where: Prisma.ArtistWhereInput,
@Query("include") include: string, @Query('include') include: string,
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number, @Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number, @Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Artist>> { ): Promise<Plage<Artist>> {
const ret = await this.service.list({ const ret = await this.service.list({
skip, skip,
@@ -107,20 +106,20 @@ export class ArtistController {
return new Plage(ret, req); return new Plage(ret, req);
} }
@Get(":id") @Get(':id')
@ApiOperation({ description: "Get an artist by id" }) @ApiOperation({ description: 'Get an artist by id' })
@ApiOkResponse({ type: _Artist }) @ApiOkResponse({ type: _Artist })
async findOne( async findOne(
@Req() req: Request, @Req() req: Request,
@Query("include") include: string, @Query('include') include: string,
@Param("id", ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
) { ) {
const res = await this.service.get( const res = await this.service.get(
{ id }, { id },
mapInclude(include, req, ArtistController.includableFields), mapInclude(include, req, ArtistController.includableFields),
); );
if (res === null) throw new NotFoundException("Artist not found"); if (res === null) throw new NotFoundException('Artist not found');
return res; return res;
} }
} }

View File

@@ -1,11 +1,10 @@
import { Module } from "@nestjs/common"; import { Module } from '@nestjs/common';
import { PrismaModule } from "src/prisma/prisma.module"; import { PrismaModule } from 'src/prisma/prisma.module';
import { ArtistController } from "./artist.controller"; import { ArtistController } from './artist.controller';
import { ArtistService } from "./artist.service"; import { ArtistService } from './artist.service';
import { SearchModule } from "src/search/search.module";
@Module({ @Module({
imports: [PrismaModule, SearchModule], imports: [PrismaModule],
controllers: [ArtistController], controllers: [ArtistController],
providers: [ArtistService], providers: [ArtistService],
}) })

View File

@@ -1,21 +1,15 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { Prisma, Artist } from "@prisma/client"; import { Prisma, Artist } from '@prisma/client';
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from 'src/prisma/prisma.service';
import { MeiliService } from "src/search/meilisearch.service";
@Injectable() @Injectable()
export class ArtistService { export class ArtistService {
constructor( constructor(private prisma: PrismaService) {}
private prisma: PrismaService,
private search: MeiliService,
) {}
async create(data: Prisma.ArtistCreateInput): Promise<Artist> { async create(data: Prisma.ArtistCreateInput): Promise<Artist> {
const ret = await this.prisma.artist.create({ return this.prisma.artist.create({
data, data,
}); });
await this.search.index("artists").addDocuments([ret]);
return ret;
} }
async get( async get(
@@ -48,10 +42,8 @@ export class ArtistService {
} }
async delete(where: Prisma.ArtistWhereUniqueInput): Promise<Artist> { async delete(where: Prisma.ArtistWhereUniqueInput): Promise<Artist> {
const ret = await this.prisma.artist.delete({ return this.prisma.artist.delete({
where, where,
}); });
await this.search.index("artists").deleteDocument(ret.id);
return ret;
} }
} }

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from "class-validator"; import { IsNotEmpty } from 'class-validator';
export class CreateArtistDto { export class CreateArtistDto {
@IsNotEmpty() @IsNotEmpty()

View File

@@ -1,794 +0,0 @@
// import Blob from "cross-blob";
import FS from "fs";
import jsdom from "jsdom";
//import headless_gl from "gl"; // this is now imported dynamically in a try catch, in case gl install fails, see #1160
import * as OSMD from "opensheetmusicdisplay"; // window needs to be available before we can require OSMD
let Blob;
/*
Render each OSMD sample, grab the generated images, andg
dump them into a local directory as PNG or SVG files.
inspired by Vexflow's generate_png_images and vexflow-tests.js
This can be used to generate PNGs or SVGs from OSMD without a browser.
It's also used with the visual regression test system (using PNGs) in
`tools/visual_regression.sh`
(see package.json, used with npm run generate:blessed and generate:current, then test:visual).
Note: this script needs to "fake" quite a few browser elements, like window, document,
and a Canvas HTMLElement (for PNG) or the DOM (for SVG) ,
which otherwise are missing in pure nodejs, causing errors in OSMD.
For PNG it needs the canvas package installed.
There are also some hacks needed to set the container size (offsetWidth) correctly.
Otherwise you'd need to run a headless browser, which is way slower,
see the semi-obsolete generateDiffImagesPuppeteerLocalhost.js
*/
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
const timestampToMs = (timestamp, wholeNoteLength) => {
return timestamp.RealValue * wholeNoteLength;
};
const getActualNoteLength = (note, wholeNoteLength) => {
let duration = timestampToMs(note.Length, wholeNoteLength);
if (note.NoteTie) {
const firstNote = note.NoteTie.Notes.at(1);
if (Object.is(note.NoteTie.StartNote, note) && firstNote) {
duration += timestampToMs(firstNote.Length, wholeNoteLength);
} else {
duration = 0;
}
}
return duration;
};
function getCursorPositions(osmd, filename, partitionDims) {
osmd.cursor.show();
const bpm = osmd.Sheet.HasBPMInfo
? osmd.Sheet.getExpressionsStartTempoInBPM()
: 60;
const wholeNoteLength = Math.round((60 / bpm) * 4000);
const curPos = [];
while (!osmd.cursor.iterator.EndReached) {
const notesToPlay = osmd.cursor
.NotesUnderCursor()
.filter((note) => {
return note.isRest() == false && note.Pitch;
})
.map((note) => {
const fixedKey =
note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments.at(0)
?.fixedKey ?? 0;
const midiNumber = note.halfTone - fixedKey * 12;
const gain = note.ParentVoiceEntry.ParentVoice.Volume;
return {
note: midiNumber,
gain: gain,
duration: getActualNoteLength(note, wholeNoteLength),
};
});
const shortestNotes = osmd.cursor
.NotesUnderCursor()
.sort((n1, n2) => n1.Length.CompareTo(n2.Length))
.at(0);
const ts = timestampToMs(
shortestNotes?.getAbsoluteTimestamp() ?? new OSMD.Fraction(-1),
wholeNoteLength,
);
const sNL = timestampToMs(
shortestNotes?.Length ?? new OSMD.Fraction(-1),
wholeNoteLength,
);
curPos.push({
x: parseFloat(osmd.cursor.cursorElement.style.left),
y: parseFloat(osmd.cursor.cursorElement.style.top),
width: osmd.cursor.cursorElement.width,
height: osmd.cursor.cursorElement.height,
notes: notesToPlay,
timestamp: ts,
timing: sNL,
});
osmd.cursor.next();
}
osmd.cursor.reset();
osmd.cursor.hide();
const cursorsFilename = `${imageDir}/${filename}.json`;
FS.writeFileSync(
cursorsFilename,
JSON.stringify({
pageWidth: partitionDims[0],
pageHeight: partitionDims[1],
cursors: curPos,
}),
);
console.log(`Saved cursor positions to ${cursorsFilename}`);
}
// global variables
// (without these being global, we'd have to pass many of these values to the generateSampleImage function)
// eslint-disable-next-line prefer-const
let assetName;
let sampleDir;
let imageDir;
let imageFormat;
let pageWidth;
let pageHeight;
let filterRegex;
let mode;
let debugSleepTimeString;
let skyBottomLinePreference;
let pageFormat;
export async function generateSongAssets(
assetName_,
sampleDir_,
imageDir_,
imageFormat_,
pageWidth_,
pageHeight_,
filterRegex_,
mode_,
debugSleepTimeString_,
skyBottomLinePreference_,
) {
assetName = assetName_;
sampleDir = sampleDir_;
imageDir = imageDir_;
imageFormat = imageFormat_;
pageWidth = pageWidth_;
pageHeight = pageHeight_;
filterRegex = filterRegex_;
mode = mode_;
debugSleepTimeString = debugSleepTimeString_;
skyBottomLinePreference = skyBottomLinePreference_;
imageFormat = imageFormat?.toLowerCase();
eval(`import("cross-blob")`).then((module) => {
Blob = module.default;
});
debug("" + sampleDir + " " + imageDir + " " + imageFormat);
if (!mode) {
mode = "";
}
if (
!assetName ||
!sampleDir ||
!imageDir ||
(imageFormat !== "png" && imageFormat !== "svg")
) {
console.log(
"usage: " +
// eslint-disable-next-line max-len
"node test/Util/generateImages_browserless.mjs osmdBuildDir sampleDirectory imageDirectory svg|png [width|0] [height|0] [filterRegex|all|allSmall] [--debug|--osmdtesting] [debugSleepTime]",
);
console.log(
" (use pageWidth and pageHeight 0 to not divide the rendering into pages (endless page))",
);
console.log(
' (use "all" to skip filterRegex parameter. "allSmall" with --osmdtesting skips two huge OSMD samples that take forever to render)',
);
console.log(
"example: node test/Util/generateImages_browserless.mjs ../../build ./test/data/ ./export png",
);
console.log(
"Error: need osmdBuildDir, sampleDir, imageDir and svg|png arguments. Exiting.",
);
Promise.reject(
"Error: need osmdBuildDir, sampleDir, imageDir and svg|png arguments. Exiting.",
);
}
await init();
}
// let OSMD; // can only be required once window was simulated
// eslint-disable-next-line @typescript-eslint/no-var-requires
async function init() {
debug("init");
const osmdTestingMode = mode.includes("osmdtesting"); // can also be --debugosmdtesting
const osmdTestingSingleMode = mode.includes("osmdtestingsingle");
const DEBUG = mode.startsWith("--debug");
// const debugSleepTime = Number.parseInt(process.env.GENERATE_DEBUG_SLEEP_TIME) || 0; // 5000 works for me [sschmidTU]
if (DEBUG) {
// debug(' (note that --debug slows down the script by about 0.3s per file, through logging)')
const debugSleepTimeMs = Number.parseInt(debugSleepTimeString, 10);
if (debugSleepTimeMs > 0) {
debug("debug sleep time: " + debugSleepTimeString);
await sleep(Number.parseInt(debugSleepTimeMs, 10));
// [VSCode] apparently this is necessary for the debugger to attach itself in time before the program closes.
// sometimes this is not enough, so you may have to try multiple times or increase the sleep timer. Unfortunately debugging nodejs isn't easy.
}
}
debug("sampleDir: " + sampleDir, DEBUG);
debug("imageDir: " + imageDir, DEBUG);
debug("imageFormat: " + imageFormat, DEBUG);
pageFormat = "Endless";
pageWidth = Number.parseInt(pageWidth, 10);
pageHeight = Number.parseInt(pageHeight, 10);
const endlessPage = !(pageHeight > 0 && pageWidth > 0);
if (!endlessPage) {
pageFormat = `${pageWidth}x${pageHeight}`;
}
// ---- hacks to fake Browser elements OSMD and Vexflow need, like window, document, and a canvas HTMLElement ----
// eslint-disable-next-line @typescript-eslint/no-var-requires
const dom = new jsdom.JSDOM("<!DOCTYPE html></html>");
// eslint-disable-next-line no-global-assign
// window = dom.window;
// eslint-disable-next-line no-global-assign
// document = dom.window.document;
// eslint-disable-next-line no-global-assign
global.window = dom.window;
// eslint-disable-next-line no-global-assign
global.document = window.document;
//window.console = console; // probably does nothing
global.HTMLElement = window.HTMLElement;
global.HTMLAnchorElement = window.HTMLAnchorElement;
global.XMLHttpRequest = window.XMLHttpRequest;
global.DOMParser = window.DOMParser;
global.Node = window.Node;
if (imageFormat === "png") {
global.Canvas = window.Canvas;
}
// For WebGLSkyBottomLineCalculatorBackend: Try to import gl dynamically
// this is so that the script doesn't fail if gl could not be installed,
// which can happen in some linux setups where gcc-11 is installed, see #1160
try {
const { default: headless_gl } = await import("gl");
const oldCreateElement = document.createElement.bind(document);
document.createElement = function (tagName, options) {
if (tagName.toLowerCase() === "canvas") {
const canvas = oldCreateElement(tagName, options);
const oldGetContext = canvas.getContext.bind(canvas);
canvas.getContext = function (contextType, contextAttributes) {
if (
contextType.toLowerCase() === "webgl" ||
contextType.toLowerCase() === "experimental-webgl"
) {
const gl = headless_gl(
canvas.width,
canvas.height,
contextAttributes,
);
gl.canvas = canvas;
return gl;
} else {
return oldGetContext(contextType, contextAttributes);
}
};
return canvas;
} else {
return oldCreateElement(tagName, options);
}
};
} catch {
if (skyBottomLinePreference === "--webgl") {
debug(
"WebGL image generation was requested but gl is not installed; using non-WebGL generation.",
);
}
}
// fix Blob not found (to support external modules like is-blob)
global.Blob = Blob;
const div = document.createElement("div");
div.id = "browserlessDiv";
document.body.appendChild(div);
// const canvas = document.createElement('canvas')
// div.canvas = document.createElement('canvas')
const zoom = 1.0;
// width of the div / PNG generated
let width = pageWidth * zoom;
// TODO sometimes the width is way too small for the score, may need to adjust zoom.
if (endlessPage) {
width = 1440;
}
let height = pageHeight;
if (endlessPage) {
height = 32767;
}
div.width = width;
div.height = height;
// div.offsetWidth = width; // doesn't work, offsetWidth is always 0 from this. see below
// div.clientWidth = width;
// div.clientHeight = height;
// div.scrollHeight = height;
// div.scrollWidth = width;
div.setAttribute("width", width);
div.setAttribute("height", height);
div.setAttribute("offsetWidth", width);
// debug('div.offsetWidth: ' + div.offsetWidth, DEBUG) // 0 here, set correctly later
// debug('div.height: ' + div.height, DEBUG)
// hack: set offsetWidth reliably
Object.defineProperties(window.HTMLElement.prototype, {
offsetLeft: {
get: function () {
return parseFloat(window.getComputedStyle(this).marginTop) || 0;
},
},
offsetTop: {
get: function () {
return parseFloat(window.getComputedStyle(this).marginTop) || 0;
},
},
offsetHeight: {
get: function () {
return height;
},
},
offsetWidth: {
get: function () {
return width;
},
},
});
debug("div.offsetWidth: " + div.offsetWidth, DEBUG);
debug("div.height: " + div.height, DEBUG);
// ---- end browser hacks (hopefully) ----
// load globally
// Create the image directory if it doesn't exist.
FS.mkdirSync(imageDir, { recursive: true });
// const sampleDirFilenames = FS.readdirSync(sampleDir);
let samplesToProcess = []; // samples we want to process/generate pngs of, excluding the filtered out files/filenames
// sampleDir is the direct path to a single file but is then only keept as a the directory containing the file
if (sampleDir.match("^.*(.xml)|(.musicxml)|(.mxl)$")) {
let pathParts = sampleDir.split("/");
let filename = pathParts[pathParts.length - 1];
sampleDir = pathParts.slice(0, pathParts.length - 1).join("/");
samplesToProcess.push(filename);
} else {
debug("not a correct extension sampleDir: " + sampleDir, DEBUG);
}
// for (const sampleFilename of sampleDirFilenames) {
// if (osmdTestingMode && filterRegex === "allSmall") {
// if (sampleFilename.match("^(Actor)|(Gounod)")) {
// // TODO maybe filter by file size instead
// debug("filtering big file: " + sampleFilename, DEBUG);
// continue;
// }
// }
// // eslint-disable-next-line no-useless-escape
// if (sampleFilename.match("^.*(.xml)|(.musicxml)|(.mxl)$")) {
// // debug('found musicxml/mxl: ' + sampleFilename)
// samplesToProcess.push(sampleFilename);
// } else {
// debug("discarded file/directory: " + sampleFilename, DEBUG);
// }
// }
// filter samples to process by regex if given
if (
filterRegex &&
filterRegex !== "" &&
filterRegex !== "all" &&
!(osmdTestingMode && filterRegex === "allSmall")
) {
debug("filtering samples for regex: " + filterRegex, DEBUG);
samplesToProcess = samplesToProcess.filter((filename) =>
filename.match(filterRegex),
);
debug(`found ${samplesToProcess.length} matches: `, DEBUG);
for (let i = 0; i < samplesToProcess.length; i++) {
debug(samplesToProcess[i], DEBUG);
}
}
const backend = imageFormat === "png" ? "canvas" : "svg";
const osmdInstance = new OSMD.OpenSheetMusicDisplay(div, {
autoResize: false,
backend: backend,
pageBackgroundColor: "#FFFFFF",
pageFormat: pageFormat,
// defaultFontFamily: 'Arial',
drawTitle: false,
renderSingleHorizontalStaffline: true,
drawComposer: false,
drawCredits: false,
drawLyrics: false,
drawPartNames: false,
followCursor: false,
cursorsOptions: [{ type: 0, color: "green", alpha: 0.5, follow: false }],
});
// for more options check OSMDOptions.ts
// you can set finer-grained rendering/engraving settings in EngravingRules:
// osmdInstance.EngravingRules.TitleTopDistance = 5.0 // 5.0 is default
// (unless in osmdTestingMode, these will be reset with drawingParameters default)
// osmdInstance.EngravingRules.PageTopMargin = 5.0 // 5 is default
// osmdInstance.EngravingRules.PageBottomMargin = 5.0 // 5 is default. <5 can cut off scores that extend in the last staffline
// note that for now the png and canvas will still have the height given in the script argument,
// so even with a margin of 0 the image will be filled to the full height.
// osmdInstance.EngravingRules.PageLeftMargin = 5.0 // 5 is default
// osmdInstance.EngravingRules.PageRightMargin = 5.0 // 5 is default
// osmdInstance.EngravingRules.MetronomeMarkXShift = -8; // -6 is default
// osmdInstance.EngravingRules.DistanceBetweenVerticalSystemLines = 0.15; // 0.35 is default
// for more options check EngravingRules.ts (though not all of these are meant and fully supported to be changed at will)
if (DEBUG) {
osmdInstance.setLogLevel("debug");
// debug(`osmd PageFormat: ${osmdInstance.EngravingRules.PageFormat.width}x${osmdInstance.EngravingRules.PageFormat.height}`)
debug(
`osmd PageFormat idString: ${osmdInstance.EngravingRules.PageFormat.idString}`,
);
debug("PageHeight: " + osmdInstance.EngravingRules.PageHeight);
} else {
osmdInstance.setLogLevel("info"); // doesn't seem to work, log.debug still logs
}
debug(
"[OSMD.generateImages] starting loop over samples, saving images to " +
imageDir,
DEBUG,
);
for (let i = 0; i < samplesToProcess.length; i++) {
const sampleFilename = samplesToProcess[i];
debug("sampleFilename: " + sampleFilename, DEBUG);
await generateSampleImage(
sampleFilename,
sampleDir,
osmdInstance,
osmdTestingMode,
{},
DEBUG,
);
if (
osmdTestingMode &&
!osmdTestingSingleMode &&
sampleFilename.startsWith("Beethoven") &&
sampleFilename.includes("Geliebte")
) {
// generate one more testing image with skyline and bottomline. (startsWith 'Beethoven' don't catch the function test)
await generateSampleImage(
sampleFilename,
sampleDir,
osmdInstance,
osmdTestingMode,
{ skyBottomLine: true },
DEBUG,
);
// generate one more testing image with GraphicalNote positions
await generateSampleImage(
sampleFilename,
sampleDir,
osmdInstance,
osmdTestingMode,
{ boundingBoxes: "VexFlowGraphicalNote" },
DEBUG,
);
}
}
debug("done, exiting.");
return Promise.resolve();
}
// eslint-disable-next-line
// let maxRss = 0, maxRssFilename = '' // to log memory usage (debug)
async function generateSampleImage(
sampleFilename,
directory,
osmdInstance,
osmdTestingMode,
options = {},
DEBUG = false,
) {
function makeSkyBottomLineOptions() {
const preference = skyBottomLinePreference ?? "";
if (preference === "--batch") {
return {
preferredSkyBottomLineBatchCalculatorBackend: 0, // plain
skyBottomLineBatchCriteria: 0, // use batch algorithm only
};
} else if (preference === "--webgl") {
return {
preferredSkyBottomLineBatchCalculatorBackend: 1, // webgl
skyBottomLineBatchCriteria: 0, // use batch algorithm only
};
} else {
return {
preferredSkyBottomLineBatchCalculatorBackend: 0, // plain
skyBottomLineBatchCriteria: Infinity, // use non-batch algorithm only
};
}
}
const samplePath = directory + "/" + sampleFilename;
let loadParameter = FS.readFileSync(samplePath);
if (sampleFilename.endsWith(".mxl")) {
loadParameter = await OSMD.MXLHelper.MXLtoXMLstring(loadParameter);
} else {
loadParameter = loadParameter.toString();
}
// debug('loadParameter: ' + loadParameter)
// debug('typeof loadParameter: ' + typeof loadParameter)
// set sample-specific options for OSMD visual regression testing
let includeSkyBottomLine = false;
let drawBoundingBoxString;
let isTestOctaveShiftInvisibleInstrument;
let isTestInvisibleMeasureNotAffectingLayout;
if (osmdTestingMode) {
const isFunctionTestAutobeam = sampleFilename.startsWith(
"OSMD_function_test_autobeam",
);
const isFunctionTestAutoColoring = sampleFilename.startsWith(
"OSMD_function_test_auto-custom-coloring",
);
const isFunctionTestSystemAndPageBreaks = sampleFilename.startsWith(
"OSMD_Function_Test_System_and_Page_Breaks",
);
const isFunctionTestDrawingRange = sampleFilename.startsWith(
"OSMD_function_test_measuresToDraw_",
);
const defaultOrCompactTightMode = sampleFilename.startsWith(
"OSMD_Function_Test_Container_height",
)
? "compacttight"
: "default";
const isTestFlatBeams = sampleFilename.startsWith("test_drum_tuplet_beams");
const isTestEndClefStaffEntryBboxes = sampleFilename.startsWith(
"test_end_measure_clefs_staffentry_bbox",
);
const isTestPageBreakImpliesSystemBreak = sampleFilename.startsWith(
"test_pagebreak_implies_systembreak",
);
const isTestPageBottomMargin0 =
sampleFilename.includes("PageBottomMargin0");
const isTestTupletBracketTupletNumber = sampleFilename.includes(
"test_tuplet_bracket_tuplet_number",
);
const isTestCajon2NoteSystem = sampleFilename.includes(
"test_cajon_2-note-system",
);
isTestOctaveShiftInvisibleInstrument = sampleFilename.includes(
"test_octaveshift_first_instrument_invisible",
);
const isTextOctaveShiftExtraGraphicalMeasure = sampleFilename.includes(
"test_octaveshift_extragraphicalmeasure",
);
isTestInvisibleMeasureNotAffectingLayout = sampleFilename.includes(
"test_invisible_measure_not_affecting_layout",
);
const isTestWedgeMultilineCrescendo = sampleFilename.includes(
"test_wedge_multiline_crescendo",
);
const isTestWedgeMultilineDecrescendo = sampleFilename.includes(
"test_wedge_multiline_decrescendo",
);
osmdInstance.EngravingRules.loadDefaultValues(); // note this may also be executed in setOptions below via drawingParameters default
if (isTestEndClefStaffEntryBboxes) {
drawBoundingBoxString = "VexFlowStaffEntry";
} else {
drawBoundingBoxString = options.boundingBoxes; // undefined is also a valid value: no bboxes
}
osmdInstance.setOptions({
autoBeam: isFunctionTestAutobeam, // only set to true for function test autobeam
coloringMode: isFunctionTestAutoColoring ? 2 : 0,
// eslint-disable-next-line max-len
coloringSetCustom: isFunctionTestAutoColoring
? [
"#d82c6b",
"#F89D15",
"#FFE21A",
"#4dbd5c",
"#009D96",
"#43469d",
"#76429c",
"#ff0000",
]
: undefined,
colorStemsLikeNoteheads: isFunctionTestAutoColoring,
drawingParameters: defaultOrCompactTightMode, // note: default resets all EngravingRules. could be solved differently
drawFromMeasureNumber: isFunctionTestDrawingRange ? 9 : 1,
drawUpToMeasureNumber: isFunctionTestDrawingRange
? 12
: Number.MAX_SAFE_INTEGER,
newSystemFromXML: isFunctionTestSystemAndPageBreaks,
newSystemFromNewPageInXML: isTestPageBreakImpliesSystemBreak,
newPageFromXML: isFunctionTestSystemAndPageBreaks,
pageBackgroundColor: "#FFFFFF", // reset by drawingparameters default
pageFormat: pageFormat, // reset by drawingparameters default,
...makeSkyBottomLineOptions(),
});
// note that loadDefaultValues() may be executed in setOptions with drawingParameters default
//osmdInstance.EngravingRules.RenderSingleHorizontalStaffline = true; // to use this option here, place it after setOptions(), see above
osmdInstance.EngravingRules.AlwaysSetPreferredSkyBottomLineBackendAutomatically = false; // this would override the command line options (--plain etc)
includeSkyBottomLine = options.skyBottomLine
? options.skyBottomLine
: false; // apparently es6 doesn't have ?? operator
osmdInstance.drawSkyLine = includeSkyBottomLine; // if includeSkyBottomLine, draw skyline and bottomline, else not
osmdInstance.drawBottomLine = includeSkyBottomLine;
osmdInstance.setDrawBoundingBox(drawBoundingBoxString, false); // false: don't render (now). also (re-)set if undefined!
if (isTestFlatBeams) {
osmdInstance.EngravingRules.FlatBeams = true;
// osmdInstance.EngravingRules.FlatBeamOffset = 30;
osmdInstance.EngravingRules.FlatBeamOffset = 10;
osmdInstance.EngravingRules.FlatBeamOffsetPerBeam = 10;
} else {
osmdInstance.EngravingRules.FlatBeams = false;
}
if (isTestPageBottomMargin0) {
osmdInstance.EngravingRules.PageBottomMargin = 0;
}
if (isTestTupletBracketTupletNumber) {
osmdInstance.EngravingRules.TupletNumberLimitConsecutiveRepetitions = true;
osmdInstance.EngravingRules.TupletNumberMaxConsecutiveRepetitions = 2;
osmdInstance.EngravingRules.TupletNumberAlwaysDisableAfterFirstMax = true; // necessary to trigger bug
}
if (isTestCajon2NoteSystem) {
osmdInstance.EngravingRules.PercussionUseCajon2NoteSystem = true;
}
if (
isTextOctaveShiftExtraGraphicalMeasure ||
isTestOctaveShiftInvisibleInstrument ||
isTestWedgeMultilineCrescendo ||
isTestWedgeMultilineDecrescendo
) {
osmdInstance.EngravingRules.NewSystemAtXMLNewSystemAttribute = true;
}
}
try {
debug("loading sample " + sampleFilename, DEBUG);
await osmdInstance.load(loadParameter, sampleFilename); // if using load.then() without await, memory will not be freed up between renders
if (isTestOctaveShiftInvisibleInstrument) {
osmdInstance.Sheet.Instruments[0].Visible = false;
}
if (isTestInvisibleMeasureNotAffectingLayout) {
if (osmdInstance.Sheet.Instruments[1]) {
// some systems can't handle ?. in this script (just a safety check anyways)
osmdInstance.Sheet.Instruments[1].Visible = false;
}
}
} catch (ex) {
debug(
"couldn't load sample " + sampleFilename + ", skipping. Error: \n" + ex,
);
return Promise.reject(ex);
}
debug("xml loaded", DEBUG);
try {
osmdInstance.render();
// there were reports that await could help here, but render isn't a synchronous function, and it seems to work. see #932
} catch (ex) {
debug("renderError: " + ex);
}
debug("rendered", DEBUG);
const markupStrings = []; // svg
const dataUrls = []; // png
let canvasImage;
// intended to use only for the chromacase partition use case (always 1 page in svg)
let partitionDims = [-1, -1];
for (
let pageNumber = 1;
pageNumber < Number.POSITIVE_INFINITY;
pageNumber++
) {
if (imageFormat === "png") {
canvasImage = document.getElementById(
"osmdCanvasVexFlowBackendCanvas" + pageNumber,
);
if (!canvasImage) {
break;
}
if (!canvasImage.toDataURL) {
debug(
`error: could not get canvas image for page ${pageNumber} for file: ${sampleFilename}`,
);
break;
}
dataUrls.push(canvasImage.toDataURL());
} else if (imageFormat === "svg") {
const svgElement = document.getElementById("osmdSvgPage" + pageNumber);
if (!svgElement) {
break;
}
// The important xmlns attribute is not serialized unless we set it here
svgElement.setAttribute("xmlns", "http://www.w3.org/2000/svg");
const width = svgElement.getAttribute("width");
const height = svgElement.getAttribute("height");
partitionDims = [width, height];
markupStrings.push(svgElement.outerHTML);
}
}
// create the cursor positions file
getCursorPositions(osmdInstance, assetName, partitionDims);
for (
let pageIndex = 0;
pageIndex < Math.max(dataUrls.length, markupStrings.length);
pageIndex++
) {
const pageNumberingString = `${pageIndex + 1}`;
const skybottomlineString = includeSkyBottomLine ? "skybottomline_" : "";
const graphicalNoteBboxesString = drawBoundingBoxString
? "bbox" + drawBoundingBoxString + "_"
: "";
// pageNumberingString = dataUrls.length > 0 ? pageNumberingString : '' // don't put '_1' at the end if only one page. though that may cause more work
const pageFilename = `${imageDir}/${assetName}.${imageFormat}`;
if (imageFormat === "png") {
const dataUrl = dataUrls[pageIndex];
if (!dataUrl || !dataUrl.split) {
debug(
`error: could not get dataUrl (imageData) for page ${
pageIndex + 1
} of sample: ${sampleFilename}`,
);
continue;
}
const imageData = dataUrl.split(";base64,").pop();
const imageBuffer = Buffer.from(imageData, "base64");
debug("got image data, saving to: " + pageFilename, DEBUG);
FS.writeFileSync(pageFilename, imageBuffer, { encoding: "base64" });
} else if (imageFormat === "svg") {
const markup = markupStrings[pageIndex];
if (!markup) {
debug(
`error: could not get markup (SVG data) for page ${
pageIndex + 1
} of sample: ${sampleFilename}`,
);
continue;
}
debug("got svg markup data, saving to: " + pageFilename, DEBUG);
// replace every bounding-box by none (react native doesn't support bounding-box)
FS.writeFileSync(pageFilename, markup.replace(/bounding-box/g, "none"), {
encoding: "utf-8",
});
}
// debug: log memory usage
// const usage = process.memoryUsage()
// for (const entry of Object.entries(usage)) {
// if (entry[0] === 'rss') {
// if (entry[1] > maxRss) {
// maxRss = entry[1]
// maxRssFilename = pageFilename
// }
// }
// debug(entry[0] + ': ' + entry[1] / (1024 * 1024) + 'mb')
// }
// debug('maxRss: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
}
// debug('maxRss total: ' + (maxRss / 1024 / 1024) + 'mb' + ' for ' + maxRssFilename)
// await sleep(5000)
// }) // end read file
}
function debug(msg, debugEnabled = true) {
if (debugEnabled) {
console.log("[generateImages] " + msg);
}
}
// init();

View File

@@ -1,5 +0,0 @@
import { Injectable } from "@nestjs/common";
import { AuthGuard } from "@nestjs/passport";
@Injectable()
export class ApiKeyAuthGuard extends AuthGuard("api-key") {}

View File

@@ -1,31 +0,0 @@
import { Injectable, UnauthorizedException } from "@nestjs/common";
import { AuthService } from "./auth.service";
import { PassportStrategy } from "@nestjs/passport";
import Strategy from "passport-headerapikey";
import { ConfigService } from "@nestjs/config";
@Injectable()
export class HeaderApiKeyStrategy extends PassportStrategy(
Strategy,
"api-key",
) {
constructor(private readonly configService: ConfigService) {
super(
{ header: "Authorization", prefix: "API Key " },
true,
async (apiKey, done) => {
return this.validate(apiKey, done);
},
);
}
public validate = (apiKey: string, done: (error: Error, data) => {}) => {
if (
this.configService.get<string>("API_KEYS")?.split(",").includes(apiKey)
) {
//@ts-expect-error
done(null, true);
}
done(new UnauthorizedException(), null);
};
}

View File

@@ -21,13 +21,12 @@ import {
Response, Response,
Query, Query,
Param, Param,
ParseIntPipe, } from '@nestjs/common';
} from "@nestjs/common"; import { AuthService } from './auth.service';
import { AuthService } from "./auth.service"; import { JwtAuthGuard } from './jwt-auth.guard';
import { JwtAuthGuard } from "./jwt-auth.guard"; import { LocalAuthGuard } from './local-auth.guard';
import { LocalAuthGuard } from "./local-auth.guard"; import { RegisterDto } from './dto/register.dto';
import { RegisterDto } from "./dto/register.dto"; import { UsersService } from 'src/users/users.service';
import { UsersService } from "src/users/users.service";
import { import {
ApiBadRequestResponse, ApiBadRequestResponse,
ApiBearerAuth, ApiBearerAuth,
@@ -37,24 +36,21 @@ import {
ApiOperation, ApiOperation,
ApiTags, ApiTags,
ApiUnauthorizedResponse, ApiUnauthorizedResponse,
} from "@nestjs/swagger"; } from '@nestjs/swagger';
import { User } from "../models/user"; import { User } from '../models/user';
import { JwtToken } from "./models/jwt"; import { JwtToken } from './models/jwt';
import { LoginDto } from "./dto/login.dto"; import { LoginDto } from './dto/login.dto';
import { Profile } from "./dto/profile.dto"; import { Profile } from './dto/profile.dto';
import { Setting } from "src/models/setting"; import { Setting } from 'src/models/setting';
import { UpdateSettingDto } from "src/settings/dto/update-setting.dto"; import { UpdateSettingDto } from 'src/settings/dto/update-setting.dto';
import { SettingsService } from "src/settings/settings.service"; import { SettingsService } from 'src/settings/settings.service';
import { AuthGuard } from "@nestjs/passport"; import { AuthGuard } from '@nestjs/passport';
import { FileInterceptor } from "@nestjs/platform-express"; import { FileInterceptor } from '@nestjs/platform-express';
import { writeFile } from "fs"; import { writeFile } from 'fs';
import { PasswordResetDto } from "./dto/password_reset.dto "; import { PasswordResetDto } from './dto/password_reset.dto ';
import { mapInclude } from "src/utils/include";
import { SongController } from "src/song/song.controller";
import { ChromaAuthGuard } from "./chroma-auth.guard";
@ApiTags("auth") @ApiTags('auth')
@Controller("auth") @Controller('auth')
export class AuthController { export class AuthController {
constructor( constructor(
private authService: AuthService, private authService: AuthService,
@@ -62,17 +58,17 @@ export class AuthController {
private settingsService: SettingsService, private settingsService: SettingsService,
) {} ) {}
@Get("login/google") @Get('login/google')
@UseGuards(AuthGuard("google")) @UseGuards(AuthGuard('google'))
@ApiOperation({ description: "Redirect to google login page" }) @ApiOperation({ description: 'Redirect to google login page' })
googleLogin() {} googleLogin() {}
@Get("logged/google") @Get('logged/google')
@ApiOperation({ @ApiOperation({
description: description:
"Redirect to the front page after connecting to the google account", 'Redirect to the front page after connecting to the google account',
}) })
@UseGuards(AuthGuard("google")) @UseGuards(AuthGuard('google'))
async googleLoginCallbakc(@Req() req: any) { async googleLoginCallbakc(@Req() req: any) {
let user = await this.usersService.user({ googleID: req.user.googleID }); let user = await this.usersService.user({ googleID: req.user.googleID });
if (!user) { if (!user) {
@@ -82,13 +78,13 @@ export class AuthController {
return this.authService.login(user); return this.authService.login(user);
} }
@Post("register") @Post('register')
@ApiOperation({ description: "Register a new user" }) @ApiOperation({ description: 'Register a new user' })
@ApiConflictResponse({ description: "Username or email already taken" }) @ApiConflictResponse({ description: 'Username or email already taken' })
@ApiOkResponse({ @ApiOkResponse({
description: "Successfully registered, email sent to verify", description: 'Successfully registered, email sent to verify',
}) })
@ApiBadRequestResponse({ description: "Invalid data or database error" }) @ApiBadRequestResponse({ description: 'Invalid data or database error' })
async register(@Body() registerDto: RegisterDto): Promise<void> { async register(@Body() registerDto: RegisterDto): Promise<void> {
try { try {
const user = await this.usersService.createUser(registerDto); const user = await this.usersService.createUser(registerDto);
@@ -96,102 +92,102 @@ export class AuthController {
await this.authService.sendVerifyMail(user); await this.authService.sendVerifyMail(user);
} catch (e) { } catch (e) {
// check if the error is a duplicate key error // check if the error is a duplicate key error
if (e.code === "P2002") { if (e.code === 'P2002') {
throw new ConflictException("Username or email already taken"); throw new ConflictException('Username or email already taken');
} }
console.error(e); console.error(e);
throw new BadRequestException(); throw new BadRequestException();
} }
} }
@Put("verify") @Put('verify')
@HttpCode(200) @HttpCode(200)
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiOperation({ description: "Verify the email of the user" }) @ApiOperation({ description: 'Verify the email of the user' })
@ApiOkResponse({ description: "Successfully verified" }) @ApiOkResponse({ description: 'Successfully verified' })
@ApiBadRequestResponse({ description: "Invalid or expired token" }) @ApiBadRequestResponse({ description: 'Invalid or expired token' })
async verify( async verify(
@Request() req: any, @Request() req: any,
@Query("token") token: string, @Query('token') token: string,
): Promise<void> { ): Promise<void> {
if (await this.authService.verifyMail(req.user.id, token)) return; if (await this.authService.verifyMail(req.user.id, token)) return;
throw new BadRequestException("Invalid token. Expired or invalid."); throw new BadRequestException('Invalid token. Expired or invalid.');
} }
@Put("reverify") @Put('reverify')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@HttpCode(200) @HttpCode(200)
@ApiOperation({ description: "Resend the verification email" }) @ApiOperation({ description: 'Resend the verification email' })
async reverify(@Request() req: any): Promise<void> { async reverify(@Request() req: any): Promise<void> {
const user = await this.usersService.user({ id: req.user.id }); const user = await this.usersService.user({ id: req.user.id });
if (!user) throw new BadRequestException("Invalid user"); if (!user) throw new BadRequestException('Invalid user');
await this.authService.sendVerifyMail(user); await this.authService.sendVerifyMail(user);
} }
@HttpCode(200) @HttpCode(200)
@Put("password-reset") @Put('password-reset')
async password_reset( async password_reset(
@Body() resetDto: PasswordResetDto, @Body() resetDto: PasswordResetDto,
@Query("token") token: string, @Query('token') token: string,
): Promise<void> { ): Promise<void> {
if (await this.authService.changePassword(resetDto.password, token)) return; if (await this.authService.changePassword(resetDto.password, token)) return;
throw new BadRequestException("Invalid token. Expired or invalid."); throw new BadRequestException('Invalid token. Expired or invalid.');
} }
@HttpCode(200) @HttpCode(200)
@Put("forgot-password") @Put('forgot-password')
async forgot_password(@Query("email") email: string): Promise<void> { async forgot_password(@Query('email') email: string): Promise<void> {
console.log(email); console.log(email);
const user = await this.usersService.user({ email: email }); const user = await this.usersService.user({ email: email });
if (!user) throw new BadRequestException("Invalid user"); if (!user) throw new BadRequestException('Invalid user');
await this.authService.sendPasswordResetMail(user); await this.authService.sendPasswordResetMail(user);
} }
@Post("login") @Post('login')
@ApiBody({ type: LoginDto }) @ApiBody({ type: LoginDto })
@HttpCode(200) @HttpCode(200)
@UseGuards(LocalAuthGuard) @UseGuards(LocalAuthGuard)
@ApiBody({ type: LoginDto }) @ApiBody({ type: LoginDto })
@ApiOperation({ description: "Login with username and password" }) @ApiOperation({ description: 'Login with username and password' })
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken }) @ApiOkResponse({ description: 'Successfully logged in', type: JwtToken })
@ApiUnauthorizedResponse({ description: "Invalid credentials" }) @ApiUnauthorizedResponse({ description: 'Invalid credentials' })
async login(@Request() req: any): Promise<JwtToken> { async login(@Request() req: any): Promise<JwtToken> {
return this.authService.login(req.user); return this.authService.login(req.user);
} }
@Post("guest") @Post('guest')
@HttpCode(200) @HttpCode(200)
@ApiOperation({ description: "Login as a guest account" }) @ApiOperation({ description: 'Login as a guest account' })
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken }) @ApiOkResponse({ description: 'Successfully logged in', type: JwtToken })
async guest(): Promise<JwtToken> { async guest(): Promise<JwtToken> {
const user = await this.usersService.createGuest(); const user = await this.usersService.createGuest();
await this.settingsService.createUserSetting(user.id); await this.settingsService.createUserSetting(user.id);
return this.authService.login(user); return this.authService.login(user);
} }
@UseGuards(ChromaAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOperation({ description: "Get the profile picture of connected user" }) @ApiOperation({ description: 'Get the profile picture of connected user' })
@ApiOkResponse({ description: "The user profile picture" }) @ApiOkResponse({ description: 'The user profile picture' })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
@Get("me/picture") @Get('me/picture')
async getProfilePicture(@Request() req: any, @Response() res: any) { async getProfilePicture(@Request() req: any, @Response() res: any) {
return await this.usersService.getProfilePicture(req.user.id, res); return await this.usersService.getProfilePicture(req.user.id, res);
} }
@UseGuards(ChromaAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOkResponse({ description: "The user profile picture" }) @ApiOkResponse({ description: 'The user profile picture' })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
@Post("me/picture") @Post('me/picture')
@ApiOperation({ description: "Upload a new profile picture" }) @ApiOperation({ description: 'Upload a new profile picture' })
@UseInterceptors(FileInterceptor("file")) @UseInterceptors(FileInterceptor('file'))
async postProfilePicture( async postProfilePicture(
@Request() req: any, @Request() req: any,
@UploadedFile( @UploadedFile(
new ParseFilePipeBuilder() new ParseFilePipeBuilder()
.addFileTypeValidator({ .addFileTypeValidator({
fileType: "jpeg", fileType: 'jpeg',
}) })
.build({ .build({
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY, errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
@@ -207,22 +203,22 @@ export class AuthController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOkResponse({ description: "Successfully logged in", type: User }) @ApiOkResponse({ description: 'Successfully logged in', type: User })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
@Get("me") @Get('me')
@ApiOperation({ description: "Get the user info of connected user" }) @ApiOperation({ description: 'Get the user info of connected user' })
async getProfile(@Request() req: any): Promise<User> { async getProfile(@Request() req: any): Promise<User> {
const user = await this.usersService.user({ id: req.user.id }); const user = await this.usersService.user({ id: req.user.id });
if (!user) throw new InternalServerErrorException(); if (!user) throw new InternalServerErrorException();
return user; return user;
} }
@UseGuards(ChromaAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOkResponse({ description: "Successfully edited profile", type: User }) @ApiOkResponse({ description: 'Successfully edited profile', type: User })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
@Put("me") @Put('me')
@ApiOperation({ description: "Edit the profile of connected user" }) @ApiOperation({ description: 'Edit the profile of connected user' })
editProfile( editProfile(
@Request() req: any, @Request() req: any,
@Body() profile: Partial<Profile>, @Body() profile: Partial<Profile>,
@@ -245,20 +241,20 @@ export class AuthController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOkResponse({ description: "Successfully deleted", type: User }) @ApiOkResponse({ description: 'Successfully deleted', type: User })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
@Delete("me") @Delete('me')
@ApiOperation({ description: "Delete the profile of connected user" }) @ApiOperation({ description: 'Delete the profile of connected user' })
deleteSelf(@Request() req: any): Promise<User> { deleteSelf(@Request() req: any): Promise<User> {
return this.usersService.deleteUser({ id: req.user.id }); return this.usersService.deleteUser({ id: req.user.id });
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOkResponse({ description: "Successfully edited settings", type: Setting }) @ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
@Patch("me/settings") @Patch('me/settings')
@ApiOperation({ description: "Edit the settings of connected user" }) @ApiOperation({ description: 'Edit the settings of connected user' })
udpateSettings( udpateSettings(
@Request() req: any, @Request() req: any,
@Body() settingUserDto: UpdateSettingDto, @Body() settingUserDto: UpdateSettingDto,
@@ -271,10 +267,10 @@ export class AuthController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOkResponse({ description: "Successfully edited settings", type: Setting }) @ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
@Get("me/settings") @Get('me/settings')
@ApiOperation({ description: "Get the settings of connected user" }) @ApiOperation({ description: 'Get the settings of connected user' })
async getSettings(@Request() req: any): Promise<Setting> { async getSettings(@Request() req: any): Promise<Setting> {
const result = await this.settingsService.getUserSetting({ const result = await this.settingsService.getUserSetting({
userId: +req.user.id, userId: +req.user.id,
@@ -285,43 +281,28 @@ export class AuthController {
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOkResponse({ description: "Successfully added liked song" }) @ApiOkResponse({ description: 'Successfully added liked song' })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
@Post("me/likes/:id") @Post('me/likes/:id')
addLikedSong(@Request() req: any, @Param("id", ParseIntPipe) songId: number) { addLikedSong(@Request() req: any, @Param('id') songId: number) {
return this.usersService.addLikedSong(+req.user.id, songId); return this.usersService.addLikedSong(+req.user.id, +songId);
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOkResponse({ description: "Successfully removed liked song" }) @ApiOkResponse({ description: 'Successfully removed liked song' })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
@Delete("me/likes/:id") @Delete('me/likes/:id')
removeLikedSong( removeLikedSong(@Request() req: any, @Param('id') songId: number) {
@Request() req: any, return this.usersService.removeLikedSong(+req.user.id, +songId);
@Param("id", ParseIntPipe) songId: number,
) {
return this.usersService.removeLikedSong(+req.user.id, songId);
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiBearerAuth() @ApiBearerAuth()
@ApiOkResponse({ description: "Successfully retrieved liked song" }) @ApiOkResponse({ description: 'Successfully retrieved liked song' })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
@Get("me/likes") @Get('me/likes')
getLikedSongs(@Request() req: any, @Query("include") include: string) { getLikedSongs(@Request() req: any) {
return this.usersService.getLikedSongs( return this.usersService.getLikedSongs(+req.user.id);
+req.user.id,
mapInclude(include, req, SongController.includableFields),
);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: "Successfully added score" })
@ApiUnauthorizedResponse({ description: "Invalid token" })
@Patch("me/score/:score")
addScore(@Request() req: any, @Param("score", ParseIntPipe) score: number) {
return this.usersService.addScore(+req.user.id, score);
} }
} }

View File

@@ -1,16 +1,15 @@
import { Module } from "@nestjs/common"; import { Module } from '@nestjs/common';
import { UsersModule } from "src/users/users.module"; import { UsersModule } from 'src/users/users.module';
import { AuthService } from "./auth.service"; import { AuthService } from './auth.service';
import { PassportModule } from "@nestjs/passport"; import { PassportModule } from '@nestjs/passport';
import { AuthController } from "./auth.controller"; import { AuthController } from './auth.controller';
import { LocalStrategy } from "./local.strategy"; import { LocalStrategy } from './local.strategy';
import { JwtModule } from "@nestjs/jwt"; import { JwtModule } from '@nestjs/jwt';
import { ConfigModule } from "@nestjs/config"; import { ConfigModule } from '@nestjs/config';
import { ConfigService } from "@nestjs/config"; import { ConfigService } from '@nestjs/config';
import { JwtStrategy } from "./jwt.strategy"; import { JwtStrategy } from './jwt.strategy';
import { SettingsModule } from "src/settings/settings.module"; import { SettingsModule } from 'src/settings/settings.module';
import { GoogleStrategy } from "./google.strategy"; import { GoogleStrategy } from './google.strategy';
import { HeaderApiKeyStrategy } from "./apikey.strategy";
@Module({ @Module({
imports: [ imports: [
@@ -21,19 +20,13 @@ import { HeaderApiKeyStrategy } from "./apikey.strategy";
JwtModule.registerAsync({ JwtModule.registerAsync({
imports: [ConfigModule], imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({ useFactory: async (configService: ConfigService) => ({
secret: configService.get("JWT_SECRET"), secret: configService.get('JWT_SECRET'),
signOptions: { expiresIn: "365d" }, signOptions: { expiresIn: '365d' },
}), }),
inject: [ConfigService], inject: [ConfigService],
}), }),
], ],
providers: [ providers: [AuthService, LocalStrategy, JwtStrategy, GoogleStrategy],
AuthService,
LocalStrategy,
JwtStrategy,
GoogleStrategy,
HeaderApiKeyStrategy,
],
controllers: [AuthController], controllers: [AuthController],
}) })
export class AuthModule {} export class AuthModule {}

View File

@@ -1,10 +1,10 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { UsersService } from "../users/users.service"; import { UsersService } from '../users/users.service';
import { JwtService } from "@nestjs/jwt"; import { JwtService } from '@nestjs/jwt';
import * as bcrypt from "bcryptjs"; import * as bcrypt from 'bcryptjs';
import PayloadInterface from "./interface/payload.interface"; import PayloadInterface from './interface/payload.interface';
import { User } from "src/models/user"; import { User } from 'src/models/user';
import { MailerService } from "@nestjs-modules/mailer"; import { MailerService } from '@nestjs-modules/mailer';
@Injectable() @Injectable()
export class AuthService { export class AuthService {
constructor( constructor(
@@ -13,12 +13,6 @@ export class AuthService {
private emailService: MailerService, private emailService: MailerService,
) {} ) {}
validateApiKey(apikey: string): boolean {
if (process.env.API_KEYS == null) return false;
const keys = process.env.API_KEYS.split(",");
return keys.includes(apikey);
}
async validateUser( async validateUser(
username: string, username: string,
password: string, password: string,
@@ -42,37 +36,37 @@ export class AuthService {
} }
async sendVerifyMail(user: User) { async sendVerifyMail(user: User) {
if (process.env.IGNORE_MAILS === "true") return; if (process.env.IGNORE_MAILS === 'true') return;
if (user.email == null) return; if (user.email == null) return;
console.log("Sending verification mail to", user.email); console.log('Sending verification mail to', user.email);
const token = await this.jwtService.signAsync( const token = await this.jwtService.signAsync(
{ {
userId: user.id, userId: user.id,
}, },
{ expiresIn: "10h" }, { expiresIn: '10h' },
); );
await this.emailService.sendMail({ await this.emailService.sendMail({
to: user.email, to: user.email,
from: "chromacase@octohub.app", from: 'chromacase@octohub.app',
subject: "Mail verification for Chromacase", subject: 'Mail verification for Chromacase',
html: `To verify your mail, please click on this <a href="${process.env.PUBLIC_URL}/verify?token=${token}">link</a>.`, html: `To verify your mail, please click on this <a href="${process.env.PUBLIC_URL}/verify?token=${token}">link</a>.`,
}); });
} }
async sendPasswordResetMail(user: User) { async sendPasswordResetMail(user: User) {
if (process.env.IGNORE_MAILS === "true") return; if (process.env.IGNORE_MAILS === 'true') return;
if (user.email == null) return; if (user.email == null) return;
console.log("Sending password reset mail to", user.email); console.log('Sending password reset mail to', user.email);
const token = await this.jwtService.signAsync( const token = await this.jwtService.signAsync(
{ {
userId: user.id, userId: user.id,
}, },
{ expiresIn: "10h" }, { expiresIn: '10h' },
); );
await this.emailService.sendMail({ await this.emailService.sendMail({
to: user.email, to: user.email,
from: "chromacase@octohub.app", from: 'chromacase@octohub.app',
subject: "Password reset for Chromacase", subject: 'Password reset for Chromacase',
html: `To reset your password, please click on this <a href="${process.env.PUBLIC_URL}/password_reset?token=${token}">link</a>.`, html: `To reset your password, please click on this <a href="${process.env.PUBLIC_URL}/password_reset?token=${token}">link</a>.`,
}); });
} }
@@ -82,7 +76,7 @@ export class AuthService {
try { try {
verified = await this.jwtService.verifyAsync(token); verified = await this.jwtService.verifyAsync(token);
} catch (e) { } catch (e) {
console.log("Password reset token failure", e); console.log('Password reset token failure', e);
return false; return false;
} }
console.log(verified); console.log(verified);
@@ -97,7 +91,7 @@ export class AuthService {
try { try {
await this.jwtService.verifyAsync(token); await this.jwtService.verifyAsync(token);
} catch (e) { } catch (e) {
console.log("Verify mail token failure", e); console.log('Verify mail token failure', e);
return false; return false;
} }
await this.userService.updateUser({ await this.userService.updateUser({

View File

@@ -1,22 +0,0 @@
import { ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { AuthGuard } from "@nestjs/passport";
import { IS_PUBLIC_KEY } from "./public";
@Injectable()
export class ChromaAuthGuard extends AuthGuard(["jwt", "api-key"]) {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
return super.canActivate(context);
}
}

View File

@@ -1,11 +1,11 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { ConfigService } from "@nestjs/config"; import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class Constants { export class Constants {
constructor(private configService: ConfigService) {} constructor(private configService: ConfigService) {}
getSecret = () => { getSecret = () => {
return this.configService.get("JWT_SECRET"); return this.configService.get('JWT_SECRET');
}; };
} }

View File

@@ -1,5 +1,5 @@
import { IsNotEmpty } from "class-validator"; import { IsNotEmpty } from 'class-validator';
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
export class LoginDto { export class LoginDto {
@ApiProperty() @ApiProperty()

View File

@@ -1,5 +1,5 @@
import { IsNotEmpty } from "class-validator"; import { IsNotEmpty } from 'class-validator';
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
export class PasswordResetDto { export class PasswordResetDto {
@ApiProperty() @ApiProperty()

View File

@@ -1,5 +1,5 @@
import { IsNotEmpty } from "class-validator"; import { IsNotEmpty } from 'class-validator';
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
export class Profile { export class Profile {
@ApiProperty() @ApiProperty()

View File

@@ -1,5 +1,5 @@
import { IsNotEmpty } from "class-validator"; import { IsNotEmpty } from 'class-validator';
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
export class RegisterDto { export class RegisterDto {
@ApiProperty() @ApiProperty()

View File

@@ -1,7 +1,7 @@
import { PassportStrategy } from "@nestjs/passport"; import { PassportStrategy } from '@nestjs/passport';
import { Strategy, VerifyCallback } from "passport-google-oauth20"; import { Strategy, VerifyCallback } from 'passport-google-oauth20';
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { User } from "@prisma/client"; import { User } from '@prisma/client';
@Injectable() @Injectable()
export class GoogleStrategy extends PassportStrategy(Strategy) { export class GoogleStrategy extends PassportStrategy(Strategy) {
@@ -10,7 +10,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
clientID: process.env.GOOGLE_CLIENT_ID, clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_SECRET, clientSecret: process.env.GOOGLE_SECRET,
callbackURL: process.env.GOOGLE_CALLBACK_URL, callbackURL: process.env.GOOGLE_CALLBACK_URL,
scope: ["email", "profile"], scope: ['email', 'profile'],
}); });
} }

View File

@@ -1,19 +1,21 @@
import { ExecutionContext, Injectable } from "@nestjs/common"; import { ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from "@nestjs/core"; import { Reflector } from '@nestjs/core';
import { AuthGuard } from "@nestjs/passport"; import { AuthGuard } from '@nestjs/passport';
import { IS_PUBLIC_KEY } from "./public"; import { IS_PUBLIC_KEY } from './public';
@Injectable() @Injectable()
export class JwtAuthGuard extends AuthGuard("jwt") { export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) { constructor(private reflector: Reflector) {
super(); super();
} }
canActivate(context: ExecutionContext) { canActivate(context: ExecutionContext) {
console.log(context);
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [ const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(), context.getHandler(),
context.getClass(), context.getClass(),
]); ]);
console.log(isPublic);
if (isPublic) { if (isPublic) {
return true; return true;
} }

View File

@@ -1,7 +1,7 @@
import { ExtractJwt, Strategy } from "passport-jwt"; import { ExtractJwt, Strategy } from 'passport-jwt';
import { PassportStrategy } from "@nestjs/passport"; import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { ConfigService } from "@nestjs/config"; import { ConfigService } from '@nestjs/config';
@Injectable() @Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) { export class JwtStrategy extends PassportStrategy(Strategy) {
@@ -9,7 +9,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
super({ super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false, ignoreExpiration: false,
secretOrKey: configService.get("JWT_SECRET"), secretOrKey: configService.get('JWT_SECRET'),
}); });
} }

View File

@@ -1,5 +1,5 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { AuthGuard } from "@nestjs/passport"; import { AuthGuard } from '@nestjs/passport';
@Injectable() @Injectable()
export class LocalAuthGuard extends AuthGuard("local") {} export class LocalAuthGuard extends AuthGuard('local') {}

View File

@@ -1,8 +1,8 @@
import { Strategy } from "passport-local"; import { Strategy } from 'passport-local';
import { PassportStrategy } from "@nestjs/passport"; import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from "@nestjs/common"; import { Injectable, UnauthorizedException } from '@nestjs/common';
import { AuthService } from "./auth.service"; import { AuthService } from './auth.service';
import PayloadInterface from "./interface/payload.interface"; import PayloadInterface from './interface/payload.interface';
@Injectable() @Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) { export class LocalStrategy extends PassportStrategy(Strategy) {

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
export class JwtToken { export class JwtToken {
@ApiProperty() @ApiProperty()

View File

@@ -1,4 +1,4 @@
import { SetMetadata } from "@nestjs/common"; import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = "isPublic"; export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from "class-validator"; import { IsNotEmpty } from 'class-validator';
export class CreateGenreDto { export class CreateGenreDto {
@IsNotEmpty() @IsNotEmpty()

View File

@@ -14,26 +14,25 @@ import {
Req, Req,
StreamableFile, StreamableFile,
UseGuards, UseGuards,
} from "@nestjs/common"; } from '@nestjs/common';
import { ApiOkResponsePlaginated, Plage } from "src/models/plage"; import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
import { CreateGenreDto } from "./dto/create-genre.dto"; import { CreateGenreDto } from './dto/create-genre.dto';
import { Request } from "express"; import { Request } from 'express';
import { GenreService } from "./genre.service"; import { GenreService } from './genre.service';
import { Prisma, Genre } from "@prisma/client"; import { Prisma, Genre } from '@prisma/client';
import { ApiTags } from "@nestjs/swagger"; import { ApiTags } from '@nestjs/swagger';
import { createReadStream, existsSync } from "fs"; import { createReadStream, existsSync } from 'fs';
import { FilterQuery } from "src/utils/filter.pipe"; import { FilterQuery } from 'src/utils/filter.pipe';
import { Genre as _Genre } from "src/_gen/prisma-class/genre"; import { Genre as _Genre } from 'src/_gen/prisma-class/genre';
import { IncludeMap, mapInclude } from "src/utils/include"; import { IncludeMap, mapInclude } from 'src/utils/include';
import { Public } from "src/auth/public"; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { AuthGuard } from "@nestjs/passport"; import { Public } from 'src/auth/public';
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
@Controller("genre") @Controller('genre')
@ApiTags("genre") @ApiTags('genre')
@UseGuards(ChromaAuthGuard) @UseGuards(JwtAuthGuard)
export class GenreController { export class GenreController {
static filterableFields: string[] = ["+id", "name"]; static filterableFields: string[] = ['+id', 'name'];
static includableFields: IncludeMap<Prisma.GenreInclude> = { static includableFields: IncludeMap<Prisma.GenreInclude> = {
Song: true, Song: true,
}; };
@@ -49,23 +48,23 @@ export class GenreController {
} }
} }
@Delete(":id") @Delete(':id')
async remove(@Param("id", ParseIntPipe) id: number) { async remove(@Param('id', ParseIntPipe) id: number) {
try { try {
return await this.service.delete({ id }); return await this.service.delete({ id });
} catch { } catch {
throw new NotFoundException("Invalid ID"); throw new NotFoundException('Invalid ID');
} }
} }
@Get(":id/illustration") @Get(':id/illustration')
@Public() @Public()
async getIllustration(@Param("id", ParseIntPipe) id: number) { async getIllustration(@Param('id', ParseIntPipe) id: number) {
const genre = await this.service.get({ id }); const genre = await this.service.get({ id });
if (!genre) throw new NotFoundException("Genre not found"); if (!genre) throw new NotFoundException('Genre not found');
const path = `/assets/genres/${genre.name}/illustration.png`; const path = `/assets/genres/${genre.name}/illustration.png`;
if (!existsSync(path)) if (!existsSync(path))
throw new NotFoundException("Illustration not found"); throw new NotFoundException('Illustration not found');
try { try {
const file = createReadStream(path); const file = createReadStream(path);
@@ -81,9 +80,9 @@ export class GenreController {
@Req() req: Request, @Req() req: Request,
@FilterQuery(GenreController.filterableFields) @FilterQuery(GenreController.filterableFields)
where: Prisma.GenreWhereInput, where: Prisma.GenreWhereInput,
@Query("include") include: string, @Query('include') include: string,
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number, @Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number, @Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Genre>> { ): Promise<Plage<Genre>> {
const ret = await this.service.list({ const ret = await this.service.list({
skip, skip,
@@ -94,18 +93,18 @@ export class GenreController {
return new Plage(ret, req); return new Plage(ret, req);
} }
@Get(":id") @Get(':id')
async findOne( async findOne(
@Req() req: Request, @Req() req: Request,
@Query("include") include: string, @Query('include') include: string,
@Param("id", ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
) { ) {
const res = await this.service.get( const res = await this.service.get(
{ id }, { id },
mapInclude(include, req, GenreController.includableFields), mapInclude(include, req, GenreController.includableFields),
); );
if (res === null) throw new NotFoundException("Genre not found"); if (res === null) throw new NotFoundException('Genre not found');
return res; return res;
} }
} }

View File

@@ -1,7 +1,7 @@
import { Module } from "@nestjs/common"; import { Module } from '@nestjs/common';
import { PrismaModule } from "src/prisma/prisma.module"; import { PrismaModule } from 'src/prisma/prisma.module';
import { GenreController } from "./genre.controller"; import { GenreController } from './genre.controller';
import { GenreService } from "./genre.service"; import { GenreService } from './genre.service';
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule],

View File

@@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { Prisma, Genre } from "@prisma/client"; import { Prisma, Genre } from '@prisma/client';
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from 'src/prisma/prisma.service';
@Injectable() @Injectable()
export class GenreService { export class GenreService {

View File

@@ -1,9 +1,9 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
export class SearchHistoryDto { export class SearchHistoryDto {
@ApiProperty() @ApiProperty()
query: string; query: string;
@ApiProperty() @ApiProperty()
type: "song" | "artist" | "album" | "genre"; type: 'song' | 'artist' | 'album' | 'genre';
} }

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from "class-validator"; import { IsNumber } from 'class-validator';
export class SongHistoryDto { export class SongHistoryDto {
@ApiProperty() @ApiProperty()

View File

@@ -9,75 +9,68 @@ import {
Query, Query,
Request, Request,
UseGuards, UseGuards,
} from "@nestjs/common"; } from '@nestjs/common';
import { import {
ApiCreatedResponse, ApiCreatedResponse,
ApiOkResponse, ApiOkResponse,
ApiOperation, ApiOperation,
ApiTags, ApiTags,
ApiUnauthorizedResponse, ApiUnauthorizedResponse,
} from "@nestjs/swagger"; } from '@nestjs/swagger';
import { SearchHistory, SongHistory } from "@prisma/client"; import { SearchHistory, SongHistory } from '@prisma/client';
import { JwtAuthGuard } from "src/auth/jwt-auth.guard"; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { SongHistoryDto } from "./dto/SongHistoryDto"; import { SongHistoryDto } from './dto/SongHistoryDto';
import { HistoryService } from "./history.service"; import { HistoryService } from './history.service';
import { SearchHistoryDto } from "./dto/SearchHistoryDto"; import { SearchHistoryDto } from './dto/SearchHistoryDto';
import { SongHistory as _SongHistory } from "src/_gen/prisma-class/song_history"; import { SongHistory as _SongHistory } from 'src/_gen/prisma-class/song_history';
import { SearchHistory as _SearchHistory } from "src/_gen/prisma-class/search_history"; import { SearchHistory as _SearchHistory } from 'src/_gen/prisma-class/search_history';
import { SongController } from "src/song/song.controller";
import { mapInclude } from "src/utils/include";
@Controller("history") @Controller('history')
@ApiTags("history") @ApiTags('history')
export class HistoryController { export class HistoryController {
constructor(private readonly historyService: HistoryService) {} constructor(private readonly historyService: HistoryService) {}
@Get() @Get()
@HttpCode(200) @HttpCode(200)
@ApiOperation({ description: "Get song history of connected user" }) @ApiOperation({ description: 'Get song history of connected user' })
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: _SongHistory, isArray: true }) @ApiOkResponse({ type: _SongHistory, isArray: true })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
async getHistory( async getHistory(
@Request() req: any, @Request() req: any,
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number, @Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number, @Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
@Query("include") include: string,
): Promise<SongHistory[]> { ): Promise<SongHistory[]> {
return this.historyService.getHistory( return this.historyService.getHistory(req.user.id, { skip, take });
req.user.id,
{ skip, take },
mapInclude(include, req, SongController.includableFields),
);
} }
@Get("search") @Get('search')
@HttpCode(200) @HttpCode(200)
@ApiOperation({ description: "Get search history of connected user" }) @ApiOperation({ description: 'Get search history of connected user' })
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: _SearchHistory, isArray: true }) @ApiOkResponse({ type: _SearchHistory, isArray: true })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
async getSearchHistory( async getSearchHistory(
@Request() req: any, @Request() req: any,
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number, @Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number, @Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<SearchHistory[]> { ): Promise<SearchHistory[]> {
return this.historyService.getSearchHistory(req.user.id, { skip, take }); return this.historyService.getSearchHistory(req.user.id, { skip, take });
} }
@Post() @Post()
@HttpCode(201) @HttpCode(201)
@ApiOperation({ description: "Create a record of a song played by a user" }) @ApiOperation({ description: 'Create a record of a song played by a user' })
@ApiCreatedResponse({ description: "Succesfully created a record" }) @ApiCreatedResponse({ description: 'Succesfully created a record' })
async create(@Body() record: SongHistoryDto): Promise<SongHistory> { async create(@Body() record: SongHistoryDto): Promise<SongHistory> {
return this.historyService.createSongHistoryRecord(record); return this.historyService.createSongHistoryRecord(record);
} }
@Post("search") @Post('search')
@HttpCode(201) @HttpCode(201)
@ApiOperation({ description: "Creates a search record in the users history" }) @ApiOperation({ description: 'Creates a search record in the users history' })
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
async createSearchHistory( async createSearchHistory(
@Request() req: any, @Request() req: any,
@Body() record: SearchHistoryDto, @Body() record: SearchHistoryDto,

View File

@@ -1,7 +1,7 @@
import { Module } from "@nestjs/common"; import { Module } from '@nestjs/common';
import { PrismaModule } from "src/prisma/prisma.module"; import { PrismaModule } from 'src/prisma/prisma.module';
import { HistoryService } from "./history.service"; import { HistoryService } from './history.service';
import { HistoryController } from "./history.controller"; import { HistoryController } from './history.controller';
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule],

View File

@@ -1,7 +1,7 @@
import { Test, TestingModule } from "@nestjs/testing"; import { Test, TestingModule } from '@nestjs/testing';
import { HistoryService } from "./history.service"; import { HistoryService } from './history.service';
describe("HistoryService", () => { describe('HistoryService', () => {
let service: HistoryService; let service: HistoryService;
beforeEach(async () => { beforeEach(async () => {
@@ -12,7 +12,7 @@ describe("HistoryService", () => {
service = module.get<HistoryService>(HistoryService); service = module.get<HistoryService>(HistoryService);
}); });
it("should be defined", () => { it('should be defined', () => {
expect(service).toBeDefined(); expect(service).toBeDefined();
}); });
}); });

View File

@@ -1,8 +1,8 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { Prisma, SearchHistory, SongHistory } from "@prisma/client"; import { SearchHistory, SongHistory } from '@prisma/client';
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from 'src/prisma/prisma.service';
import { SearchHistoryDto } from "./dto/SearchHistoryDto"; import { SearchHistoryDto } from './dto/SearchHistoryDto';
import { SongHistoryDto } from "./dto/SongHistoryDto"; import { SongHistoryDto } from './dto/SongHistoryDto';
@Injectable() @Injectable()
export class HistoryService { export class HistoryService {
@@ -45,14 +45,12 @@ export class HistoryService {
async getHistory( async getHistory(
playerId: number, playerId: number,
{ skip, take }: { skip?: number; take?: number }, { skip, take }: { skip?: number; take?: number },
include?: Prisma.SongInclude,
): Promise<SongHistory[]> { ): Promise<SongHistory[]> {
return this.prisma.songHistory.findMany({ return this.prisma.songHistory.findMany({
where: { user: { id: playerId } }, where: { user: { id: playerId } },
orderBy: { playDate: "desc" }, orderBy: { playDate: 'desc' },
skip, skip,
take, take,
include: { song: include ? { include } : true },
}); });
} }
@@ -65,7 +63,7 @@ export class HistoryService {
}): Promise<{ best: number; history: SongHistory[] }> { }): Promise<{ best: number; history: SongHistory[] }> {
const history = await this.prisma.songHistory.findMany({ const history = await this.prisma.songHistory.findMany({
where: { user: { id: playerId }, song: { id: songId } }, where: { user: { id: playerId }, song: { id: songId } },
orderBy: { playDate: "desc" }, orderBy: { playDate: 'desc' },
}); });
return { return {
@@ -97,7 +95,7 @@ export class HistoryService {
): Promise<SearchHistory[]> { ): Promise<SearchHistory[]> {
return this.prisma.searchHistory.findMany({ return this.prisma.searchHistory.findMany({
where: { user: { id: playerId } }, where: { user: { id: playerId } },
orderBy: { searchDate: "desc" }, orderBy: { searchDate: 'desc' },
skip, skip,
take, take,
}); });

View File

@@ -12,17 +12,16 @@ import {
Delete, Delete,
NotFoundException, NotFoundException,
UseGuards, UseGuards,
} from "@nestjs/common"; } from '@nestjs/common';
import { ApiOkResponsePlaginated, Plage } from "src/models/plage"; import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
import { LessonService } from "./lesson.service"; import { LessonService } from './lesson.service';
import { ApiOperation, ApiProperty, ApiTags } from "@nestjs/swagger"; import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
import { Prisma, Skill } from "@prisma/client"; import { Prisma, Skill } from '@prisma/client';
import { FilterQuery } from "src/utils/filter.pipe"; import { FilterQuery } from 'src/utils/filter.pipe';
import { Lesson as _Lesson } from "src/_gen/prisma-class/lesson"; import { Lesson as _Lesson } from 'src/_gen/prisma-class/lesson';
import { IncludeMap, mapInclude } from "src/utils/include"; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { Request } from "express"; import { IncludeMap, mapInclude } from 'src/utils/include';
import { AuthGuard } from "@nestjs/passport"; import { Request } from 'express';
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
export class Lesson { export class Lesson {
@ApiProperty() @ApiProperty()
@@ -37,15 +36,15 @@ export class Lesson {
mainSkill: Skill; mainSkill: Skill;
} }
@ApiTags("lessons") @ApiTags('lessons')
@Controller("lesson") @Controller('lesson')
@UseGuards(ChromaAuthGuard) @UseGuards(JwtAuthGuard)
export class LessonController { export class LessonController {
static filterableFields: string[] = [ static filterableFields: string[] = [
"+id", '+id',
"name", 'name',
"+requiredLevel", '+requiredLevel',
"mainSkill", 'mainSkill',
]; ];
static includableFields: IncludeMap<Prisma.LessonInclude> = { static includableFields: IncludeMap<Prisma.LessonInclude> = {
LessonHistory: true, LessonHistory: true,
@@ -54,7 +53,7 @@ export class LessonController {
constructor(private lessonService: LessonService) {} constructor(private lessonService: LessonService) {}
@ApiOperation({ @ApiOperation({
summary: "Get all lessons", summary: 'Get all lessons',
}) })
@Get() @Get()
@ApiOkResponsePlaginated(_Lesson) @ApiOkResponsePlaginated(_Lesson)
@@ -62,9 +61,9 @@ export class LessonController {
@Req() request: Request, @Req() request: Request,
@FilterQuery(LessonController.filterableFields) @FilterQuery(LessonController.filterableFields)
where: Prisma.LessonWhereInput, where: Prisma.LessonWhereInput,
@Query("include") include: string, @Query('include') include: string,
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number, @Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number, @Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Lesson>> { ): Promise<Plage<Lesson>> {
const ret = await this.lessonService.getAll({ const ret = await this.lessonService.getAll({
skip, skip,
@@ -76,13 +75,13 @@ export class LessonController {
} }
@ApiOperation({ @ApiOperation({
summary: "Get a particular lessons", summary: 'Get a particular lessons',
}) })
@Get(":id") @Get(':id')
async get( async get(
@Req() req: Request, @Req() req: Request,
@Query("include") include: string, @Query('include') include: string,
@Param("id", ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
): Promise<Lesson> { ): Promise<Lesson> {
const ret = await this.lessonService.get( const ret = await this.lessonService.get(
id, id,
@@ -93,7 +92,7 @@ export class LessonController {
} }
@ApiOperation({ @ApiOperation({
summary: "Create a lessons", summary: 'Create a lessons',
}) })
@Post() @Post()
async post(@Body() lesson: Lesson): Promise<Lesson> { async post(@Body() lesson: Lesson): Promise<Lesson> {
@@ -106,10 +105,10 @@ export class LessonController {
} }
@ApiOperation({ @ApiOperation({
summary: "Delete a lessons", summary: 'Delete a lessons',
}) })
@Delete(":id") @Delete(':id')
async delete(@Param("id", ParseIntPipe) id: number): Promise<Lesson> { async delete(@Param('id', ParseIntPipe) id: number): Promise<Lesson> {
try { try {
return await this.lessonService.delete(id); return await this.lessonService.delete(id);
} catch { } catch {

View File

@@ -1,7 +1,7 @@
import { Module } from "@nestjs/common"; import { Module } from '@nestjs/common';
import { PrismaModule } from "src/prisma/prisma.module"; import { PrismaModule } from 'src/prisma/prisma.module';
import { LessonController } from "./lesson.controller"; import { LessonController } from './lesson.controller';
import { LessonService } from "./lesson.service"; import { LessonService } from './lesson.service';
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule],

View File

@@ -1,7 +1,7 @@
import { Test, TestingModule } from "@nestjs/testing"; import { Test, TestingModule } from '@nestjs/testing';
import { LessonService } from "./lesson.service"; import { LessonService } from './lesson.service';
describe("LessonService", () => { describe('LessonService', () => {
let service: LessonService; let service: LessonService;
beforeEach(async () => { beforeEach(async () => {
@@ -12,7 +12,7 @@ describe("LessonService", () => {
service = module.get<LessonService>(LessonService); service = module.get<LessonService>(LessonService);
}); });
it("should be defined", () => { it('should be defined', () => {
expect(service).toBeDefined(); expect(service).toBeDefined();
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { Lesson, Prisma } from "@prisma/client"; import { Lesson, Prisma } from '@prisma/client';
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from 'src/prisma/prisma.service';
@Injectable() @Injectable()
export class LessonService { export class LessonService {

View File

@@ -1,17 +1,17 @@
import { NestFactory } from "@nestjs/core"; import { NestFactory } from '@nestjs/core';
import { AppModule } from "./app.module"; import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger"; import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { import {
CallHandler, CallHandler,
ExecutionContext, ExecutionContext,
Injectable, Injectable,
NestInterceptor, NestInterceptor,
ValidationPipe, ValidationPipe,
} from "@nestjs/common"; } from '@nestjs/common';
import { RequestLogger, RequestLoggerOptions } from "json-logger-service"; import { RequestLogger, RequestLoggerOptions } from 'json-logger-service';
import { tap } from "rxjs"; import { tap } from 'rxjs';
import { PrismaModel } from "./_gen/prisma-class"; import { PrismaModel } from './_gen/prisma-class';
import { PrismaService } from "./prisma/prisma.service"; import { PrismaService } from './prisma/prisma.service';
@Injectable() @Injectable()
export class AspectLogger implements NestInterceptor { export class AspectLogger implements NestInterceptor {
@@ -27,8 +27,8 @@ export class AspectLogger implements NestInterceptor {
params, params,
query, query,
body, body,
userId: user?.id ?? "not logged in", userId: user?.id ?? 'not logged in',
username: user?.username ?? "not logged in", username: user?.username ?? 'not logged in',
}; };
return next.handle().pipe( return next.handle().pipe(
@@ -48,24 +48,24 @@ async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create(AppModule);
app.use( app.use(
RequestLogger.buildExpressRequestLogger({ RequestLogger.buildExpressRequestLogger({
doNotLogPaths: ["/health"], doNotLogPaths: ['/health'],
} as RequestLoggerOptions), } as RequestLoggerOptions),
); );
app.enableShutdownHooks(); app.enableShutdownHooks();
const config = new DocumentBuilder() const config = new DocumentBuilder()
.setTitle("Chromacase") .setTitle('Chromacase')
.setDescription("The chromacase API") .setDescription('The chromacase API')
.setVersion("1.0") .setVersion('1.0')
.build(); .build();
const document = SwaggerModule.createDocument(app, config, { const document = SwaggerModule.createDocument(app, config, {
extraModels: [...PrismaModel.extraModels], extraModels: [...PrismaModel.extraModels],
}); });
SwaggerModule.setup("api", app, document); SwaggerModule.setup('api', app, document);
app.useGlobalPipes(new ValidationPipe()); app.useGlobalPipes(new ValidationPipe());
app.enableCors(); app.enableCors();
//app.useGlobalInterceptors(new AspectLogger()); app.useGlobalInterceptors(new AspectLogger());
await app.listen(3000); await app.listen(3000);
} }

View File

@@ -2,25 +2,25 @@
* Thanks to https://github.com/Arthi-chaud/Meelo/blob/master/src/pagination/models/paginated-response.ts * Thanks to https://github.com/Arthi-chaud/Meelo/blob/master/src/pagination/models/paginated-response.ts
*/ */
import { Type, applyDecorators } from "@nestjs/common"; import { Type, applyDecorators } from '@nestjs/common';
import { import {
ApiExtraModels, ApiExtraModels,
ApiOkResponse, ApiOkResponse,
ApiProperty, ApiProperty,
getSchemaPath, getSchemaPath,
} from "@nestjs/swagger"; } from '@nestjs/swagger';
export class PlageMetadata { export class PlageMetadata {
@ApiProperty() @ApiProperty()
this: string; this: string;
@ApiProperty({ @ApiProperty({
type: "string", type: 'string',
nullable: true, nullable: true,
description: "null if there is no next page, couldn't set it in swagger", description: "null if there is no next page, couldn't set it in swagger",
}) })
next: string | null; next: string | null;
@ApiProperty({ @ApiProperty({
type: "string", type: 'string',
nullable: true, nullable: true,
description: description:
"null if there is no previous page, couldn't set it in swagger", "null if there is no previous page, couldn't set it in swagger",
@@ -35,9 +35,9 @@ export class Plage<T extends object> {
constructor(data: T[], request: Request | any) { constructor(data: T[], request: Request | any) {
this.data = data; this.data = data;
let take = Number(request.query["take"] ?? 20).valueOf(); let take = Number(request.query['take'] ?? 20).valueOf();
if (take == 0) take = 20; if (take == 0) take = 20;
let skipped: number = Number(request.query["skip"] ?? 0).valueOf(); let skipped: number = Number(request.query['skip'] ?? 0).valueOf();
if (skipped % take) { if (skipped % take) {
skipped += take - (skipped % take); skipped += take - (skipped % take);
} }
@@ -81,7 +81,7 @@ export const ApiOkResponsePlaginated = <DataDto extends Type<unknown>>(
{ {
properties: { properties: {
data: { data: {
type: "array", type: 'array',
items: { $ref: getSchemaPath(dataDto) }, items: { $ref: getSchemaPath(dataDto) },
}, },
}, },

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
export class Setting { export class Setting {
@ApiProperty() @ApiProperty()

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
export class User { export class User {
@ApiProperty() @ApiProperty()
@@ -11,6 +11,4 @@ export class User {
isGuest: boolean; isGuest: boolean;
@ApiProperty() @ApiProperty()
partyPlayed: number; partyPlayed: number;
@ApiProperty()
totalScore: number;
} }

View File

@@ -1,5 +1,5 @@
import { Module } from "@nestjs/common"; import { Module } from '@nestjs/common';
import { PrismaService } from "./prisma.service"; import { PrismaService } from './prisma.service';
@Module({ @Module({
providers: [PrismaService], providers: [PrismaService],

View File

@@ -1,7 +1,7 @@
import { Test, TestingModule } from "@nestjs/testing"; import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from "./prisma.service"; import { PrismaService } from './prisma.service';
describe("PrismaService", () => { describe('PrismaService', () => {
let service: PrismaService; let service: PrismaService;
beforeEach(async () => { beforeEach(async () => {
@@ -12,7 +12,7 @@ describe("PrismaService", () => {
service = module.get<PrismaService>(PrismaService); service = module.get<PrismaService>(PrismaService);
}); });
it("should be defined", () => { it('should be defined', () => {
expect(service).toBeDefined(); expect(service).toBeDefined();
}); });
}); });

View File

@@ -1,5 +1,5 @@
import { Injectable, OnModuleInit } from "@nestjs/common"; import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from '@prisma/client';
@Injectable() @Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit { export class PrismaService extends PrismaClient implements OnModuleInit {

View File

@@ -1,22 +0,0 @@
import { Controller, Get, Put } from "@nestjs/common";
import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
import { ScoresService } from "./scores.service";
import { User } from "@prisma/client";
@ApiTags("scores")
@Controller("scores")
export class ScoresController {
constructor(private readonly scoresService: ScoresService) {}
@ApiOkResponse({ description: "Successfully sent the Top 20 players" })
@Get("top/20")
getTopTwenty(): Promise<User[]> {
return this.scoresService.topTwenty();
}
// @ApiOkResponse{{description: "Successfully updated the user's total score"}}
// @Put("/add")
// addScore(): Promise<void> {
// return this.ScoresService.add()
// }
}

View File

@@ -1,11 +0,0 @@
import { Module } from "@nestjs/common";
import { ScoresService } from "./scores.service";
import { ScoresController } from "./scores.controller";
import { PrismaModule } from "src/prisma/prisma.module";
@Module({
imports: [PrismaModule],
controllers: [ScoresController],
providers: [ScoresService],
})
export class ScoresModule {}

View File

@@ -1,17 +0,0 @@
import { Injectable } from "@nestjs/common";
import { User } from "@prisma/client";
import { PrismaService } from "src/prisma/prisma.service";
@Injectable()
export class ScoresService {
constructor(private prisma: PrismaService) {}
async topTwenty(): Promise<User[]> {
return this.prisma.user.findMany({
orderBy: {
totalScore: "desc",
},
take: 20,
});
}
}

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
export class SearchSongDto { export class SearchSongDto {
@ApiProperty() @ApiProperty()

View File

@@ -1,29 +0,0 @@
import { Injectable, OnModuleInit } from "@nestjs/common";
import MeiliSearch, { DocumentOptions, Settings } from "meilisearch";
@Injectable()
export class MeiliService extends MeiliSearch implements OnModuleInit {
constructor() {
super({
host: process.env.MEILI_ADDR || "http://meilisearch:7700",
apiKey: process.env.MEILI_MASTER_KEY,
});
}
async definedIndex(uid: string, opts: Settings) {
let task = await this.createIndex(uid, { primaryKey: "id" });
await this.waitForTask(task.taskUid);
task = await this.index(uid).updateSettings(opts);
await this.waitForTask(task.taskUid);
}
async onModuleInit() {
await this.definedIndex("songs", {
searchableAttributes: ["name", "artist"],
filterableAttributes: ["artistId", "genreId"],
});
await this.definedIndex("artists", {
searchableAttributes: ["name"],
});
}
}

View File

@@ -1,73 +1,101 @@
import { import {
Controller, Controller,
DefaultValuePipe,
Get, Get,
ParseIntPipe, InternalServerErrorException,
NotFoundException,
Param,
Query, Query,
Request, Request,
UseGuards, UseGuards,
} from "@nestjs/common"; } from '@nestjs/common';
import { import {
ApiOkResponse, ApiOkResponse,
ApiOperation, ApiOperation,
ApiTags, ApiTags,
ApiUnauthorizedResponse, ApiUnauthorizedResponse,
} from "@nestjs/swagger"; } from '@nestjs/swagger';
import { Artist, Song } from "@prisma/client"; import { Artist, Genre, Song } from '@prisma/client';
import { JwtAuthGuard } from "src/auth/jwt-auth.guard"; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { SearchService } from "./search.service"; import { SearchService } from './search.service';
import { Song as _Song } from "src/_gen/prisma-class/song"; import { Song as _Song } from 'src/_gen/prisma-class/song';
import { Artist as _Artist } from "src/_gen/prisma-class/artist"; import { Genre as _Genre } from 'src/_gen/prisma-class/genre';
import { mapInclude } from "src/utils/include"; import { Artist as _Artist } from 'src/_gen/prisma-class/artist';
import { SongController } from "src/song/song.controller"; import { mapInclude } from 'src/utils/include';
import { ArtistController } from "src/artist/artist.controller"; import { SongController } from 'src/song/song.controller';
import { GenreController } from 'src/genre/genre.controller';
import { ArtistController } from 'src/artist/artist.controller';
@ApiTags("search") @ApiTags('search')
@Controller("search") @Controller('search')
@UseGuards(JwtAuthGuard)
export class SearchController { export class SearchController {
constructor(private readonly searchService: SearchService) {} constructor(private readonly searchService: SearchService) {}
@Get("songs") @Get('songs/:query')
@ApiOkResponse({ type: _Song, isArray: true }) @ApiOkResponse({ type: _Song, isArray: true })
@ApiOperation({ description: "Search a song" }) @ApiOperation({ description: 'Search a song' })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
@UseGuards(JwtAuthGuard)
async searchSong( async searchSong(
@Request() req: any, @Request() req: any,
@Query("q") query: string | null, @Query('include') include: string,
@Query("artistId", new ParseIntPipe({ optional: true })) artistId: number, @Param('query') query: string,
@Query("genreId", new ParseIntPipe({ optional: true })) genreId: number, ): Promise<Song[] | null> {
@Query("include") include: string, try {
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number, const ret = await this.searchService.songByGuess(
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number, query,
): Promise<Song[]> { req.user?.id,
return await this.searchService.searchSong( mapInclude(include, req, SongController.includableFields),
query ?? "", );
artistId, if (!ret.length) throw new NotFoundException();
genreId, else return ret;
mapInclude(include, req, SongController.includableFields), } catch (error) {
skip, throw new InternalServerErrorException();
take, }
);
} }
@Get("artists") @Get('genres/:query')
@UseGuards(JwtAuthGuard)
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@ApiOkResponse({ type: _Genre, isArray: true })
@ApiOperation({ description: 'Search a genre' })
async searchGenre(
@Request() req: any,
@Query('include') include: string,
@Param('query') query: string,
): Promise<Genre[] | null> {
try {
const ret = await this.searchService.genreByGuess(
query,
req.user?.id,
mapInclude(include, req, GenreController.includableFields),
);
if (!ret.length) throw new NotFoundException();
else return ret;
} catch (error) {
throw new InternalServerErrorException();
}
}
@Get('artists/:query')
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: _Artist, isArray: true }) @ApiOkResponse({ type: _Artist, isArray: true })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
@ApiOperation({ description: "Search an artist" }) @ApiOperation({ description: 'Search an artist' })
async searchArtists( async searchArtists(
@Request() req: any, @Request() req: any,
@Query("include") include: string, @Query('include') include: string,
@Query("q") query: string | null, @Param('query') query: string,
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number, ): Promise<Artist[] | null> {
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number, try {
): Promise<Artist[]> { const ret = await this.searchService.artistByGuess(
return await this.searchService.searchArtists( query,
query ?? "", req.user?.id,
mapInclude(include, req, ArtistController.includableFields), mapInclude(include, req, ArtistController.includableFields),
skip, );
take, if (!ret.length) throw new NotFoundException();
); else return ret;
} catch (error) {
throw new InternalServerErrorException();
}
} }
} }

View File

@@ -1,15 +1,14 @@
import { Module } from "@nestjs/common"; import { Module } from '@nestjs/common';
import { SearchService } from "./search.service"; import { SearchService } from './search.service';
import { SearchController } from "./search.controller"; import { SearchController } from './search.controller';
import { HistoryModule } from "src/history/history.module"; import { HistoryModule } from 'src/history/history.module';
import { PrismaModule } from "src/prisma/prisma.module"; import { PrismaModule } from 'src/prisma/prisma.module';
import { SongService } from "src/song/song.service"; import { SongService } from 'src/song/song.service';
import { MeiliService } from "./meilisearch.service";
@Module({ @Module({
imports: [PrismaModule, HistoryModule], imports: [PrismaModule, HistoryModule],
controllers: [SearchController], controllers: [SearchController],
providers: [SearchService, SongService, MeiliService], providers: [SearchService, SongService],
exports: [SearchService, MeiliService], exports: [SearchService],
}) })
export class SearchModule {} export class SearchModule {}

View File

@@ -1,84 +1,51 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { Artist, Prisma, Song, Genre } from "@prisma/client"; import { Artist, Prisma, Song, Genre } from '@prisma/client';
import { HistoryService } from "src/history/history.service"; import { HistoryService } from 'src/history/history.service';
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from 'src/prisma/prisma.service';
import { MeiliService } from "./meilisearch.service";
@Injectable() @Injectable()
export class SearchService { export class SearchService {
constructor( constructor(
private prisma: PrismaService, private prisma: PrismaService,
private history: HistoryService, private history: HistoryService,
private search: MeiliService,
) {} ) {}
async searchSong( async songByGuess(
query: string, query: string,
artistId?: number, userID: number,
genreId?: number,
include?: Prisma.SongInclude, include?: Prisma.SongInclude,
skip?: number,
take?: number,
): Promise<Song[]> { ): Promise<Song[]> {
if (query.length === 0) { return this.prisma.song.findMany({
return await this.prisma.song.findMany({ where: {
where: { name: { contains: query, mode: 'insensitive' },
artistId, },
genreId, include,
}, });
take,
skip,
include,
});
}
const ids = (
await this.search.index("songs").search(query, {
limit: take,
offset: skip,
filter: [
...(artistId ? [`artistId = ${artistId}`] : []),
...(genreId ? [`genreId = ${genreId}`] : []),
].join(" AND "),
})
).hits.map((x) => x.id);
return (
await this.prisma.song.findMany({
where: {
id: { in: ids },
},
include,
})
).sort((x) => ids.indexOf(x.id));
} }
async searchArtists( async genreByGuess(
query: string, query: string,
include?: Prisma.ArtistInclude, userID: number,
skip?: number, include?: Prisma.GenreInclude,
take?: number, ): Promise<Genre[]> {
): Promise<Artist[]> { return this.prisma.genre.findMany({
if (query.length === 0) { where: {
return this.prisma.artist.findMany({ name: { contains: query, mode: 'insensitive' },
take, },
skip, include,
include, });
}); }
}
const ids = (
await this.search.index("artists").search(query, {
limit: take,
offset: skip,
})
).hits.map((x) => x.id);
return ( async artistByGuess(
await this.prisma.artist.findMany({ query: string,
where: { userID: number,
id: { in: ids }, include?: Prisma.ArtistInclude,
}, ): Promise<Artist[]> {
include, return this.prisma.artist.findMany({
}) where: {
).sort((x) => ids.indexOf(x.id)); name: { contains: query, mode: 'insensitive' },
},
include,
});
} }
} }

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
export class UpdateSettingDto { export class UpdateSettingDto {
@ApiProperty() @ApiProperty()

View File

@@ -1,6 +1,6 @@
import { Module } from "@nestjs/common"; import { Module } from '@nestjs/common';
import { SettingsService } from "./settings.service"; import { SettingsService } from './settings.service';
import { PrismaModule } from "src/prisma/prisma.module"; import { PrismaModule } from 'src/prisma/prisma.module';
@Module({ @Module({
imports: [PrismaModule], imports: [PrismaModule],

View File

@@ -1,6 +1,6 @@
import { Injectable } from "@nestjs/common"; import { Injectable } from '@nestjs/common';
import { Prisma, UserSettings } from "@prisma/client"; import { Prisma, UserSettings } from '@prisma/client';
import { PrismaService } from "src/prisma/prisma.service"; import { PrismaService } from 'src/prisma/prisma.service';
@Injectable() @Injectable()
export class SettingsService { export class SettingsService {

View File

@@ -1,5 +1,5 @@
import { ApiProperty } from "@nestjs/swagger"; import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from "class-validator"; import { IsNotEmpty } from 'class-validator';
export class CreateSongDto { export class CreateSongDto {
@IsNotEmpty() @IsNotEmpty()

View File

@@ -15,14 +15,13 @@ import {
Req, Req,
StreamableFile, StreamableFile,
UseGuards, UseGuards,
Header, } from '@nestjs/common';
} from "@nestjs/common"; import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
import { ApiOkResponsePlaginated, Plage } from "src/models/plage"; import { CreateSongDto } from './dto/create-song.dto';
import { CreateSongDto } from "./dto/create-song.dto"; import { SongService } from './song.service';
import { SongService } from "./song.service"; import { Request } from 'express';
import { Request } from "express"; import { Prisma, Song } from '@prisma/client';
import { Prisma, Song } from "@prisma/client"; import { createReadStream, existsSync } from 'fs';
import { createReadStream, existsSync, readFileSync } from "fs";
import { import {
ApiNotFoundResponse, ApiNotFoundResponse,
ApiOkResponse, ApiOkResponse,
@@ -30,15 +29,15 @@ import {
ApiProperty, ApiProperty,
ApiTags, ApiTags,
ApiUnauthorizedResponse, ApiUnauthorizedResponse,
} from "@nestjs/swagger"; } from '@nestjs/swagger';
import { HistoryService } from "src/history/history.service"; import { HistoryService } from 'src/history/history.service';
import { JwtAuthGuard } from "src/auth/jwt-auth.guard"; import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
import { FilterQuery } from "src/utils/filter.pipe"; import { FilterQuery } from 'src/utils/filter.pipe';
import { Song as _Song } from "src/_gen/prisma-class/song"; import { Song as _Song } from 'src/_gen/prisma-class/song';
import { SongHistory } from "src/_gen/prisma-class/song_history"; import { SongHistory } from 'src/_gen/prisma-class/song_history';
import { IncludeMap, mapInclude } from "src/utils/include"; import { IncludeMap, mapInclude } from 'src/utils/include';
import { Public } from "src/auth/public"; import { Public } from 'src/auth/public';
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
class SongHistoryResult { class SongHistoryResult {
@ApiProperty() @ApiProperty()
best: number; best: number;
@@ -46,16 +45,16 @@ class SongHistoryResult {
history: SongHistory[]; history: SongHistory[];
} }
@Controller("song") @Controller('song')
@ApiTags("song") @ApiTags('song')
@UseGuards(ChromaAuthGuard) @UseGuards(JwtAuthGuard)
export class SongController { export class SongController {
static filterableFields: string[] = [ static filterableFields: string[] = [
"+id", '+id',
"name", 'name',
"+artistId", '+artistId',
"+albumId", '+albumId',
"+genreId", '+genreId',
]; ];
static includableFields: IncludeMap<Prisma.SongInclude> = { static includableFields: IncludeMap<Prisma.SongInclude> = {
artist: true, artist: true,
@@ -70,37 +69,36 @@ export class SongController {
private readonly historyService: HistoryService, private readonly historyService: HistoryService,
) {} ) {}
@Get(":id/midi") @Get(':id/midi')
@ApiOperation({ description: "Streams the midi file of the requested song" }) @ApiOperation({ description: 'Streams the midi file of the requested song' })
@ApiNotFoundResponse({ description: "Song not found" }) @ApiNotFoundResponse({ description: 'Song not found' })
@ApiOkResponse({ description: "Returns the midi file succesfully" }) @ApiOkResponse({ description: 'Returns the midi file succesfully' })
async getMidi(@Param("id", ParseIntPipe) id: number) { async getMidi(@Param('id', ParseIntPipe) id: number) {
const song = await this.songService.song({ id }); const song = await this.songService.song({ id });
if (!song) throw new NotFoundException("Song not found"); if (!song) throw new NotFoundException('Song not found');
try { try {
const file = createReadStream(song.midiPath); const file = createReadStream(song.midiPath);
return new StreamableFile(file, { type: "audio/midi" }); return new StreamableFile(file, { type: 'audio/midi' });
} catch { } catch {
throw new InternalServerErrorException(); throw new InternalServerErrorException();
} }
} }
@Get(":id/illustration") @Get(':id/illustration')
@ApiOperation({ @ApiOperation({
description: "Streams the illustration of the requested song", description: 'Streams the illustration of the requested song',
}) })
@ApiNotFoundResponse({ description: "Song not found" }) @ApiNotFoundResponse({ description: 'Song not found' })
@ApiOkResponse({ description: "Returns the illustration succesfully" }) @ApiOkResponse({ description: 'Returns the illustration succesfully' })
@Header("Cache-Control", "max-age=86400")
@Public() @Public()
async getIllustration(@Param("id", ParseIntPipe) id: number) { async getIllustration(@Param('id', ParseIntPipe) id: number) {
const song = await this.songService.song({ id }); const song = await this.songService.song({ id });
if (!song) throw new NotFoundException("Song not found"); if (!song) throw new NotFoundException('Song not found');
if (song.illustrationPath === null) throw new NotFoundException(); if (song.illustrationPath === null) throw new NotFoundException();
if (!existsSync(song.illustrationPath)) if (!existsSync(song.illustrationPath))
throw new NotFoundException("Illustration not found"); throw new NotFoundException('Illustration not found');
try { try {
const file = createReadStream(song.illustrationPath); const file = createReadStream(song.illustrationPath);
@@ -110,102 +108,24 @@ export class SongController {
} }
} }
@Get(":id/musicXml") @Get(':id/musicXml')
@ApiOperation({ @ApiOperation({
description: "Streams the musicXML file of the requested song", description: 'Streams the musicXML file of the requested song',
}) })
@ApiNotFoundResponse({ description: "Song not found" }) @ApiNotFoundResponse({ description: 'Song not found' })
@ApiOkResponse({ description: "Returns the musicXML file succesfully" }) @ApiOkResponse({ description: 'Returns the musicXML file succesfully' })
async getMusicXml(@Param("id", ParseIntPipe) id: number) { async getMusicXml(@Param('id', ParseIntPipe) id: number) {
const song = await this.songService.song({ id }); const song = await this.songService.song({ id });
if (!song) throw new NotFoundException("Song not found"); if (!song) throw new NotFoundException('Song not found');
const file = createReadStream(song.musicXmlPath, { encoding: "binary" }); const file = createReadStream(song.musicXmlPath, { encoding: 'binary' });
return new StreamableFile(file); return new StreamableFile(file);
} }
@Get(":id/assets/partition")
@ApiOperation({
description: "Streams the svg partition of the requested song",
})
@ApiNotFoundResponse({ description: "Song not found" })
@ApiOkResponse({ description: "Returns the svg partition succesfully" })
@Header("Cache-Control", "max-age=86400")
@Header("Content-Type", "image/svg+xml")
@Public()
async getPartition(@Param("id", ParseIntPipe) id: number) {
const song = await this.songService.song({ id });
if (!song) throw new NotFoundException("Song not found");
// check if /data/cache/songs/id exists
if (!existsSync("/data/cache/songs/" + id + ".svg")) {
// if not, generate assets
await this.songService.createAssets(song.musicXmlPath, id);
}
try {
const file = readFileSync("/data/cache/songs/" + id + ".svg");
return file.toString();
} catch {
throw new InternalServerErrorException();
}
}
@Get(":id/assets/cursors")
@ApiOperation({
description: "Streams the partition cursors of the requested song",
})
@ApiNotFoundResponse({ description: "Song not found" })
@ApiOkResponse({ description: "Returns the partition cursors succesfully" })
@Header("Cache-Control", "max-age=86400")
@Header("Content-Type", "application/json")
async getCursors(@Param("id", ParseIntPipe) id: number) {
const song = await this.songService.song({ id });
if (!song) throw new NotFoundException("Song not found");
// check if /data/cache/songs/id exists
if (!existsSync("/data/cache/songs/" + id + ".json")) {
// if not, generate assets
await this.songService.createAssets(song.musicXmlPath, id);
}
try {
const file = readFileSync("/data/cache/songs/" + id + ".json");
return JSON.parse(file.toString());
} catch {
throw new InternalServerErrorException();
}
}
@Get(":id/assets/melody")
@ApiOperation({
description: "Streams the mp3 file of the requested song",
})
@ApiNotFoundResponse({ description: "Song not found" })
@ApiOkResponse({ description: "Returns the mp3 file succesfully" })
@Header("Cache-Control", "max-age=86400")
@Header("Content-Type", "audio/mpeg")
@Public()
async getMelody(@Param("id", ParseIntPipe) id: number) {
const song = await this.songService.song({ id });
if (!song) throw new NotFoundException("Song not found");
const path = song.musicXmlPath;
// mp3 file is next to the musicXML file and called melody.mp3
const pathWithoutFile = path.substring(0, path.lastIndexOf("/"));
try {
const file = createReadStream(pathWithoutFile + "/melody.mp3");
return new StreamableFile(file, { type: "audio/mpeg" });
} catch {
throw new InternalServerErrorException();
}
}
@Post() @Post()
@ApiOperation({ @ApiOperation({
description: description:
"register a new song in the database, should not be used by the frontend", 'register a new song in the database, should not be used by the frontend',
}) })
async create(@Body() createSongDto: CreateSongDto) { async create(@Body() createSongDto: CreateSongDto) {
try { try {
@@ -228,13 +148,13 @@ export class SongController {
} }
} }
@Delete(":id") @Delete(':id')
@ApiOperation({ description: "delete a song by id" }) @ApiOperation({ description: 'delete a song by id' })
async remove(@Param("id", ParseIntPipe) id: number) { async remove(@Param('id', ParseIntPipe) id: number) {
try { try {
return await this.songService.deleteSong({ id }); return await this.songService.deleteSong({ id });
} catch { } catch {
throw new NotFoundException("Invalid ID"); throw new NotFoundException('Invalid ID');
} }
} }
@@ -243,9 +163,9 @@ export class SongController {
async findAll( async findAll(
@Req() req: Request, @Req() req: Request,
@FilterQuery(SongController.filterableFields) where: Prisma.SongWhereInput, @FilterQuery(SongController.filterableFields) where: Prisma.SongWhereInput,
@Query("include") include: string, @Query('include') include: string,
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number, @Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number, @Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Plage<Song>> { ): Promise<Plage<Song>> {
const ret = await this.songService.songs({ const ret = await this.songService.songs({
skip, skip,
@@ -256,14 +176,14 @@ export class SongController {
return new Plage(ret, req); return new Plage(ret, req);
} }
@Get(":id") @Get(':id')
@ApiOperation({ description: "Get a specific song data" }) @ApiOperation({ description: 'Get a specific song data' })
@ApiNotFoundResponse({ description: "Song not found" }) @ApiNotFoundResponse({ description: 'Song not found' })
@ApiOkResponse({ type: _Song, description: "Requested song" }) @ApiOkResponse({ type: _Song, description: 'Requested song' })
async findOne( async findOne(
@Req() req: Request, @Req() req: Request,
@Param("id", ParseIntPipe) id: number, @Param('id', ParseIntPipe) id: number,
@Query("include") include: string, @Query('include') include: string,
) { ) {
const res = await this.songService.song( const res = await this.songService.song(
{ {
@@ -272,22 +192,21 @@ export class SongController {
mapInclude(include, req, SongController.includableFields), mapInclude(include, req, SongController.includableFields),
); );
if (res === null) throw new NotFoundException("Song not found"); if (res === null) throw new NotFoundException('Song not found');
return res; return res;
} }
@Get(":id/history") @Get(':id/history')
@HttpCode(200) @HttpCode(200)
@UseGuards(JwtAuthGuard)
@ApiOperation({ @ApiOperation({
description: "get the history of the connected user on a specific song", description: 'get the history of the connected user on a specific song',
}) })
@ApiOkResponse({ @ApiOkResponse({
type: SongHistoryResult, type: SongHistoryResult,
description: "Records of previous games of the user", description: 'Records of previous games of the user',
}) })
@ApiUnauthorizedResponse({ description: "Invalid token" }) @ApiUnauthorizedResponse({ description: 'Invalid token' })
async getHistory(@Req() req: any, @Param("id", ParseIntPipe) id: number) { async getHistory(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
return this.historyService.getForSong({ return this.historyService.getForSong({
playerId: req.user.id, playerId: req.user.id,
songId: id, songId: id,

Some files were not shown because too many files have changed in this diff Show More