├── .dockerignore
├── .github
└── workflows
│ └── container.yml
├── .gitignore
├── Dockerfile
├── README.md
├── TROUBLESHOOTING.md
├── docker-compose.yml
├── images
├── lidarr-deemix-conf.png
└── logo.webp
├── package.json
├── pnpm-lock.yaml
├── python
├── deemix-server.py
├── http-redirect-request.py
└── requirements.txt
├── run.sh
├── src
├── deemix.ts
├── helpers.ts
├── index.ts
└── lidarr.ts
├── tsconfig.json
└── tsconfig.tsnode.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | deploy.sh
3 | python/config
4 | certs
5 | __pycache__
6 | dist/*
--------------------------------------------------------------------------------
/.github/workflows/container.yml:
--------------------------------------------------------------------------------
1 | name: Container
2 | run-name: ${{ github.ref_name }} - Publishing Container
3 | on:
4 | push:
5 | tags:
6 | - "*"
7 | env:
8 | REGISTRY: ghcr.io
9 | IMAGE_NAME: ${{ github.repository }}
10 | jobs:
11 | build-and-push-image:
12 | runs-on: ubuntu-latest
13 | # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
14 | permissions:
15 | contents: read
16 | packages: write
17 | #
18 | steps:
19 | - name: Checkout repository
20 | uses: actions/checkout@v4
21 | # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
22 | - name: Log in to the Container registry
23 | uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
24 | with:
25 | registry: ${{ env.REGISTRY }}
26 | username: ${{ github.actor }}
27 | password: ${{ secrets.GITHUB_TOKEN }}
28 | # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
29 | - name: Extract metadata (tags, labels) for Docker
30 | id: meta
31 | uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
32 | with:
33 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
34 | # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
35 | # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
36 | # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
37 | - name: Build and push Docker image
38 | uses: docker/build-push-action@v5
39 | with:
40 | context: .
41 | push: true
42 | tags: ${{ steps.meta.outputs.tags }}
43 | labels: ${{ steps.meta.outputs.labels }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
2 |
3 | # Logs
4 |
5 | logs
6 | *.log
7 | notes
8 | python/config
9 | certs
10 | __pycache__
11 | node_modules
12 | dist/*
13 | deploy.sh
14 | .env.*
15 | .env
16 | python/env
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:alpine
2 |
3 | WORKDIR /app
4 | RUN apk add --no-cache nodejs npm curl rust cargo build-base openssl-dev bsd-compat-headers bash
5 | COPY python/requirements.txt ./python/requirements.txt
6 | RUN python -m pip install -r python/requirements.txt
7 | RUN npm i -g pnpm
8 | COPY package.json pnpm-lock.yaml ./
9 | RUN pnpm i
10 | COPY . .
11 | EXPOSE 8080
12 | CMD ["/app/run.sh"]
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Due to lack of time, this project is not maintained anymore. Feel free to fork!
2 |
3 |
4 |

5 |
Lidarr++Deemix
6 |
"If Lidarr and Deemix had a child"
7 |
8 |
9 | 
10 | []()
11 | []()
12 | []()
13 | []()
14 |
15 | ## 💡 How it works
16 |
17 | Lidarr usually pulls artist and album infos from their own api api.lidarr.audio, which pulls the data from MusicBrainz.
18 |
19 | However, MusicBrainz does not have many artists/albums, especially for some regional _niche_ artist.
20 |
21 | This tool helps to enrich Lidarr, by providing a custom proxy, that _hooks into_ the process _without modifying Lidarr itself_, and **_injects additional artists/albums from deemix_**.
22 |
23 | #### To do that, the following steps are performed:
24 |
25 | - [mitmproxy](https://mitmproxy.org/) runs as a proxy
26 | - Lidarr needs to be configured to use that proxy.
27 | - The proxy then **_redirects all_** api.lidarr.audio calls to an internally running **NodeJS service** (_127.0.0.1:7171_)
28 | - That NodeJS service **enriches** the missing artists/albums with the ones found in deemix
29 | - Lidarr has now additiona artists/albums, and can do its thing.
30 |
31 | ## 💻️ Installation
32 |
33 | > [!CAUTION]
34 | > If you have installed an older version, please adjust the Proxy settings as described below, otherwise the HTTP-requests will fail
35 |
36 | > [!WARNING]
37 | > This image does not come with Lidarr nor with the deemix-gui. It's an addition to your existing setup.
38 |
39 | > [!NOTE]
40 | > The previous setup required to map additional volumes for certificate validation. Thx to @codefaux, here's now a simpler way for installation.
41 |
42 | - Use the provided [docker-compose.yml](./docker-compose.yml) as an example.
43 | - **DEEMIX_ARL=xxx** your deezer ARL (get it from your browsers cookies)
44 | - **PRIO_DEEMIX=true** If albums with the same name exist, prioritize the ones comming from deemix
45 | - **OVERRIDE_MB=true** override MusicBrainz completely - **WARNING!** This will delete all your artists/albums imported from MusicBrainz.
46 | - **LIDARR_URL=http://lidarr:8686** The URL of your Lidarr instance (with port), so this library can communicate with it. Important for **OVERRIDE_MB**
47 | - **LIDARR_API_KEY=xxx** The Lidarr API Key. Important for **OVERRIDE_MB**
48 | - Go to **Lidarr -> Settings -> General**
49 | - **Certificate Validation:** to _Disabled_
50 | - **Use Proxy:** ✅
51 | - **Proxy Type:** HTTP(S)
52 | - **Hostname:** container-name/IP of the machine where lidarr-deemix is running
53 | - **Port:** 8080 (if using container-name), otherwise the port you exposed the service to
54 | - **Bypass Proxy for local addresses:** ✅
55 |
56 | 
57 |
--------------------------------------------------------------------------------
/TROUBLESHOOTING.md:
--------------------------------------------------------------------------------
1 | # Troubleshooting
2 |
3 | If you're a developer or tinkerer, this one is for you.
4 |
5 | ## Local testing
6 |
7 | The easiest way to test locally is to:
8 |
9 | - Clone Lidarr
10 | - Build Lidarr
11 | - Clone this repo
12 | - Install mitmproxy
13 | - Run Lidarr and this repo locally
14 |
15 | ### 1 Clone and Build Lidarr
16 |
17 | You need `dotnet` installed for this. See [Lidarr contribution guide](https://wiki.servarr.com/lidarr/contributing) for further information.
18 |
19 | ```bash
20 | git clone https://github.com/Lidarr/Lidarr.git
21 | cd Lidarr
22 | dotnet msbuild -restore src/Lidarr.sln -p:Configuration=Debug -p:Platform=Posix -t:PublishAllRids
23 | # grab a coffee
24 | ```
25 |
26 | ### 2 Clone this repo and install deps
27 |
28 | You'll need **python, nodejs, pnpm** for this one. Also, download and install [mitmproxy](https://mitmproxy.org/) on your system.
29 |
30 | ```bash
31 | git clone git@github.com:ad-on-is/lidarr-deemix.git
32 | cd lidarr-deemix
33 | pnpm i
34 | python -m pip install -r python/requirements.txt
35 | ```
36 |
37 | ### 3 Run
38 |
39 | ```bash
40 | # terminal 1 (lidarr-deemix):
41 | pnpm run dev
42 | # terminal 2 (lidarr-deemix):
43 | DEEMIX_ARL=xxxx python ./python/deemix-server.py #
44 | # terminal 3 (lidarr-deemix):
45 | mitmweb -s ./python/http-redirect-requests.py # this will open a new browser, where you can inspect the requests from lidarr.
46 |
47 | # terminal 4 (Lidarr)
48 | ./_output/net6.0/linux-x64/Lidarr # this will open a new browser
49 | ```
50 |
51 | ### Join our channel
52 |
53 | [Telegram](https://t.me/+_JzcldvJAoZiNmZk)
54 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3"
2 | services:
3 | lidarr-deemix:
4 | image: ghcr.io/ad-on-is/lidarr-deemix
5 | container_name: lidarr-deemix
6 | ports:
7 | - 8080:8080 # optional, only if you want to expose the port to the host
8 | environment:
9 | - DEEMIX_ARL=xxxx
10 | - OVERRIDE_MB=false # set to true to override MusicBrainz completely !!! CAUTION !!! will delete all artists/albums imported from MB
11 | - PRIO_DEEMIX=false # set to true to prioritize Deemix albums over Lidarr (adds dupliactes on existing albums, needs cleanup and rescan)
12 |
--------------------------------------------------------------------------------
/images/lidarr-deemix-conf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/lidarr-deemix/ceb7184764c4ba2450a6489f99a4ce85962545b7/images/lidarr-deemix-conf.png
--------------------------------------------------------------------------------
/images/logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ad-on-is/lidarr-deemix/ceb7184764c4ba2450a6489f99a4ce85962545b7/images/logo.webp
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lidarr-deemix",
3 | "type": "module",
4 | "scripts": {
5 | "start": "tsc && ts-node --project tsconfig.tsnode.json dist/index",
6 | "dev": "tsc-watch --onSuccess \"node ./dist/index.js\"",
7 | "dev:override": "OVERRIDE_MB=true tsc-watch --onSuccess \"node ./dist/index.js\"",
8 | "dev:prio": "PRIO_DEEMIX=true tsc-watch --onSuccess \" node ./dist/index.js\""
9 | },
10 | "devDependencies": {
11 | "@tsconfig/node20": "^20.1.4",
12 | "@tsconfig/strictest": "^2.0.5",
13 | "@types/latinize": "^0.2.18",
14 | "@types/node": "^20.12.3"
15 | },
16 | "peerDependencies": {
17 | "typescript": "^5.0.0"
18 | },
19 | "dependencies": {
20 | "@types/lodash": "^4.17.0",
21 | "@types/node-fetch": "^2.6.11",
22 | "dotenv": "^16.4.5",
23 | "fastify": "^4.26.2",
24 | "latinize": "^2.0.0",
25 | "lodash": "^4.17.21",
26 | "node-fetch": "^2.7.0",
27 | "ts-node": "11.0.0-beta.1",
28 | "tsc-watch": "^6.2.0"
29 | }
30 | }
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '6.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | dependencies:
8 | '@types/lodash':
9 | specifier: ^4.17.0
10 | version: 4.17.0
11 | '@types/node-fetch':
12 | specifier: ^2.6.11
13 | version: 2.6.11
14 | dotenv:
15 | specifier: ^16.4.5
16 | version: 16.4.5
17 | fastify:
18 | specifier: ^4.26.2
19 | version: 4.26.2
20 | latinize:
21 | specifier: ^2.0.0
22 | version: 2.0.0
23 | lodash:
24 | specifier: ^4.17.21
25 | version: 4.17.21
26 | node-fetch:
27 | specifier: ^2.7.0
28 | version: 2.7.0
29 | ts-node:
30 | specifier: 11.0.0-beta.1
31 | version: 11.0.0-beta.1(@types/node@20.12.3)(typescript@5.4.3)
32 | tsc-watch:
33 | specifier: ^6.2.0
34 | version: 6.2.0(typescript@5.4.3)
35 | typescript:
36 | specifier: ^5.0.0
37 | version: 5.4.3
38 |
39 | devDependencies:
40 | '@tsconfig/node20':
41 | specifier: ^20.1.4
42 | version: 20.1.4
43 | '@tsconfig/strictest':
44 | specifier: ^2.0.5
45 | version: 2.0.5
46 | '@types/latinize':
47 | specifier: ^0.2.18
48 | version: 0.2.18
49 | '@types/node':
50 | specifier: ^20.12.3
51 | version: 20.12.3
52 |
53 | packages:
54 |
55 | /@cspotcode/source-map-support@0.8.1:
56 | resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
57 | engines: {node: '>=12'}
58 | dependencies:
59 | '@jridgewell/trace-mapping': 0.3.9
60 | dev: false
61 |
62 | /@fastify/ajv-compiler@3.5.0:
63 | resolution: {integrity: sha512-ebbEtlI7dxXF5ziNdr05mOY8NnDiPB1XvAlLHctRt/Rc+C3LCOVW5imUVX+mhvUhnNzmPBHewUkOFgGlCxgdAA==}
64 | dependencies:
65 | ajv: 8.12.0
66 | ajv-formats: 2.1.1(ajv@8.12.0)
67 | fast-uri: 2.3.0
68 | dev: false
69 |
70 | /@fastify/error@3.4.1:
71 | resolution: {integrity: sha512-wWSvph+29GR783IhmvdwWnN4bUxTD01Vm5Xad4i7i1VuAOItLvbPAb69sb0IQ2N57yprvhNIwAP5B6xfKTmjmQ==}
72 | dev: false
73 |
74 | /@fastify/fast-json-stringify-compiler@4.3.0:
75 | resolution: {integrity: sha512-aZAXGYo6m22Fk1zZzEUKBvut/CIIQe/BapEORnxiD5Qr0kPHqqI69NtEMCme74h+at72sPhbkb4ZrLd1W3KRLA==}
76 | dependencies:
77 | fast-json-stringify: 5.13.0
78 | dev: false
79 |
80 | /@fastify/merge-json-schemas@0.1.1:
81 | resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==}
82 | dependencies:
83 | fast-deep-equal: 3.1.3
84 | dev: false
85 |
86 | /@jridgewell/resolve-uri@3.1.2:
87 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
88 | engines: {node: '>=6.0.0'}
89 | dev: false
90 |
91 | /@jridgewell/sourcemap-codec@1.4.15:
92 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
93 | dev: false
94 |
95 | /@jridgewell/trace-mapping@0.3.9:
96 | resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
97 | dependencies:
98 | '@jridgewell/resolve-uri': 3.1.2
99 | '@jridgewell/sourcemap-codec': 1.4.15
100 | dev: false
101 |
102 | /@tsconfig/node14@1.0.3:
103 | resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
104 | dev: false
105 |
106 | /@tsconfig/node16@1.0.4:
107 | resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
108 | dev: false
109 |
110 | /@tsconfig/node18@18.2.4:
111 | resolution: {integrity: sha512-5xxU8vVs9/FNcvm3gE07fPbn9tl6tqGGWA9tSlwsUEkBxtRnTsNmwrV8gasZ9F/EobaSv9+nu8AxUKccw77JpQ==}
112 | dev: false
113 |
114 | /@tsconfig/node20@20.1.4:
115 | resolution: {integrity: sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg==}
116 |
117 | /@tsconfig/strictest@2.0.5:
118 | resolution: {integrity: sha512-ec4tjL2Rr0pkZ5hww65c+EEPYwxOi4Ryv+0MtjeaSQRJyq322Q27eOQiFbuNgw2hpL4hB1/W/HBGk3VKS43osg==}
119 | dev: true
120 |
121 | /@types/latinize@0.2.18:
122 | resolution: {integrity: sha512-QjX4tcVEOcqDakv96sLMA2ESMcxSJel5KHxmcBgeTlZI7v6gFx9Tjqmt5+Pp8ijfGGr6CxuXzDNYk8A/iZ/GiA==}
123 | dev: true
124 |
125 | /@types/lodash@4.17.0:
126 | resolution: {integrity: sha512-t7dhREVv6dbNj0q17X12j7yDG4bD/DHYX7o5/DbDxobP0HnGPgpRz2Ej77aL7TZT3DSw13fqUTj8J4mMnqa7WA==}
127 | dev: false
128 |
129 | /@types/node-fetch@2.6.11:
130 | resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==}
131 | dependencies:
132 | '@types/node': 20.12.3
133 | form-data: 4.0.0
134 | dev: false
135 |
136 | /@types/node@20.12.3:
137 | resolution: {integrity: sha512-sD+ia2ubTeWrOu+YMF+MTAB7E+O7qsMqAbMfW7DG3K1URwhZ5hN1pLlRVGbf4wDFzSfikL05M17EyorS86jShw==}
138 | dependencies:
139 | undici-types: 5.26.5
140 |
141 | /abort-controller@3.0.0:
142 | resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
143 | engines: {node: '>=6.5'}
144 | dependencies:
145 | event-target-shim: 5.0.1
146 | dev: false
147 |
148 | /abstract-logging@2.0.1:
149 | resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==}
150 | dev: false
151 |
152 | /acorn-walk@8.3.2:
153 | resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
154 | engines: {node: '>=0.4.0'}
155 | dev: false
156 |
157 | /acorn@8.11.3:
158 | resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==}
159 | engines: {node: '>=0.4.0'}
160 | hasBin: true
161 | dev: false
162 |
163 | /ajv-formats@2.1.1(ajv@8.12.0):
164 | resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
165 | peerDependencies:
166 | ajv: ^8.0.0
167 | peerDependenciesMeta:
168 | ajv:
169 | optional: true
170 | dependencies:
171 | ajv: 8.12.0
172 | dev: false
173 |
174 | /ajv@8.12.0:
175 | resolution: {integrity: sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==}
176 | dependencies:
177 | fast-deep-equal: 3.1.3
178 | json-schema-traverse: 1.0.0
179 | require-from-string: 2.0.2
180 | uri-js: 4.4.1
181 | dev: false
182 |
183 | /archy@1.0.0:
184 | resolution: {integrity: sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==}
185 | dev: false
186 |
187 | /arg@4.1.3:
188 | resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
189 | dev: false
190 |
191 | /asynckit@0.4.0:
192 | resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
193 | dev: false
194 |
195 | /atomic-sleep@1.0.0:
196 | resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==}
197 | engines: {node: '>=8.0.0'}
198 | dev: false
199 |
200 | /avvio@8.3.0:
201 | resolution: {integrity: sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==}
202 | dependencies:
203 | '@fastify/error': 3.4.1
204 | archy: 1.0.0
205 | debug: 4.3.4
206 | fastq: 1.17.1
207 | transitivePeerDependencies:
208 | - supports-color
209 | dev: false
210 |
211 | /base64-js@1.5.1:
212 | resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
213 | dev: false
214 |
215 | /buffer@6.0.3:
216 | resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
217 | dependencies:
218 | base64-js: 1.5.1
219 | ieee754: 1.2.1
220 | dev: false
221 |
222 | /combined-stream@1.0.8:
223 | resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
224 | engines: {node: '>= 0.8'}
225 | dependencies:
226 | delayed-stream: 1.0.0
227 | dev: false
228 |
229 | /cookie@0.6.0:
230 | resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==}
231 | engines: {node: '>= 0.6'}
232 | dev: false
233 |
234 | /cross-spawn@7.0.3:
235 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
236 | engines: {node: '>= 8'}
237 | dependencies:
238 | path-key: 3.1.1
239 | shebang-command: 2.0.0
240 | which: 2.0.2
241 | dev: false
242 |
243 | /debug@4.3.4:
244 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
245 | engines: {node: '>=6.0'}
246 | peerDependencies:
247 | supports-color: '*'
248 | peerDependenciesMeta:
249 | supports-color:
250 | optional: true
251 | dependencies:
252 | ms: 2.1.2
253 | dev: false
254 |
255 | /delayed-stream@1.0.0:
256 | resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
257 | engines: {node: '>=0.4.0'}
258 | dev: false
259 |
260 | /diff@4.0.2:
261 | resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
262 | engines: {node: '>=0.3.1'}
263 | dev: false
264 |
265 | /dotenv@16.4.5:
266 | resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
267 | engines: {node: '>=12'}
268 | dev: false
269 |
270 | /duplexer@0.1.2:
271 | resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==}
272 | dev: false
273 |
274 | /event-stream@3.3.4:
275 | resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==}
276 | dependencies:
277 | duplexer: 0.1.2
278 | from: 0.1.7
279 | map-stream: 0.1.0
280 | pause-stream: 0.0.11
281 | split: 0.3.3
282 | stream-combiner: 0.0.4
283 | through: 2.3.8
284 | dev: false
285 |
286 | /event-target-shim@5.0.1:
287 | resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
288 | engines: {node: '>=6'}
289 | dev: false
290 |
291 | /events@3.3.0:
292 | resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
293 | engines: {node: '>=0.8.x'}
294 | dev: false
295 |
296 | /fast-content-type-parse@1.1.0:
297 | resolution: {integrity: sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==}
298 | dev: false
299 |
300 | /fast-decode-uri-component@1.0.1:
301 | resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==}
302 | dev: false
303 |
304 | /fast-deep-equal@3.1.3:
305 | resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
306 | dev: false
307 |
308 | /fast-json-stringify@5.13.0:
309 | resolution: {integrity: sha512-XjTDWKHP3GoMQUOfnjYUbqeHeEt+PvYgvBdG2fRSmYaORILbSr8xTJvZX+w1YSAP5pw2NwKrGRmQleYueZEoxw==}
310 | dependencies:
311 | '@fastify/merge-json-schemas': 0.1.1
312 | ajv: 8.12.0
313 | ajv-formats: 2.1.1(ajv@8.12.0)
314 | fast-deep-equal: 3.1.3
315 | fast-uri: 2.3.0
316 | json-schema-ref-resolver: 1.0.1
317 | rfdc: 1.3.1
318 | dev: false
319 |
320 | /fast-querystring@1.1.2:
321 | resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==}
322 | dependencies:
323 | fast-decode-uri-component: 1.0.1
324 | dev: false
325 |
326 | /fast-redact@3.5.0:
327 | resolution: {integrity: sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==}
328 | engines: {node: '>=6'}
329 | dev: false
330 |
331 | /fast-uri@2.3.0:
332 | resolution: {integrity: sha512-eel5UKGn369gGEWOqBShmFJWfq/xSJvsgDzgLYC845GneayWvXBf0lJCBn5qTABfewy1ZDPoaR5OZCP+kssfuw==}
333 | dev: false
334 |
335 | /fastify@4.26.2:
336 | resolution: {integrity: sha512-90pjTuPGrfVKtdpLeLzND5nyC4woXZN5VadiNQCicj/iJU4viNHKhsAnb7jmv1vu2IzkLXyBiCzdWuzeXgQ5Ug==}
337 | dependencies:
338 | '@fastify/ajv-compiler': 3.5.0
339 | '@fastify/error': 3.4.1
340 | '@fastify/fast-json-stringify-compiler': 4.3.0
341 | abstract-logging: 2.0.1
342 | avvio: 8.3.0
343 | fast-content-type-parse: 1.1.0
344 | fast-json-stringify: 5.13.0
345 | find-my-way: 8.1.0
346 | light-my-request: 5.12.0
347 | pino: 8.19.0
348 | process-warning: 3.0.0
349 | proxy-addr: 2.0.7
350 | rfdc: 1.3.1
351 | secure-json-parse: 2.7.0
352 | semver: 7.6.0
353 | toad-cache: 3.7.0
354 | transitivePeerDependencies:
355 | - supports-color
356 | dev: false
357 |
358 | /fastq@1.17.1:
359 | resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
360 | dependencies:
361 | reusify: 1.0.4
362 | dev: false
363 |
364 | /find-my-way@8.1.0:
365 | resolution: {integrity: sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==}
366 | engines: {node: '>=14'}
367 | dependencies:
368 | fast-deep-equal: 3.1.3
369 | fast-querystring: 1.1.2
370 | safe-regex2: 2.0.0
371 | dev: false
372 |
373 | /form-data@4.0.0:
374 | resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
375 | engines: {node: '>= 6'}
376 | dependencies:
377 | asynckit: 0.4.0
378 | combined-stream: 1.0.8
379 | mime-types: 2.1.35
380 | dev: false
381 |
382 | /forwarded@0.2.0:
383 | resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
384 | engines: {node: '>= 0.6'}
385 | dev: false
386 |
387 | /from@0.1.7:
388 | resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==}
389 | dev: false
390 |
391 | /ieee754@1.2.1:
392 | resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
393 | dev: false
394 |
395 | /ipaddr.js@1.9.1:
396 | resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
397 | engines: {node: '>= 0.10'}
398 | dev: false
399 |
400 | /isexe@2.0.0:
401 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
402 | dev: false
403 |
404 | /json-schema-ref-resolver@1.0.1:
405 | resolution: {integrity: sha512-EJAj1pgHc1hxF6vo2Z3s69fMjO1INq6eGHXZ8Z6wCQeldCuwxGK9Sxf4/cScGn3FZubCVUehfWtcDM/PLteCQw==}
406 | dependencies:
407 | fast-deep-equal: 3.1.3
408 | dev: false
409 |
410 | /json-schema-traverse@1.0.0:
411 | resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
412 | dev: false
413 |
414 | /latinize@2.0.0:
415 | resolution: {integrity: sha512-bMWAnS0C6s9rNqahOEzBEIb9RWECgD1sfmDPws/YoFGAVbC3cjNGCS3Y1THhaQ5wtTJfrq1RO69IM2dGA0DuEg==}
416 | dev: false
417 |
418 | /light-my-request@5.12.0:
419 | resolution: {integrity: sha512-P526OX6E7aeCIfw/9UyJNsAISfcFETghysaWHQAlQYayynShT08MOj4c6fBCvTWBrHXSvqBAKDp3amUPSCQI4w==}
420 | dependencies:
421 | cookie: 0.6.0
422 | process-warning: 3.0.0
423 | set-cookie-parser: 2.6.0
424 | dev: false
425 |
426 | /lodash@4.17.21:
427 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
428 | dev: false
429 |
430 | /lru-cache@6.0.0:
431 | resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
432 | engines: {node: '>=10'}
433 | dependencies:
434 | yallist: 4.0.0
435 | dev: false
436 |
437 | /make-error@1.3.6:
438 | resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
439 | dev: false
440 |
441 | /map-stream@0.1.0:
442 | resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==}
443 | dev: false
444 |
445 | /mime-db@1.52.0:
446 | resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
447 | engines: {node: '>= 0.6'}
448 | dev: false
449 |
450 | /mime-types@2.1.35:
451 | resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
452 | engines: {node: '>= 0.6'}
453 | dependencies:
454 | mime-db: 1.52.0
455 | dev: false
456 |
457 | /ms@2.1.2:
458 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
459 | dev: false
460 |
461 | /node-cleanup@2.1.2:
462 | resolution: {integrity: sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==}
463 | dev: false
464 |
465 | /node-fetch@2.7.0:
466 | resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
467 | engines: {node: 4.x || >=6.0.0}
468 | peerDependencies:
469 | encoding: ^0.1.0
470 | peerDependenciesMeta:
471 | encoding:
472 | optional: true
473 | dependencies:
474 | whatwg-url: 5.0.0
475 | dev: false
476 |
477 | /on-exit-leak-free@2.1.2:
478 | resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==}
479 | engines: {node: '>=14.0.0'}
480 | dev: false
481 |
482 | /path-key@3.1.1:
483 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
484 | engines: {node: '>=8'}
485 | dev: false
486 |
487 | /pause-stream@0.0.11:
488 | resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==}
489 | dependencies:
490 | through: 2.3.8
491 | dev: false
492 |
493 | /pino-abstract-transport@1.1.0:
494 | resolution: {integrity: sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==}
495 | dependencies:
496 | readable-stream: 4.5.2
497 | split2: 4.2.0
498 | dev: false
499 |
500 | /pino-std-serializers@6.2.2:
501 | resolution: {integrity: sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==}
502 | dev: false
503 |
504 | /pino@8.19.0:
505 | resolution: {integrity: sha512-oswmokxkav9bADfJ2ifrvfHUwad6MLp73Uat0IkQWY3iAw5xTRoznXbXksZs8oaOUMpmhVWD+PZogNzllWpJaA==}
506 | hasBin: true
507 | dependencies:
508 | atomic-sleep: 1.0.0
509 | fast-redact: 3.5.0
510 | on-exit-leak-free: 2.1.2
511 | pino-abstract-transport: 1.1.0
512 | pino-std-serializers: 6.2.2
513 | process-warning: 3.0.0
514 | quick-format-unescaped: 4.0.4
515 | real-require: 0.2.0
516 | safe-stable-stringify: 2.4.3
517 | sonic-boom: 3.8.0
518 | thread-stream: 2.4.1
519 | dev: false
520 |
521 | /process-warning@3.0.0:
522 | resolution: {integrity: sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==}
523 | dev: false
524 |
525 | /process@0.11.10:
526 | resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==}
527 | engines: {node: '>= 0.6.0'}
528 | dev: false
529 |
530 | /proxy-addr@2.0.7:
531 | resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
532 | engines: {node: '>= 0.10'}
533 | dependencies:
534 | forwarded: 0.2.0
535 | ipaddr.js: 1.9.1
536 | dev: false
537 |
538 | /ps-tree@1.2.0:
539 | resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==}
540 | engines: {node: '>= 0.10'}
541 | hasBin: true
542 | dependencies:
543 | event-stream: 3.3.4
544 | dev: false
545 |
546 | /punycode@2.3.1:
547 | resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
548 | engines: {node: '>=6'}
549 | dev: false
550 |
551 | /quick-format-unescaped@4.0.4:
552 | resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
553 | dev: false
554 |
555 | /readable-stream@4.5.2:
556 | resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==}
557 | engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
558 | dependencies:
559 | abort-controller: 3.0.0
560 | buffer: 6.0.3
561 | events: 3.3.0
562 | process: 0.11.10
563 | string_decoder: 1.3.0
564 | dev: false
565 |
566 | /real-require@0.2.0:
567 | resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
568 | engines: {node: '>= 12.13.0'}
569 | dev: false
570 |
571 | /require-from-string@2.0.2:
572 | resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
573 | engines: {node: '>=0.10.0'}
574 | dev: false
575 |
576 | /ret@0.2.2:
577 | resolution: {integrity: sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==}
578 | engines: {node: '>=4'}
579 | dev: false
580 |
581 | /reusify@1.0.4:
582 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==}
583 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
584 | dev: false
585 |
586 | /rfdc@1.3.1:
587 | resolution: {integrity: sha512-r5a3l5HzYlIC68TpmYKlxWjmOP6wiPJ1vWv2HeLhNsRZMrCkxeqxiHlQ21oXmQ4F3SiryXBHhAD7JZqvOJjFmg==}
588 | dev: false
589 |
590 | /safe-buffer@5.2.1:
591 | resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
592 | dev: false
593 |
594 | /safe-regex2@2.0.0:
595 | resolution: {integrity: sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==}
596 | dependencies:
597 | ret: 0.2.2
598 | dev: false
599 |
600 | /safe-stable-stringify@2.4.3:
601 | resolution: {integrity: sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==}
602 | engines: {node: '>=10'}
603 | dev: false
604 |
605 | /secure-json-parse@2.7.0:
606 | resolution: {integrity: sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==}
607 | dev: false
608 |
609 | /semver@7.6.0:
610 | resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
611 | engines: {node: '>=10'}
612 | hasBin: true
613 | dependencies:
614 | lru-cache: 6.0.0
615 | dev: false
616 |
617 | /set-cookie-parser@2.6.0:
618 | resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==}
619 | dev: false
620 |
621 | /shebang-command@2.0.0:
622 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
623 | engines: {node: '>=8'}
624 | dependencies:
625 | shebang-regex: 3.0.0
626 | dev: false
627 |
628 | /shebang-regex@3.0.0:
629 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
630 | engines: {node: '>=8'}
631 | dev: false
632 |
633 | /sonic-boom@3.8.0:
634 | resolution: {integrity: sha512-ybz6OYOUjoQQCQ/i4LU8kaToD8ACtYP+Cj5qd2AO36bwbdewxWJ3ArmJ2cr6AvxlL2o0PqnCcPGUgkILbfkaCA==}
635 | dependencies:
636 | atomic-sleep: 1.0.0
637 | dev: false
638 |
639 | /split2@4.2.0:
640 | resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
641 | engines: {node: '>= 10.x'}
642 | dev: false
643 |
644 | /split@0.3.3:
645 | resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==}
646 | dependencies:
647 | through: 2.3.8
648 | dev: false
649 |
650 | /stream-combiner@0.0.4:
651 | resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==}
652 | dependencies:
653 | duplexer: 0.1.2
654 | dev: false
655 |
656 | /string-argv@0.3.2:
657 | resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
658 | engines: {node: '>=0.6.19'}
659 | dev: false
660 |
661 | /string_decoder@1.3.0:
662 | resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
663 | dependencies:
664 | safe-buffer: 5.2.1
665 | dev: false
666 |
667 | /thread-stream@2.4.1:
668 | resolution: {integrity: sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==}
669 | dependencies:
670 | real-require: 0.2.0
671 | dev: false
672 |
673 | /through@2.3.8:
674 | resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
675 | dev: false
676 |
677 | /toad-cache@3.7.0:
678 | resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==}
679 | engines: {node: '>=12'}
680 | dev: false
681 |
682 | /tr46@0.0.3:
683 | resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
684 | dev: false
685 |
686 | /ts-node@11.0.0-beta.1(@types/node@20.12.3)(typescript@5.4.3):
687 | resolution: {integrity: sha512-WMSROP+1pU22Q/Tm40mjfRg130yD8i0g6ROST04ZpocfH8sl1zD75ON4XQMcBEVViXMVemJBH0alflE7xePdRA==}
688 | hasBin: true
689 | peerDependencies:
690 | '@swc/core': '>=1.3.85'
691 | '@swc/wasm': '>=1.3.85'
692 | '@types/node': '*'
693 | typescript: '>=4.4'
694 | peerDependenciesMeta:
695 | '@swc/core':
696 | optional: true
697 | '@swc/wasm':
698 | optional: true
699 | dependencies:
700 | '@cspotcode/source-map-support': 0.8.1
701 | '@tsconfig/node14': 1.0.3
702 | '@tsconfig/node16': 1.0.4
703 | '@tsconfig/node18': 18.2.4
704 | '@tsconfig/node20': 20.1.4
705 | '@types/node': 20.12.3
706 | acorn: 8.11.3
707 | acorn-walk: 8.3.2
708 | arg: 4.1.3
709 | diff: 4.0.2
710 | make-error: 1.3.6
711 | typescript: 5.4.3
712 | v8-compile-cache-lib: 3.0.1
713 | dev: false
714 |
715 | /tsc-watch@6.2.0(typescript@5.4.3):
716 | resolution: {integrity: sha512-2LBhf9kjKXnz7KQ/puLHlozMzzUNHAdYBNMkg3eksQJ9GBAgMg8czznM83T5PmsoUvDnXzfIeQn2lNcIYDr8LA==}
717 | engines: {node: '>=12.12.0'}
718 | hasBin: true
719 | peerDependencies:
720 | typescript: '*'
721 | dependencies:
722 | cross-spawn: 7.0.3
723 | node-cleanup: 2.1.2
724 | ps-tree: 1.2.0
725 | string-argv: 0.3.2
726 | typescript: 5.4.3
727 | dev: false
728 |
729 | /typescript@5.4.3:
730 | resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==}
731 | engines: {node: '>=14.17'}
732 | hasBin: true
733 | dev: false
734 |
735 | /undici-types@5.26.5:
736 | resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
737 |
738 | /uri-js@4.4.1:
739 | resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
740 | dependencies:
741 | punycode: 2.3.1
742 | dev: false
743 |
744 | /v8-compile-cache-lib@3.0.1:
745 | resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
746 | dev: false
747 |
748 | /webidl-conversions@3.0.1:
749 | resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
750 | dev: false
751 |
752 | /whatwg-url@5.0.0:
753 | resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
754 | dependencies:
755 | tr46: 0.0.3
756 | webidl-conversions: 3.0.1
757 | dev: false
758 |
759 | /which@2.0.2:
760 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
761 | engines: {node: '>= 8'}
762 | hasBin: true
763 | dependencies:
764 | isexe: 2.0.0
765 | dev: false
766 |
767 | /yallist@4.0.0:
768 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
769 | dev: false
770 |
--------------------------------------------------------------------------------
/python/deemix-server.py:
--------------------------------------------------------------------------------
1 | from os import environ
2 | from deezer import Deezer
3 | from pathlib import Path
4 |
5 | from deemix import generateDownloadObject
6 | from deemix.__main__ import LogListener
7 | from deemix.utils import getBitrateNumberFromText
8 | from deemix.settings import load as loadSettings
9 | from deemix.downloader import Downloader
10 |
11 | from flask import Flask
12 | from flask import request
13 |
14 | app = Flask(__name__)
15 |
16 | listener = LogListener()
17 | local_path = Path('.')
18 | config_folder = local_path / 'config'
19 | settings = loadSettings(config_folder)
20 |
21 | arl = environ.get('DEEMIX_ARL')
22 | # arl = 'ARL'
23 |
24 | dz = Deezer()
25 | dz.login_via_arl(arl)
26 |
27 |
28 | def get_search_params():
29 | return request.args.get('q'), request.args.get('offset'), request.args.get('limit')
30 |
31 |
32 | @app.route('/search')
33 | def search():
34 | (query, offset, limit) = get_search_params()
35 | return dz.api.search_track(query=query, index=offset, limit=limit)
36 |
37 |
38 | @app.route('/search/artists')
39 | def search_artists():
40 | (query, offset, limit) = get_search_params()
41 | return dz.api.search_artist(query=query, index=offset, limit=limit)
42 |
43 |
44 | @app.route('/search/albums')
45 | def search_albums():
46 | (query, offset, limit) = get_search_params()
47 | return dz.api.search_album(query=query, index=offset, limit=limit)
48 |
49 |
50 | @app.route('/search/advanced')
51 | def advanced_search():
52 | (query, offset, limit) = get_search_params()
53 | return dz.api.advanced_search(
54 | track=request.args.get('track'),
55 | artist=request.args.get('artist'),
56 | album=request.args.get('album'),
57 | index=offset,
58 | limit=limit
59 | )
60 |
61 |
62 | @app.route('/albums/')
63 | def album(album_id):
64 | return dz.api.get_album(album_id)
65 |
66 |
67 | @app.route('/artists/')
68 | def artist(artist_id):
69 | artist = dz.api.get_artist(artist_id)
70 | artist.update(artist | {'top': dz.api.get_artist_top(artist_id, limit=100)})
71 | artist.update(artist | {'albums': dz.api.get_artist_albums(artist_id, limit=200)})
72 | return artist
73 |
74 |
75 | @app.route('/artists//top')
76 | def artist_top(artist_id):
77 | return dz.api.get_artist_top(artist_id, limit=100)
78 |
79 | @app.route('/album//tracks')
80 | def album_tracks(album_id):
81 | return dz.api.get_album_tracks(album_id)
82 |
83 |
84 | @app.route('/artists//albums')
85 | def artist_albums(artist_id):
86 | return dz.api.get_artist_albums(artist_id, limit=200)
87 |
88 |
89 | @app.route('/dl//', defaults={'bitrate': 'flac'})
90 | @app.route('/dl///')
91 | def download(type, object_id, bitrate):
92 | bitrate = getBitrateNumberFromText(bitrate)
93 | track = generateDownloadObject(dz, f"https://www.deezer.com/us/{type}/{object_id}", bitrate)
94 | Downloader(dz, track, settings, listener).start()
95 | return track.toDict()
96 |
97 |
98 | if __name__ == '__main__':
99 | from waitress import serve
100 | print("DeemixApiHelper running at http://0.0.0.0:7272")
101 | serve(app, host="0.0.0.0", port=7272)
102 |
--------------------------------------------------------------------------------
/python/http-redirect-request.py:
--------------------------------------------------------------------------------
1 | """Redirect HTTP requests to another server."""
2 |
3 | from mitmproxy import http
4 |
5 |
6 | def request(flow: http.HTTPFlow) -> None:
7 | # pretty_host takes the "Host" header of the request into account,
8 | # which is useful in transparent mode where we usually only have the IP
9 | # otherwise.
10 | if flow.request.pretty_host == "https://api.lidarr.audio/api/v0.4/spotify/":
11 | pass
12 | elif flow.request.pretty_host == "api.lidarr.audio" or flow.request.pretty_host == "ws.audioscrobbler.com":
13 | flow.request.headers["X-Proxy-Host"] = flow.request.pretty_host
14 | flow.request.scheme = "http"
15 | flow.request.host = "127.0.0.1"
16 | flow.request.port = 7171
17 |
--------------------------------------------------------------------------------
/python/requirements.txt:
--------------------------------------------------------------------------------
1 | deemix==3.6.6
2 | deezer_py==1.3.7
3 | Flask==2.2.5
4 | mitmproxy==10.2.4
5 | waitress==3.0.0
6 |
--------------------------------------------------------------------------------
/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | nohup python ./python/deemix-server.py > ~/nohup_deemix.txt 2>&1 &
4 | nohup pnpm run start > ~/nohup_server.txt 2>&1 &
5 | nohup mitmdump -s ./python/http-redirect-request.py > ~/nohup_mitmdump.txt 2>&1 &
6 |
7 | tail -f ~/nohup_*.txt
8 |
--------------------------------------------------------------------------------
/src/deemix.ts:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 | const deemixUrl = "http://127.0.0.1:7272";
3 | import { getAllLidarrArtists } from "./lidarr.js";
4 | import { titleCase, normalize } from "./helpers.js";
5 | import { link } from "fs";
6 |
7 | function fakeId(id: any, type: string) {
8 | // artist
9 | let p = "a";
10 |
11 | if (type === "album") {
12 | p = "b";
13 | }
14 | if (type === "track") {
15 | p = "c";
16 | }
17 | if (type === "release") {
18 | p = "d";
19 | }
20 | if (type === "recording") {
21 | p = "e";
22 | }
23 | id = `${id}`;
24 | id = id.padStart(12, p);
25 |
26 | return `${"".padStart(8, p)}-${"".padStart(4, p)}-${"".padStart(
27 | 4,
28 | p
29 | )}-${"".padStart(4, p)}-${id}`;
30 | }
31 |
32 | async function deemixArtists(name: string): Promise<[]> {
33 | const data = await fetch(
34 | `${deemixUrl}/search/artists?limit=100&offset=0&q=${name}`
35 | );
36 | const j = (await data.json()) as any;
37 | return j["data"] as [];
38 | }
39 |
40 | export async function deemixAlbum(id: string): Promise {
41 | const data = await fetch(`${deemixUrl}/albums/${id}`);
42 | const j = (await data.json()) as any;
43 | return j;
44 | }
45 |
46 | export async function deemixTracks(id: string): Promise {
47 | const data = await fetch(`${deemixUrl}/album/${id}/tracks`);
48 | const j = (await data.json()) as any;
49 | return j.data as [];
50 | }
51 |
52 | export async function deemixArtist(id: string): Promise {
53 | const data = await fetch(`${deemixUrl}/artists/${id}`);
54 | const j = (await data.json()) as any;
55 |
56 | return {
57 | Albums: [
58 | ...j["albums"]["data"].map((a: any) => ({
59 | Id: fakeId(a["id"], "album"),
60 | OldIds: [],
61 | ReleaseStatuses: ["Official"],
62 | SecondaryTypes: a["title"].toLowerCase().includes("live")
63 | ? ["Live"]
64 | : [],
65 | Title: a["title"],
66 | Type: getType(a["record_type"]),
67 | })),
68 | ],
69 | artistaliases: [],
70 | artistname: j["name"],
71 | disambiguation: "",
72 |
73 | genres: [],
74 | id: `${fakeId(j["id"], "artist")}`,
75 | images: [{ CoverType: "Poster", Url: j["picture_xl"] }],
76 | links: [
77 | {
78 | target: j["link"],
79 | type: "deezer",
80 | },
81 | ],
82 | oldids: [],
83 | overview: "!!--Imported from Deemix--!!",
84 | rating: { Count: 0, Value: null },
85 | sortname: (j["name"] as string).split(" ").reverse().join(", "),
86 | status: "active",
87 | type: "Artist",
88 | };
89 | }
90 |
91 | async function deemixAlbums(name: string): Promise {
92 | let total = 0;
93 | let start = 0;
94 | const data = await fetch(
95 | `${deemixUrl}/search/albums?limit=1&offset=0&q=${name}`
96 | );
97 |
98 | const j = (await data.json()) as any;
99 | total = j["total"] as number;
100 |
101 | const albums: any[] = [];
102 | while (start < total) {
103 | const data = await fetch(
104 | `${deemixUrl}/search/albums?limit=100&offset=${start}&q=${name}`
105 | );
106 | const j = (await data.json()) as any;
107 | albums.push(...(j["data"] as []));
108 | start += 100;
109 | }
110 |
111 | return albums.filter(
112 | (a) =>
113 | normalize(a["artist"]["name"]) === normalize(name) ||
114 | a["artist"]["name"] === "Verschillende artiesten"
115 | );
116 | }
117 |
118 | function getType(rc: string) {
119 | let type = rc.charAt(0).toUpperCase() + rc.slice(1);
120 |
121 | if (type === "Ep") {
122 | type = "EP";
123 | }
124 | return type;
125 | }
126 |
127 | export async function getAlbum(id: string) {
128 | const d = await deemixAlbum(id);
129 | const contributors = d["contributors"].map((c: any) => ({
130 | id: fakeId(c["id"], "artist"),
131 | artistaliases: [],
132 | artistname: c["name"],
133 | disambiguation: "",
134 | overview: "!!--Imported from Deemix--!!",
135 | genres: [],
136 | images: [],
137 | links: [],
138 | oldids: [],
139 | sortname: (c["name"] as string).split(" ").reverse().join(", "),
140 | status: "active",
141 | type: "Artist",
142 | }));
143 |
144 | const lidarrArtists = await getAllLidarrArtists();
145 |
146 | let lidarr = null;
147 | let deemix = null;
148 |
149 | for (let la of lidarrArtists) {
150 | for (let c of contributors) {
151 | if (
152 | la["artistName"] === c["artistname"] ||
153 | normalize(la["artistName"]) === normalize(c["artistname"])
154 | ) {
155 | lidarr = la;
156 | deemix = c;
157 | }
158 | }
159 | }
160 |
161 | let lidarr2: any = {};
162 |
163 | if (process.env.OVERRIDE_MB === "true") {
164 | lidarr = deemix;
165 | lidarr2 = {
166 | id: lidarr["id"],
167 | artistname: lidarr["artistname"],
168 | artistaliases: [],
169 | disambiguation: "",
170 | overview: "",
171 | genres: [],
172 | images: [],
173 | links: [],
174 | oldids: [],
175 | sortname: lidarr["artistname"].split(" ").reverse().join(", "),
176 | status: "active",
177 | type: "Arist",
178 | };
179 | } else {
180 | lidarr2 = {
181 | id: lidarr!["foreignArtistId"],
182 | artistname: lidarr!["artistName"],
183 | artistaliases: [],
184 | disambiguation: "",
185 | overview: "",
186 | genres: [],
187 | images: [],
188 | links: [],
189 | oldids: [],
190 | sortname: lidarr!["artistName"].split(" ").reverse().join(", "),
191 | status: "active",
192 | type: "Arist",
193 | };
194 | }
195 |
196 | const tracks = await deemixTracks(d["id"]);
197 | return {
198 | aliases: [],
199 | artistid: lidarr2["id"],
200 | artists: [lidarr2],
201 | disambiguation: "",
202 | genres: [],
203 | id: `${fakeId(d["id"], "album")}`,
204 | images: [{ CoverType: "Cover", Url: d["cover_xl"] }],
205 | links: [],
206 | oldids: [],
207 | overview: "!!--Imported from Deemix--!!",
208 | rating: { Count: 0, Value: null },
209 | releasedate: d["release_date"],
210 | releases: [
211 | {
212 | country: ["Worldwide"],
213 | disambiguation: "",
214 | id: `${fakeId(d["id"], "release")}`,
215 | label: [d["label"]],
216 |
217 | media: _.uniqBy(tracks, "disk_number").map((t: any) => ({
218 | Format: "CD",
219 | Name: "",
220 | Position: t["disk_number"],
221 | })),
222 | oldids: [],
223 | releasedate: d["release_date"],
224 | status: "Official",
225 | title: titleCase(d["title"]),
226 | track_count: d["nb_tracks"],
227 | tracks: tracks.map((t: any, idx: number) => ({
228 | artistid: lidarr2["id"],
229 | durationms: t["duration"] * 1000,
230 | id: `${fakeId(t["id"], "track")}`,
231 | mediumnumber: t["disk_number"],
232 | oldids: [],
233 | oldrecordingids: [],
234 | recordingid: fakeId(t["id"], "recording"),
235 | trackname: t["title"],
236 | tracknumber: `${idx + 1}`,
237 | trackposition: idx + 1,
238 | })),
239 | },
240 | ],
241 | secondarytypes: d["title"].toLowerCase().includes("live") ? ["Live"] : [],
242 | title: titleCase(d["title"]),
243 | type: getType(d["record_type"]),
244 | };
245 | }
246 |
247 | export async function getAlbums(name: string) {
248 | const dalbums = await deemixAlbums(name);
249 |
250 | let dtoRalbums = dalbums.map((d) => ({
251 | Id: `${fakeId(d["id"], "album")}`,
252 | OldIds: [],
253 | ReleaseStatuses: ["Official"],
254 | SecondaryTypes: d["title"].toLowerCase().includes("live") ? ["Live"] : [],
255 | Title: titleCase(d["title"]),
256 | LowerTitle: d["title"].toLowerCase(),
257 | Type: getType(d["record_type"]),
258 | }));
259 |
260 | dtoRalbums = _.uniqBy(dtoRalbums, "LowerTitle");
261 |
262 | return dtoRalbums;
263 | }
264 |
265 | export async function search(
266 | lidarr: any,
267 | query: string,
268 | isManual: boolean = true
269 | ) {
270 | const dartists = await deemixArtists(query);
271 |
272 | let lartist;
273 | let lidx = -1;
274 | let didx = -1;
275 | if (process.env.OVERRIDE_MB !== "true") {
276 | for (const [i, artist] of lidarr.entries()) {
277 | if (artist["album"] === null) {
278 | lartist = artist;
279 | lidx = i;
280 | break;
281 | }
282 | }
283 | }
284 | if (lartist) {
285 | let dartist;
286 | for (const [i, d] of dartists.entries()) {
287 | if (
288 | lartist["artist"]["artistname"] === d["name"] ||
289 | normalize(lartist["artist"]["artistname"]) === normalize(d["name"])
290 | ) {
291 | dartist = d;
292 | didx = i;
293 | break;
294 | }
295 | }
296 | if (dartist) {
297 | let posterFound = false;
298 | for (const img of lartist["artist"]["images"] as any[]) {
299 | if (img["CoverType"] === "Poster") {
300 | posterFound = true;
301 | break;
302 | }
303 | }
304 | if (!posterFound) {
305 | (lartist["artist"]["images"] as any[]).push({
306 | CoverType: "Poster",
307 | Url: dartist["picture_xl"],
308 | });
309 | }
310 | lartist["artist"]["oldids"].push(fakeId(dartist["id"], "artist"));
311 | }
312 |
313 | lidarr[lidx] = lartist;
314 | }
315 |
316 | if (didx > -1) {
317 | dartists.splice(didx, 1);
318 | }
319 |
320 | let dtolartists: any[] = dartists.map((d) => ({
321 | artist: {
322 | artistaliases: [],
323 | artistname: d["name"],
324 | sortname: (d["name"] as string).split(" ").reverse().join(", "),
325 | genres: [],
326 | id: `${fakeId(d["id"], "artist")}`,
327 | images: [
328 | {
329 | CoverType: "Poster",
330 | Url: d["picture_xl"],
331 | },
332 | ],
333 | links: [
334 | {
335 | target: d["link"],
336 | type: "deezer",
337 | },
338 | ],
339 | type:
340 | (d["type"] as string).charAt(0).toUpperCase() +
341 | (d["type"] as string).slice(1),
342 | },
343 | }));
344 |
345 | if (lidarr.length === 0) {
346 | const sorted = [];
347 |
348 | for (const a of dtolartists) {
349 | if (
350 | a.artist.artistname === decodeURIComponent(query) ||
351 | normalize(a.artist.artistname) === normalize(decodeURIComponent(query))
352 | ) {
353 | sorted.unshift(a);
354 | } else {
355 | sorted.push(a);
356 | }
357 | }
358 | dtolartists = sorted;
359 | }
360 |
361 | if (!isManual) {
362 | dtolartists = dtolartists.map((a) => a.artist);
363 | if (process.env.OVERRIDE_MB === "true") {
364 | dtolartists = [
365 | dtolartists.filter((a) => {
366 | return (
367 | a["artistname"] === decodeURIComponent(query) ||
368 | normalize(a["artistname"]) === normalize(decodeURIComponent(query))
369 | );
370 | })[0],
371 | ];
372 | }
373 | }
374 |
375 | lidarr = [...lidarr, ...dtolartists];
376 |
377 | if (process.env.OVERRIDE_MB === "true") {
378 | lidarr = dtolartists;
379 | }
380 |
381 | return lidarr;
382 | }
383 |
384 | async function getAritstByName(name: string) {
385 | const artists = await deemixArtists(name);
386 | const artist = artists.find(
387 | (a) => a["name"] === name || normalize(a["name"]) === normalize(name)
388 | );
389 | return artist;
390 | }
391 |
392 | export async function getArtist(lidarr: any) {
393 | if (lidarr["error"]) return lidarr;
394 | const artist = await getAritstByName(lidarr["artistname"]);
395 | if (typeof artist === "undefined") {
396 | return lidarr;
397 | }
398 | let posterFound = false;
399 | for (const img of lidarr["images"] as any[]) {
400 | if (img["CoverType"] === "Poster") {
401 | posterFound = true;
402 | break;
403 | }
404 | }
405 | if (!posterFound) {
406 | (lidarr["images"] as any[]).push({
407 | CoverType: "Poster",
408 | Url: artist!["picture_xl"],
409 | });
410 | }
411 |
412 | const albums = await getAlbums(lidarr["artistname"]);
413 |
414 | let existing = lidarr["Albums"].map((a: any) => normalize(a["Title"]));
415 | if (process.env.PRIO_DEEMIX === "true") {
416 | existing = albums.map((a: any) => normalize(a["Title"]));
417 | }
418 | if (process.env.OVERRIDE_MB === "true") {
419 | lidarr["images"] = [
420 | {
421 | CoverType: "Poster",
422 | Url: artist!["picture_xl"],
423 | },
424 | ];
425 | lidarr["Albums"] = albums;
426 | } else {
427 | if (process.env.PRIO_DEEMIX === "true") {
428 | lidarr["Albums"] = [
429 | ...lidarr["Albums"].filter(
430 | (a: any) => !existing.includes(normalize(a["Title"]))
431 | ),
432 | ...albums,
433 | ];
434 | } else {
435 | lidarr["Albums"] = [
436 | ...lidarr["Albums"],
437 | ...albums.filter((a) => !existing.includes(normalize(a["Title"]))),
438 | ];
439 | }
440 | }
441 |
442 | return lidarr;
443 | }
444 |
445 | //
446 |
--------------------------------------------------------------------------------
/src/helpers.ts:
--------------------------------------------------------------------------------
1 | import latinize from "latinize";
2 | export function titleCase(str: string) {
3 | var splitStr = str.toLowerCase().split(" ");
4 | for (var i = 0; i < splitStr.length; i++) {
5 | // You do not need to check if i is larger than splitStr length, as your for does that for you
6 | // Assign it back to the array
7 | splitStr[i] =
8 | splitStr[i].charAt(0).toUpperCase() + splitStr[i].substring(1);
9 | }
10 | // Directly return the joined string
11 | return splitStr.join(" ");
12 | }
13 |
14 | export function normalize(str: string) {
15 | return latinize(str.toLowerCase());
16 | }
17 |
18 | export function removeKeys(obj: any, keys: any) {
19 | var index;
20 | for (var prop in obj) {
21 | if (obj.hasOwnProperty(prop)) {
22 | switch (typeof obj[prop]) {
23 | case "string":
24 | index = keys.indexOf(prop);
25 | if (index > -1) {
26 | delete obj[prop];
27 | }
28 | break;
29 | case "object":
30 | index = keys.indexOf(prop);
31 | if (index > -1) {
32 | delete obj[prop];
33 | } else {
34 | removeKeys(obj[prop], keys);
35 | }
36 | break;
37 | }
38 | }
39 | }
40 | return obj;
41 | }
42 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import fetch from "node-fetch";
2 | import Fastify from "fastify";
3 | import _ from "lodash";
4 | import dotenv from "dotenv";
5 | import {
6 | search,
7 | getArtist,
8 | getAlbum,
9 | deemixArtist,
10 | deemixAlbum,
11 | deemixTracks,
12 | } from "./deemix.js";
13 | import { removeKeys } from "./helpers.js";
14 |
15 | const lidarrApiUrl = "https://api.lidarr.audio";
16 | const scrobblerApiUrl = "https://ws.audioscrobbler.com";
17 |
18 | dotenv.config();
19 |
20 | const fastify = Fastify({
21 | logger: {
22 | level: "error",
23 | },
24 | });
25 |
26 | async function doScrobbler(req: any, res: any) {
27 | let headers = req.headers;
28 | const u = new URL(`http://localhost${req.url}`);
29 | const method = req.method;
30 |
31 | const body = req.body?.toString();
32 | let status = 200;
33 |
34 | let nh: any = {};
35 |
36 | Object.entries(headers).forEach(([key, value]) => {
37 | if (key !== "host" && key !== "connection") {
38 | nh[key] = value;
39 | }
40 | });
41 | const url = `${u.pathname}${u.search}`;
42 | let data: any;
43 | try {
44 | data = await fetch(`${scrobblerApiUrl}${url}`, {
45 | method: method,
46 | body: body,
47 | headers: nh,
48 | });
49 | status = data.status;
50 | } catch (e) {
51 | console.error(e);
52 | }
53 | res.statusCode = status;
54 | res.headers = data.headers;
55 | let json = await data.json();
56 |
57 | if (process.env.OVERRIDE_MB === "true") {
58 | json = removeKeys(json, "mbid");
59 | }
60 |
61 | return { newres: res, data: json };
62 | }
63 |
64 | async function doApi(req: any, res: any) {
65 | let headers = req.headers;
66 | const u = new URL(`http://localhost${req.url}`);
67 | const method = req.method;
68 |
69 | const body = req.body?.toString();
70 | let status = 200;
71 |
72 | let nh: any = {};
73 |
74 | Object.entries(headers).forEach(([key, value]) => {
75 | if (key !== "host" && key !== "connection") {
76 | nh[key] = value;
77 | }
78 | });
79 |
80 | const url = `${u.pathname}${u.search}`;
81 | let data: any;
82 | try {
83 | data = await fetch(`${lidarrApiUrl}${url}`, {
84 | method: method,
85 | body: body,
86 | headers: nh,
87 | });
88 | status = data.status;
89 | } catch (e) {
90 | console.error(e);
91 | }
92 |
93 | let lidarr: any;
94 | try {
95 | lidarr = await data.json();
96 | } catch (e) {
97 | console.error(e);
98 | }
99 | if (url.includes("/v0.4/search")) {
100 | lidarr = await search(
101 | lidarr,
102 | u.searchParams.get("query") as string,
103 | url.includes("type=all")
104 | );
105 | }
106 |
107 | if (url.includes("/v0.4/artist/")) {
108 | if (url.includes("-aaaa-")) {
109 | let id = url.split("/").pop()?.split("-").pop()?.replaceAll("a", "");
110 | lidarr = await deemixArtist(id!);
111 | status = lidarr === null ? 404 : 200;
112 | } else {
113 | lidarr = await getArtist(lidarr);
114 | if (process.env.OVERRIDE_MB === "true") {
115 | // prevent refetching from musicbrainz
116 | status = 404;
117 | lidarr = {};
118 | }
119 | }
120 | }
121 | if (url.includes("/v0.4/album/")) {
122 | if (url.includes("-bbbb-")) {
123 | let id = url.split("/").pop()?.split("-").pop()?.replaceAll("b", "");
124 | lidarr = await getAlbum(id!);
125 | status = lidarr === null ? 404 : 200;
126 | }
127 | }
128 |
129 | data.headers.delete("content-encoding");
130 | console.log(status, method, url);
131 | res.statusCode = status;
132 | res.headers = data.headers;
133 | return { newres: res, data: lidarr };
134 | // return new Response(JSON.stringify(lidarr), {
135 | // status: status,
136 | // headers: data.headers,
137 | // });
138 | }
139 |
140 | fastify.get("*", async (req, res) => {
141 | let headers = req.headers;
142 | const host = headers["x-proxy-host"];
143 | if (host === "ws.audioscrobbler.com") {
144 | const { newres, data } = await doScrobbler(req, res);
145 | res = newres;
146 | return data;
147 | }
148 | const { newres, data } = await doApi(req, res);
149 | res = newres;
150 | return data;
151 | });
152 |
153 | fastify.listen({ port: 7171, host: "0.0.0.0" }, (err, address) => {
154 | console.log("Lidarr++Deemix running at " + address);
155 | if (process.env.OVERRIDE_MB === "true") {
156 | console.log("Overriding MusicBrainz API with Deemix API");
157 | }
158 | });
159 |
--------------------------------------------------------------------------------
/src/lidarr.ts:
--------------------------------------------------------------------------------
1 | import { normalize } from "./helpers.js";
2 |
3 | const lidarrApiUrl = "https://api.lidarr.audio";
4 |
5 | export async function getLidarArtist(name: string) {
6 | const res = await fetch(
7 | `${lidarrApiUrl}/api/v0.4/search?type=all&query=${name}`
8 | );
9 | const json = (await res.json()) as [];
10 | const a = json.find(
11 | (a) =>
12 | a["album"] === null &&
13 | typeof a["artist"] !== "undefined" &&
14 | normalize(a["artist"]["artistname"]) === normalize(name)
15 | );
16 | if (typeof a !== "undefined") {
17 | return a["artist"];
18 | }
19 | return null;
20 | }
21 |
22 | export async function getAllLidarrArtists() {
23 | const res = await fetch(`${process.env.LIDARR_URL}/api/v1/artist`, {
24 | headers: { "X-Api-Key": process.env.LIDARR_API_KEY as string },
25 | });
26 | const json = (await res.json()) as [];
27 | return json;
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "extends": [
4 | "@tsconfig/node20/tsconfig.json"
5 | // "@tsconfig/strictest/tsconfig.json"
6 | ],
7 | "compilerOptions": {
8 | "outDir": "./dist",
9 | "rootDir": "./src"
10 | },
11 |
12 | "include": ["./*.ts", "src/index.ts", "src/deemix.ts", "src/lidarr.ts"]
13 | }
14 |
--------------------------------------------------------------------------------
/tsconfig.tsnode.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "outDir": "./dist",
4 | "rootDir": "./src"
5 | },
6 | "files": ["./deemix.ts", "./index.ts", "./lidarr.ts"],
7 | "include": ["./*.ts"]
8 | }
9 |
--------------------------------------------------------------------------------