├── .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 | ![container](https://github.com/ad-on-is/lidarr-deemix/actions/workflows/container.yml/badge.svg?branch=) 10 | [![Version](https://img.shields.io/github/tag/ad-on-is/lidarr-deemix.svg?style=flat?branch=)]() 11 | [![GitHub stars](https://img.shields.io/github/stars/ad-on-is/lidarr-deemix.svg?style=social&label=Star)]() 12 | [![GitHub watchers](https://img.shields.io/github/watchers/ad-on-is/lidarr-deemix.svg?style=social&label=Watch)]() 13 | [![GitHub forks](https://img.shields.io/github/forks/ad-on-is/lidarr-deemix.svg?style=social&label=Fork)]() 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 | ![settings](./images/lidarr-deemix-conf.png) 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 | --------------------------------------------------------------------------------