Compare commits
344 Commits
front/nati
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74f804bb9a | ||
|
|
0b78772d0b | ||
|
|
9e3c2d1cca | ||
|
|
1b097163a4 | ||
|
|
c61d17baa7 | ||
|
|
be8867e12f | ||
|
|
00f98151c1 | ||
|
|
5d78d8b5dd | ||
|
|
0bd12bbf34 | ||
|
|
88cb7b2b65 | ||
|
|
69329118f7 | ||
|
|
2781276c12 | ||
|
|
a24a960184 | ||
|
|
9fd70d3110 | ||
|
|
c1d714e02a | ||
|
|
c08a1a2c74 | ||
|
|
23a1ff8d19 | ||
|
|
b80167001f | ||
|
|
8c2a53aa41 | ||
|
|
dcca780f2d | ||
|
|
9150817c05 | ||
|
|
d57606dd53 | ||
|
|
52f2c94fb7 | ||
|
|
1952625098 | ||
|
|
10dbfda8a4 | ||
|
|
234335cf61 | ||
|
|
52d40b43f0 | ||
|
|
50522bbe63 | ||
|
|
ce927ea1a4 | ||
|
|
aebf409cea | ||
|
|
5f0ea41c04 | ||
|
|
d3c7e4a0a1 | ||
|
|
a3893bdb2b | ||
|
|
4ba4303b1e | ||
| e779876f54 | |||
| bd9edaa60e | |||
|
|
f2ad34c8ab | ||
|
|
131d7bf688 | ||
|
|
d44e75a83a | ||
| e487d6d91e | |||
| 7a63a66da5 | |||
| 17f64cd849 | |||
| ec17aa741f | |||
|
|
38110d2840 | ||
|
|
fd60f2d171 | ||
|
|
86b2c1be50 | ||
|
|
627b8df658 | ||
|
|
3f0d0d523b | ||
|
|
29a9ffce74 | ||
|
|
a69e5ac009 | ||
|
|
caa3322676 | ||
|
|
358841abd5 | ||
|
|
64e7dbc71e | ||
|
|
5a0809c1d0 | ||
|
|
4b5e3d2b04 | ||
|
|
5f24c6e7bd | ||
|
|
8bdf8ce334 | ||
|
|
9012a6a9d8 | ||
|
|
c5fd4aa7d5 | ||
|
|
65cd04a494 | ||
|
|
c79ae7c6e8 | ||
|
|
ddc97f0923 | ||
|
|
a9b902a427 | ||
|
|
96d8e649c8 | ||
|
|
22c93b7571 | ||
|
|
0644d4b580 | ||
|
|
ee6a76cdd9 | ||
|
|
934010a0c1 | ||
|
|
29b2bedae0 | ||
|
|
5ba815590a | ||
|
|
dd09827d08 | ||
| b5b94adc83 | |||
| 3c04e8bb39 | |||
| 17a4328af5 | |||
| e81f2c1f75 | |||
| f77874bec4 | |||
| cfc72b8bc1 | |||
| 359b20fc6d | |||
| a3659618ea | |||
| fa60fc65a9 | |||
| b1727b7838 | |||
| a3f4703dae | |||
| 038918c212 | |||
| 42a947dfb0 | |||
| 5525110d39 | |||
| 7160b77607 | |||
| b5183f84b4 | |||
|
|
7a2b877714 | ||
|
|
9416393618 | ||
|
|
eb245118dc | ||
|
|
13050e52f9 | ||
|
|
5ef3885f72 | ||
|
|
a103666caf | ||
|
|
29da5c2788 | ||
|
|
40f16ab9ca | ||
|
|
a33d56bd61 | ||
|
|
c7c9250594 | ||
|
|
1b1659fe92 | ||
|
|
3c9d71a757 | ||
|
|
342099157e | ||
|
|
bb7a17fc22 | ||
|
|
1880b89b0c | ||
|
|
e769ff1f13 | ||
|
|
0ea8cb86bb | ||
|
|
90f9574a6f | ||
|
|
f2f7ec3f8d | ||
|
|
9e7873cdd7 | ||
|
|
f46c2cfb4a | ||
|
|
9f14061efd | ||
|
|
88b111529b | ||
|
|
851ee7420f | ||
|
|
ef57eb752d | ||
|
|
fcb29ae484 | ||
|
|
5c4847ae2c | ||
|
|
60a73781bd | ||
|
|
4e3b378d6a | ||
|
|
2bf1e783a9 | ||
|
|
375d36f6c5 | ||
|
|
495380ec43 | ||
|
|
af0531bb0c | ||
|
|
c5124fa6ad | ||
|
|
962cf58e77 | ||
|
|
60988dd599 | ||
|
|
004a541302 | ||
|
|
f4cd9e18ea | ||
|
|
2dc301addf | ||
|
|
e85a959c26 | ||
|
|
5fc937d81b | ||
|
|
b3853646cb | ||
|
|
339e808d27 | ||
|
|
22d1a97abd | ||
|
|
ce4baa61dc | ||
|
|
e90c7f05a8 | ||
|
|
fb0e43af88 | ||
|
|
4577997b1c | ||
|
|
9bb256f2ee | ||
|
|
d3994ff26e | ||
|
|
00d097f643 | ||
|
|
99da77f23e | ||
|
|
7a6dc8b0c9 | ||
|
|
b4f04f9b71 | ||
|
|
9df0c98100 | ||
|
|
a47f8744f8 | ||
|
|
dac9849ef5 | ||
|
|
11ed8f90fd | ||
|
|
5d103c6687 | ||
| 80329e240e | |||
| 70b109e78b | |||
| a6a96d6a1e | |||
| cc4b69ca50 | |||
|
|
e733c6acc8 | ||
|
|
afa6f421d3 | ||
|
|
7d7f886661 | ||
|
|
be926dcaed | ||
|
|
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
|
SMTP_TRANSPORT=smtps://toto:tata@relay
|
||||||
MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>'
|
MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>'
|
||||||
IGNORE_MAILS=true
|
IGNORE_MAILS=true
|
||||||
|
API_KEYS=SCOROTEST,ROBOTO,SCORO,POPULATE
|
||||||
|
API_KEY_SCORO_TEST=SCOROTEST
|
||||||
|
API_KEY_ROBOT=ROBOTO
|
||||||
|
API_KEY_SCORO=SCORO
|
||||||
|
API_KEY_POPULATE=POPULATE
|
||||||
|
MEILI_MASTER_KEY="ghvjkgisbgkbgskegblfqbgjkebbhgwkjfb"
|
||||||
|
# vi: ft=sh
|
||||||
|
|||||||
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:
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- closed
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- '*'
|
- main
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
deployment:
|
||||||
## Build Back ##
|
|
||||||
|
|
||||||
Build_Back:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
if: github.event.pull_request.merged == true
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./back
|
|
||||||
environment: Staging
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Build Docker
|
|
||||||
run: docker build -t testback .
|
|
||||||
|
|
||||||
## Build App ##
|
|
||||||
|
|
||||||
Check_Front:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./front
|
|
||||||
environment: Staging
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Install Yarn
|
|
||||||
run: npm install -g yarn
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install
|
|
||||||
- name: Type Check
|
|
||||||
run: yarn tsc
|
|
||||||
- name: Check Prettier
|
|
||||||
run: yarn pretty:check .
|
|
||||||
- name: Run Linter
|
|
||||||
run: yarn lint
|
|
||||||
|
|
||||||
Build_Front:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./front
|
|
||||||
environment: Staging
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Install Yarn
|
|
||||||
run: npm install -g yarn
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install
|
|
||||||
|
|
||||||
- name: 🏗 Setup Expo
|
|
||||||
uses: expo/expo-github-action@v7
|
|
||||||
with:
|
|
||||||
expo-version: latest
|
|
||||||
eas-version: 3.3.1
|
|
||||||
token: ${{ secrets.EXPO_TOKEN }}
|
|
||||||
- name: Build Web App
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
context: ./front
|
|
||||||
push: false
|
|
||||||
tags: ${{steps.meta_front.outputs.tags}}
|
|
||||||
build-args: |
|
|
||||||
API_URL=${{secrets.API_URL}}
|
|
||||||
SCORO_URL=${{secrets.SCORO_URL}}
|
|
||||||
|
|
||||||
- name: Build Android APK
|
|
||||||
run: |
|
|
||||||
eas build -p android --profile production --local --non-interactive
|
|
||||||
mv *.apk chromacase.apk
|
|
||||||
|
|
||||||
- name: Upload Artifact
|
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: chromacase.apk
|
|
||||||
path: front/
|
|
||||||
|
|
||||||
## Test Backend ##
|
|
||||||
|
|
||||||
Test_Back:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
needs: [ Build_Back ]
|
|
||||||
environment: Staging
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Copy env file to github secret env file
|
|
||||||
run: cp .env.example .env
|
|
||||||
|
|
||||||
- name: Start the service
|
|
||||||
run: docker-compose up -d back db
|
|
||||||
|
|
||||||
- name: Perform healthchecks
|
|
||||||
run: |
|
|
||||||
docker-compose ps -a
|
|
||||||
docker-compose logs
|
|
||||||
wget --retry-connrefused http://localhost:3000 || (docker-compose logs && exit 1)
|
|
||||||
|
|
||||||
- name: Run scorometer tests
|
|
||||||
run: |
|
|
||||||
pip install -r scorometer/requirements.txt
|
|
||||||
cd scorometer/tests && ./runner.sh
|
|
||||||
|
|
||||||
- name: Run robot tests
|
|
||||||
run: |
|
|
||||||
pip install -r back/test/robot/requirements.txt
|
|
||||||
robot -d out back/test/robot/
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
if: always()
|
|
||||||
with:
|
|
||||||
name: results
|
|
||||||
path: out
|
|
||||||
|
|
||||||
- name: Write results to Pull Request and Summary
|
|
||||||
if: always() && github.event_name == 'pull_request'
|
|
||||||
uses: joonvena/robotframework-reporter-action@v2.1
|
|
||||||
with:
|
|
||||||
report_path: out/
|
|
||||||
gh_access_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
only_summary: false
|
|
||||||
|
|
||||||
- name: Write results to Summary
|
|
||||||
if: always() && github.event_name != 'pull_request'
|
|
||||||
uses: joonvena/robotframework-reporter-action@v2.1
|
|
||||||
with:
|
|
||||||
report_path: out/
|
|
||||||
gh_access_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
only_summary: true
|
|
||||||
|
|
||||||
- name: Remove .env && stop the service
|
|
||||||
run: docker-compose down && rm .env
|
|
||||||
|
|
||||||
## Test App ##
|
|
||||||
|
|
||||||
## Deployement ##
|
|
||||||
|
|
||||||
Deployement_Docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: github.ref == 'refs/heads/main'
|
|
||||||
needs: [ Test_Back ]
|
|
||||||
environment: Production
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -203,6 +57,7 @@ jobs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
API_URL=${{secrets.API_URL}}
|
API_URL=${{secrets.API_URL}}
|
||||||
SCORO_URL=${{secrets.SCORO_URL}}
|
SCORO_URL=${{secrets.SCORO_URL}}
|
||||||
|
|
||||||
- name: Docker meta scorometer
|
- name: Docker meta scorometer
|
||||||
id: meta_scorometer
|
id: meta_scorometer
|
||||||
uses: docker/metadata-action@v4
|
uses: docker/metadata-action@v4
|
||||||
|
|||||||
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
|
.data
|
||||||
.DS_Store
|
.DS_Store
|
||||||
_gen
|
_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
|
from configparser import ConfigParser
|
||||||
|
|
||||||
url = os.environ.get("API_URL")
|
url = os.environ.get("API_URL")
|
||||||
|
api_key = os.environ.get("API_KEY_POPULATE")
|
||||||
|
auth_headers = {}
|
||||||
|
auth_headers["Authorization"] = f"API Key {api_key}"
|
||||||
|
|
||||||
|
|
||||||
def getOrCreateAlbum(name, artistId):
|
def getOrCreateAlbum(name, artistId):
|
||||||
if not name:
|
if not name:
|
||||||
@@ -15,7 +19,7 @@ def getOrCreateAlbum(name, artistId):
|
|||||||
res = requests.post(f"{url}/album", json={
|
res = requests.post(f"{url}/album", json={
|
||||||
"name": name,
|
"name": name,
|
||||||
"artist": artistId,
|
"artist": artistId,
|
||||||
})
|
},headers=auth_headers)
|
||||||
out = res.json()
|
out = res.json()
|
||||||
print(out)
|
print(out)
|
||||||
return out["id"]
|
return out["id"]
|
||||||
@@ -25,7 +29,7 @@ def getOrCreateGenre(names):
|
|||||||
for name in names.split(","):
|
for name in names.split(","):
|
||||||
res = requests.post(f"{url}/genre", json={
|
res = requests.post(f"{url}/genre", json={
|
||||||
"name": name,
|
"name": name,
|
||||||
})
|
},headers=auth_headers)
|
||||||
out = res.json()
|
out = res.json()
|
||||||
print(out)
|
print(out)
|
||||||
ids += [out["id"]]
|
ids += [out["id"]]
|
||||||
@@ -35,7 +39,7 @@ def getOrCreateGenre(names):
|
|||||||
def getOrCreateArtist(name):
|
def getOrCreateArtist(name):
|
||||||
res = requests.post(f"{url}/artist", json={
|
res = requests.post(f"{url}/artist", json={
|
||||||
"name": name,
|
"name": name,
|
||||||
})
|
},headers=auth_headers)
|
||||||
out = res.json()
|
out = res.json()
|
||||||
print(out)
|
print(out)
|
||||||
return out["id"]
|
return out["id"]
|
||||||
@@ -49,6 +53,7 @@ def populateFile(path, midi, mxl):
|
|||||||
difficulties["length"] = round((mid.length), 2)
|
difficulties["length"] = round((mid.length), 2)
|
||||||
artistId = getOrCreateArtist(metadata["Artist"])
|
artistId = getOrCreateArtist(metadata["Artist"])
|
||||||
print(f"Populating {metadata['Name']}")
|
print(f"Populating {metadata['Name']}")
|
||||||
|
print(auth_headers)
|
||||||
res = requests.post(f"{url}/song", json={
|
res = requests.post(f"{url}/song", json={
|
||||||
"name": metadata["Name"],
|
"name": metadata["Name"],
|
||||||
"midiPath": f"/assets/{midi}",
|
"midiPath": f"/assets/{midi}",
|
||||||
@@ -58,9 +63,10 @@ def populateFile(path, midi, mxl):
|
|||||||
"album": getOrCreateAlbum(metadata["Album"], artistId),
|
"album": getOrCreateAlbum(metadata["Album"], artistId),
|
||||||
"genre": getOrCreateGenre(metadata["Genre"]),
|
"genre": getOrCreateGenre(metadata["Genre"]),
|
||||||
"illustrationPath": f"/assets/{os.path.commonpath([midi, mxl])}/illustration.png"
|
"illustrationPath": f"/assets/{os.path.commonpath([midi, mxl])}/illustration.png"
|
||||||
})
|
}, headers=auth_headers)
|
||||||
print(res.json())
|
print(res.json())
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global url
|
global url
|
||||||
if url == None:
|
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"
|
"trailingComma": "all"
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
FROM node:17
|
FROM node:18.10.0
|
||||||
WORKDIR /app
|
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
|
||||||
|
|||||||
3201
back/package-lock.json
generated
3201
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/bcryptjs": "^2.4.2",
|
||||||
"@types/passport": "^1.0.12",
|
"@types/passport": "^1.0.12",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
|
"canvas": "^2.11.2",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.13.2",
|
|
||||||
"json-logger-service": "^9.0.1",
|
|
||||||
"class-validator": "^0.14.0",
|
"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",
|
"node-fetch": "^2.6.12",
|
||||||
"nodemailer": "^6.9.5",
|
"nodemailer": "^6.9.5",
|
||||||
|
"opensheetmusicdisplay": "^1.8.4",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
|
"passport-headerapikey": "^1.2.2",
|
||||||
"passport-jwt": "^4.0.1",
|
"passport-jwt": "^4.0.1",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
"prisma-class-generator": "^0.2.7",
|
"prisma-class-generator": "^0.2.7",
|
||||||
@@ -81,7 +87,8 @@
|
|||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
"js",
|
"js",
|
||||||
"json",
|
"json",
|
||||||
"ts"
|
"ts",
|
||||||
|
"mjs"
|
||||||
],
|
],
|
||||||
"rootDir": "src",
|
"rootDir": "src",
|
||||||
"testRegex": ".*\\.spec\\.ts$",
|
"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
|
googleID String? @unique
|
||||||
isGuest Boolean @default(false)
|
isGuest Boolean @default(false)
|
||||||
partyPlayed Int @default(0)
|
partyPlayed Int @default(0)
|
||||||
|
totalScore Int @default(0)
|
||||||
LessonHistory LessonHistory[]
|
LessonHistory LessonHistory[]
|
||||||
SongHistory SongHistory[]
|
SongHistory SongHistory[]
|
||||||
searchHistory SearchHistory[]
|
searchHistory SearchHistory[]
|
||||||
|
|||||||
@@ -12,23 +12,24 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
Req,
|
Req,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from "@nestjs/common";
|
||||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||||
import { CreateAlbumDto } from './dto/create-album.dto';
|
import { CreateAlbumDto } from "./dto/create-album.dto";
|
||||||
import { AlbumService } from './album.service';
|
import { AlbumService } from "./album.service";
|
||||||
import { Request } from 'express';
|
import { Request } from "express";
|
||||||
import { Prisma, Album } from '@prisma/client';
|
import { Prisma, Album } from "@prisma/client";
|
||||||
import { ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiOkResponse, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
import { FilterQuery } from "src/utils/filter.pipe";
|
||||||
import { Album as _Album } from 'src/_gen/prisma-class/album';
|
import { Album as _Album } from "src/_gen/prisma-class/album";
|
||||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||||
|
|
||||||
@Controller('album')
|
@Controller("album")
|
||||||
@ApiTags('album')
|
@ApiTags("album")
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(ChromaAuthGuard)
|
||||||
export class AlbumController {
|
export class AlbumController {
|
||||||
static filterableFields: string[] = ['+id', 'name', '+artistId'];
|
static filterableFields: string[] = ["+id", "name", "+artistId"];
|
||||||
static includableFields: IncludeMap<Prisma.AlbumInclude> = {
|
static includableFields: IncludeMap<Prisma.AlbumInclude> = {
|
||||||
artist: true,
|
artist: true,
|
||||||
Song: true,
|
Song: true,
|
||||||
@@ -38,7 +39,7 @@ export class AlbumController {
|
|||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description: 'Register a new album, should not be used by frontend',
|
description: "Register a new album, should not be used by frontend",
|
||||||
})
|
})
|
||||||
async create(@Body() createAlbumDto: CreateAlbumDto) {
|
async create(@Body() createAlbumDto: CreateAlbumDto) {
|
||||||
try {
|
try {
|
||||||
@@ -55,26 +56,26 @@ export class AlbumController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(":id")
|
||||||
@ApiOperation({ description: 'Delete an album by id' })
|
@ApiOperation({ description: "Delete an album by id" })
|
||||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||||
try {
|
try {
|
||||||
return await this.albumService.deleteAlbum({ id });
|
return await this.albumService.deleteAlbum({ id });
|
||||||
} catch {
|
} catch {
|
||||||
throw new NotFoundException('Invalid ID');
|
throw new NotFoundException("Invalid ID");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOkResponsePlaginated(_Album)
|
@ApiOkResponsePlaginated(_Album)
|
||||||
@ApiOperation({ description: 'Get all albums paginated' })
|
@ApiOperation({ description: "Get all albums paginated" })
|
||||||
async findAll(
|
async findAll(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@FilterQuery(AlbumController.filterableFields)
|
@FilterQuery(AlbumController.filterableFields)
|
||||||
where: Prisma.AlbumWhereInput,
|
where: Prisma.AlbumWhereInput,
|
||||||
@Query('include') include: string,
|
@Query("include") include: string,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<Plage<Album>> {
|
): Promise<Plage<Album>> {
|
||||||
const ret = await this.albumService.albums({
|
const ret = await this.albumService.albums({
|
||||||
skip,
|
skip,
|
||||||
@@ -85,20 +86,20 @@ export class AlbumController {
|
|||||||
return new Plage(ret, req);
|
return new Plage(ret, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(":id")
|
||||||
@ApiOperation({ description: 'Get an album by id' })
|
@ApiOperation({ description: "Get an album by id" })
|
||||||
@ApiOkResponse({ type: _Album })
|
@ApiOkResponse({ type: _Album })
|
||||||
async findOne(
|
async findOne(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@Query('include') include: string,
|
@Query("include") include: string,
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param("id", ParseIntPipe) id: number,
|
||||||
) {
|
) {
|
||||||
const res = await this.albumService.album(
|
const res = await this.albumService.album(
|
||||||
{ id },
|
{ id },
|
||||||
mapInclude(include, req, AlbumController.includableFields),
|
mapInclude(include, req, AlbumController.includableFields),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res === null) throw new NotFoundException('Album not found');
|
if (res === null) throw new NotFoundException("Album not found");
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
import { AlbumController } from './album.controller';
|
import { AlbumController } from "./album.controller";
|
||||||
import { AlbumService } from './album.service';
|
import { AlbumService } from "./album.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma, Album } from '@prisma/client';
|
import { Prisma, Album } from "@prisma/client";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AlbumService {
|
export class AlbumService {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
|
||||||
export class CreateAlbumDto {
|
export class CreateAlbumDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from "./app.controller";
|
||||||
import { AppService } from './app.service';
|
import { AppService } from "./app.service";
|
||||||
|
|
||||||
describe('AppController', () => {
|
describe("AppController", () => {
|
||||||
let appController: AppController;
|
let appController: AppController;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -14,9 +14,9 @@ describe('AppController', () => {
|
|||||||
appController = app.get<AppController>(AppController);
|
appController = app.get<AppController>(AppController);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('root', () => {
|
describe("root", () => {
|
||||||
it('should return "Hello World!"', () => {
|
it('should return "Hello World!"', () => {
|
||||||
expect(appController.getHello()).toBe('Hello World!');
|
expect(appController.getHello()).toBe("Hello World!");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get } from "@nestjs/common";
|
||||||
import { AppService } from './app.service';
|
import { AppService } from "./app.service";
|
||||||
import { ApiOkResponse } from '@nestjs/swagger';
|
import { ApiOkResponse } from "@nestjs/swagger";
|
||||||
|
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
@@ -8,7 +8,7 @@ export class AppController {
|
|||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
description: 'Return a hello world message, used as a health route',
|
description: "Return a hello world message, used as a health route",
|
||||||
})
|
})
|
||||||
getHello(): string {
|
getHello(): string {
|
||||||
return this.appService.getHello();
|
return this.appService.getHello();
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from "./app.controller";
|
||||||
import { AppService } from './app.service';
|
import { AppService } from "./app.service";
|
||||||
import { PrismaService } from './prisma/prisma.service';
|
import { PrismaService } from "./prisma/prisma.service";
|
||||||
import { UsersModule } from './users/users.module';
|
import { UsersModule } from "./users/users.module";
|
||||||
import { PrismaModule } from './prisma/prisma.module';
|
import { PrismaModule } from "./prisma/prisma.module";
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from "./auth/auth.module";
|
||||||
import { SongModule } from './song/song.module';
|
import { SongModule } from "./song/song.module";
|
||||||
import { LessonModule } from './lesson/lesson.module';
|
import { LessonModule } from "./lesson/lesson.module";
|
||||||
import { SettingsModule } from './settings/settings.module';
|
import { SettingsModule } from "./settings/settings.module";
|
||||||
import { ArtistService } from './artist/artist.service';
|
import { ArtistService } from "./artist/artist.service";
|
||||||
import { GenreModule } from './genre/genre.module';
|
import { GenreModule } from "./genre/genre.module";
|
||||||
import { ArtistModule } from './artist/artist.module';
|
import { ArtistModule } from "./artist/artist.module";
|
||||||
import { AlbumModule } from './album/album.module';
|
import { AlbumModule } from "./album/album.module";
|
||||||
import { SearchModule } from './search/search.module';
|
import { SearchModule } from "./search/search.module";
|
||||||
import { HistoryModule } from './history/history.module';
|
import { HistoryModule } from "./history/history.module";
|
||||||
import { MailerModule } from '@nestjs-modules/mailer';
|
import { MailerModule } from "@nestjs-modules/mailer";
|
||||||
|
import { ScoresModule } from "./scores/scores.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -29,6 +30,7 @@ import { MailerModule } from '@nestjs-modules/mailer';
|
|||||||
SearchModule,
|
SearchModule,
|
||||||
SettingsModule,
|
SettingsModule,
|
||||||
HistoryModule,
|
HistoryModule,
|
||||||
|
ScoresModule,
|
||||||
MailerModule.forRoot({
|
MailerModule.forRoot({
|
||||||
transport: process.env.SMTP_TRANSPORT,
|
transport: process.env.SMTP_TRANSPORT,
|
||||||
defaults: {
|
defaults: {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppService {
|
export class AppService {
|
||||||
getHello(): string {
|
getHello(): string {
|
||||||
return 'Hello World!';
|
return "Hello World!";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,30 +14,31 @@ import {
|
|||||||
Req,
|
Req,
|
||||||
StreamableFile,
|
StreamableFile,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from "@nestjs/common";
|
||||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||||
import { CreateArtistDto } from './dto/create-artist.dto';
|
import { CreateArtistDto } from "./dto/create-artist.dto";
|
||||||
import { Request } from 'express';
|
import { Request } from "express";
|
||||||
import { ArtistService } from './artist.service';
|
import { ArtistService } from "./artist.service";
|
||||||
import { Prisma, Artist } from '@prisma/client';
|
import { Prisma, Artist } from "@prisma/client";
|
||||||
import {
|
import {
|
||||||
ApiNotFoundResponse,
|
ApiNotFoundResponse,
|
||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
} from '@nestjs/swagger';
|
} from "@nestjs/swagger";
|
||||||
import { createReadStream, existsSync } from 'fs';
|
import { createReadStream, existsSync } from "fs";
|
||||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
import { FilterQuery } from "src/utils/filter.pipe";
|
||||||
import { Artist as _Artist } from 'src/_gen/prisma-class/artist';
|
import { Artist as _Artist } from "src/_gen/prisma-class/artist";
|
||||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
import { Public } from "src/auth/public";
|
||||||
import { Public } from 'src/auth/public';
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||||
|
|
||||||
@Controller('artist')
|
@Controller("artist")
|
||||||
@ApiTags('artist')
|
@ApiTags("artist")
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(ChromaAuthGuard)
|
||||||
export class ArtistController {
|
export class ArtistController {
|
||||||
static filterableFields = ['+id', 'name'];
|
static filterableFields = ["+id", "name"];
|
||||||
static includableFields: IncludeMap<Prisma.ArtistInclude> = {
|
static includableFields: IncludeMap<Prisma.ArtistInclude> = {
|
||||||
Song: true,
|
Song: true,
|
||||||
Album: true,
|
Album: true,
|
||||||
@@ -47,7 +48,7 @@ export class ArtistController {
|
|||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description: 'Register a new artist, should not be used by frontend',
|
description: "Register a new artist, should not be used by frontend",
|
||||||
})
|
})
|
||||||
async create(@Body() dto: CreateArtistDto) {
|
async create(@Body() dto: CreateArtistDto) {
|
||||||
try {
|
try {
|
||||||
@@ -57,26 +58,26 @@ export class ArtistController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(":id")
|
||||||
@ApiOperation({ description: 'Delete an artist by id' })
|
@ApiOperation({ description: "Delete an artist by id" })
|
||||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||||
try {
|
try {
|
||||||
return await this.service.delete({ id });
|
return await this.service.delete({ id });
|
||||||
} catch {
|
} catch {
|
||||||
throw new NotFoundException('Invalid ID');
|
throw new NotFoundException("Invalid ID");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/illustration')
|
@Get(":id/illustration")
|
||||||
@ApiOperation({ description: "Get an artist's illustration" })
|
@ApiOperation({ description: "Get an artist's illustration" })
|
||||||
@ApiNotFoundResponse({ description: 'Artist or illustration not found' })
|
@ApiNotFoundResponse({ description: "Artist or illustration not found" })
|
||||||
@Public()
|
@Public()
|
||||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||||
const artist = await this.service.get({ id });
|
const artist = await this.service.get({ id });
|
||||||
if (!artist) throw new NotFoundException('Artist not found');
|
if (!artist) throw new NotFoundException("Artist not found");
|
||||||
const path = `/assets/artists/${artist.name}/illustration.png`;
|
const path = `/assets/artists/${artist.name}/illustration.png`;
|
||||||
if (!existsSync(path))
|
if (!existsSync(path))
|
||||||
throw new NotFoundException('Illustration not found');
|
throw new NotFoundException("Illustration not found");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = createReadStream(path);
|
const file = createReadStream(path);
|
||||||
@@ -87,15 +88,15 @@ export class ArtistController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOperation({ description: 'Get all artists paginated' })
|
@ApiOperation({ description: "Get all artists paginated" })
|
||||||
@ApiOkResponsePlaginated(_Artist)
|
@ApiOkResponsePlaginated(_Artist)
|
||||||
async findAll(
|
async findAll(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@FilterQuery(ArtistController.filterableFields)
|
@FilterQuery(ArtistController.filterableFields)
|
||||||
where: Prisma.ArtistWhereInput,
|
where: Prisma.ArtistWhereInput,
|
||||||
@Query('include') include: string,
|
@Query("include") include: string,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<Plage<Artist>> {
|
): Promise<Plage<Artist>> {
|
||||||
const ret = await this.service.list({
|
const ret = await this.service.list({
|
||||||
skip,
|
skip,
|
||||||
@@ -106,20 +107,20 @@ export class ArtistController {
|
|||||||
return new Plage(ret, req);
|
return new Plage(ret, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(":id")
|
||||||
@ApiOperation({ description: 'Get an artist by id' })
|
@ApiOperation({ description: "Get an artist by id" })
|
||||||
@ApiOkResponse({ type: _Artist })
|
@ApiOkResponse({ type: _Artist })
|
||||||
async findOne(
|
async findOne(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@Query('include') include: string,
|
@Query("include") include: string,
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param("id", ParseIntPipe) id: number,
|
||||||
) {
|
) {
|
||||||
const res = await this.service.get(
|
const res = await this.service.get(
|
||||||
{ id },
|
{ id },
|
||||||
mapInclude(include, req, ArtistController.includableFields),
|
mapInclude(include, req, ArtistController.includableFields),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res === null) throw new NotFoundException('Artist not found');
|
if (res === null) throw new NotFoundException("Artist not found");
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
import { ArtistController } from './artist.controller';
|
import { ArtistController } from "./artist.controller";
|
||||||
import { ArtistService } from './artist.service';
|
import { ArtistService } from "./artist.service";
|
||||||
|
import { SearchModule } from "src/search/search.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule, SearchModule],
|
||||||
controllers: [ArtistController],
|
controllers: [ArtistController],
|
||||||
providers: [ArtistService],
|
providers: [ArtistService],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma, Artist } from '@prisma/client';
|
import { Prisma, Artist } from "@prisma/client";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { MeiliService } from "src/search/meilisearch.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ArtistService {
|
export class ArtistService {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(
|
||||||
|
private prisma: PrismaService,
|
||||||
|
private search: MeiliService,
|
||||||
|
) {}
|
||||||
|
|
||||||
async create(data: Prisma.ArtistCreateInput): Promise<Artist> {
|
async create(data: Prisma.ArtistCreateInput): Promise<Artist> {
|
||||||
return this.prisma.artist.create({
|
const ret = await this.prisma.artist.create({
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
|
await this.search.index("artists").addDocuments([ret]);
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(
|
async get(
|
||||||
@@ -42,8 +48,10 @@ export class ArtistService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(where: Prisma.ArtistWhereUniqueInput): Promise<Artist> {
|
async delete(where: Prisma.ArtistWhereUniqueInput): Promise<Artist> {
|
||||||
return this.prisma.artist.delete({
|
const ret = await this.prisma.artist.delete({
|
||||||
where,
|
where,
|
||||||
});
|
});
|
||||||
|
await this.search.index("artists").deleteDocument(ret.id);
|
||||||
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
|
||||||
export class CreateArtistDto {
|
export class CreateArtistDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
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,
|
Response,
|
||||||
Query,
|
Query,
|
||||||
Param,
|
Param,
|
||||||
} from '@nestjs/common';
|
ParseIntPipe,
|
||||||
import { AuthService } from './auth.service';
|
} from "@nestjs/common";
|
||||||
import { JwtAuthGuard } from './jwt-auth.guard';
|
import { AuthService } from "./auth.service";
|
||||||
import { LocalAuthGuard } from './local-auth.guard';
|
import { JwtAuthGuard } from "./jwt-auth.guard";
|
||||||
import { RegisterDto } from './dto/register.dto';
|
import { LocalAuthGuard } from "./local-auth.guard";
|
||||||
import { UsersService } from 'src/users/users.service';
|
import { RegisterDto } from "./dto/register.dto";
|
||||||
|
import { UsersService } from "src/users/users.service";
|
||||||
import {
|
import {
|
||||||
ApiBadRequestResponse,
|
ApiBadRequestResponse,
|
||||||
ApiBearerAuth,
|
ApiBearerAuth,
|
||||||
@@ -36,21 +37,24 @@ import {
|
|||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiUnauthorizedResponse,
|
ApiUnauthorizedResponse,
|
||||||
} from '@nestjs/swagger';
|
} from "@nestjs/swagger";
|
||||||
import { User } from '../models/user';
|
import { User } from "../models/user";
|
||||||
import { JwtToken } from './models/jwt';
|
import { JwtToken } from "./models/jwt";
|
||||||
import { LoginDto } from './dto/login.dto';
|
import { LoginDto } from "./dto/login.dto";
|
||||||
import { Profile } from './dto/profile.dto';
|
import { Profile } from "./dto/profile.dto";
|
||||||
import { Setting } from 'src/models/setting';
|
import { Setting } from "src/models/setting";
|
||||||
import { UpdateSettingDto } from 'src/settings/dto/update-setting.dto';
|
import { UpdateSettingDto } from "src/settings/dto/update-setting.dto";
|
||||||
import { SettingsService } from 'src/settings/settings.service';
|
import { SettingsService } from "src/settings/settings.service";
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from "@nestjs/platform-express";
|
||||||
import { writeFile } from 'fs';
|
import { writeFile } from "fs";
|
||||||
import { PasswordResetDto } from './dto/password_reset.dto ';
|
import { PasswordResetDto } from "./dto/password_reset.dto ";
|
||||||
|
import { mapInclude } from "src/utils/include";
|
||||||
|
import { SongController } from "src/song/song.controller";
|
||||||
|
import { ChromaAuthGuard } from "./chroma-auth.guard";
|
||||||
|
|
||||||
@ApiTags('auth')
|
@ApiTags("auth")
|
||||||
@Controller('auth')
|
@Controller("auth")
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
constructor(
|
constructor(
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
@@ -58,17 +62,17 @@ export class AuthController {
|
|||||||
private settingsService: SettingsService,
|
private settingsService: SettingsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get('login/google')
|
@Get("login/google")
|
||||||
@UseGuards(AuthGuard('google'))
|
@UseGuards(AuthGuard("google"))
|
||||||
@ApiOperation({ description: 'Redirect to google login page' })
|
@ApiOperation({ description: "Redirect to google login page" })
|
||||||
googleLogin() {}
|
googleLogin() {}
|
||||||
|
|
||||||
@Get('logged/google')
|
@Get("logged/google")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description:
|
description:
|
||||||
'Redirect to the front page after connecting to the google account',
|
"Redirect to the front page after connecting to the google account",
|
||||||
})
|
})
|
||||||
@UseGuards(AuthGuard('google'))
|
@UseGuards(AuthGuard("google"))
|
||||||
async googleLoginCallbakc(@Req() req: any) {
|
async googleLoginCallbakc(@Req() req: any) {
|
||||||
let user = await this.usersService.user({ googleID: req.user.googleID });
|
let user = await this.usersService.user({ googleID: req.user.googleID });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -78,13 +82,13 @@ export class AuthController {
|
|||||||
return this.authService.login(user);
|
return this.authService.login(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('register')
|
@Post("register")
|
||||||
@ApiOperation({ description: 'Register a new user' })
|
@ApiOperation({ description: "Register a new user" })
|
||||||
@ApiConflictResponse({ description: 'Username or email already taken' })
|
@ApiConflictResponse({ description: "Username or email already taken" })
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
description: 'Successfully registered, email sent to verify',
|
description: "Successfully registered, email sent to verify",
|
||||||
})
|
})
|
||||||
@ApiBadRequestResponse({ description: 'Invalid data or database error' })
|
@ApiBadRequestResponse({ description: "Invalid data or database error" })
|
||||||
async register(@Body() registerDto: RegisterDto): Promise<void> {
|
async register(@Body() registerDto: RegisterDto): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const user = await this.usersService.createUser(registerDto);
|
const user = await this.usersService.createUser(registerDto);
|
||||||
@@ -92,102 +96,102 @@ export class AuthController {
|
|||||||
await this.authService.sendVerifyMail(user);
|
await this.authService.sendVerifyMail(user);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// check if the error is a duplicate key error
|
// check if the error is a duplicate key error
|
||||||
if (e.code === 'P2002') {
|
if (e.code === "P2002") {
|
||||||
throw new ConflictException('Username or email already taken');
|
throw new ConflictException("Username or email already taken");
|
||||||
}
|
}
|
||||||
console.error(e);
|
console.error(e);
|
||||||
throw new BadRequestException();
|
throw new BadRequestException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('verify')
|
@Put("verify")
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiOperation({ description: 'Verify the email of the user' })
|
@ApiOperation({ description: "Verify the email of the user" })
|
||||||
@ApiOkResponse({ description: 'Successfully verified' })
|
@ApiOkResponse({ description: "Successfully verified" })
|
||||||
@ApiBadRequestResponse({ description: 'Invalid or expired token' })
|
@ApiBadRequestResponse({ description: "Invalid or expired token" })
|
||||||
async verify(
|
async verify(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Query('token') token: string,
|
@Query("token") token: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (await this.authService.verifyMail(req.user.id, token)) return;
|
if (await this.authService.verifyMail(req.user.id, token)) return;
|
||||||
throw new BadRequestException('Invalid token. Expired or invalid.');
|
throw new BadRequestException("Invalid token. Expired or invalid.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Put('reverify')
|
@Put("reverify")
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@ApiOperation({ description: 'Resend the verification email' })
|
@ApiOperation({ description: "Resend the verification email" })
|
||||||
async reverify(@Request() req: any): Promise<void> {
|
async reverify(@Request() req: any): Promise<void> {
|
||||||
const user = await this.usersService.user({ id: req.user.id });
|
const user = await this.usersService.user({ id: req.user.id });
|
||||||
if (!user) throw new BadRequestException('Invalid user');
|
if (!user) throw new BadRequestException("Invalid user");
|
||||||
await this.authService.sendVerifyMail(user);
|
await this.authService.sendVerifyMail(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@Put('password-reset')
|
@Put("password-reset")
|
||||||
async password_reset(
|
async password_reset(
|
||||||
@Body() resetDto: PasswordResetDto,
|
@Body() resetDto: PasswordResetDto,
|
||||||
@Query('token') token: string,
|
@Query("token") token: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (await this.authService.changePassword(resetDto.password, token)) return;
|
if (await this.authService.changePassword(resetDto.password, token)) return;
|
||||||
throw new BadRequestException('Invalid token. Expired or invalid.');
|
throw new BadRequestException("Invalid token. Expired or invalid.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@Put('forgot-password')
|
@Put("forgot-password")
|
||||||
async forgot_password(@Query('email') email: string): Promise<void> {
|
async forgot_password(@Query("email") email: string): Promise<void> {
|
||||||
console.log(email);
|
console.log(email);
|
||||||
const user = await this.usersService.user({ email: email });
|
const user = await this.usersService.user({ email: email });
|
||||||
if (!user) throw new BadRequestException('Invalid user');
|
if (!user) throw new BadRequestException("Invalid user");
|
||||||
await this.authService.sendPasswordResetMail(user);
|
await this.authService.sendPasswordResetMail(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('login')
|
@Post("login")
|
||||||
@ApiBody({ type: LoginDto })
|
@ApiBody({ type: LoginDto })
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@UseGuards(LocalAuthGuard)
|
@UseGuards(LocalAuthGuard)
|
||||||
@ApiBody({ type: LoginDto })
|
@ApiBody({ type: LoginDto })
|
||||||
@ApiOperation({ description: 'Login with username and password' })
|
@ApiOperation({ description: "Login with username and password" })
|
||||||
@ApiOkResponse({ description: 'Successfully logged in', type: JwtToken })
|
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid credentials' })
|
@ApiUnauthorizedResponse({ description: "Invalid credentials" })
|
||||||
async login(@Request() req: any): Promise<JwtToken> {
|
async login(@Request() req: any): Promise<JwtToken> {
|
||||||
return this.authService.login(req.user);
|
return this.authService.login(req.user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('guest')
|
@Post("guest")
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@ApiOperation({ description: 'Login as a guest account' })
|
@ApiOperation({ description: "Login as a guest account" })
|
||||||
@ApiOkResponse({ description: 'Successfully logged in', type: JwtToken })
|
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
|
||||||
async guest(): Promise<JwtToken> {
|
async guest(): Promise<JwtToken> {
|
||||||
const user = await this.usersService.createGuest();
|
const user = await this.usersService.createGuest();
|
||||||
await this.settingsService.createUserSetting(user.id);
|
await this.settingsService.createUserSetting(user.id);
|
||||||
return this.authService.login(user);
|
return this.authService.login(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(ChromaAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOperation({ description: 'Get the profile picture of connected user' })
|
@ApiOperation({ description: "Get the profile picture of connected user" })
|
||||||
@ApiOkResponse({ description: 'The user profile picture' })
|
@ApiOkResponse({ description: "The user profile picture" })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Get('me/picture')
|
@Get("me/picture")
|
||||||
async getProfilePicture(@Request() req: any, @Response() res: any) {
|
async getProfilePicture(@Request() req: any, @Response() res: any) {
|
||||||
return await this.usersService.getProfilePicture(req.user.id, res);
|
return await this.usersService.getProfilePicture(req.user.id, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(ChromaAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'The user profile picture' })
|
@ApiOkResponse({ description: "The user profile picture" })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Post('me/picture')
|
@Post("me/picture")
|
||||||
@ApiOperation({ description: 'Upload a new profile picture' })
|
@ApiOperation({ description: "Upload a new profile picture" })
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor("file"))
|
||||||
async postProfilePicture(
|
async postProfilePicture(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@UploadedFile(
|
@UploadedFile(
|
||||||
new ParseFilePipeBuilder()
|
new ParseFilePipeBuilder()
|
||||||
.addFileTypeValidator({
|
.addFileTypeValidator({
|
||||||
fileType: 'jpeg',
|
fileType: "jpeg",
|
||||||
})
|
})
|
||||||
.build({
|
.build({
|
||||||
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
errorHttpStatusCode: HttpStatus.UNPROCESSABLE_ENTITY,
|
||||||
@@ -203,22 +207,22 @@ export class AuthController {
|
|||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully logged in', type: User })
|
@ApiOkResponse({ description: "Successfully logged in", type: User })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Get('me')
|
@Get("me")
|
||||||
@ApiOperation({ description: 'Get the user info of connected user' })
|
@ApiOperation({ description: "Get the user info of connected user" })
|
||||||
async getProfile(@Request() req: any): Promise<User> {
|
async getProfile(@Request() req: any): Promise<User> {
|
||||||
const user = await this.usersService.user({ id: req.user.id });
|
const user = await this.usersService.user({ id: req.user.id });
|
||||||
if (!user) throw new InternalServerErrorException();
|
if (!user) throw new InternalServerErrorException();
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(ChromaAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully edited profile', type: User })
|
@ApiOkResponse({ description: "Successfully edited profile", type: User })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Put('me')
|
@Put("me")
|
||||||
@ApiOperation({ description: 'Edit the profile of connected user' })
|
@ApiOperation({ description: "Edit the profile of connected user" })
|
||||||
editProfile(
|
editProfile(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Body() profile: Partial<Profile>,
|
@Body() profile: Partial<Profile>,
|
||||||
@@ -241,20 +245,20 @@ export class AuthController {
|
|||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully deleted', type: User })
|
@ApiOkResponse({ description: "Successfully deleted", type: User })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Delete('me')
|
@Delete("me")
|
||||||
@ApiOperation({ description: 'Delete the profile of connected user' })
|
@ApiOperation({ description: "Delete the profile of connected user" })
|
||||||
deleteSelf(@Request() req: any): Promise<User> {
|
deleteSelf(@Request() req: any): Promise<User> {
|
||||||
return this.usersService.deleteUser({ id: req.user.id });
|
return this.usersService.deleteUser({ id: req.user.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
|
@ApiOkResponse({ description: "Successfully edited settings", type: Setting })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Patch('me/settings')
|
@Patch("me/settings")
|
||||||
@ApiOperation({ description: 'Edit the settings of connected user' })
|
@ApiOperation({ description: "Edit the settings of connected user" })
|
||||||
udpateSettings(
|
udpateSettings(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Body() settingUserDto: UpdateSettingDto,
|
@Body() settingUserDto: UpdateSettingDto,
|
||||||
@@ -267,10 +271,10 @@ export class AuthController {
|
|||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully edited settings', type: Setting })
|
@ApiOkResponse({ description: "Successfully edited settings", type: Setting })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Get('me/settings')
|
@Get("me/settings")
|
||||||
@ApiOperation({ description: 'Get the settings of connected user' })
|
@ApiOperation({ description: "Get the settings of connected user" })
|
||||||
async getSettings(@Request() req: any): Promise<Setting> {
|
async getSettings(@Request() req: any): Promise<Setting> {
|
||||||
const result = await this.settingsService.getUserSetting({
|
const result = await this.settingsService.getUserSetting({
|
||||||
userId: +req.user.id,
|
userId: +req.user.id,
|
||||||
@@ -281,28 +285,43 @@ export class AuthController {
|
|||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully added liked song' })
|
@ApiOkResponse({ description: "Successfully added liked song" })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Post('me/likes/:id')
|
@Post("me/likes/:id")
|
||||||
addLikedSong(@Request() req: any, @Param('id') songId: number) {
|
addLikedSong(@Request() req: any, @Param("id", ParseIntPipe) songId: number) {
|
||||||
return this.usersService.addLikedSong(+req.user.id, +songId);
|
return this.usersService.addLikedSong(+req.user.id, songId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully removed liked song' })
|
@ApiOkResponse({ description: "Successfully removed liked song" })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Delete('me/likes/:id')
|
@Delete("me/likes/:id")
|
||||||
removeLikedSong(@Request() req: any, @Param('id') songId: number) {
|
removeLikedSong(
|
||||||
return this.usersService.removeLikedSong(+req.user.id, +songId);
|
@Request() req: any,
|
||||||
|
@Param("id", ParseIntPipe) songId: number,
|
||||||
|
) {
|
||||||
|
return this.usersService.removeLikedSong(+req.user.id, songId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiOkResponse({ description: 'Successfully retrieved liked song' })
|
@ApiOkResponse({ description: "Successfully retrieved liked song" })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Get('me/likes')
|
@Get("me/likes")
|
||||||
getLikedSongs(@Request() req: any) {
|
getLikedSongs(@Request() req: any, @Query("include") include: string) {
|
||||||
return this.usersService.getLikedSongs(+req.user.id);
|
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 { Module } from "@nestjs/common";
|
||||||
import { UsersModule } from 'src/users/users.module';
|
import { UsersModule } from "src/users/users.module";
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from "./auth.service";
|
||||||
import { PassportModule } from '@nestjs/passport';
|
import { PassportModule } from "@nestjs/passport";
|
||||||
import { AuthController } from './auth.controller';
|
import { AuthController } from "./auth.controller";
|
||||||
import { LocalStrategy } from './local.strategy';
|
import { LocalStrategy } from "./local.strategy";
|
||||||
import { JwtModule } from '@nestjs/jwt';
|
import { JwtModule } from "@nestjs/jwt";
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from "@nestjs/config";
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from "@nestjs/config";
|
||||||
import { JwtStrategy } from './jwt.strategy';
|
import { JwtStrategy } from "./jwt.strategy";
|
||||||
import { SettingsModule } from 'src/settings/settings.module';
|
import { SettingsModule } from "src/settings/settings.module";
|
||||||
import { GoogleStrategy } from './google.strategy';
|
import { GoogleStrategy } from "./google.strategy";
|
||||||
|
import { HeaderApiKeyStrategy } from "./apikey.strategy";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -20,13 +21,19 @@ import { GoogleStrategy } from './google.strategy';
|
|||||||
JwtModule.registerAsync({
|
JwtModule.registerAsync({
|
||||||
imports: [ConfigModule],
|
imports: [ConfigModule],
|
||||||
useFactory: async (configService: ConfigService) => ({
|
useFactory: async (configService: ConfigService) => ({
|
||||||
secret: configService.get('JWT_SECRET'),
|
secret: configService.get("JWT_SECRET"),
|
||||||
signOptions: { expiresIn: '365d' },
|
signOptions: { expiresIn: "365d" },
|
||||||
}),
|
}),
|
||||||
inject: [ConfigService],
|
inject: [ConfigService],
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
providers: [AuthService, LocalStrategy, JwtStrategy, GoogleStrategy],
|
providers: [
|
||||||
|
AuthService,
|
||||||
|
LocalStrategy,
|
||||||
|
JwtStrategy,
|
||||||
|
GoogleStrategy,
|
||||||
|
HeaderApiKeyStrategy,
|
||||||
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
})
|
})
|
||||||
export class AuthModule {}
|
export class AuthModule {}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { UsersService } from '../users/users.service';
|
import { UsersService } from "../users/users.service";
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from "@nestjs/jwt";
|
||||||
import * as bcrypt from 'bcryptjs';
|
import * as bcrypt from "bcryptjs";
|
||||||
import PayloadInterface from './interface/payload.interface';
|
import PayloadInterface from "./interface/payload.interface";
|
||||||
import { User } from 'src/models/user';
|
import { User } from "src/models/user";
|
||||||
import { MailerService } from '@nestjs-modules/mailer';
|
import { MailerService } from "@nestjs-modules/mailer";
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -13,6 +13,12 @@ export class AuthService {
|
|||||||
private emailService: MailerService,
|
private emailService: MailerService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
validateApiKey(apikey: string): boolean {
|
||||||
|
if (process.env.API_KEYS == null) return false;
|
||||||
|
const keys = process.env.API_KEYS.split(",");
|
||||||
|
return keys.includes(apikey);
|
||||||
|
}
|
||||||
|
|
||||||
async validateUser(
|
async validateUser(
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
@@ -36,37 +42,37 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async sendVerifyMail(user: User) {
|
async sendVerifyMail(user: User) {
|
||||||
if (process.env.IGNORE_MAILS === 'true') return;
|
if (process.env.IGNORE_MAILS === "true") return;
|
||||||
if (user.email == null) return;
|
if (user.email == null) return;
|
||||||
console.log('Sending verification mail to', user.email);
|
console.log("Sending verification mail to", user.email);
|
||||||
const token = await this.jwtService.signAsync(
|
const token = await this.jwtService.signAsync(
|
||||||
{
|
{
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
{ expiresIn: '10h' },
|
{ expiresIn: "10h" },
|
||||||
);
|
);
|
||||||
await this.emailService.sendMail({
|
await this.emailService.sendMail({
|
||||||
to: user.email,
|
to: user.email,
|
||||||
from: 'chromacase@octohub.app',
|
from: "chromacase@octohub.app",
|
||||||
subject: 'Mail verification for Chromacase',
|
subject: "Mail verification for Chromacase",
|
||||||
html: `To verify your mail, please click on this <a href="${process.env.PUBLIC_URL}/verify?token=${token}">link</a>.`,
|
html: `To verify your mail, please click on this <a href="${process.env.PUBLIC_URL}/verify?token=${token}">link</a>.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendPasswordResetMail(user: User) {
|
async sendPasswordResetMail(user: User) {
|
||||||
if (process.env.IGNORE_MAILS === 'true') return;
|
if (process.env.IGNORE_MAILS === "true") return;
|
||||||
if (user.email == null) return;
|
if (user.email == null) return;
|
||||||
console.log('Sending password reset mail to', user.email);
|
console.log("Sending password reset mail to", user.email);
|
||||||
const token = await this.jwtService.signAsync(
|
const token = await this.jwtService.signAsync(
|
||||||
{
|
{
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
},
|
},
|
||||||
{ expiresIn: '10h' },
|
{ expiresIn: "10h" },
|
||||||
);
|
);
|
||||||
await this.emailService.sendMail({
|
await this.emailService.sendMail({
|
||||||
to: user.email,
|
to: user.email,
|
||||||
from: 'chromacase@octohub.app',
|
from: "chromacase@octohub.app",
|
||||||
subject: 'Password reset for Chromacase',
|
subject: "Password reset for Chromacase",
|
||||||
html: `To reset your password, please click on this <a href="${process.env.PUBLIC_URL}/password_reset?token=${token}">link</a>.`,
|
html: `To reset your password, please click on this <a href="${process.env.PUBLIC_URL}/password_reset?token=${token}">link</a>.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -76,7 +82,7 @@ export class AuthService {
|
|||||||
try {
|
try {
|
||||||
verified = await this.jwtService.verifyAsync(token);
|
verified = await this.jwtService.verifyAsync(token);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Password reset token failure', e);
|
console.log("Password reset token failure", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
console.log(verified);
|
console.log(verified);
|
||||||
@@ -91,7 +97,7 @@ export class AuthService {
|
|||||||
try {
|
try {
|
||||||
await this.jwtService.verifyAsync(token);
|
await this.jwtService.verifyAsync(token);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Verify mail token failure', e);
|
console.log("Verify mail token failure", e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
await this.userService.updateUser({
|
await this.userService.updateUser({
|
||||||
|
|||||||
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 { Injectable } from "@nestjs/common";
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class Constants {
|
export class Constants {
|
||||||
constructor(private configService: ConfigService) {}
|
constructor(private configService: ConfigService) {}
|
||||||
|
|
||||||
getSecret = () => {
|
getSecret = () => {
|
||||||
return this.configService.get('JWT_SECRET');
|
return this.configService.get("JWT_SECRET");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class LoginDto {
|
export class LoginDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class PasswordResetDto {
|
export class PasswordResetDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class Profile {
|
export class Profile {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class RegisterDto {
|
export class RegisterDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
import { Strategy, VerifyCallback } from 'passport-google-oauth20';
|
import { Strategy, VerifyCallback } from "passport-google-oauth20";
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { User } from '@prisma/client';
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GoogleStrategy extends PassportStrategy(Strategy) {
|
export class GoogleStrategy extends PassportStrategy(Strategy) {
|
||||||
@@ -10,7 +10,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy) {
|
|||||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||||
clientSecret: process.env.GOOGLE_SECRET,
|
clientSecret: process.env.GOOGLE_SECRET,
|
||||||
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||||
scope: ['email', 'profile'],
|
scope: ["email", "profile"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,19 @@
|
|||||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
import { ExecutionContext, Injectable } from "@nestjs/common";
|
||||||
import { Reflector } from '@nestjs/core';
|
import { Reflector } from "@nestjs/core";
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
import { IS_PUBLIC_KEY } from './public';
|
import { IS_PUBLIC_KEY } from "./public";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
export class JwtAuthGuard extends AuthGuard("jwt") {
|
||||||
constructor(private reflector: Reflector) {
|
constructor(private reflector: Reflector) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
canActivate(context: ExecutionContext) {
|
canActivate(context: ExecutionContext) {
|
||||||
console.log(context);
|
|
||||||
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
|
||||||
context.getHandler(),
|
context.getHandler(),
|
||||||
context.getClass(),
|
context.getClass(),
|
||||||
]);
|
]);
|
||||||
console.log(isPublic);
|
|
||||||
if (isPublic) {
|
if (isPublic) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
import { ExtractJwt, Strategy } from "passport-jwt";
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from "@nestjs/config";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
@@ -9,7 +9,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
|||||||
super({
|
super({
|
||||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
ignoreExpiration: false,
|
ignoreExpiration: false,
|
||||||
secretOrKey: configService.get('JWT_SECRET'),
|
secretOrKey: configService.get("JWT_SECRET"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { AuthGuard } from '@nestjs/passport';
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalAuthGuard extends AuthGuard('local') {}
|
export class LocalAuthGuard extends AuthGuard("local") {}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Strategy } from 'passport-local';
|
import { Strategy } from "passport-local";
|
||||||
import { PassportStrategy } from '@nestjs/passport';
|
import { PassportStrategy } from "@nestjs/passport";
|
||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from "./auth.service";
|
||||||
import PayloadInterface from './interface/payload.interface';
|
import PayloadInterface from "./interface/payload.interface";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LocalStrategy extends PassportStrategy(Strategy) {
|
export class LocalStrategy extends PassportStrategy(Strategy) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class JwtToken {
|
export class JwtToken {
|
||||||
@ApiProperty()
|
@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);
|
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
|
||||||
export class CreateGenreDto {
|
export class CreateGenreDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -14,25 +14,26 @@ import {
|
|||||||
Req,
|
Req,
|
||||||
StreamableFile,
|
StreamableFile,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from "@nestjs/common";
|
||||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||||
import { CreateGenreDto } from './dto/create-genre.dto';
|
import { CreateGenreDto } from "./dto/create-genre.dto";
|
||||||
import { Request } from 'express';
|
import { Request } from "express";
|
||||||
import { GenreService } from './genre.service';
|
import { GenreService } from "./genre.service";
|
||||||
import { Prisma, Genre } from '@prisma/client';
|
import { Prisma, Genre } from "@prisma/client";
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
import { ApiTags } from "@nestjs/swagger";
|
||||||
import { createReadStream, existsSync } from 'fs';
|
import { createReadStream, existsSync } from "fs";
|
||||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
import { FilterQuery } from "src/utils/filter.pipe";
|
||||||
import { Genre as _Genre } from 'src/_gen/prisma-class/genre';
|
import { Genre as _Genre } from "src/_gen/prisma-class/genre";
|
||||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
import { Public } from "src/auth/public";
|
||||||
import { Public } from 'src/auth/public';
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||||
|
|
||||||
@Controller('genre')
|
@Controller("genre")
|
||||||
@ApiTags('genre')
|
@ApiTags("genre")
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(ChromaAuthGuard)
|
||||||
export class GenreController {
|
export class GenreController {
|
||||||
static filterableFields: string[] = ['+id', 'name'];
|
static filterableFields: string[] = ["+id", "name"];
|
||||||
static includableFields: IncludeMap<Prisma.GenreInclude> = {
|
static includableFields: IncludeMap<Prisma.GenreInclude> = {
|
||||||
Song: true,
|
Song: true,
|
||||||
};
|
};
|
||||||
@@ -48,23 +49,23 @@ export class GenreController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(":id")
|
||||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||||
try {
|
try {
|
||||||
return await this.service.delete({ id });
|
return await this.service.delete({ id });
|
||||||
} catch {
|
} catch {
|
||||||
throw new NotFoundException('Invalid ID');
|
throw new NotFoundException("Invalid ID");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/illustration')
|
@Get(":id/illustration")
|
||||||
@Public()
|
@Public()
|
||||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||||
const genre = await this.service.get({ id });
|
const genre = await this.service.get({ id });
|
||||||
if (!genre) throw new NotFoundException('Genre not found');
|
if (!genre) throw new NotFoundException("Genre not found");
|
||||||
const path = `/assets/genres/${genre.name}/illustration.png`;
|
const path = `/assets/genres/${genre.name}/illustration.png`;
|
||||||
if (!existsSync(path))
|
if (!existsSync(path))
|
||||||
throw new NotFoundException('Illustration not found');
|
throw new NotFoundException("Illustration not found");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = createReadStream(path);
|
const file = createReadStream(path);
|
||||||
@@ -80,9 +81,9 @@ export class GenreController {
|
|||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@FilterQuery(GenreController.filterableFields)
|
@FilterQuery(GenreController.filterableFields)
|
||||||
where: Prisma.GenreWhereInput,
|
where: Prisma.GenreWhereInput,
|
||||||
@Query('include') include: string,
|
@Query("include") include: string,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<Plage<Genre>> {
|
): Promise<Plage<Genre>> {
|
||||||
const ret = await this.service.list({
|
const ret = await this.service.list({
|
||||||
skip,
|
skip,
|
||||||
@@ -93,18 +94,18 @@ export class GenreController {
|
|||||||
return new Plage(ret, req);
|
return new Plage(ret, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(":id")
|
||||||
async findOne(
|
async findOne(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@Query('include') include: string,
|
@Query("include") include: string,
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param("id", ParseIntPipe) id: number,
|
||||||
) {
|
) {
|
||||||
const res = await this.service.get(
|
const res = await this.service.get(
|
||||||
{ id },
|
{ id },
|
||||||
mapInclude(include, req, GenreController.includableFields),
|
mapInclude(include, req, GenreController.includableFields),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res === null) throw new NotFoundException('Genre not found');
|
if (res === null) throw new NotFoundException("Genre not found");
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
import { GenreController } from './genre.controller';
|
import { GenreController } from "./genre.controller";
|
||||||
import { GenreService } from './genre.service';
|
import { GenreService } from "./genre.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma, Genre } from '@prisma/client';
|
import { Prisma, Genre } from "@prisma/client";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class GenreService {
|
export class GenreService {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class SearchHistoryDto {
|
export class SearchHistoryDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
query: string;
|
query: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
type: 'song' | 'artist' | 'album' | 'genre';
|
type: "song" | "artist" | "album" | "genre";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsNumber } from 'class-validator';
|
import { IsNumber } from "class-validator";
|
||||||
|
|
||||||
export class SongHistoryDto {
|
export class SongHistoryDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
@@ -9,68 +9,75 @@ import {
|
|||||||
Query,
|
Query,
|
||||||
Request,
|
Request,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from "@nestjs/common";
|
||||||
import {
|
import {
|
||||||
ApiCreatedResponse,
|
ApiCreatedResponse,
|
||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiUnauthorizedResponse,
|
ApiUnauthorizedResponse,
|
||||||
} from '@nestjs/swagger';
|
} from "@nestjs/swagger";
|
||||||
import { SearchHistory, SongHistory } from '@prisma/client';
|
import { SearchHistory, SongHistory } from "@prisma/client";
|
||||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from "src/auth/jwt-auth.guard";
|
||||||
import { SongHistoryDto } from './dto/SongHistoryDto';
|
import { SongHistoryDto } from "./dto/SongHistoryDto";
|
||||||
import { HistoryService } from './history.service';
|
import { HistoryService } from "./history.service";
|
||||||
import { SearchHistoryDto } from './dto/SearchHistoryDto';
|
import { SearchHistoryDto } from "./dto/SearchHistoryDto";
|
||||||
import { SongHistory as _SongHistory } from 'src/_gen/prisma-class/song_history';
|
import { SongHistory as _SongHistory } from "src/_gen/prisma-class/song_history";
|
||||||
import { SearchHistory as _SearchHistory } from 'src/_gen/prisma-class/search_history';
|
import { SearchHistory as _SearchHistory } from "src/_gen/prisma-class/search_history";
|
||||||
|
import { SongController } from "src/song/song.controller";
|
||||||
|
import { mapInclude } from "src/utils/include";
|
||||||
|
|
||||||
@Controller('history')
|
@Controller("history")
|
||||||
@ApiTags('history')
|
@ApiTags("history")
|
||||||
export class HistoryController {
|
export class HistoryController {
|
||||||
constructor(private readonly historyService: HistoryService) {}
|
constructor(private readonly historyService: HistoryService) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
@ApiOperation({ description: 'Get song history of connected user' })
|
@ApiOperation({ description: "Get song history of connected user" })
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiOkResponse({ type: _SongHistory, isArray: true })
|
@ApiOkResponse({ type: _SongHistory, isArray: true })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
async getHistory(
|
async getHistory(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
|
@Query("include") include: string,
|
||||||
): Promise<SongHistory[]> {
|
): Promise<SongHistory[]> {
|
||||||
return this.historyService.getHistory(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)
|
@HttpCode(200)
|
||||||
@ApiOperation({ description: 'Get search history of connected user' })
|
@ApiOperation({ description: "Get search history of connected user" })
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiOkResponse({ type: _SearchHistory, isArray: true })
|
@ApiOkResponse({ type: _SearchHistory, isArray: true })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
async getSearchHistory(
|
async getSearchHistory(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<SearchHistory[]> {
|
): Promise<SearchHistory[]> {
|
||||||
return this.historyService.getSearchHistory(req.user.id, { skip, take });
|
return this.historyService.getSearchHistory(req.user.id, { skip, take });
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@HttpCode(201)
|
@HttpCode(201)
|
||||||
@ApiOperation({ description: 'Create a record of a song played by a user' })
|
@ApiOperation({ description: "Create a record of a song played by a user" })
|
||||||
@ApiCreatedResponse({ description: 'Succesfully created a record' })
|
@ApiCreatedResponse({ description: "Succesfully created a record" })
|
||||||
async create(@Body() record: SongHistoryDto): Promise<SongHistory> {
|
async create(@Body() record: SongHistoryDto): Promise<SongHistory> {
|
||||||
return this.historyService.createSongHistoryRecord(record);
|
return this.historyService.createSongHistoryRecord(record);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('search')
|
@Post("search")
|
||||||
@HttpCode(201)
|
@HttpCode(201)
|
||||||
@ApiOperation({ description: 'Creates a search record in the users history' })
|
@ApiOperation({ description: "Creates a search record in the users history" })
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
async createSearchHistory(
|
async createSearchHistory(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Body() record: SearchHistoryDto,
|
@Body() record: SearchHistoryDto,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
import { HistoryService } from './history.service';
|
import { HistoryService } from "./history.service";
|
||||||
import { HistoryController } from './history.controller';
|
import { HistoryController } from "./history.controller";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { HistoryService } from './history.service';
|
import { HistoryService } from "./history.service";
|
||||||
|
|
||||||
describe('HistoryService', () => {
|
describe("HistoryService", () => {
|
||||||
let service: HistoryService;
|
let service: HistoryService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -12,7 +12,7 @@ describe('HistoryService', () => {
|
|||||||
service = module.get<HistoryService>(HistoryService);
|
service = module.get<HistoryService>(HistoryService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it("should be defined", () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { SearchHistory, SongHistory } from '@prisma/client';
|
import { Prisma, SearchHistory, SongHistory } from "@prisma/client";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { SearchHistoryDto } from './dto/SearchHistoryDto';
|
import { SearchHistoryDto } from "./dto/SearchHistoryDto";
|
||||||
import { SongHistoryDto } from './dto/SongHistoryDto';
|
import { SongHistoryDto } from "./dto/SongHistoryDto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class HistoryService {
|
export class HistoryService {
|
||||||
@@ -45,12 +45,14 @@ export class HistoryService {
|
|||||||
async getHistory(
|
async getHistory(
|
||||||
playerId: number,
|
playerId: number,
|
||||||
{ skip, take }: { skip?: number; take?: number },
|
{ skip, take }: { skip?: number; take?: number },
|
||||||
|
include?: Prisma.SongInclude,
|
||||||
): Promise<SongHistory[]> {
|
): Promise<SongHistory[]> {
|
||||||
return this.prisma.songHistory.findMany({
|
return this.prisma.songHistory.findMany({
|
||||||
where: { user: { id: playerId } },
|
where: { user: { id: playerId } },
|
||||||
orderBy: { playDate: 'desc' },
|
orderBy: { playDate: "desc" },
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
|
include: { song: include ? { include } : true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +65,7 @@ export class HistoryService {
|
|||||||
}): Promise<{ best: number; history: SongHistory[] }> {
|
}): Promise<{ best: number; history: SongHistory[] }> {
|
||||||
const history = await this.prisma.songHistory.findMany({
|
const history = await this.prisma.songHistory.findMany({
|
||||||
where: { user: { id: playerId }, song: { id: songId } },
|
where: { user: { id: playerId }, song: { id: songId } },
|
||||||
orderBy: { playDate: 'desc' },
|
orderBy: { playDate: "desc" },
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -95,7 +97,7 @@ export class HistoryService {
|
|||||||
): Promise<SearchHistory[]> {
|
): Promise<SearchHistory[]> {
|
||||||
return this.prisma.searchHistory.findMany({
|
return this.prisma.searchHistory.findMany({
|
||||||
where: { user: { id: playerId } },
|
where: { user: { id: playerId } },
|
||||||
orderBy: { searchDate: 'desc' },
|
orderBy: { searchDate: "desc" },
|
||||||
skip,
|
skip,
|
||||||
take,
|
take,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,16 +12,17 @@ import {
|
|||||||
Delete,
|
Delete,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from "@nestjs/common";
|
||||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||||
import { LessonService } from './lesson.service';
|
import { LessonService } from "./lesson.service";
|
||||||
import { ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiProperty, ApiTags } from "@nestjs/swagger";
|
||||||
import { Prisma, Skill } from '@prisma/client';
|
import { Prisma, Skill } from "@prisma/client";
|
||||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
import { FilterQuery } from "src/utils/filter.pipe";
|
||||||
import { Lesson as _Lesson } from 'src/_gen/prisma-class/lesson';
|
import { Lesson as _Lesson } from "src/_gen/prisma-class/lesson";
|
||||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
import { Request } from "express";
|
||||||
import { Request } from 'express';
|
import { AuthGuard } from "@nestjs/passport";
|
||||||
|
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||||
|
|
||||||
export class Lesson {
|
export class Lesson {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -36,15 +37,15 @@ export class Lesson {
|
|||||||
mainSkill: Skill;
|
mainSkill: Skill;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiTags('lessons')
|
@ApiTags("lessons")
|
||||||
@Controller('lesson')
|
@Controller("lesson")
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(ChromaAuthGuard)
|
||||||
export class LessonController {
|
export class LessonController {
|
||||||
static filterableFields: string[] = [
|
static filterableFields: string[] = [
|
||||||
'+id',
|
"+id",
|
||||||
'name',
|
"name",
|
||||||
'+requiredLevel',
|
"+requiredLevel",
|
||||||
'mainSkill',
|
"mainSkill",
|
||||||
];
|
];
|
||||||
static includableFields: IncludeMap<Prisma.LessonInclude> = {
|
static includableFields: IncludeMap<Prisma.LessonInclude> = {
|
||||||
LessonHistory: true,
|
LessonHistory: true,
|
||||||
@@ -53,7 +54,7 @@ export class LessonController {
|
|||||||
constructor(private lessonService: LessonService) {}
|
constructor(private lessonService: LessonService) {}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get all lessons',
|
summary: "Get all lessons",
|
||||||
})
|
})
|
||||||
@Get()
|
@Get()
|
||||||
@ApiOkResponsePlaginated(_Lesson)
|
@ApiOkResponsePlaginated(_Lesson)
|
||||||
@@ -61,9 +62,9 @@ export class LessonController {
|
|||||||
@Req() request: Request,
|
@Req() request: Request,
|
||||||
@FilterQuery(LessonController.filterableFields)
|
@FilterQuery(LessonController.filterableFields)
|
||||||
where: Prisma.LessonWhereInput,
|
where: Prisma.LessonWhereInput,
|
||||||
@Query('include') include: string,
|
@Query("include") include: string,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<Plage<Lesson>> {
|
): Promise<Plage<Lesson>> {
|
||||||
const ret = await this.lessonService.getAll({
|
const ret = await this.lessonService.getAll({
|
||||||
skip,
|
skip,
|
||||||
@@ -75,13 +76,13 @@ export class LessonController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Get a particular lessons',
|
summary: "Get a particular lessons",
|
||||||
})
|
})
|
||||||
@Get(':id')
|
@Get(":id")
|
||||||
async get(
|
async get(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@Query('include') include: string,
|
@Query("include") include: string,
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param("id", ParseIntPipe) id: number,
|
||||||
): Promise<Lesson> {
|
): Promise<Lesson> {
|
||||||
const ret = await this.lessonService.get(
|
const ret = await this.lessonService.get(
|
||||||
id,
|
id,
|
||||||
@@ -92,7 +93,7 @@ export class LessonController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Create a lessons',
|
summary: "Create a lessons",
|
||||||
})
|
})
|
||||||
@Post()
|
@Post()
|
||||||
async post(@Body() lesson: Lesson): Promise<Lesson> {
|
async post(@Body() lesson: Lesson): Promise<Lesson> {
|
||||||
@@ -105,10 +106,10 @@ export class LessonController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Delete a lessons',
|
summary: "Delete a lessons",
|
||||||
})
|
})
|
||||||
@Delete(':id')
|
@Delete(":id")
|
||||||
async delete(@Param('id', ParseIntPipe) id: number): Promise<Lesson> {
|
async delete(@Param("id", ParseIntPipe) id: number): Promise<Lesson> {
|
||||||
try {
|
try {
|
||||||
return await this.lessonService.delete(id);
|
return await this.lessonService.delete(id);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
import { LessonController } from './lesson.controller';
|
import { LessonController } from "./lesson.controller";
|
||||||
import { LessonService } from './lesson.service';
|
import { LessonService } from "./lesson.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { LessonService } from './lesson.service';
|
import { LessonService } from "./lesson.service";
|
||||||
|
|
||||||
describe('LessonService', () => {
|
describe("LessonService", () => {
|
||||||
let service: LessonService;
|
let service: LessonService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -12,7 +12,7 @@ describe('LessonService', () => {
|
|||||||
service = module.get<LessonService>(LessonService);
|
service = module.get<LessonService>(LessonService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it("should be defined", () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Lesson, Prisma } from '@prisma/client';
|
import { Lesson, Prisma } from "@prisma/client";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LessonService {
|
export class LessonService {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from "@nestjs/core";
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from "./app.module";
|
||||||
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
|
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
|
||||||
import {
|
import {
|
||||||
CallHandler,
|
CallHandler,
|
||||||
ExecutionContext,
|
ExecutionContext,
|
||||||
Injectable,
|
Injectable,
|
||||||
NestInterceptor,
|
NestInterceptor,
|
||||||
ValidationPipe,
|
ValidationPipe,
|
||||||
} from '@nestjs/common';
|
} from "@nestjs/common";
|
||||||
import { RequestLogger, RequestLoggerOptions } from 'json-logger-service';
|
import { RequestLogger, RequestLoggerOptions } from "json-logger-service";
|
||||||
import { tap } from 'rxjs';
|
import { tap } from "rxjs";
|
||||||
import { PrismaModel } from './_gen/prisma-class';
|
import { PrismaModel } from "./_gen/prisma-class";
|
||||||
import { PrismaService } from './prisma/prisma.service';
|
import { PrismaService } from "./prisma/prisma.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AspectLogger implements NestInterceptor {
|
export class AspectLogger implements NestInterceptor {
|
||||||
@@ -27,8 +27,8 @@ export class AspectLogger implements NestInterceptor {
|
|||||||
params,
|
params,
|
||||||
query,
|
query,
|
||||||
body,
|
body,
|
||||||
userId: user?.id ?? 'not logged in',
|
userId: user?.id ?? "not logged in",
|
||||||
username: user?.username ?? 'not logged in',
|
username: user?.username ?? "not logged in",
|
||||||
};
|
};
|
||||||
|
|
||||||
return next.handle().pipe(
|
return next.handle().pipe(
|
||||||
@@ -48,24 +48,24 @@ async function bootstrap() {
|
|||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
app.use(
|
app.use(
|
||||||
RequestLogger.buildExpressRequestLogger({
|
RequestLogger.buildExpressRequestLogger({
|
||||||
doNotLogPaths: ['/health'],
|
doNotLogPaths: ["/health"],
|
||||||
} as RequestLoggerOptions),
|
} as RequestLoggerOptions),
|
||||||
);
|
);
|
||||||
app.enableShutdownHooks();
|
app.enableShutdownHooks();
|
||||||
|
|
||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
.setTitle('Chromacase')
|
.setTitle("Chromacase")
|
||||||
.setDescription('The chromacase API')
|
.setDescription("The chromacase API")
|
||||||
.setVersion('1.0')
|
.setVersion("1.0")
|
||||||
.build();
|
.build();
|
||||||
const document = SwaggerModule.createDocument(app, config, {
|
const document = SwaggerModule.createDocument(app, config, {
|
||||||
extraModels: [...PrismaModel.extraModels],
|
extraModels: [...PrismaModel.extraModels],
|
||||||
});
|
});
|
||||||
SwaggerModule.setup('api', app, document);
|
SwaggerModule.setup("api", app, document);
|
||||||
|
|
||||||
app.useGlobalPipes(new ValidationPipe());
|
app.useGlobalPipes(new ValidationPipe());
|
||||||
app.enableCors();
|
app.enableCors();
|
||||||
app.useGlobalInterceptors(new AspectLogger());
|
//app.useGlobalInterceptors(new AspectLogger());
|
||||||
|
|
||||||
await app.listen(3000);
|
await app.listen(3000);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,25 +2,25 @@
|
|||||||
* Thanks to https://github.com/Arthi-chaud/Meelo/blob/master/src/pagination/models/paginated-response.ts
|
* Thanks to https://github.com/Arthi-chaud/Meelo/blob/master/src/pagination/models/paginated-response.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Type, applyDecorators } from '@nestjs/common';
|
import { Type, applyDecorators } from "@nestjs/common";
|
||||||
import {
|
import {
|
||||||
ApiExtraModels,
|
ApiExtraModels,
|
||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
ApiProperty,
|
ApiProperty,
|
||||||
getSchemaPath,
|
getSchemaPath,
|
||||||
} from '@nestjs/swagger';
|
} from "@nestjs/swagger";
|
||||||
|
|
||||||
export class PlageMetadata {
|
export class PlageMetadata {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
this: string;
|
this: string;
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: 'string',
|
type: "string",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description: "null if there is no next page, couldn't set it in swagger",
|
description: "null if there is no next page, couldn't set it in swagger",
|
||||||
})
|
})
|
||||||
next: string | null;
|
next: string | null;
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
type: 'string',
|
type: "string",
|
||||||
nullable: true,
|
nullable: true,
|
||||||
description:
|
description:
|
||||||
"null if there is no previous page, couldn't set it in swagger",
|
"null if there is no previous page, couldn't set it in swagger",
|
||||||
@@ -35,9 +35,9 @@ export class Plage<T extends object> {
|
|||||||
|
|
||||||
constructor(data: T[], request: Request | any) {
|
constructor(data: T[], request: Request | any) {
|
||||||
this.data = data;
|
this.data = data;
|
||||||
let take = Number(request.query['take'] ?? 20).valueOf();
|
let take = Number(request.query["take"] ?? 20).valueOf();
|
||||||
if (take == 0) take = 20;
|
if (take == 0) take = 20;
|
||||||
let skipped: number = Number(request.query['skip'] ?? 0).valueOf();
|
let skipped: number = Number(request.query["skip"] ?? 0).valueOf();
|
||||||
if (skipped % take) {
|
if (skipped % take) {
|
||||||
skipped += take - (skipped % take);
|
skipped += take - (skipped % take);
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ export const ApiOkResponsePlaginated = <DataDto extends Type<unknown>>(
|
|||||||
{
|
{
|
||||||
properties: {
|
properties: {
|
||||||
data: {
|
data: {
|
||||||
type: 'array',
|
type: "array",
|
||||||
items: { $ref: getSchemaPath(dataDto) },
|
items: { $ref: getSchemaPath(dataDto) },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class Setting {
|
export class Setting {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
|
|
||||||
export class User {
|
export class User {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@@ -11,4 +11,6 @@ export class User {
|
|||||||
isGuest: boolean;
|
isGuest: boolean;
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
partyPlayed: number;
|
partyPlayed: number;
|
||||||
|
@ApiProperty()
|
||||||
|
totalScore: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { PrismaService } from './prisma.service';
|
import { PrismaService } from "./prisma.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [PrismaService],
|
providers: [PrismaService],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
import { PrismaService } from './prisma.service';
|
import { PrismaService } from "./prisma.service";
|
||||||
|
|
||||||
describe('PrismaService', () => {
|
describe("PrismaService", () => {
|
||||||
let service: PrismaService;
|
let service: PrismaService;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -12,7 +12,7 @@ describe('PrismaService', () => {
|
|||||||
service = module.get<PrismaService>(PrismaService);
|
service = module.get<PrismaService>(PrismaService);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it("should be defined", () => {
|
||||||
expect(service).toBeDefined();
|
expect(service).toBeDefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Injectable, OnModuleInit } from '@nestjs/common';
|
import { Injectable, OnModuleInit } from "@nestjs/common";
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrismaService extends PrismaClient implements OnModuleInit {
|
export class PrismaService extends PrismaClient implements OnModuleInit {
|
||||||
|
|||||||
22
back/src/scores/scores.controller.ts
Normal file
22
back/src/scores/scores.controller.ts
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { Controller, Get, Put } from "@nestjs/common";
|
||||||
|
import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
|
||||||
|
import { ScoresService } from "./scores.service";
|
||||||
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
|
@ApiTags("scores")
|
||||||
|
@Controller("scores")
|
||||||
|
export class ScoresController {
|
||||||
|
constructor(private readonly scoresService: ScoresService) {}
|
||||||
|
|
||||||
|
@ApiOkResponse({ description: "Successfully sent the Top 20 players" })
|
||||||
|
@Get("top/20")
|
||||||
|
getTopTwenty(): Promise<User[]> {
|
||||||
|
return this.scoresService.topTwenty();
|
||||||
|
}
|
||||||
|
|
||||||
|
// @ApiOkResponse{{description: "Successfully updated the user's total score"}}
|
||||||
|
// @Put("/add")
|
||||||
|
// addScore(): Promise<void> {
|
||||||
|
// return this.ScoresService.add()
|
||||||
|
// }
|
||||||
|
}
|
||||||
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 {
|
export class SearchSongDto {
|
||||||
@ApiProperty()
|
@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 {
|
import {
|
||||||
Controller,
|
Controller,
|
||||||
|
DefaultValuePipe,
|
||||||
Get,
|
Get,
|
||||||
InternalServerErrorException,
|
ParseIntPipe,
|
||||||
NotFoundException,
|
|
||||||
Param,
|
|
||||||
Query,
|
Query,
|
||||||
Request,
|
Request,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
} from "@nestjs/common";
|
||||||
import {
|
import {
|
||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
ApiOperation,
|
ApiOperation,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiUnauthorizedResponse,
|
ApiUnauthorizedResponse,
|
||||||
} from '@nestjs/swagger';
|
} from "@nestjs/swagger";
|
||||||
import { Artist, Genre, Song } from '@prisma/client';
|
import { Artist, Song } from "@prisma/client";
|
||||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from "src/auth/jwt-auth.guard";
|
||||||
import { SearchService } from './search.service';
|
import { SearchService } from "./search.service";
|
||||||
import { Song as _Song } from 'src/_gen/prisma-class/song';
|
import { Song as _Song } from "src/_gen/prisma-class/song";
|
||||||
import { Genre as _Genre } from 'src/_gen/prisma-class/genre';
|
import { Artist as _Artist } from "src/_gen/prisma-class/artist";
|
||||||
import { Artist as _Artist } from 'src/_gen/prisma-class/artist';
|
import { mapInclude } from "src/utils/include";
|
||||||
import { mapInclude } from 'src/utils/include';
|
import { SongController } from "src/song/song.controller";
|
||||||
import { SongController } from 'src/song/song.controller';
|
import { ArtistController } from "src/artist/artist.controller";
|
||||||
import { GenreController } from 'src/genre/genre.controller';
|
|
||||||
import { ArtistController } from 'src/artist/artist.controller';
|
|
||||||
|
|
||||||
@ApiTags('search')
|
@ApiTags("search")
|
||||||
@Controller('search')
|
@Controller("search")
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
export class SearchController {
|
export class SearchController {
|
||||||
constructor(private readonly searchService: SearchService) {}
|
constructor(private readonly searchService: SearchService) {}
|
||||||
|
|
||||||
@Get('songs/:query')
|
@Get("songs")
|
||||||
@ApiOkResponse({ type: _Song, isArray: true })
|
@ApiOkResponse({ type: _Song, isArray: true })
|
||||||
@ApiOperation({ description: 'Search a song' })
|
@ApiOperation({ description: "Search a song" })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
async searchSong(
|
async searchSong(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Query('include') include: string,
|
@Query("q") query: string | null,
|
||||||
@Param('query') query: string,
|
@Query("artistId", new ParseIntPipe({ optional: true })) artistId: number,
|
||||||
): Promise<Song[] | null> {
|
@Query("genreId", new ParseIntPipe({ optional: true })) genreId: number,
|
||||||
try {
|
@Query("include") include: string,
|
||||||
const ret = await this.searchService.songByGuess(
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
query,
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
req.user?.id,
|
): Promise<Song[]> {
|
||||||
mapInclude(include, req, SongController.includableFields),
|
return await this.searchService.searchSong(
|
||||||
);
|
query ?? "",
|
||||||
if (!ret.length) throw new NotFoundException();
|
artistId,
|
||||||
else return ret;
|
genreId,
|
||||||
} catch (error) {
|
mapInclude(include, req, SongController.includableFields),
|
||||||
throw new InternalServerErrorException();
|
skip,
|
||||||
}
|
take,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('genres/:query')
|
@Get("artists")
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
|
||||||
@ApiOkResponse({ type: _Genre, isArray: true })
|
|
||||||
@ApiOperation({ description: 'Search a genre' })
|
|
||||||
async searchGenre(
|
|
||||||
@Request() req: any,
|
|
||||||
@Query('include') include: string,
|
|
||||||
@Param('query') query: string,
|
|
||||||
): Promise<Genre[] | null> {
|
|
||||||
try {
|
|
||||||
const ret = await this.searchService.genreByGuess(
|
|
||||||
query,
|
|
||||||
req.user?.id,
|
|
||||||
mapInclude(include, req, GenreController.includableFields),
|
|
||||||
);
|
|
||||||
if (!ret.length) throw new NotFoundException();
|
|
||||||
else return ret;
|
|
||||||
} catch (error) {
|
|
||||||
throw new InternalServerErrorException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Get('artists/:query')
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiOkResponse({ type: _Artist, isArray: true })
|
@ApiOkResponse({ type: _Artist, isArray: true })
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@ApiOperation({ description: 'Search an artist' })
|
@ApiOperation({ description: "Search an artist" })
|
||||||
async searchArtists(
|
async searchArtists(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Query('include') include: string,
|
@Query("include") include: string,
|
||||||
@Param('query') query: string,
|
@Query("q") query: string | null,
|
||||||
): Promise<Artist[] | null> {
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
try {
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
const ret = await this.searchService.artistByGuess(
|
): Promise<Artist[]> {
|
||||||
query,
|
return await this.searchService.searchArtists(
|
||||||
req.user?.id,
|
query ?? "",
|
||||||
mapInclude(include, req, ArtistController.includableFields),
|
mapInclude(include, req, ArtistController.includableFields),
|
||||||
);
|
skip,
|
||||||
if (!ret.length) throw new NotFoundException();
|
take,
|
||||||
else return ret;
|
);
|
||||||
} catch (error) {
|
|
||||||
throw new InternalServerErrorException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { SearchService } from './search.service';
|
import { SearchService } from "./search.service";
|
||||||
import { SearchController } from './search.controller';
|
import { SearchController } from "./search.controller";
|
||||||
import { HistoryModule } from 'src/history/history.module';
|
import { HistoryModule } from "src/history/history.module";
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
import { SongService } from 'src/song/song.service';
|
import { SongService } from "src/song/song.service";
|
||||||
|
import { MeiliService } from "./meilisearch.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule, HistoryModule],
|
imports: [PrismaModule, HistoryModule],
|
||||||
controllers: [SearchController],
|
controllers: [SearchController],
|
||||||
providers: [SearchService, SongService],
|
providers: [SearchService, SongService, MeiliService],
|
||||||
exports: [SearchService],
|
exports: [SearchService, MeiliService],
|
||||||
})
|
})
|
||||||
export class SearchModule {}
|
export class SearchModule {}
|
||||||
|
|||||||
@@ -1,51 +1,84 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Artist, Prisma, Song, Genre } from '@prisma/client';
|
import { Artist, Prisma, Song, Genre } from "@prisma/client";
|
||||||
import { HistoryService } from 'src/history/history.service';
|
import { HistoryService } from "src/history/history.service";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
import { MeiliService } from "./meilisearch.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService {
|
export class SearchService {
|
||||||
constructor(
|
constructor(
|
||||||
private prisma: PrismaService,
|
private prisma: PrismaService,
|
||||||
private history: HistoryService,
|
private history: HistoryService,
|
||||||
|
private search: MeiliService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async songByGuess(
|
async searchSong(
|
||||||
query: string,
|
query: string,
|
||||||
userID: number,
|
artistId?: number,
|
||||||
|
genreId?: number,
|
||||||
include?: Prisma.SongInclude,
|
include?: Prisma.SongInclude,
|
||||||
|
skip?: number,
|
||||||
|
take?: number,
|
||||||
): Promise<Song[]> {
|
): Promise<Song[]> {
|
||||||
return this.prisma.song.findMany({
|
if (query.length === 0) {
|
||||||
where: {
|
return await this.prisma.song.findMany({
|
||||||
name: { contains: query, mode: 'insensitive' },
|
where: {
|
||||||
},
|
artistId,
|
||||||
include,
|
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,
|
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,
|
include?: Prisma.ArtistInclude,
|
||||||
|
skip?: number,
|
||||||
|
take?: number,
|
||||||
): Promise<Artist[]> {
|
): Promise<Artist[]> {
|
||||||
return this.prisma.artist.findMany({
|
if (query.length === 0) {
|
||||||
where: {
|
return this.prisma.artist.findMany({
|
||||||
name: { contains: query, mode: 'insensitive' },
|
take,
|
||||||
},
|
skip,
|
||||||
include,
|
include,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
const ids = (
|
||||||
|
await this.search.index("artists").search(query, {
|
||||||
|
limit: take,
|
||||||
|
offset: skip,
|
||||||
|
})
|
||||||
|
).hits.map((x) => x.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
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 {
|
export class UpdateSettingDto {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from "@nestjs/common";
|
||||||
import { SettingsService } from './settings.service';
|
import { SettingsService } from "./settings.service";
|
||||||
import { PrismaModule } from 'src/prisma/prisma.module';
|
import { PrismaModule } from "src/prisma/prisma.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [PrismaModule],
|
imports: [PrismaModule],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from "@nestjs/common";
|
||||||
import { Prisma, UserSettings } from '@prisma/client';
|
import { Prisma, UserSettings } from "@prisma/client";
|
||||||
import { PrismaService } from 'src/prisma/prisma.service';
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SettingsService {
|
export class SettingsService {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from "@nestjs/swagger";
|
||||||
import { IsNotEmpty } from 'class-validator';
|
import { IsNotEmpty } from "class-validator";
|
||||||
|
|
||||||
export class CreateSongDto {
|
export class CreateSongDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ import {
|
|||||||
Req,
|
Req,
|
||||||
StreamableFile,
|
StreamableFile,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
} from '@nestjs/common';
|
Header,
|
||||||
import { ApiOkResponsePlaginated, Plage } from 'src/models/plage';
|
} from "@nestjs/common";
|
||||||
import { CreateSongDto } from './dto/create-song.dto';
|
import { ApiOkResponsePlaginated, Plage } from "src/models/plage";
|
||||||
import { SongService } from './song.service';
|
import { CreateSongDto } from "./dto/create-song.dto";
|
||||||
import { Request } from 'express';
|
import { SongService } from "./song.service";
|
||||||
import { Prisma, Song } from '@prisma/client';
|
import { Request } from "express";
|
||||||
import { createReadStream, existsSync } from 'fs';
|
import { Prisma, Song } from "@prisma/client";
|
||||||
|
import { createReadStream, existsSync, readFileSync } from "fs";
|
||||||
import {
|
import {
|
||||||
ApiNotFoundResponse,
|
ApiNotFoundResponse,
|
||||||
ApiOkResponse,
|
ApiOkResponse,
|
||||||
@@ -29,15 +30,15 @@ import {
|
|||||||
ApiProperty,
|
ApiProperty,
|
||||||
ApiTags,
|
ApiTags,
|
||||||
ApiUnauthorizedResponse,
|
ApiUnauthorizedResponse,
|
||||||
} from '@nestjs/swagger';
|
} from "@nestjs/swagger";
|
||||||
import { HistoryService } from 'src/history/history.service';
|
import { HistoryService } from "src/history/history.service";
|
||||||
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';
|
import { JwtAuthGuard } from "src/auth/jwt-auth.guard";
|
||||||
import { FilterQuery } from 'src/utils/filter.pipe';
|
import { FilterQuery } from "src/utils/filter.pipe";
|
||||||
import { Song as _Song } from 'src/_gen/prisma-class/song';
|
import { Song as _Song } from "src/_gen/prisma-class/song";
|
||||||
import { SongHistory } from 'src/_gen/prisma-class/song_history';
|
import { SongHistory } from "src/_gen/prisma-class/song_history";
|
||||||
import { IncludeMap, mapInclude } from 'src/utils/include';
|
import { IncludeMap, mapInclude } from "src/utils/include";
|
||||||
import { Public } from 'src/auth/public';
|
import { Public } from "src/auth/public";
|
||||||
|
import { ChromaAuthGuard } from "src/auth/chroma-auth.guard";
|
||||||
class SongHistoryResult {
|
class SongHistoryResult {
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
best: number;
|
best: number;
|
||||||
@@ -45,16 +46,16 @@ class SongHistoryResult {
|
|||||||
history: SongHistory[];
|
history: SongHistory[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@Controller('song')
|
@Controller("song")
|
||||||
@ApiTags('song')
|
@ApiTags("song")
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(ChromaAuthGuard)
|
||||||
export class SongController {
|
export class SongController {
|
||||||
static filterableFields: string[] = [
|
static filterableFields: string[] = [
|
||||||
'+id',
|
"+id",
|
||||||
'name',
|
"name",
|
||||||
'+artistId',
|
"+artistId",
|
||||||
'+albumId',
|
"+albumId",
|
||||||
'+genreId',
|
"+genreId",
|
||||||
];
|
];
|
||||||
static includableFields: IncludeMap<Prisma.SongInclude> = {
|
static includableFields: IncludeMap<Prisma.SongInclude> = {
|
||||||
artist: true,
|
artist: true,
|
||||||
@@ -69,36 +70,37 @@ export class SongController {
|
|||||||
private readonly historyService: HistoryService,
|
private readonly historyService: HistoryService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get(':id/midi')
|
@Get(":id/midi")
|
||||||
@ApiOperation({ description: 'Streams the midi file of the requested song' })
|
@ApiOperation({ description: "Streams the midi file of the requested song" })
|
||||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
@ApiNotFoundResponse({ description: "Song not found" })
|
||||||
@ApiOkResponse({ description: 'Returns the midi file succesfully' })
|
@ApiOkResponse({ description: "Returns the midi file succesfully" })
|
||||||
async getMidi(@Param('id', ParseIntPipe) id: number) {
|
async getMidi(@Param("id", ParseIntPipe) id: number) {
|
||||||
const song = await this.songService.song({ id });
|
const song = await this.songService.song({ id });
|
||||||
if (!song) throw new NotFoundException('Song not found');
|
if (!song) throw new NotFoundException("Song not found");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = createReadStream(song.midiPath);
|
const file = createReadStream(song.midiPath);
|
||||||
return new StreamableFile(file, { type: 'audio/midi' });
|
return new StreamableFile(file, { type: "audio/midi" });
|
||||||
} catch {
|
} catch {
|
||||||
throw new InternalServerErrorException();
|
throw new InternalServerErrorException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/illustration')
|
@Get(":id/illustration")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description: 'Streams the illustration of the requested song',
|
description: "Streams the illustration of the requested song",
|
||||||
})
|
})
|
||||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
@ApiNotFoundResponse({ description: "Song not found" })
|
||||||
@ApiOkResponse({ description: 'Returns the illustration succesfully' })
|
@ApiOkResponse({ description: "Returns the illustration succesfully" })
|
||||||
|
@Header("Cache-Control", "max-age=86400")
|
||||||
@Public()
|
@Public()
|
||||||
async getIllustration(@Param('id', ParseIntPipe) id: number) {
|
async getIllustration(@Param("id", ParseIntPipe) id: number) {
|
||||||
const song = await this.songService.song({ id });
|
const song = await this.songService.song({ id });
|
||||||
if (!song) throw new NotFoundException('Song not found');
|
if (!song) throw new NotFoundException("Song not found");
|
||||||
|
|
||||||
if (song.illustrationPath === null) throw new NotFoundException();
|
if (song.illustrationPath === null) throw new NotFoundException();
|
||||||
if (!existsSync(song.illustrationPath))
|
if (!existsSync(song.illustrationPath))
|
||||||
throw new NotFoundException('Illustration not found');
|
throw new NotFoundException("Illustration not found");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const file = createReadStream(song.illustrationPath);
|
const file = createReadStream(song.illustrationPath);
|
||||||
@@ -108,24 +110,102 @@ export class SongController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/musicXml')
|
@Get(":id/musicXml")
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description: 'Streams the musicXML file of the requested song',
|
description: "Streams the musicXML file of the requested song",
|
||||||
})
|
})
|
||||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
@ApiNotFoundResponse({ description: "Song not found" })
|
||||||
@ApiOkResponse({ description: 'Returns the musicXML file succesfully' })
|
@ApiOkResponse({ description: "Returns the musicXML file succesfully" })
|
||||||
async getMusicXml(@Param('id', ParseIntPipe) id: number) {
|
async getMusicXml(@Param("id", ParseIntPipe) id: number) {
|
||||||
const song = await this.songService.song({ id });
|
const song = await this.songService.song({ id });
|
||||||
if (!song) throw new NotFoundException('Song not found');
|
if (!song) throw new NotFoundException("Song not found");
|
||||||
|
|
||||||
const file = createReadStream(song.musicXmlPath, { encoding: 'binary' });
|
const file = createReadStream(song.musicXmlPath, { encoding: "binary" });
|
||||||
return new StreamableFile(file);
|
return new StreamableFile(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Get(":id/assets/partition")
|
||||||
|
@ApiOperation({
|
||||||
|
description: "Streams the svg partition of the requested song",
|
||||||
|
})
|
||||||
|
@ApiNotFoundResponse({ description: "Song not found" })
|
||||||
|
@ApiOkResponse({ description: "Returns the svg partition succesfully" })
|
||||||
|
@Header("Cache-Control", "max-age=86400")
|
||||||
|
@Header("Content-Type", "image/svg+xml")
|
||||||
|
@Public()
|
||||||
|
async getPartition(@Param("id", ParseIntPipe) id: number) {
|
||||||
|
const song = await this.songService.song({ id });
|
||||||
|
if (!song) throw new NotFoundException("Song not found");
|
||||||
|
|
||||||
|
// check if /data/cache/songs/id exists
|
||||||
|
if (!existsSync("/data/cache/songs/" + id + ".svg")) {
|
||||||
|
// if not, generate assets
|
||||||
|
await this.songService.createAssets(song.musicXmlPath, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file = readFileSync("/data/cache/songs/" + id + ".svg");
|
||||||
|
return file.toString();
|
||||||
|
} catch {
|
||||||
|
throw new InternalServerErrorException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":id/assets/cursors")
|
||||||
|
@ApiOperation({
|
||||||
|
description: "Streams the partition cursors of the requested song",
|
||||||
|
})
|
||||||
|
@ApiNotFoundResponse({ description: "Song not found" })
|
||||||
|
@ApiOkResponse({ description: "Returns the partition cursors succesfully" })
|
||||||
|
@Header("Cache-Control", "max-age=86400")
|
||||||
|
@Header("Content-Type", "application/json")
|
||||||
|
async getCursors(@Param("id", ParseIntPipe) id: number) {
|
||||||
|
const song = await this.songService.song({ id });
|
||||||
|
if (!song) throw new NotFoundException("Song not found");
|
||||||
|
|
||||||
|
// check if /data/cache/songs/id exists
|
||||||
|
if (!existsSync("/data/cache/songs/" + id + ".json")) {
|
||||||
|
// if not, generate assets
|
||||||
|
await this.songService.createAssets(song.musicXmlPath, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file = readFileSync("/data/cache/songs/" + id + ".json");
|
||||||
|
return JSON.parse(file.toString());
|
||||||
|
} catch {
|
||||||
|
throw new InternalServerErrorException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":id/assets/melody")
|
||||||
|
@ApiOperation({
|
||||||
|
description: "Streams the mp3 file of the requested song",
|
||||||
|
})
|
||||||
|
@ApiNotFoundResponse({ description: "Song not found" })
|
||||||
|
@ApiOkResponse({ description: "Returns the mp3 file succesfully" })
|
||||||
|
@Header("Cache-Control", "max-age=86400")
|
||||||
|
@Header("Content-Type", "audio/mpeg")
|
||||||
|
@Public()
|
||||||
|
async getMelody(@Param("id", ParseIntPipe) id: number) {
|
||||||
|
const song = await this.songService.song({ id });
|
||||||
|
if (!song) throw new NotFoundException("Song not found");
|
||||||
|
|
||||||
|
const path = song.musicXmlPath;
|
||||||
|
// mp3 file is next to the musicXML file and called melody.mp3
|
||||||
|
const pathWithoutFile = path.substring(0, path.lastIndexOf("/"));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const file = createReadStream(pathWithoutFile + "/melody.mp3");
|
||||||
|
return new StreamableFile(file, { type: "audio/mpeg" });
|
||||||
|
} catch {
|
||||||
|
throw new InternalServerErrorException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description:
|
description:
|
||||||
'register a new song in the database, should not be used by the frontend',
|
"register a new song in the database, should not be used by the frontend",
|
||||||
})
|
})
|
||||||
async create(@Body() createSongDto: CreateSongDto) {
|
async create(@Body() createSongDto: CreateSongDto) {
|
||||||
try {
|
try {
|
||||||
@@ -148,13 +228,13 @@ export class SongController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(":id")
|
||||||
@ApiOperation({ description: 'delete a song by id' })
|
@ApiOperation({ description: "delete a song by id" })
|
||||||
async remove(@Param('id', ParseIntPipe) id: number) {
|
async remove(@Param("id", ParseIntPipe) id: number) {
|
||||||
try {
|
try {
|
||||||
return await this.songService.deleteSong({ id });
|
return await this.songService.deleteSong({ id });
|
||||||
} catch {
|
} catch {
|
||||||
throw new NotFoundException('Invalid ID');
|
throw new NotFoundException("Invalid ID");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,9 +243,9 @@ export class SongController {
|
|||||||
async findAll(
|
async findAll(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@FilterQuery(SongController.filterableFields) where: Prisma.SongWhereInput,
|
@FilterQuery(SongController.filterableFields) where: Prisma.SongWhereInput,
|
||||||
@Query('include') include: string,
|
@Query("include") include: string,
|
||||||
@Query('skip', new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
@Query('take', new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<Plage<Song>> {
|
): Promise<Plage<Song>> {
|
||||||
const ret = await this.songService.songs({
|
const ret = await this.songService.songs({
|
||||||
skip,
|
skip,
|
||||||
@@ -176,14 +256,14 @@ export class SongController {
|
|||||||
return new Plage(ret, req);
|
return new Plage(ret, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@Get(":id")
|
||||||
@ApiOperation({ description: 'Get a specific song data' })
|
@ApiOperation({ description: "Get a specific song data" })
|
||||||
@ApiNotFoundResponse({ description: 'Song not found' })
|
@ApiNotFoundResponse({ description: "Song not found" })
|
||||||
@ApiOkResponse({ type: _Song, description: 'Requested song' })
|
@ApiOkResponse({ type: _Song, description: "Requested song" })
|
||||||
async findOne(
|
async findOne(
|
||||||
@Req() req: Request,
|
@Req() req: Request,
|
||||||
@Param('id', ParseIntPipe) id: number,
|
@Param("id", ParseIntPipe) id: number,
|
||||||
@Query('include') include: string,
|
@Query("include") include: string,
|
||||||
) {
|
) {
|
||||||
const res = await this.songService.song(
|
const res = await this.songService.song(
|
||||||
{
|
{
|
||||||
@@ -192,21 +272,22 @@ export class SongController {
|
|||||||
mapInclude(include, req, SongController.includableFields),
|
mapInclude(include, req, SongController.includableFields),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (res === null) throw new NotFoundException('Song not found');
|
if (res === null) throw new NotFoundException("Song not found");
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id/history')
|
@Get(":id/history")
|
||||||
@HttpCode(200)
|
@HttpCode(200)
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description: 'get the history of the connected user on a specific song',
|
description: "get the history of the connected user on a specific song",
|
||||||
})
|
})
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
type: SongHistoryResult,
|
type: SongHistoryResult,
|
||||||
description: 'Records of previous games of the user',
|
description: "Records of previous games of the user",
|
||||||
})
|
})
|
||||||
@ApiUnauthorizedResponse({ description: 'Invalid token' })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
async getHistory(@Req() req: any, @Param('id', ParseIntPipe) id: number) {
|
async getHistory(@Req() req: any, @Param("id", ParseIntPipe) id: number) {
|
||||||
return this.historyService.getForSong({
|
return this.historyService.getForSong({
|
||||||
playerId: req.user.id,
|
playerId: req.user.id,
|
||||||
songId: id,
|
songId: id,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user