Compare commits
281 Commits
feat/cover
...
clem-fixes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aae0bbea3a | ||
|
|
f5136ae59b | ||
|
|
f632ed42a3 | ||
|
|
00ee5cd531 | ||
|
|
1d496301d9 | ||
|
|
d44e75a83a | ||
| e487d6d91e | |||
| 7a63a66da5 | |||
| 17f64cd849 | |||
| ec17aa741f | |||
|
|
358841abd5 | ||
|
|
64e7dbc71e | ||
|
|
5a0809c1d0 | ||
|
|
4b5e3d2b04 | ||
|
|
5f24c6e7bd | ||
|
|
8bdf8ce334 | ||
|
|
9012a6a9d8 | ||
|
|
c5fd4aa7d5 | ||
|
|
65cd04a494 | ||
|
|
c79ae7c6e8 | ||
|
|
ddc97f0923 | ||
|
|
a9b902a427 | ||
|
|
96d8e649c8 | ||
|
|
22c93b7571 | ||
|
|
0644d4b580 | ||
|
|
ee6a76cdd9 | ||
|
|
5ba815590a | ||
|
|
dd09827d08 | ||
| b5b94adc83 | |||
| 3c04e8bb39 | |||
| 17a4328af5 | |||
| e81f2c1f75 | |||
| f77874bec4 | |||
| cfc72b8bc1 | |||
| 359b20fc6d | |||
| a3659618ea | |||
| fa60fc65a9 | |||
| b1727b7838 | |||
| a3f4703dae | |||
| 038918c212 | |||
| 42a947dfb0 | |||
| 5525110d39 | |||
| 7160b77607 | |||
| b5183f84b4 | |||
|
|
13050e52f9 | ||
|
|
5ef3885f72 | ||
|
|
a103666caf | ||
|
|
29da5c2788 | ||
|
|
1880b89b0c | ||
|
|
e769ff1f13 | ||
|
|
9e7873cdd7 | ||
|
|
f46c2cfb4a | ||
|
|
9f14061efd | ||
|
|
851ee7420f | ||
|
|
ef57eb752d | ||
|
|
fcb29ae484 | ||
|
|
5c4847ae2c | ||
|
|
60a73781bd | ||
|
|
4e3b378d6a | ||
|
|
2bf1e783a9 | ||
|
|
375d36f6c5 | ||
|
|
495380ec43 | ||
|
|
af0531bb0c | ||
|
|
c5124fa6ad | ||
|
|
962cf58e77 | ||
|
|
60988dd599 | ||
|
|
004a541302 | ||
|
|
f4cd9e18ea | ||
|
|
2dc301addf | ||
|
|
e85a959c26 | ||
|
|
339e808d27 | ||
|
|
22d1a97abd | ||
|
|
ce4baa61dc | ||
|
|
e90c7f05a8 | ||
|
|
fb0e43af88 | ||
|
|
4577997b1c | ||
|
|
9bb256f2ee | ||
|
|
d3994ff26e | ||
|
|
00d097f643 | ||
|
|
99da77f23e | ||
|
|
7a6dc8b0c9 | ||
|
|
b4f04f9b71 | ||
|
|
9df0c98100 | ||
|
|
a47f8744f8 | ||
| 80329e240e | |||
| 70b109e78b | |||
| a6a96d6a1e | |||
| cc4b69ca50 | |||
|
|
e733c6acc8 | ||
|
|
afa6f421d3 | ||
|
|
7d7f886661 | ||
|
|
fd22b8afe5 | ||
|
|
3353a17611 | ||
|
|
1c1596b44a | ||
|
|
9b05dc3ae3 | ||
|
|
d717269563 | ||
|
|
cba8815cfc | ||
|
|
647f7b2676 | ||
|
|
ef4f2355bf | ||
|
|
24a226b283 | ||
|
|
81717ec5b1 | ||
|
|
f9cb289eff | ||
|
|
022490ae10 | ||
|
|
ca4818c070 | ||
|
|
fe8e9cb262 | ||
|
|
9683d83298 | ||
|
|
69d9a4c499 | ||
| 7678776872 | |||
|
|
f590b573fb | ||
|
|
2c9ec4a7d3 | ||
|
|
393782b4b8 | ||
|
|
c33e1bbaa3 | ||
|
|
63a9271617 | ||
|
|
6469d4763a | ||
|
|
922e36093e | ||
|
|
81976206f9 | ||
|
|
4ac6369deb | ||
|
|
dc0c7fa4e7 | ||
|
|
61ebf58631 | ||
|
|
1d61b1e652 | ||
|
|
d0f9c4a032 | ||
|
|
27119056a4 | ||
|
|
044dd59d8f | ||
|
|
e5ab9b9310 | ||
|
|
f11cddf55a | ||
|
|
f076bf9794 | ||
|
|
fe510e148a | ||
|
|
0a84c9daac | ||
|
|
f496ae5bc1 | ||
|
|
1379cbd3f6 | ||
|
|
ab221bd393 | ||
|
|
e5fb1dfb7e | ||
|
|
c113b70fee | ||
|
|
d2a8f9a1ef | ||
|
|
ee56a53b40 | ||
| ece93f79b2 | |||
| 14e241db37 | |||
| 3becdcff46 | |||
| c0bc611268 | |||
| eff5eae706 | |||
| 59a48ad060 | |||
|
|
d3f7eded41 | ||
|
|
bbf3a317ec | ||
|
|
c6365113c4 | ||
|
|
454835338f | ||
|
|
3f0c2472cb | ||
| a36afa3a47 | |||
| 9bce8d74c9 | |||
|
|
e5acd56b0f | ||
|
|
685e79d76b | ||
|
|
183dee193c | ||
|
|
7167b49edc | ||
|
|
8b465731f0 | ||
|
|
0e26dbfc65 | ||
|
|
347c075ab1 | ||
|
|
01829c7b8b | ||
|
|
e148f9edb8 | ||
|
|
8a00b99f9a | ||
|
|
4d16723e38 | ||
|
|
683984efe9 | ||
|
|
6018028afd | ||
|
|
eac21844c4 | ||
|
|
0cb8dd2693 | ||
|
|
a4a10eb7f2 | ||
|
|
ff4926fa80 | ||
|
|
dd581a8418 | ||
|
|
5f0d7dda59 | ||
|
|
dfdbbdc51c | ||
|
|
72f17c018e | ||
|
|
397dfbcf5f | ||
|
|
df682327d6 | ||
|
|
46d5614e4c | ||
|
|
7e1f03af57 | ||
|
|
b54032fe63 | ||
|
|
b417076ee6 | ||
|
|
95da2cc500 | ||
|
|
84ea0b3743 | ||
|
|
00433ee7ba | ||
|
|
7f282e2ec5 | ||
|
|
3b89387b12 | ||
|
|
3b24cefd3f | ||
|
|
4de420e4dc | ||
|
|
36041369db | ||
|
|
b33ff55167 | ||
|
|
e8e6012bf2 | ||
|
|
92169bf485 | ||
|
|
9f57e8ac67 | ||
|
|
262353376c | ||
|
|
fd50b2268b | ||
|
|
6839cda5b8 | ||
|
|
d2aca488ad | ||
|
|
1fe7491bcd | ||
|
|
6a8fe074e0 | ||
|
|
624b640e01 | ||
|
|
ce4e09f1f6 | ||
|
|
c085e9aa22 | ||
|
|
dc491983f5 | ||
|
|
a0587fbad6 | ||
|
|
702caed232 | ||
|
|
cb65e08465 | ||
|
|
c1e862e6bd | ||
|
|
533dc0e7ad | ||
|
|
ecac53516e | ||
|
|
9133a369d5 | ||
|
|
4c580f1693 | ||
|
|
732f8e2577 | ||
|
|
4b3ec157c2 | ||
|
|
58de04924e | ||
|
|
66048ca793 | ||
|
|
b0b5579cb3 | ||
|
|
005cc7410f | ||
|
|
a4c2c4932d | ||
|
|
617d31cb22 | ||
|
|
384fb10f54 | ||
|
|
72c615ffed | ||
|
|
ce2da1d859 | ||
|
|
9d6beb74c0 | ||
|
|
1f25521900 | ||
|
|
f5c0d6967b | ||
|
|
c5e5519426 | ||
|
|
4c98759ded | ||
|
|
c910b0e617 | ||
|
|
94218558a7 | ||
|
|
f1662ca18b | ||
|
|
4a5658c4ca | ||
|
|
61ed8855ea | ||
|
|
94838ef1fc | ||
|
|
3ce69228a8 | ||
|
|
c522258d04 | ||
|
|
f91ab4c430 | ||
|
|
57cba61d1b | ||
|
|
3fbcb23089 | ||
|
|
9b0c633a87 | ||
|
|
c91bbfd2f1 | ||
|
|
9882fd240e | ||
|
|
ea6073eb71 | ||
|
|
22722082eb | ||
|
|
36316b0333 | ||
|
|
a814eec2cf | ||
|
|
4c96f78a46 | ||
|
|
cc65a3bd09 | ||
|
|
5f9e9f5327 | ||
|
|
b1d54d8665 | ||
|
|
d01aabe788 | ||
|
|
19d64c1bc5 | ||
|
|
ee98e6e352 | ||
|
|
bf52e7385b | ||
|
|
2d6fd3a3dc | ||
|
|
4bb5a11fff | ||
|
|
d4a758d262 | ||
|
|
9397de8cb9 | ||
|
|
d2e1ba51c6 | ||
|
|
ebed646c07 | ||
|
|
7067fb9708 | ||
|
|
e499bb2f9f | ||
|
|
b87ec1dd44 | ||
|
|
77f0c2f06f | ||
|
|
4c1891fb44 | ||
|
|
b3dade1a38 | ||
|
|
be2617e1ee | ||
|
|
35e1268f36 | ||
|
|
a8a3ed0e7b | ||
|
|
0eef957a90 | ||
|
|
6a8ca7d0fa | ||
|
|
ddd29f5530 | ||
|
|
b6e8b20168 | ||
|
|
96c43bcbad | ||
|
|
ab1ad17d21 | ||
|
|
5c85296810 | ||
|
|
06bfc181c7 | ||
|
|
0473665bb4 | ||
|
|
f610de3045 | ||
|
|
1228eb603e | ||
|
|
3ca17338e8 | ||
|
|
614ce105bd | ||
|
|
a0ca945c72 | ||
|
|
291d7698d4 | ||
|
|
e8956c50ee | ||
|
|
450fe1e7bd | ||
|
|
fbf4dfcfa5 | ||
|
|
d251929ede |
@@ -16,3 +16,10 @@ GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google
|
||||
SMTP_TRANSPORT=smtps://toto:tata@relay
|
||||
MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>'
|
||||
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
|
||||
|
||||
1
.git-blame-ignore-revs
Normal file
1
.git-blame-ignore-revs
Normal file
@@ -0,0 +1 @@
|
||||
14e241db37c4080bc0bd87363cf7a57ef8379f46
|
||||
165
.github/workflows/CI.yml
vendored
165
.github/workflows/CI.yml
vendored
@@ -1,164 +1,18 @@
|
||||
name: CI
|
||||
|
||||
name: Deploy
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
branches:
|
||||
- main
|
||||
push:
|
||||
branches:
|
||||
- '*'
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
- main
|
||||
|
||||
jobs:
|
||||
|
||||
## Build Back ##
|
||||
|
||||
Build_Back:
|
||||
deployment:
|
||||
runs-on: ubuntu-latest
|
||||
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
|
||||
if: github.event.pull_request.merged == true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@@ -203,6 +57,7 @@ jobs:
|
||||
build-args: |
|
||||
API_URL=${{secrets.API_URL}}
|
||||
SCORO_URL=${{secrets.SCORO_URL}}
|
||||
|
||||
- name: Docker meta scorometer
|
||||
id: meta_scorometer
|
||||
uses: docker/metadata-action@v4
|
||||
|
||||
101
.github/workflows/back.yml
vendored
Normal file
101
.github/workflows/back.yml
vendored
Normal file
@@ -0,0 +1,101 @@
|
||||
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
|
||||
98
.github/workflows/front.yml
vendored
Normal file
98
.github/workflows/front.yml
vendored
Normal file
@@ -0,0 +1,98 @@
|
||||
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/
|
||||
63
.github/workflows/scoro.yml
vendored
Normal file
63
.github/workflows/scoro.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
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
1
.gitignore
vendored
@@ -16,3 +16,4 @@ node_modules/
|
||||
.data
|
||||
.DS_Store
|
||||
_gen
|
||||
venv
|
||||
|
||||
19
assets/create_melodies.sh
Executable file
19
assets/create_melodies.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/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
|
||||
|
||||
BIN
assets/musics/Bach Minuet in G Minor (BWV Anh. 115)/melody.mp3
Normal file
BIN
assets/musics/Bach Minuet in G Minor (BWV Anh. 115)/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
assets/musics/Canon in D (easy)/melody.mp3
Normal file
BIN
assets/musics/Canon in D (easy)/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
assets/musics/French National Anthem La Marseillaise/melody.mp3
Normal file
BIN
assets/musics/French National Anthem La Marseillaise/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/musics/Jesus Alegria dos Homens/melody.mp3
Normal file
BIN
assets/musics/Jesus Alegria dos Homens/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/Liebestraum (easy)/melody.mp3
Normal file
BIN
assets/musics/Liebestraum (easy)/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/Mary, Did You Know/melody.mp3
Normal file
BIN
assets/musics/Mary, Did You Know/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/SCORO_TEST/melody.mp3
Normal file
BIN
assets/musics/SCORO_TEST/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/Sarabande - William Gillock/melody.mp3
Normal file
BIN
assets/musics/Sarabande - William Gillock/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/musics/Short/melody.mp3
Normal file
BIN
assets/musics/Short/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/Silent Night/melody.mp3
Normal file
BIN
assets/musics/Silent Night/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/musics/Twinkle Twinkle Little Star/melody.mp3
Normal file
BIN
assets/musics/Twinkle Twinkle Little Star/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
@@ -8,6 +8,10 @@ from mido import MidiFile
|
||||
from configparser import ConfigParser
|
||||
|
||||
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):
|
||||
if not name:
|
||||
@@ -15,7 +19,7 @@ def getOrCreateAlbum(name, artistId):
|
||||
res = requests.post(f"{url}/album", json={
|
||||
"name": name,
|
||||
"artist": artistId,
|
||||
})
|
||||
},headers=auth_headers)
|
||||
out = res.json()
|
||||
print(out)
|
||||
return out["id"]
|
||||
@@ -25,7 +29,7 @@ def getOrCreateGenre(names):
|
||||
for name in names.split(","):
|
||||
res = requests.post(f"{url}/genre", json={
|
||||
"name": name,
|
||||
})
|
||||
},headers=auth_headers)
|
||||
out = res.json()
|
||||
print(out)
|
||||
ids += [out["id"]]
|
||||
@@ -35,7 +39,7 @@ def getOrCreateGenre(names):
|
||||
def getOrCreateArtist(name):
|
||||
res = requests.post(f"{url}/artist", json={
|
||||
"name": name,
|
||||
})
|
||||
},headers=auth_headers)
|
||||
out = res.json()
|
||||
print(out)
|
||||
return out["id"]
|
||||
@@ -49,6 +53,7 @@ def populateFile(path, midi, mxl):
|
||||
difficulties["length"] = round((mid.length), 2)
|
||||
artistId = getOrCreateArtist(metadata["Artist"])
|
||||
print(f"Populating {metadata['Name']}")
|
||||
print(auth_headers)
|
||||
res = requests.post(f"{url}/song", json={
|
||||
"name": metadata["Name"],
|
||||
"midiPath": f"/assets/{midi}",
|
||||
@@ -58,9 +63,10 @@ def populateFile(path, midi, mxl):
|
||||
"album": getOrCreateAlbum(metadata["Album"], artistId),
|
||||
"genre": getOrCreateGenre(metadata["Genre"]),
|
||||
"illustrationPath": f"/assets/{os.path.commonpath([midi, mxl])}/illustration.png"
|
||||
})
|
||||
}, headers=auth_headers)
|
||||
print(res.json())
|
||||
|
||||
|
||||
def main():
|
||||
global url
|
||||
if url == None:
|
||||
|
||||
2
assets/requirements.txt
Normal file
2
assets/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
mido
|
||||
requests
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
FROM node:17
|
||||
WORKDIR /app
|
||||
CMD npx prisma generate ; npx prisma migrate dev ; npm run start:dev
|
||||
CMD npm i ; npx prisma generate ; npx prisma migrate dev ; npm run start:dev
|
||||
|
||||
11080
back/package-lock.json
generated
11080
back/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -35,13 +35,19 @@
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
"@types/passport": "^1.0.12",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"canvas": "^2.11.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.13.2",
|
||||
"json-logger-service": "^9.0.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"cross-blob": "^3.0.2",
|
||||
"fs": "^0.0.1-security",
|
||||
"jsdom": "^22.1.0",
|
||||
"json-logger-service": "^9.0.1",
|
||||
"meilisearch": "^0.35.0",
|
||||
"node-fetch": "^2.6.12",
|
||||
"nodemailer": "^6.9.5",
|
||||
"opensheetmusicdisplay": "^1.8.4",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-headerapikey": "^1.2.2",
|
||||
"passport-jwt": "^4.0.1",
|
||||
"passport-local": "^1.0.0",
|
||||
"prisma-class-generator": "^0.2.7",
|
||||
@@ -81,7 +87,8 @@
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
"ts",
|
||||
"mjs"
|
||||
],
|
||||
"rootDir": "src",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "totalScore" INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -24,6 +24,7 @@ model User {
|
||||
googleID String? @unique
|
||||
isGuest Boolean @default(false)
|
||||
partyPlayed Int @default(0)
|
||||
totalScore Int @default(0)
|
||||
LessonHistory LessonHistory[]
|
||||
SongHistory SongHistory[]
|
||||
searchHistory SearchHistory[]
|
||||
|
||||
@@ -12,23 +12,24 @@ import {
|
||||
Query,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { CreateAlbumDto } from './dto/create-album.dto';
|
||||
import { AlbumService } from './album.service';
|
||||
import { Request } from 'express';
|
||||
import { Prisma, Album } from '@prisma/client';
|
||||
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Album as _Album } from 'src/_gen/prisma-class/album';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { CreateAlbumDto } from "./dto/create-album.dto";
|
||||
import { AlbumService } from "./album.service";
|
||||
import { Request } from "express";
|
||||
import { Prisma, Album } from "@prisma/client";
|
||||
import { ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||
import { FilterQuery } from "src/utils/filter.pipe";
|
||||
import { Album as _Album } from "src/_gen/prisma-class/album";
|
||||
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||
|
||||
@Controller('album')
|
||||
@ApiTags('album')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller("album")
|
||||
@ApiTags("album")
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
export class AlbumController {
|
||||
static filterableFields: string[] = ['+id', 'name', '+artistId'];
|
||||
static filterableFields: string[] = ["+id", "name", "+artistId"];
|
||||
static includableFields: IncludeMap<Prisma.AlbumInclude> = {
|
||||
artist: true,
|
||||
Song: true,
|
||||
@@ -38,7 +39,7 @@ export class AlbumController {
|
||||
|
||||
@Post()
|
||||
@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) {
|
||||
try {
|
||||
@@ -55,26 +56,26 @@ export class AlbumController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ description: 'Delete an album by id' })
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
@Delete(":id")
|
||||
@ApiOperation({ description: "Delete an album by id" })
|
||||
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.albumService.deleteAlbum({ id });
|
||||
} catch {
|
||||
throw new NotFoundException('Invalid ID');
|
||||
throw new NotFoundException("Invalid ID");
|
||||
}
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOkResponsePlaginated(_Album)
|
||||
@ApiOperation({ description: 'Get all albums paginated' })
|
||||
@ApiOperation({ description: "Get all albums paginated" })
|
||||
async findAll(
|
||||
@Req() req: Request,
|
||||
@FilterQuery(AlbumController.filterableFields)
|
||||
where: Prisma.AlbumWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Album>> {
|
||||
const ret = await this.albumService.albums({
|
||||
skip,
|
||||
@@ -85,20 +86,20 @@ export class AlbumController {
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ description: 'Get an album by id' })
|
||||
@Get(":id")
|
||||
@ApiOperation({ description: "Get an album by id" })
|
||||
@ApiOkResponse({ type: _Album })
|
||||
async findOne(
|
||||
@Req() req: Request,
|
||||
@Query('include') include: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Query("include") include: string,
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
) {
|
||||
const res = await this.albumService.album(
|
||||
{ id },
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { AlbumController } from './album.controller';
|
||||
import { AlbumService } from './album.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { AlbumController } from "./album.controller";
|
||||
import { AlbumService } from "./album.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Album } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, Album } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class AlbumService {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
|
||||
export class CreateAlbumDto {
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { AppController } from "./app.controller";
|
||||
import { AppService } from "./app.service";
|
||||
|
||||
describe('AppController', () => {
|
||||
describe("AppController", () => {
|
||||
let appController: AppController;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -14,9 +14,9 @@ describe('AppController', () => {
|
||||
appController = app.get<AppController>(AppController);
|
||||
});
|
||||
|
||||
describe('root', () => {
|
||||
describe("root", () => {
|
||||
it('should return "Hello World!"', () => {
|
||||
expect(appController.getHello()).toBe('Hello World!');
|
||||
expect(appController.getHello()).toBe("Hello World!");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { AppService } from './app.service';
|
||||
import { ApiOkResponse } from '@nestjs/swagger';
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { AppService } from "./app.service";
|
||||
import { ApiOkResponse } from "@nestjs/swagger";
|
||||
|
||||
@Controller()
|
||||
export class AppController {
|
||||
@@ -8,7 +8,7 @@ export class AppController {
|
||||
|
||||
@Get()
|
||||
@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 {
|
||||
return this.appService.getHello();
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { PrismaService } from './prisma/prisma.service';
|
||||
import { UsersModule } from './users/users.module';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { SongModule } from './song/song.module';
|
||||
import { LessonModule } from './lesson/lesson.module';
|
||||
import { SettingsModule } from './settings/settings.module';
|
||||
import { ArtistService } from './artist/artist.service';
|
||||
import { GenreModule } from './genre/genre.module';
|
||||
import { ArtistModule } from './artist/artist.module';
|
||||
import { AlbumModule } from './album/album.module';
|
||||
import { SearchModule } from './search/search.module';
|
||||
import { HistoryModule } from './history/history.module';
|
||||
import { MailerModule } from '@nestjs-modules/mailer';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AppController } from "./app.controller";
|
||||
import { AppService } from "./app.service";
|
||||
import { PrismaService } from "./prisma/prisma.service";
|
||||
import { UsersModule } from "./users/users.module";
|
||||
import { PrismaModule } from "./prisma/prisma.module";
|
||||
import { AuthModule } from "./auth/auth.module";
|
||||
import { SongModule } from "./song/song.module";
|
||||
import { LessonModule } from "./lesson/lesson.module";
|
||||
import { SettingsModule } from "./settings/settings.module";
|
||||
import { ArtistService } from "./artist/artist.service";
|
||||
import { GenreModule } from "./genre/genre.module";
|
||||
import { ArtistModule } from "./artist/artist.module";
|
||||
import { AlbumModule } from "./album/album.module";
|
||||
import { SearchModule } from "./search/search.module";
|
||||
import { HistoryModule } from "./history/history.module";
|
||||
import { MailerModule } from "@nestjs-modules/mailer";
|
||||
import { ScoresModule } from "./scores/scores.module";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -29,6 +30,7 @@ import { MailerModule } from '@nestjs-modules/mailer';
|
||||
SearchModule,
|
||||
SettingsModule,
|
||||
HistoryModule,
|
||||
ScoresModule,
|
||||
MailerModule.forRoot({
|
||||
transport: process.env.SMTP_TRANSPORT,
|
||||
defaults: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class AppService {
|
||||
getHello(): string {
|
||||
return 'Hello World!';
|
||||
return "Hello World!";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,30 +14,31 @@ import {
|
||||
Req,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { CreateArtistDto } from './dto/create-artist.dto';
|
||||
import { Request } from 'express';
|
||||
import { ArtistService } from './artist.service';
|
||||
import { Prisma, Artist } from '@prisma/client';
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { CreateArtistDto } from "./dto/create-artist.dto";
|
||||
import { Request } from "express";
|
||||
import { ArtistService } from "./artist.service";
|
||||
import { Prisma, Artist } from "@prisma/client";
|
||||
import {
|
||||
ApiNotFoundResponse,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Artist as _Artist } from 'src/_gen/prisma-class/artist';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { Public } from 'src/auth/public';
|
||||
} from "@nestjs/swagger";
|
||||
import { createReadStream, existsSync } from "fs";
|
||||
import { FilterQuery } from "src/utils/filter.pipe";
|
||||
import { Artist as _Artist } from "src/_gen/prisma-class/artist";
|
||||
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||
import { Public } from "src/auth/public";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||
|
||||
@Controller('artist')
|
||||
@ApiTags('artist')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller("artist")
|
||||
@ApiTags("artist")
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
export class ArtistController {
|
||||
static filterableFields = ['+id', 'name'];
|
||||
static filterableFields = ["+id", "name"];
|
||||
static includableFields: IncludeMap<Prisma.ArtistInclude> = {
|
||||
Song: true,
|
||||
Album: true,
|
||||
@@ -47,7 +48,7 @@ export class ArtistController {
|
||||
|
||||
@Post()
|
||||
@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) {
|
||||
try {
|
||||
@@ -57,26 +58,26 @@ export class ArtistController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ description: 'Delete an artist by id' })
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
@Delete(":id")
|
||||
@ApiOperation({ description: "Delete an artist by id" })
|
||||
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.service.delete({ id });
|
||||
} catch {
|
||||
throw new NotFoundException('Invalid ID');
|
||||
throw new NotFoundException("Invalid ID");
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/illustration')
|
||||
@Get(":id/illustration")
|
||||
@ApiOperation({ description: "Get an artist's illustration" })
|
||||
@ApiNotFoundResponse({ description: 'Artist or illustration not found' })
|
||||
@ApiNotFoundResponse({ description: "Artist or illustration not found" })
|
||||
@Public()
|
||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
||||
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||
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`;
|
||||
if (!existsSync(path))
|
||||
throw new NotFoundException('Illustration not found');
|
||||
throw new NotFoundException("Illustration not found");
|
||||
|
||||
try {
|
||||
const file = createReadStream(path);
|
||||
@@ -87,15 +88,15 @@ export class ArtistController {
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ description: 'Get all artists paginated' })
|
||||
@ApiOperation({ description: "Get all artists paginated" })
|
||||
@ApiOkResponsePlaginated(_Artist)
|
||||
async findAll(
|
||||
@Req() req: Request,
|
||||
@FilterQuery(ArtistController.filterableFields)
|
||||
where: Prisma.ArtistWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Artist>> {
|
||||
const ret = await this.service.list({
|
||||
skip,
|
||||
@@ -106,20 +107,20 @@ export class ArtistController {
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ description: 'Get an artist by id' })
|
||||
@Get(":id")
|
||||
@ApiOperation({ description: "Get an artist by id" })
|
||||
@ApiOkResponse({ type: _Artist })
|
||||
async findOne(
|
||||
@Req() req: Request,
|
||||
@Query('include') include: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Query("include") include: string,
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
) {
|
||||
const res = await this.service.get(
|
||||
{ id },
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { ArtistController } from './artist.controller';
|
||||
import { ArtistService } from './artist.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { ArtistController } from "./artist.controller";
|
||||
import { ArtistService } from "./artist.service";
|
||||
import { SearchModule } from "src/search/search.module";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaModule, SearchModule],
|
||||
controllers: [ArtistController],
|
||||
providers: [ArtistService],
|
||||
})
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Artist } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, Artist } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { MeiliService } from "src/search/meilisearch.service";
|
||||
|
||||
@Injectable()
|
||||
export class ArtistService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private search: MeiliService,
|
||||
) {}
|
||||
|
||||
async create(data: Prisma.ArtistCreateInput): Promise<Artist> {
|
||||
return this.prisma.artist.create({
|
||||
const ret = await this.prisma.artist.create({
|
||||
data,
|
||||
});
|
||||
await this.search.index("artists").addDocuments([ret]);
|
||||
return ret;
|
||||
}
|
||||
|
||||
async get(
|
||||
@@ -42,8 +48,10 @@ export class ArtistService {
|
||||
}
|
||||
|
||||
async delete(where: Prisma.ArtistWhereUniqueInput): Promise<Artist> {
|
||||
return this.prisma.artist.delete({
|
||||
const ret = await this.prisma.artist.delete({
|
||||
where,
|
||||
});
|
||||
await this.search.index("artists").deleteDocument(ret.id);
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
|
||||
export class CreateArtistDto {
|
||||
@IsNotEmpty()
|
||||
|
||||
794
back/src/assetsgenerator/generateImages_browserless.js
Normal file
794
back/src/assetsgenerator/generateImages_browserless.js
Normal file
@@ -0,0 +1,794 @@
|
||||
// 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();
|
||||
5
back/src/auth/apikey-auth.guard.ts
Normal file
5
back/src/auth/apikey-auth.guard.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeyAuthGuard extends AuthGuard("api-key") {}
|
||||
31
back/src/auth/apikey.strategy.ts
Normal file
31
back/src/auth/apikey.strategy.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
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);
|
||||
};
|
||||
}
|
||||
@@ -21,12 +21,13 @@ import {
|
||||
Response,
|
||||
Query,
|
||||
Param,
|
||||
} from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
||||
import { LocalAuthGuard } from './local-auth.guard';
|
||||
import { RegisterDto } from './dto/register.dto';
|
||||
import { UsersService } from 'src/users/users.service';
|
||||
ParseIntPipe,
|
||||
} from "@nestjs/common";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { JwtAuthGuard } from "./jwt-auth.guard";
|
||||
import { LocalAuthGuard } from "./local-auth.guard";
|
||||
import { RegisterDto } from "./dto/register.dto";
|
||||
import { UsersService } from "src/users/users.service";
|
||||
import {
|
||||
ApiBadRequestResponse,
|
||||
ApiBearerAuth,
|
||||
@@ -36,21 +37,24 @@ import {
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { User } from '../models/user';
|
||||
import { JwtToken } from './models/jwt';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { Profile } from './dto/profile.dto';
|
||||
import { Setting } from 'src/models/setting';
|
||||
import { UpdateSettingDto } from 'src/settings/dto/update-setting.dto';
|
||||
import { SettingsService } from 'src/settings/settings.service';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { FileInterceptor } from '@nestjs/platform-express';
|
||||
import { writeFile } from 'fs';
|
||||
import { PasswordResetDto } from './dto/password_reset.dto ';
|
||||
} from "@nestjs/swagger";
|
||||
import { User } from "../models/user";
|
||||
import { JwtToken } from "./models/jwt";
|
||||
import { LoginDto } from "./dto/login.dto";
|
||||
import { Profile } from "./dto/profile.dto";
|
||||
import { Setting } from "src/models/setting";
|
||||
import { UpdateSettingDto } from "src/settings/dto/update-setting.dto";
|
||||
import { SettingsService } from "src/settings/settings.service";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { FileInterceptor } from "@nestjs/platform-express";
|
||||
import { writeFile } from "fs";
|
||||
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')
|
||||
@Controller('auth')
|
||||
@ApiTags("auth")
|
||||
@Controller("auth")
|
||||
export class AuthController {
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
@@ -58,17 +62,17 @@ export class AuthController {
|
||||
private settingsService: SettingsService,
|
||||
) {}
|
||||
|
||||
@Get('login/google')
|
||||
@UseGuards(AuthGuard('google'))
|
||||
@ApiOperation({ description: 'Redirect to google login page' })
|
||||
@Get("login/google")
|
||||
@UseGuards(AuthGuard("google"))
|
||||
@ApiOperation({ description: "Redirect to google login page" })
|
||||
googleLogin() {}
|
||||
|
||||
@Get('logged/google')
|
||||
@Get("logged/google")
|
||||
@ApiOperation({
|
||||
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) {
|
||||
let user = await this.usersService.user({ googleID: req.user.googleID });
|
||||
if (!user) {
|
||||
@@ -78,13 +82,13 @@ export class AuthController {
|
||||
return this.authService.login(user);
|
||||
}
|
||||
|
||||
@Post('register')
|
||||
@ApiOperation({ description: 'Register a new user' })
|
||||
@ApiConflictResponse({ description: 'Username or email already taken' })
|
||||
@Post("register")
|
||||
@ApiOperation({ description: "Register a new user" })
|
||||
@ApiConflictResponse({ description: "Username or email already taken" })
|
||||
@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> {
|
||||
try {
|
||||
const user = await this.usersService.createUser(registerDto);
|
||||
@@ -92,102 +96,102 @@ export class AuthController {
|
||||
await this.authService.sendVerifyMail(user);
|
||||
} catch (e) {
|
||||
// check if the error is a duplicate key error
|
||||
if (e.code === 'P2002') {
|
||||
throw new ConflictException('Username or email already taken');
|
||||
if (e.code === "P2002") {
|
||||
throw new ConflictException("Username or email already taken");
|
||||
}
|
||||
console.error(e);
|
||||
throw new BadRequestException();
|
||||
}
|
||||
}
|
||||
|
||||
@Put('verify')
|
||||
@Put("verify")
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ description: 'Verify the email of the user' })
|
||||
@ApiOkResponse({ description: 'Successfully verified' })
|
||||
@ApiBadRequestResponse({ description: 'Invalid or expired token' })
|
||||
@ApiOperation({ description: "Verify the email of the user" })
|
||||
@ApiOkResponse({ description: "Successfully verified" })
|
||||
@ApiBadRequestResponse({ description: "Invalid or expired token" })
|
||||
async verify(
|
||||
@Request() req: any,
|
||||
@Query('token') token: string,
|
||||
@Query("token") token: string,
|
||||
): Promise<void> {
|
||||
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)
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ description: 'Resend the verification email' })
|
||||
@ApiOperation({ description: "Resend the verification email" })
|
||||
async reverify(@Request() req: any): Promise<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
@HttpCode(200)
|
||||
@Put('password-reset')
|
||||
@Put("password-reset")
|
||||
async password_reset(
|
||||
@Body() resetDto: PasswordResetDto,
|
||||
@Query('token') token: string,
|
||||
@Query("token") token: string,
|
||||
): Promise<void> {
|
||||
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)
|
||||
@Put('forgot-password')
|
||||
async forgot_password(@Query('email') email: string): Promise<void> {
|
||||
@Put("forgot-password")
|
||||
async forgot_password(@Query("email") email: string): Promise<void> {
|
||||
console.log(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);
|
||||
}
|
||||
|
||||
@Post('login')
|
||||
@Post("login")
|
||||
@ApiBody({ type: LoginDto })
|
||||
@HttpCode(200)
|
||||
@UseGuards(LocalAuthGuard)
|
||||
@ApiBody({ type: LoginDto })
|
||||
@ApiOperation({ description: 'Login with username and password' })
|
||||
@ApiOkResponse({ description: 'Successfully logged in', type: JwtToken })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid credentials' })
|
||||
@ApiOperation({ description: "Login with username and password" })
|
||||
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid credentials" })
|
||||
async login(@Request() req: any): Promise<JwtToken> {
|
||||
return this.authService.login(req.user);
|
||||
}
|
||||
|
||||
@Post('guest')
|
||||
@Post("guest")
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ description: 'Login as a guest account' })
|
||||
@ApiOkResponse({ description: 'Successfully logged in', type: JwtToken })
|
||||
@ApiOperation({ description: "Login as a guest account" })
|
||||
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
|
||||
async guest(): Promise<JwtToken> {
|
||||
const user = await this.usersService.createGuest();
|
||||
await this.settingsService.createUserSetting(user.id);
|
||||
return this.authService.login(user);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOperation({ description: 'Get the profile picture of connected user' })
|
||||
@ApiOkResponse({ description: 'The user profile picture' })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Get('me/picture')
|
||||
@ApiOperation({ description: "Get the profile picture of connected user" })
|
||||
@ApiOkResponse({ description: "The user profile picture" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Get("me/picture")
|
||||
async getProfilePicture(@Request() req: any, @Response() res: any) {
|
||||
return await this.usersService.getProfilePicture(req.user.id, res);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'The user profile picture' })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Post('me/picture')
|
||||
@ApiOperation({ description: 'Upload a new profile picture' })
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
@ApiOkResponse({ description: "The user profile picture" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Post("me/picture")
|
||||
@ApiOperation({ description: "Upload a new profile picture" })
|
||||
@UseInterceptors(FileInterceptor("file"))
|
||||
async postProfilePicture(
|
||||
@Request() req: any,
|
||||
@UploadedFile(
|
||||
new ParseFilePipeBuilder()
|
||||
.addFileTypeValidator({
|
||||
fileType: 'jpeg',
|
||||
fileType: "jpeg",
|
||||
})
|
||||
.build({
|
||||
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||
@@ -203,22 +207,22 @@ export class AuthController {
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully logged in', type: User })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Get('me')
|
||||
@ApiOperation({ description: 'Get the user info of connected user' })
|
||||
@ApiOkResponse({ description: "Successfully logged in", type: User })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Get("me")
|
||||
@ApiOperation({ description: "Get the user info of connected user" })
|
||||
async getProfile(@Request() req: any): Promise<User> {
|
||||
const user = await this.usersService.user({ id: req.user.id });
|
||||
if (!user) throw new InternalServerErrorException();
|
||||
return user;
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully edited profile', type: User })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Put('me')
|
||||
@ApiOperation({ description: 'Edit the profile of connected user' })
|
||||
@ApiOkResponse({ description: "Successfully edited profile", type: User })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Put("me")
|
||||
@ApiOperation({ description: "Edit the profile of connected user" })
|
||||
editProfile(
|
||||
@Request() req: any,
|
||||
@Body() profile: Partial<Profile>,
|
||||
@@ -241,20 +245,20 @@ export class AuthController {
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully deleted', type: User })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Delete('me')
|
||||
@ApiOperation({ description: 'Delete the profile of connected user' })
|
||||
@ApiOkResponse({ description: "Successfully deleted", type: User })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Delete("me")
|
||||
@ApiOperation({ description: "Delete the profile of connected user" })
|
||||
deleteSelf(@Request() req: any): Promise<User> {
|
||||
return this.usersService.deleteUser({ id: req.user.id });
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Patch('me/settings')
|
||||
@ApiOperation({ description: 'Edit the settings of connected user' })
|
||||
@ApiOkResponse({ description: "Successfully edited settings", type: Setting })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Patch("me/settings")
|
||||
@ApiOperation({ description: "Edit the settings of connected user" })
|
||||
udpateSettings(
|
||||
@Request() req: any,
|
||||
@Body() settingUserDto: UpdateSettingDto,
|
||||
@@ -267,10 +271,10 @@ export class AuthController {
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Get('me/settings')
|
||||
@ApiOperation({ description: 'Get the settings of connected user' })
|
||||
@ApiOkResponse({ description: "Successfully edited settings", type: Setting })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Get("me/settings")
|
||||
@ApiOperation({ description: "Get the settings of connected user" })
|
||||
async getSettings(@Request() req: any): Promise<Setting> {
|
||||
const result = await this.settingsService.getUserSetting({
|
||||
userId: +req.user.id,
|
||||
@@ -281,28 +285,43 @@ export class AuthController {
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully added liked song' })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Post('me/likes/:id')
|
||||
addLikedSong(@Request() req: any, @Param('id') songId: number) {
|
||||
return this.usersService.addLikedSong(+req.user.id, +songId);
|
||||
@ApiOkResponse({ description: "Successfully added liked song" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Post("me/likes/:id")
|
||||
addLikedSong(@Request() req: any, @Param("id", ParseIntPipe) songId: number) {
|
||||
return this.usersService.addLikedSong(+req.user.id, songId);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully removed liked song' })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Delete('me/likes/:id')
|
||||
removeLikedSong(@Request() req: any, @Param('id') songId: number) {
|
||||
return this.usersService.removeLikedSong(+req.user.id, +songId);
|
||||
@ApiOkResponse({ description: "Successfully removed liked song" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Delete("me/likes/:id")
|
||||
removeLikedSong(
|
||||
@Request() req: any,
|
||||
@Param("id", ParseIntPipe) songId: number,
|
||||
) {
|
||||
return this.usersService.removeLikedSong(+req.user.id, songId);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
@ApiOkResponse({ description: 'Successfully retrieved liked song' })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@Get('me/likes')
|
||||
getLikedSongs(@Request() req: any) {
|
||||
return this.usersService.getLikedSongs(+req.user.id);
|
||||
@ApiOkResponse({ description: "Successfully retrieved liked song" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Get("me/likes")
|
||||
getLikedSongs(@Request() req: any, @Query("include") include: string) {
|
||||
return this.usersService.getLikedSongs(
|
||||
+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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersModule } from 'src/users/users.module';
|
||||
import { AuthService } from './auth.service';
|
||||
import { PassportModule } from '@nestjs/passport';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { LocalStrategy } from './local.strategy';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { ConfigModule } from '@nestjs/config';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { SettingsModule } from 'src/settings/settings.module';
|
||||
import { GoogleStrategy } from './google.strategy';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { UsersModule } from "src/users/users.module";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { PassportModule } from "@nestjs/passport";
|
||||
import { AuthController } from "./auth.controller";
|
||||
import { LocalStrategy } from "./local.strategy";
|
||||
import { JwtModule } from "@nestjs/jwt";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { JwtStrategy } from "./jwt.strategy";
|
||||
import { SettingsModule } from "src/settings/settings.module";
|
||||
import { GoogleStrategy } from "./google.strategy";
|
||||
import { HeaderApiKeyStrategy } from "./apikey.strategy";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -20,13 +21,19 @@ import { GoogleStrategy } from './google.strategy';
|
||||
JwtModule.registerAsync({
|
||||
imports: [ConfigModule],
|
||||
useFactory: async (configService: ConfigService) => ({
|
||||
secret: configService.get('JWT_SECRET'),
|
||||
signOptions: { expiresIn: '365d' },
|
||||
secret: configService.get("JWT_SECRET"),
|
||||
signOptions: { expiresIn: "365d" },
|
||||
}),
|
||||
inject: [ConfigService],
|
||||
}),
|
||||
],
|
||||
providers: [AuthService, LocalStrategy, JwtStrategy, GoogleStrategy],
|
||||
providers: [
|
||||
AuthService,
|
||||
LocalStrategy,
|
||||
JwtStrategy,
|
||||
GoogleStrategy,
|
||||
HeaderApiKeyStrategy,
|
||||
],
|
||||
controllers: [AuthController],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { UsersService } from '../users/users.service';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import PayloadInterface from './interface/payload.interface';
|
||||
import { User } from 'src/models/user';
|
||||
import { MailerService } from '@nestjs-modules/mailer';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { UsersService } from "../users/users.service";
|
||||
import { JwtService } from "@nestjs/jwt";
|
||||
import * as bcrypt from "bcryptjs";
|
||||
import PayloadInterface from "./interface/payload.interface";
|
||||
import { User } from "src/models/user";
|
||||
import { MailerService } from "@nestjs-modules/mailer";
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
constructor(
|
||||
@@ -13,6 +13,12 @@ export class AuthService {
|
||||
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(
|
||||
username: string,
|
||||
password: string,
|
||||
@@ -36,37 +42,37 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async sendVerifyMail(user: User) {
|
||||
if (process.env.IGNORE_MAILS === 'true') return;
|
||||
if (process.env.IGNORE_MAILS === "true") 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(
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{ expiresIn: '10h' },
|
||||
{ expiresIn: "10h" },
|
||||
);
|
||||
await this.emailService.sendMail({
|
||||
to: user.email,
|
||||
from: 'chromacase@octohub.app',
|
||||
subject: 'Mail verification for Chromacase',
|
||||
from: "chromacase@octohub.app",
|
||||
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>.`,
|
||||
});
|
||||
}
|
||||
|
||||
async sendPasswordResetMail(user: User) {
|
||||
if (process.env.IGNORE_MAILS === 'true') return;
|
||||
if (process.env.IGNORE_MAILS === "true") 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(
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{ expiresIn: '10h' },
|
||||
{ expiresIn: "10h" },
|
||||
);
|
||||
await this.emailService.sendMail({
|
||||
to: user.email,
|
||||
from: 'chromacase@octohub.app',
|
||||
subject: 'Password reset for Chromacase',
|
||||
from: "chromacase@octohub.app",
|
||||
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>.`,
|
||||
});
|
||||
}
|
||||
@@ -76,7 +82,7 @@ export class AuthService {
|
||||
try {
|
||||
verified = await this.jwtService.verifyAsync(token);
|
||||
} catch (e) {
|
||||
console.log('Password reset token failure', e);
|
||||
console.log("Password reset token failure", e);
|
||||
return false;
|
||||
}
|
||||
console.log(verified);
|
||||
@@ -91,7 +97,7 @@ export class AuthService {
|
||||
try {
|
||||
await this.jwtService.verifyAsync(token);
|
||||
} catch (e) {
|
||||
console.log('Verify mail token failure', e);
|
||||
console.log("Verify mail token failure", e);
|
||||
return false;
|
||||
}
|
||||
await this.userService.updateUser({
|
||||
|
||||
22
back/src/auth/chroma-auth.guard.ts
Normal file
22
back/src/auth/chroma-auth.guard.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Injectable()
|
||||
export class Constants {
|
||||
constructor(private configService: ConfigService) {}
|
||||
|
||||
getSecret = () => {
|
||||
return this.configService.get('JWT_SECRET');
|
||||
return this.configService.get("JWT_SECRET");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class LoginDto {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class PasswordResetDto {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class Profile {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class RegisterDto {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { User } from '@prisma/client';
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { Strategy, VerifyCallback } from "passport-google-oauth20";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||
@@ -10,7 +10,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_SECRET,
|
||||
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||
scope: ['email', 'profile'],
|
||||
scope: ["email", "profile"],
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { IS_PUBLIC_KEY } from './public';
|
||||
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 JwtAuthGuard extends AuthGuard('jwt') {
|
||||
export class JwtAuthGuard extends AuthGuard("jwt") {
|
||||
constructor(private reflector: Reflector) {
|
||||
super();
|
||||
}
|
||||
|
||||
canActivate(context: ExecutionContext) {
|
||||
console.log(context);
|
||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
console.log(isPublic);
|
||||
if (isPublic) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
@@ -9,7 +9,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: configService.get('JWT_SECRET'),
|
||||
secretOrKey: configService.get("JWT_SECRET"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
|
||||
@Injectable()
|
||||
export class LocalAuthGuard extends AuthGuard('local') {}
|
||||
export class LocalAuthGuard extends AuthGuard("local") {}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Strategy } from 'passport-local';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthService } from './auth.service';
|
||||
import PayloadInterface from './interface/payload.interface';
|
||||
import { Strategy } from "passport-local";
|
||||
import { PassportStrategy } from "@nestjs/passport";
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { AuthService } from "./auth.service";
|
||||
import PayloadInterface from "./interface/payload.interface";
|
||||
|
||||
@Injectable()
|
||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class JwtToken {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
|
||||
export class CreateGenreDto {
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -14,25 +14,26 @@ import {
|
||||
Req,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { CreateGenreDto } from './dto/create-genre.dto';
|
||||
import { Request } from 'express';
|
||||
import { GenreService } from './genre.service';
|
||||
import { Prisma, Genre } from '@prisma/client';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Genre as _Genre } from 'src/_gen/prisma-class/genre';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { Public } from 'src/auth/public';
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { CreateGenreDto } from "./dto/create-genre.dto";
|
||||
import { Request } from "express";
|
||||
import { GenreService } from "./genre.service";
|
||||
import { Prisma, Genre } from "@prisma/client";
|
||||
import { ApiTags } from "@nestjs/swagger";
|
||||
import { createReadStream, existsSync } from "fs";
|
||||
import { FilterQuery } from "src/utils/filter.pipe";
|
||||
import { Genre as _Genre } from "src/_gen/prisma-class/genre";
|
||||
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||
import { Public } from "src/auth/public";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||
|
||||
@Controller('genre')
|
||||
@ApiTags('genre')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller("genre")
|
||||
@ApiTags("genre")
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
export class GenreController {
|
||||
static filterableFields: string[] = ['+id', 'name'];
|
||||
static filterableFields: string[] = ["+id", "name"];
|
||||
static includableFields: IncludeMap<Prisma.GenreInclude> = {
|
||||
Song: true,
|
||||
};
|
||||
@@ -48,23 +49,23 @@ export class GenreController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
@Delete(":id")
|
||||
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.service.delete({ id });
|
||||
} catch {
|
||||
throw new NotFoundException('Invalid ID');
|
||||
throw new NotFoundException("Invalid ID");
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/illustration')
|
||||
@Get(":id/illustration")
|
||||
@Public()
|
||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
||||
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||
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`;
|
||||
if (!existsSync(path))
|
||||
throw new NotFoundException('Illustration not found');
|
||||
throw new NotFoundException("Illustration not found");
|
||||
|
||||
try {
|
||||
const file = createReadStream(path);
|
||||
@@ -80,9 +81,9 @@ export class GenreController {
|
||||
@Req() req: Request,
|
||||
@FilterQuery(GenreController.filterableFields)
|
||||
where: Prisma.GenreWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Genre>> {
|
||||
const ret = await this.service.list({
|
||||
skip,
|
||||
@@ -93,18 +94,18 @@ export class GenreController {
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@Get(":id")
|
||||
async findOne(
|
||||
@Req() req: Request,
|
||||
@Query('include') include: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Query("include") include: string,
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
) {
|
||||
const res = await this.service.get(
|
||||
{ id },
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { GenreController } from './genre.controller';
|
||||
import { GenreService } from './genre.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { GenreController } from "./genre.controller";
|
||||
import { GenreService } from "./genre.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, Genre } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, Genre } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class GenreService {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class SearchHistoryDto {
|
||||
@ApiProperty()
|
||||
query: string;
|
||||
|
||||
@ApiProperty()
|
||||
type: 'song' | 'artist' | 'album' | 'genre';
|
||||
type: "song" | "artist" | "album" | "genre";
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNumber } from 'class-validator';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNumber } from "class-validator";
|
||||
|
||||
export class SongHistoryDto {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -9,68 +9,75 @@ import {
|
||||
Query,
|
||||
Request,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiCreatedResponse,
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { SearchHistory, SongHistory } from '@prisma/client';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { SongHistoryDto } from './dto/SongHistoryDto';
|
||||
import { HistoryService } from './history.service';
|
||||
import { SearchHistoryDto } from './dto/SearchHistoryDto';
|
||||
import { SongHistory as _SongHistory } from 'src/_gen/prisma-class/song_history';
|
||||
import { SearchHistory as _SearchHistory } from 'src/_gen/prisma-class/search_history';
|
||||
} from "@nestjs/swagger";
|
||||
import { SearchHistory, SongHistory } from "@prisma/client";
|
||||
import { JwtAuthGuard } from "src/auth/jwt-auth.guard";
|
||||
import { SongHistoryDto } from "./dto/SongHistoryDto";
|
||||
import { HistoryService } from "./history.service";
|
||||
import { SearchHistoryDto } from "./dto/SearchHistoryDto";
|
||||
import { SongHistory as _SongHistory } from "src/_gen/prisma-class/song_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')
|
||||
@ApiTags('history')
|
||||
@Controller("history")
|
||||
@ApiTags("history")
|
||||
export class HistoryController {
|
||||
constructor(private readonly historyService: HistoryService) {}
|
||||
|
||||
@Get()
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ description: 'Get song history of connected user' })
|
||||
@ApiOperation({ description: "Get song history of connected user" })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOkResponse({ type: _SongHistory, isArray: true })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
async getHistory(
|
||||
@Request() req: any,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
): Promise<SongHistory[]> {
|
||||
return this.historyService.getHistory(req.user.id, { skip, take });
|
||||
return this.historyService.getHistory(
|
||||
req.user.id,
|
||||
{ skip, take },
|
||||
mapInclude(include, req, SongController.includableFields),
|
||||
);
|
||||
}
|
||||
|
||||
@Get('search')
|
||||
@Get("search")
|
||||
@HttpCode(200)
|
||||
@ApiOperation({ description: 'Get search history of connected user' })
|
||||
@ApiOperation({ description: "Get search history of connected user" })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOkResponse({ type: _SearchHistory, isArray: true })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
async getSearchHistory(
|
||||
@Request() req: any,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<SearchHistory[]> {
|
||||
return this.historyService.getSearchHistory(req.user.id, { skip, take });
|
||||
}
|
||||
|
||||
@Post()
|
||||
@HttpCode(201)
|
||||
@ApiOperation({ description: 'Create a record of a song played by a user' })
|
||||
@ApiCreatedResponse({ description: 'Succesfully created a record' })
|
||||
@ApiOperation({ description: "Create a record of a song played by a user" })
|
||||
@ApiCreatedResponse({ description: "Succesfully created a record" })
|
||||
async create(@Body() record: SongHistoryDto): Promise<SongHistory> {
|
||||
return this.historyService.createSongHistoryRecord(record);
|
||||
}
|
||||
|
||||
@Post('search')
|
||||
@Post("search")
|
||||
@HttpCode(201)
|
||||
@ApiOperation({ description: 'Creates a search record in the users history' })
|
||||
@ApiOperation({ description: "Creates a search record in the users history" })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
async createSearchHistory(
|
||||
@Request() req: any,
|
||||
@Body() record: SearchHistoryDto,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { HistoryService } from './history.service';
|
||||
import { HistoryController } from './history.controller';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { HistoryService } from "./history.service";
|
||||
import { HistoryController } from "./history.controller";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { HistoryService } from './history.service';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { HistoryService } from "./history.service";
|
||||
|
||||
describe('HistoryService', () => {
|
||||
describe("HistoryService", () => {
|
||||
let service: HistoryService;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -12,7 +12,7 @@ describe('HistoryService', () => {
|
||||
service = module.get<HistoryService>(HistoryService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { SearchHistory, SongHistory } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { SearchHistoryDto } from './dto/SearchHistoryDto';
|
||||
import { SongHistoryDto } from './dto/SongHistoryDto';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, SearchHistory, SongHistory } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { SearchHistoryDto } from "./dto/SearchHistoryDto";
|
||||
import { SongHistoryDto } from "./dto/SongHistoryDto";
|
||||
|
||||
@Injectable()
|
||||
export class HistoryService {
|
||||
@@ -45,12 +45,14 @@ export class HistoryService {
|
||||
async getHistory(
|
||||
playerId: number,
|
||||
{ skip, take }: { skip?: number; take?: number },
|
||||
include?: Prisma.SongInclude,
|
||||
): Promise<SongHistory[]> {
|
||||
return this.prisma.songHistory.findMany({
|
||||
where: { user: { id: playerId } },
|
||||
orderBy: { playDate: 'desc' },
|
||||
orderBy: { playDate: "desc" },
|
||||
skip,
|
||||
take,
|
||||
include: { song: include ? { include } : true },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -63,7 +65,7 @@ export class HistoryService {
|
||||
}): Promise<{ best: number; history: SongHistory[] }> {
|
||||
const history = await this.prisma.songHistory.findMany({
|
||||
where: { user: { id: playerId }, song: { id: songId } },
|
||||
orderBy: { playDate: 'desc' },
|
||||
orderBy: { playDate: "desc" },
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -95,7 +97,7 @@ export class HistoryService {
|
||||
): Promise<SearchHistory[]> {
|
||||
return this.prisma.searchHistory.findMany({
|
||||
where: { user: { id: playerId } },
|
||||
orderBy: { searchDate: 'desc' },
|
||||
orderBy: { searchDate: "desc" },
|
||||
skip,
|
||||
take,
|
||||
});
|
||||
|
||||
@@ -12,16 +12,17 @@ import {
|
||||
Delete,
|
||||
NotFoundException,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { LessonService } from './lesson.service';
|
||||
import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
||||
import { Prisma, Skill } from '@prisma/client';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Lesson as _Lesson } from 'src/_gen/prisma-class/lesson';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { Request } from 'express';
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { LessonService } from "./lesson.service";
|
||||
import { ApiOperation, ApiProperty, ApiTags } from "@nestjs/swagger";
|
||||
import { Prisma, Skill } from "@prisma/client";
|
||||
import { FilterQuery } from "src/utils/filter.pipe";
|
||||
import { Lesson as _Lesson } from "src/_gen/prisma-class/lesson";
|
||||
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||
import { Request } from "express";
|
||||
import { AuthGuard } from "@nestjs/passport";
|
||||
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||
|
||||
export class Lesson {
|
||||
@ApiProperty()
|
||||
@@ -36,15 +37,15 @@ export class Lesson {
|
||||
mainSkill: Skill;
|
||||
}
|
||||
|
||||
@ApiTags('lessons')
|
||||
@Controller('lesson')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiTags("lessons")
|
||||
@Controller("lesson")
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
export class LessonController {
|
||||
static filterableFields: string[] = [
|
||||
'+id',
|
||||
'name',
|
||||
'+requiredLevel',
|
||||
'mainSkill',
|
||||
"+id",
|
||||
"name",
|
||||
"+requiredLevel",
|
||||
"mainSkill",
|
||||
];
|
||||
static includableFields: IncludeMap<Prisma.LessonInclude> = {
|
||||
LessonHistory: true,
|
||||
@@ -53,7 +54,7 @@ export class LessonController {
|
||||
constructor(private lessonService: LessonService) {}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Get all lessons',
|
||||
summary: "Get all lessons",
|
||||
})
|
||||
@Get()
|
||||
@ApiOkResponsePlaginated(_Lesson)
|
||||
@@ -61,9 +62,9 @@ export class LessonController {
|
||||
@Req() request: Request,
|
||||
@FilterQuery(LessonController.filterableFields)
|
||||
where: Prisma.LessonWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Lesson>> {
|
||||
const ret = await this.lessonService.getAll({
|
||||
skip,
|
||||
@@ -75,13 +76,13 @@ export class LessonController {
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Get a particular lessons',
|
||||
summary: "Get a particular lessons",
|
||||
})
|
||||
@Get(':id')
|
||||
@Get(":id")
|
||||
async get(
|
||||
@Req() req: Request,
|
||||
@Query('include') include: string,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Query("include") include: string,
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
): Promise<Lesson> {
|
||||
const ret = await this.lessonService.get(
|
||||
id,
|
||||
@@ -92,7 +93,7 @@ export class LessonController {
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Create a lessons',
|
||||
summary: "Create a lessons",
|
||||
})
|
||||
@Post()
|
||||
async post(@Body() lesson: Lesson): Promise<Lesson> {
|
||||
@@ -105,10 +106,10 @@ export class LessonController {
|
||||
}
|
||||
|
||||
@ApiOperation({
|
||||
summary: 'Delete a lessons',
|
||||
summary: "Delete a lessons",
|
||||
})
|
||||
@Delete(':id')
|
||||
async delete(@Param('id', ParseIntPipe) id: number): Promise<Lesson> {
|
||||
@Delete(":id")
|
||||
async delete(@Param("id", ParseIntPipe) id: number): Promise<Lesson> {
|
||||
try {
|
||||
return await this.lessonService.delete(id);
|
||||
} catch {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { LessonController } from './lesson.controller';
|
||||
import { LessonService } from './lesson.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { LessonController } from "./lesson.controller";
|
||||
import { LessonService } from "./lesson.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { LessonService } from './lesson.service';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { LessonService } from "./lesson.service";
|
||||
|
||||
describe('LessonService', () => {
|
||||
describe("LessonService", () => {
|
||||
let service: LessonService;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -12,7 +12,7 @@ describe('LessonService', () => {
|
||||
service = module.get<LessonService>(LessonService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Lesson, Prisma } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Lesson, Prisma } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class LessonService {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
import { AppModule } from "./app.module";
|
||||
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common';
|
||||
import { RequestLogger, RequestLoggerOptions } from 'json-logger-service';
|
||||
import { tap } from 'rxjs';
|
||||
import { PrismaModel } from './_gen/prisma-class';
|
||||
import { PrismaService } from './prisma/prisma.service';
|
||||
} from "@nestjs/common";
|
||||
import { RequestLogger, RequestLoggerOptions } from "json-logger-service";
|
||||
import { tap } from "rxjs";
|
||||
import { PrismaModel } from "./_gen/prisma-class";
|
||||
import { PrismaService } from "./prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class AspectLogger implements NestInterceptor {
|
||||
@@ -27,8 +27,8 @@ export class AspectLogger implements NestInterceptor {
|
||||
params,
|
||||
query,
|
||||
body,
|
||||
userId: user?.id ?? 'not logged in',
|
||||
username: user?.username ?? 'not logged in',
|
||||
userId: user?.id ?? "not logged in",
|
||||
username: user?.username ?? "not logged in",
|
||||
};
|
||||
|
||||
return next.handle().pipe(
|
||||
@@ -48,24 +48,24 @@ async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.use(
|
||||
RequestLogger.buildExpressRequestLogger({
|
||||
doNotLogPaths: ['/health'],
|
||||
doNotLogPaths: ["/health"],
|
||||
} as RequestLoggerOptions),
|
||||
);
|
||||
app.enableShutdownHooks();
|
||||
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Chromacase')
|
||||
.setDescription('The chromacase API')
|
||||
.setVersion('1.0')
|
||||
.setTitle("Chromacase")
|
||||
.setDescription("The chromacase API")
|
||||
.setVersion("1.0")
|
||||
.build();
|
||||
const document = SwaggerModule.createDocument(app, config, {
|
||||
extraModels: [...PrismaModel.extraModels],
|
||||
});
|
||||
SwaggerModule.setup('api', app, document);
|
||||
SwaggerModule.setup("api", app, document);
|
||||
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
app.enableCors();
|
||||
app.useGlobalInterceptors(new AspectLogger());
|
||||
//app.useGlobalInterceptors(new AspectLogger());
|
||||
|
||||
await app.listen(3000);
|
||||
}
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
* 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 {
|
||||
ApiExtraModels,
|
||||
ApiOkResponse,
|
||||
ApiProperty,
|
||||
getSchemaPath,
|
||||
} from '@nestjs/swagger';
|
||||
} from "@nestjs/swagger";
|
||||
|
||||
export class PlageMetadata {
|
||||
@ApiProperty()
|
||||
this: string;
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
type: "string",
|
||||
nullable: true,
|
||||
description: "null if there is no next page, couldn't set it in swagger",
|
||||
})
|
||||
next: string | null;
|
||||
@ApiProperty({
|
||||
type: 'string',
|
||||
type: "string",
|
||||
nullable: true,
|
||||
description:
|
||||
"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) {
|
||||
this.data = data;
|
||||
let take = Number(request.query['take'] ?? 20).valueOf();
|
||||
let take = Number(request.query["take"] ?? 20).valueOf();
|
||||
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) {
|
||||
skipped += take - (skipped % take);
|
||||
}
|
||||
@@ -81,7 +81,7 @@ export const ApiOkResponsePlaginated = <DataDto extends Type<unknown>>(
|
||||
{
|
||||
properties: {
|
||||
data: {
|
||||
type: 'array',
|
||||
type: "array",
|
||||
items: { $ref: getSchemaPath(dataDto) },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class Setting {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class User {
|
||||
@ApiProperty()
|
||||
@@ -11,4 +11,6 @@ export class User {
|
||||
isGuest: boolean;
|
||||
@ApiProperty()
|
||||
partyPlayed: number;
|
||||
@ApiProperty()
|
||||
totalScore: number;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { PrismaService } from "./prisma.service";
|
||||
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { Test, TestingModule } from "@nestjs/testing";
|
||||
import { PrismaService } from "./prisma.service";
|
||||
|
||||
describe('PrismaService', () => {
|
||||
describe("PrismaService", () => {
|
||||
let service: PrismaService;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -12,7 +12,7 @@ describe('PrismaService', () => {
|
||||
service = module.get<PrismaService>(PrismaService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
it("should be defined", () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { Injectable, OnModuleInit } from "@nestjs/common";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||
|
||||
16
back/src/scores/scores.controller.ts
Normal file
16
back/src/scores/scores.controller.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Controller, Get } 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();
|
||||
}
|
||||
}
|
||||
11
back/src/scores/scores.module.ts
Normal file
11
back/src/scores/scores.module.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
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 {}
|
||||
17
back/src/scores/scores.service.ts
Normal file
17
back/src/scores/scores.service.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class SearchSongDto {
|
||||
@ApiProperty()
|
||||
|
||||
29
back/src/search/meilisearch.service.ts
Normal file
29
back/src/search/meilisearch.service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
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"],
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,101 +1,73 @@
|
||||
import {
|
||||
Controller,
|
||||
DefaultValuePipe,
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Query,
|
||||
Request,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiOkResponse,
|
||||
ApiOperation,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { Artist, Genre, Song } from '@prisma/client';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { SearchService } from './search.service';
|
||||
import { Song as _Song } from 'src/_gen/prisma-class/song';
|
||||
import { Genre as _Genre } from 'src/_gen/prisma-class/genre';
|
||||
import { Artist as _Artist } from 'src/_gen/prisma-class/artist';
|
||||
import { mapInclude } from 'src/utils/include';
|
||||
import { SongController } from 'src/song/song.controller';
|
||||
import { GenreController } from 'src/genre/genre.controller';
|
||||
import { ArtistController } from 'src/artist/artist.controller';
|
||||
} from "@nestjs/swagger";
|
||||
import { Artist, Song } from "@prisma/client";
|
||||
import { JwtAuthGuard } from "src/auth/jwt-auth.guard";
|
||||
import { SearchService } from "./search.service";
|
||||
import { Song as _Song } from "src/_gen/prisma-class/song";
|
||||
import { Artist as _Artist } from "src/_gen/prisma-class/artist";
|
||||
import { mapInclude } from "src/utils/include";
|
||||
import { SongController } from "src/song/song.controller";
|
||||
import { ArtistController } from "src/artist/artist.controller";
|
||||
|
||||
@ApiTags('search')
|
||||
@Controller('search')
|
||||
@ApiTags("search")
|
||||
@Controller("search")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class SearchController {
|
||||
constructor(private readonly searchService: SearchService) {}
|
||||
|
||||
@Get('songs/:query')
|
||||
@Get("songs/:query")
|
||||
@ApiOkResponse({ type: _Song, isArray: true })
|
||||
@ApiOperation({ description: 'Search a song' })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOperation({ description: "Search a song" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
async searchSong(
|
||||
@Request() req: any,
|
||||
@Query('include') include: string,
|
||||
@Param('query') query: string,
|
||||
): Promise<Song[] | null> {
|
||||
try {
|
||||
const ret = await this.searchService.songByGuess(
|
||||
query,
|
||||
req.user?.id,
|
||||
mapInclude(include, req, SongController.includableFields),
|
||||
);
|
||||
if (!ret.length) throw new NotFoundException();
|
||||
else return ret;
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException();
|
||||
}
|
||||
@Query("q") query: string | null,
|
||||
@Query("artistId") artistId: number,
|
||||
@Query("genreId") genreId: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Song[]> {
|
||||
return await this.searchService.searchSong(
|
||||
query ?? "",
|
||||
artistId,
|
||||
genreId,
|
||||
mapInclude(include, req, SongController.includableFields),
|
||||
skip,
|
||||
take,
|
||||
);
|
||||
}
|
||||
|
||||
@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')
|
||||
@Get("artists/:query")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOkResponse({ type: _Artist, isArray: true })
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
@ApiOperation({ description: 'Search an artist' })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@ApiOperation({ description: "Search an artist" })
|
||||
async searchArtists(
|
||||
@Request() req: any,
|
||||
@Query('include') include: string,
|
||||
@Param('query') query: string,
|
||||
): Promise<Artist[] | null> {
|
||||
try {
|
||||
const ret = await this.searchService.artistByGuess(
|
||||
query,
|
||||
req.user?.id,
|
||||
mapInclude(include, req, ArtistController.includableFields),
|
||||
);
|
||||
if (!ret.length) throw new NotFoundException();
|
||||
else return ret;
|
||||
} catch (error) {
|
||||
throw new InternalServerErrorException();
|
||||
}
|
||||
@Query("include") include: string,
|
||||
@Query("q") query: string | null,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Artist[]> {
|
||||
return await this.searchService.searchArtists(
|
||||
query ?? "",
|
||||
mapInclude(include, req, ArtistController.includableFields),
|
||||
skip,
|
||||
take,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SearchService } from './search.service';
|
||||
import { SearchController } from './search.controller';
|
||||
import { HistoryModule } from 'src/history/history.module';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { SongService } from 'src/song/song.service';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { SearchService } from "./search.service";
|
||||
import { SearchController } from "./search.controller";
|
||||
import { HistoryModule } from "src/history/history.module";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
import { SongService } from "src/song/song.service";
|
||||
import { MeiliService } from "./meilisearch.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, HistoryModule],
|
||||
controllers: [SearchController],
|
||||
providers: [SearchService, SongService],
|
||||
exports: [SearchService],
|
||||
providers: [SearchService, SongService, MeiliService],
|
||||
exports: [SearchService, MeiliService],
|
||||
})
|
||||
export class SearchModule {}
|
||||
|
||||
@@ -1,51 +1,84 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Artist, Prisma, Song, Genre } from '@prisma/client';
|
||||
import { HistoryService } from 'src/history/history.service';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Artist, Prisma, Song, Genre } from "@prisma/client";
|
||||
import { HistoryService } from "src/history/history.service";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { MeiliService } from "./meilisearch.service";
|
||||
|
||||
@Injectable()
|
||||
export class SearchService {
|
||||
constructor(
|
||||
private prisma: PrismaService,
|
||||
private history: HistoryService,
|
||||
private search: MeiliService,
|
||||
) {}
|
||||
|
||||
async songByGuess(
|
||||
async searchSong(
|
||||
query: string,
|
||||
userID: number,
|
||||
artistId?: number,
|
||||
genreId?: number,
|
||||
include?: Prisma.SongInclude,
|
||||
skip?: number,
|
||||
take?: number,
|
||||
): Promise<Song[]> {
|
||||
return this.prisma.song.findMany({
|
||||
where: {
|
||||
name: { contains: query, mode: 'insensitive' },
|
||||
},
|
||||
include,
|
||||
});
|
||||
if (query.length === 0) {
|
||||
return await this.prisma.song.findMany({
|
||||
where: {
|
||||
artistId,
|
||||
genreId,
|
||||
},
|
||||
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 genreByGuess(
|
||||
async searchArtists(
|
||||
query: string,
|
||||
userID: number,
|
||||
include?: Prisma.GenreInclude,
|
||||
): Promise<Genre[]> {
|
||||
return this.prisma.genre.findMany({
|
||||
where: {
|
||||
name: { contains: query, mode: 'insensitive' },
|
||||
},
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
async artistByGuess(
|
||||
query: string,
|
||||
userID: number,
|
||||
include?: Prisma.ArtistInclude,
|
||||
skip?: number,
|
||||
take?: number,
|
||||
): Promise<Artist[]> {
|
||||
return this.prisma.artist.findMany({
|
||||
where: {
|
||||
name: { contains: query, mode: 'insensitive' },
|
||||
},
|
||||
include,
|
||||
});
|
||||
if (query.length === 0) {
|
||||
return this.prisma.artist.findMany({
|
||||
take,
|
||||
skip,
|
||||
include,
|
||||
});
|
||||
}
|
||||
const ids = (
|
||||
await this.search.index("artists").search(query, {
|
||||
limit: take,
|
||||
offset: skip,
|
||||
})
|
||||
).hits.map((x) => x.id);
|
||||
|
||||
return (
|
||||
await this.prisma.artist.findMany({
|
||||
where: {
|
||||
id: { in: ids },
|
||||
},
|
||||
include,
|
||||
})
|
||||
).sort((x) => ids.indexOf(x.id));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class UpdateSettingDto {
|
||||
@ApiProperty()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SettingsService } from './settings.service';
|
||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||
import { Module } from "@nestjs/common";
|
||||
import { SettingsService } from "./settings.service";
|
||||
import { PrismaModule } from "src/prisma/prisma.module";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, UserSettings } from '@prisma/client';
|
||||
import { PrismaService } from 'src/prisma/prisma.service';
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Prisma, UserSettings } from "@prisma/client";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
|
||||
@Injectable()
|
||||
export class SettingsService {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsNotEmpty } from "class-validator";
|
||||
|
||||
export class CreateSongDto {
|
||||
@IsNotEmpty()
|
||||
|
||||
@@ -15,13 +15,14 @@ import {
|
||||
Req,
|
||||
StreamableFile,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
||||
import { CreateSongDto } from './dto/create-song.dto';
|
||||
import { SongService } from './song.service';
|
||||
import { Request } from 'express';
|
||||
import { Prisma, Song } from '@prisma/client';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
Header,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||
import { CreateSongDto } from "./dto/create-song.dto";
|
||||
import { SongService } from "./song.service";
|
||||
import { Request } from "express";
|
||||
import { Prisma, Song } from "@prisma/client";
|
||||
import { createReadStream, existsSync, readFileSync } from "fs";
|
||||
import {
|
||||
ApiNotFoundResponse,
|
||||
ApiOkResponse,
|
||||
@@ -29,15 +30,15 @@ import {
|
||||
ApiProperty,
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from '@nestjs/swagger';
|
||||
import { HistoryService } from 'src/history/history.service';
|
||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
||||
import { Song as _Song } from 'src/_gen/prisma-class/song';
|
||||
import { SongHistory } from 'src/_gen/prisma-class/song_history';
|
||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
||||
import { Public } from 'src/auth/public';
|
||||
|
||||
} from "@nestjs/swagger";
|
||||
import { HistoryService } from "src/history/history.service";
|
||||
import { JwtAuthGuard } from "src/auth/jwt-auth.guard";
|
||||
import { FilterQuery } from "src/utils/filter.pipe";
|
||||
import { Song as _Song } from "src/_gen/prisma-class/song";
|
||||
import { SongHistory } from "src/_gen/prisma-class/song_history";
|
||||
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||
import { Public } from "src/auth/public";
|
||||
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||
class SongHistoryResult {
|
||||
@ApiProperty()
|
||||
best: number;
|
||||
@@ -45,16 +46,16 @@ class SongHistoryResult {
|
||||
history: SongHistory[];
|
||||
}
|
||||
|
||||
@Controller('song')
|
||||
@ApiTags('song')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller("song")
|
||||
@ApiTags("song")
|
||||
@UseGuards(ChromaAuthGuard)
|
||||
export class SongController {
|
||||
static filterableFields: string[] = [
|
||||
'+id',
|
||||
'name',
|
||||
'+artistId',
|
||||
'+albumId',
|
||||
'+genreId',
|
||||
"+id",
|
||||
"name",
|
||||
"+artistId",
|
||||
"+albumId",
|
||||
"+genreId",
|
||||
];
|
||||
static includableFields: IncludeMap<Prisma.SongInclude> = {
|
||||
artist: true,
|
||||
@@ -69,36 +70,37 @@ export class SongController {
|
||||
private readonly historyService: HistoryService,
|
||||
) {}
|
||||
|
||||
@Get(':id/midi')
|
||||
@ApiOperation({ description: 'Streams the midi file of the requested song' })
|
||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
||||
@ApiOkResponse({ description: 'Returns the midi file succesfully' })
|
||||
async getMidi(@Param('id', ParseIntPipe) id: number) {
|
||||
@Get(":id/midi")
|
||||
@ApiOperation({ description: "Streams the midi file of the requested song" })
|
||||
@ApiNotFoundResponse({ description: "Song not found" })
|
||||
@ApiOkResponse({ description: "Returns the midi file succesfully" })
|
||||
async getMidi(@Param("id", ParseIntPipe) id: number) {
|
||||
const song = await this.songService.song({ id });
|
||||
if (!song) throw new NotFoundException('Song not found');
|
||||
if (!song) throw new NotFoundException("Song not found");
|
||||
|
||||
try {
|
||||
const file = createReadStream(song.midiPath);
|
||||
return new StreamableFile(file, { type: 'audio/midi' });
|
||||
return new StreamableFile(file, { type: "audio/midi" });
|
||||
} catch {
|
||||
throw new InternalServerErrorException();
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/illustration')
|
||||
@Get(":id/illustration")
|
||||
@ApiOperation({
|
||||
description: 'Streams the illustration of the requested song',
|
||||
description: "Streams the illustration of the requested song",
|
||||
})
|
||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
||||
@ApiOkResponse({ description: 'Returns the illustration succesfully' })
|
||||
@ApiNotFoundResponse({ description: "Song not found" })
|
||||
@ApiOkResponse({ description: "Returns the illustration succesfully" })
|
||||
@Header("Cache-Control", "max-age=86400")
|
||||
@Public()
|
||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
||||
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||
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 (!existsSync(song.illustrationPath))
|
||||
throw new NotFoundException('Illustration not found');
|
||||
throw new NotFoundException("Illustration not found");
|
||||
|
||||
try {
|
||||
const file = createReadStream(song.illustrationPath);
|
||||
@@ -108,24 +110,102 @@ export class SongController {
|
||||
}
|
||||
}
|
||||
|
||||
@Get(':id/musicXml')
|
||||
@Get(":id/musicXml")
|
||||
@ApiOperation({
|
||||
description: 'Streams the musicXML file of the requested song',
|
||||
description: "Streams the musicXML file of the requested song",
|
||||
})
|
||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
||||
@ApiOkResponse({ description: 'Returns the musicXML file succesfully' })
|
||||
async getMusicXml(@Param('id', ParseIntPipe) id: number) {
|
||||
@ApiNotFoundResponse({ description: "Song not found" })
|
||||
@ApiOkResponse({ description: "Returns the musicXML file succesfully" })
|
||||
async getMusicXml(@Param("id", ParseIntPipe) id: number) {
|
||||
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);
|
||||
}
|
||||
|
||||
@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()
|
||||
@ApiOperation({
|
||||
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) {
|
||||
try {
|
||||
@@ -148,13 +228,13 @@ export class SongController {
|
||||
}
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ description: 'delete a song by id' })
|
||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
||||
@Delete(":id")
|
||||
@ApiOperation({ description: "delete a song by id" })
|
||||
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||
try {
|
||||
return await this.songService.deleteSong({ id });
|
||||
} catch {
|
||||
throw new NotFoundException('Invalid ID');
|
||||
throw new NotFoundException("Invalid ID");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,9 +243,9 @@ export class SongController {
|
||||
async findAll(
|
||||
@Req() req: Request,
|
||||
@FilterQuery(SongController.filterableFields) where: Prisma.SongWhereInput,
|
||||
@Query('include') include: string,
|
||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Plage<Song>> {
|
||||
const ret = await this.songService.songs({
|
||||
skip,
|
||||
@@ -176,14 +256,14 @@ export class SongController {
|
||||
return new Plage(ret, req);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ description: 'Get a specific song data' })
|
||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
||||
@ApiOkResponse({ type: _Song, description: 'Requested song' })
|
||||
@Get(":id")
|
||||
@ApiOperation({ description: "Get a specific song data" })
|
||||
@ApiNotFoundResponse({ description: "Song not found" })
|
||||
@ApiOkResponse({ type: _Song, description: "Requested song" })
|
||||
async findOne(
|
||||
@Req() req: Request,
|
||||
@Param('id', ParseIntPipe) id: number,
|
||||
@Query('include') include: string,
|
||||
@Param("id", ParseIntPipe) id: number,
|
||||
@Query("include") include: string,
|
||||
) {
|
||||
const res = await this.songService.song(
|
||||
{
|
||||
@@ -192,21 +272,22 @@ export class SongController {
|
||||
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;
|
||||
}
|
||||
|
||||
@Get(':id/history')
|
||||
@Get(":id/history")
|
||||
@HttpCode(200)
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@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({
|
||||
type: SongHistoryResult,
|
||||
description: 'Records of previous games of the user',
|
||||
description: "Records of previous games of the user",
|
||||
})
|
||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
||||
async getHistory(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
async getHistory(@Req() req: any, @Param("id", ParseIntPipe) id: number) {
|
||||
return this.historyService.getForSong({
|
||||
playerId: req.user.id,
|
||||
songId: id,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user